vben
2021-08-24 56a966cfbf8db5b29a42185f0f25a0e800c30dbb
提交 | 用户 | age
ff2b12 1 <template>
V 2   <li :class="getClass">
3     <template v-if="!getCollapse">
4       <div :class="`${prefixCls}-submenu-title`" @click.stop="handleClick" :style="getItemStyle">
5         <slot name="title"></slot>
6         <Icon
7           icon="eva:arrow-ios-downward-outline"
8           :size="14"
9           :class="`${prefixCls}-submenu-title-icon`"
10         />
11       </div>
37f666 12       <CollapseTransition>
ff2b12 13         <ul :class="prefixCls" v-show="opened">
V 14           <slot></slot>
15         </ul>
37f666 16       </CollapseTransition>
ff2b12 17     </template>
V 18
19     <Popover
20       placement="right"
21       :overlayClassName="`${prefixCls}-menu-popover`"
22       v-else
23       :visible="getIsOpend"
24       @visibleChange="handleVisibleChange"
25       :overlayStyle="getOverlayStyle"
26       :align="{ offset: [0, 0] }"
27     >
28       <div :class="getSubClass" v-bind="getEvents(false)">
29         <div
30           :class="[
31             {
32               [`${prefixCls}-submenu-popup`]: !getParentSubMenu,
33               [`${prefixCls}-submenu-collapsed-show-tit`]: collapsedShowTitle,
34             },
35           ]"
36         >
37           <slot name="title"></slot>
38         </div>
39         <Icon
40           v-if="getParentSubMenu"
41           icon="eva:arrow-ios-downward-outline"
42           :size="14"
43           :class="`${prefixCls}-submenu-title-icon`"
44         />
45       </div>
ee1c34 46       <!-- eslint-disable-next-line -->
V 47       <template #content v-show="opened">
48         <div v-bind="getEvents(true)">
ff2b12 49           <ul :class="[prefixCls, `${prefixCls}-${getTheme}`, `${prefixCls}-popup`]">
V 50             <slot></slot>
51           </ul>
52         </div>
53       </template>
54     </Popover>
55   </li>
56 </template>
57
58 <script lang="ts">
59   import type { CSSProperties, PropType } from 'vue';
60   import type { SubMenuProvider } from './types';
61   import {
62     defineComponent,
63     computed,
64     unref,
65     getCurrentInstance,
66     toRefs,
67     reactive,
68     provide,
69     onBeforeMount,
70     inject,
71   } from 'vue';
72   import { useDesign } from '/@/hooks/web/useDesign';
73   import { propTypes } from '/@/utils/propTypes';
74   import { useMenuItem } from './useMenu';
75   import { useSimpleRootMenuContext } from './useSimpleMenuContext';
37f666 76   import { CollapseTransition } from '/@/components/Transition';
ff2b12 77   import Icon from '/@/components/Icon';
V 78   import { Popover } from 'ant-design-vue';
79   import { isBoolean, isObject } from '/@/utils/is';
d3d620 80   import mitt from '/@/utils/mitt';
ff2b12 81
ee1c34 82   const DELAY = 200;
ff2b12 83   export default defineComponent({
V 84     name: 'SubMenu',
85     components: {
86       Icon,
37f666 87       CollapseTransition,
ff2b12 88       Popover,
V 89     },
90     props: {
91       name: {
92         type: [String, Number] as PropType<string | number>,
93         required: true,
94       },
95       disabled: propTypes.bool,
96       collapsedShowTitle: propTypes.bool,
97     },
98     setup(props) {
99       const instance = getCurrentInstance();
100
101       const state = reactive({
102         active: false,
103         opened: false,
104       });
105
106       const data = reactive({
107         timeout: null as TimeoutHandle | null,
108         mouseInChild: false,
109         isChild: false,
110       });
111
00fca0 112       const { getParentSubMenu, getItemStyle, getParentMenu, getParentList } =
V 113         useMenuItem(instance);
ff2b12 114
V 115       const { prefixCls } = useDesign('menu');
116
d3d620 117       const subMenuEmitter = mitt();
ff2b12 118
V 119       const { rootMenuEmitter } = useSimpleRootMenuContext();
120
121       const {
122         addSubMenu: parentAddSubmenu,
123         removeSubMenu: parentRemoveSubmenu,
124         removeAll: parentRemoveAll,
125         getOpenNames: parentGetOpenNames,
126         isRemoveAllPopup,
127         sliceIndex,
128         level,
129         props: rootProps,
130         handleMouseleave: parentHandleMouseleave,
131       } = inject<SubMenuProvider>(`subMenu:${getParentMenu.value?.uid}`)!;
132
133       const getClass = computed(() => {
134         return [
135           `${prefixCls}-submenu`,
136           {
137             [`${prefixCls}-item-active`]: state.active,
138             [`${prefixCls}-opened`]: state.opened,
139             [`${prefixCls}-submenu-disabled`]: props.disabled,
140             [`${prefixCls}-submenu-has-parent-submenu`]: unref(getParentSubMenu),
141             [`${prefixCls}-child-item-active`]: state.active,
142           },
143         ];
144       });
145
146       const getAccordion = computed(() => rootProps.accordion);
147       const getCollapse = computed(() => rootProps.collapse);
148       const getTheme = computed(() => rootProps.theme);
149
00fca0 150       const getOverlayStyle = computed((): CSSProperties => {
V 151         return {
152           minWidth: '200px',
153         };
154       });
ff2b12 155
V 156       const getIsOpend = computed(() => {
157         const name = props.name;
158         if (unref(getCollapse)) {
159           return parentGetOpenNames().includes(name);
160         }
161         return state.opened;
162       });
163
164       const getSubClass = computed(() => {
165         const isActive = rootProps.activeSubMenuNames.includes(props.name);
166         return [
167           `${prefixCls}-submenu-title`,
168           {
169             [`${prefixCls}-submenu-active`]: isActive,
170             [`${prefixCls}-submenu-active-border`]: isActive && level === 0,
171             [`${prefixCls}-submenu-collapse`]: unref(getCollapse) && level === 0,
172           },
173         ];
174       });
175
176       function getEvents(deep: boolean) {
177         if (!unref(getCollapse)) {
178           return {};
179         }
180         return {
181           onMouseenter: handleMouseenter,
182           onMouseleave: () => handleMouseleave(deep),
183         };
184       }
185
186       function handleClick() {
187         const { disabled } = props;
188         if (disabled || unref(getCollapse)) return;
189         const opened = state.opened;
ee1c34 190
ff2b12 191         if (unref(getAccordion)) {
V 192           const { uidList } = getParentList();
193           rootMenuEmitter.emit('on-update-opened', {
194             opend: false,
195             parent: instance?.parent,
196             uidList: uidList,
197           });
ee1c34 198         } else {
V 199           rootMenuEmitter.emit('open-name-change', {
200             name: props.name,
201             opened: !opened,
202           });
ff2b12 203         }
V 204         state.opened = !opened;
205       }
206
207       function handleMouseenter() {
208         const disabled = props.disabled;
209         if (disabled) return;
210
211         subMenuEmitter.emit('submenu:mouse-enter-child');
212
213         const index = parentGetOpenNames().findIndex((item) => item === props.name);
214
215         sliceIndex(index);
216
217         const isRoot = level === 0 && parentGetOpenNames().length === 2;
218         if (isRoot) {
219           parentRemoveAll();
220         }
221         data.isChild = parentGetOpenNames().includes(props.name);
222         clearTimeout(data.timeout!);
223         data.timeout = setTimeout(() => {
224           parentAddSubmenu(props.name);
225         }, DELAY);
226       }
227
228       function handleMouseleave(deepDispatch = false) {
229         const parentName = getParentMenu.value?.props.name;
230         if (!parentName) {
231           isRemoveAllPopup.value = true;
232         }
233
234         if (parentGetOpenNames().slice(-1)[0] === props.name) {
235           data.isChild = false;
236         }
237
238         subMenuEmitter.emit('submenu:mouse-leave-child');
239         if (data.timeout) {
240           clearTimeout(data.timeout!);
241           data.timeout = setTimeout(() => {
242             if (isRemoveAllPopup.value) {
243               parentRemoveAll();
244             } else if (!data.mouseInChild) {
245               parentRemoveSubmenu(props.name);
246             }
247           }, DELAY);
248         }
249         if (deepDispatch) {
250           if (getParentSubMenu.value) {
251             parentHandleMouseleave?.(true);
252           }
253         }
254       }
255
256       onBeforeMount(() => {
257         subMenuEmitter.on('submenu:mouse-enter-child', () => {
258           data.mouseInChild = true;
259           isRemoveAllPopup.value = false;
260           clearTimeout(data.timeout!);
261         });
262         subMenuEmitter.on('submenu:mouse-leave-child', () => {
263           if (data.isChild) return;
264           data.mouseInChild = false;
265           clearTimeout(data.timeout!);
266         });
267
268         rootMenuEmitter.on(
269           'on-update-opened',
270           (data: boolean | (string | number)[] | Recordable) => {
271             if (unref(getCollapse)) return;
272             if (isBoolean(data)) {
273               state.opened = data;
274               return;
275             }
4c89ea 276             if (isObject(data) && rootProps.accordion) {
ff2b12 277               const { opend, parent, uidList } = data as Recordable;
V 278               if (parent === instance?.parent) {
279                 state.opened = opend;
280               } else if (!uidList.includes(instance?.uid)) {
281                 state.opened = false;
282               }
283               return;
284             }
285
286             if (props.name && Array.isArray(data)) {
287               state.opened = (data as (string | number)[]).includes(props.name);
288             }
56a966 289           },
ff2b12 290         );
V 291
292         rootMenuEmitter.on('on-update-active-name:submenu', (data: number[]) => {
9edc28 293           if (instance?.uid) {
V 294             state.active = data.includes(instance?.uid);
295           }
ff2b12 296         });
V 297       });
298
299       function handleVisibleChange(visible: boolean) {
300         state.opened = visible;
301       }
302
303       // provide
304       provide<SubMenuProvider>(`subMenu:${instance?.uid}`, {
305         addSubMenu: parentAddSubmenu,
306         removeSubMenu: parentRemoveSubmenu,
307         getOpenNames: parentGetOpenNames,
308         removeAll: parentRemoveAll,
309         isRemoveAllPopup,
310         sliceIndex,
311         level: level + 1,
312         handleMouseleave,
313         props: rootProps,
314       });
315
316       return {
317         getClass,
318         prefixCls,
319         getCollapse,
320         getItemStyle,
321         handleClick,
322         handleVisibleChange,
323         getParentSubMenu,
324         getOverlayStyle,
325         getTheme,
326         getIsOpend,
327         getEvents,
328         getSubClass,
329         ...toRefs(state),
330         ...toRefs(data),
331       };
332     },
333   });
334 </script>