menu.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import { isArray, isObject, isString, isUndefined as isUndefined$1 } from "../../../utils/types.mjs";
  2. import { buildProps, definePropType } from "../../../utils/vue/props/runtime.mjs";
  3. import { iconPropType } from "../../../utils/vue/icon.mjs";
  4. import { flattedChildren } from "../../../utils/vue/vnode.mjs";
  5. import { mutable } from "../../../utils/typescript.mjs";
  6. import { useNamespace } from "../../../hooks/use-namespace/index.mjs";
  7. import { ElIcon } from "../../icon/index.mjs";
  8. import ClickOutside from "../../../directives/click-outside/index.mjs";
  9. import Menu from "./utils/menu-bar.mjs";
  10. import menu_collapse_transition_default from "./menu-collapse-transition.mjs";
  11. import { useMenuCssVar } from "./use-menu-css-var.mjs";
  12. import { MENU_INJECTION_KEY, SUB_MENU_INJECTION_KEY } from "./tokens.mjs";
  13. import sub_menu_default from "./sub-menu.mjs";
  14. import { unrefElement, useResizeObserver } from "@vueuse/core";
  15. import { isNil } from "lodash-unified";
  16. import { More } from "@element-plus/icons-vue";
  17. import { computed, defineComponent, getCurrentInstance, h, nextTick, onMounted, provide, reactive, ref, watch, watchEffect, withDirectives } from "vue";
  18. //#region ../../packages/components/menu/src/menu.ts
  19. const menuProps = buildProps({
  20. /**
  21. * @description menu display mode
  22. */
  23. mode: {
  24. type: String,
  25. values: ["horizontal", "vertical"],
  26. default: "vertical"
  27. },
  28. /**
  29. * @description index of active menu on page load
  30. */
  31. defaultActive: {
  32. type: String,
  33. default: ""
  34. },
  35. /**
  36. * @description array that contains indexes of currently active sub-menus
  37. */
  38. defaultOpeneds: {
  39. type: definePropType(Array),
  40. default: () => mutable([])
  41. },
  42. /**
  43. * @description whether only one sub-menu can be active
  44. */
  45. uniqueOpened: Boolean,
  46. /**
  47. * @description whether `vue-router` mode is activated. If true, index will be used as 'path' to activate the route action. Use with `default-active` to set the active item on load.
  48. */
  49. router: Boolean,
  50. /**
  51. * @description how sub-menus are triggered, only works when `mode` is 'horizontal'
  52. */
  53. menuTrigger: {
  54. type: String,
  55. values: ["hover", "click"],
  56. default: "hover"
  57. },
  58. /**
  59. * @description whether the menu is collapsed (available only in vertical mode)
  60. */
  61. collapse: Boolean,
  62. /**
  63. * @description background color of Menu (hex format) (deprecated, use `--bg-color` instead)
  64. * @deprecated use `--bg-color` instead
  65. */
  66. backgroundColor: String,
  67. /**
  68. * @description text color of Menu (hex format) (deprecated, use `--text-color` instead)
  69. * @deprecated use `--text-color` instead
  70. */
  71. textColor: String,
  72. /**
  73. * @description text color of currently active menu item (hex format) (deprecated, use `--active-color` instead)
  74. * @deprecated use `--active-color` instead
  75. */
  76. activeTextColor: String,
  77. /**
  78. * @description optional, whether menu is collapsed when clicking outside
  79. */
  80. closeOnClickOutside: Boolean,
  81. /**
  82. * @description whether to enable the collapse transition
  83. */
  84. collapseTransition: {
  85. type: Boolean,
  86. default: true
  87. },
  88. /**
  89. * @description whether the menu is ellipsis (available only in horizontal mode)
  90. */
  91. ellipsis: {
  92. type: Boolean,
  93. default: true
  94. },
  95. /**
  96. * @description offset of the popper (effective for all submenus)
  97. */
  98. popperOffset: {
  99. type: Number,
  100. default: 6
  101. },
  102. /**
  103. * @description custom ellipsis icon (available only in horizontal mode and ellipsis is true)
  104. */
  105. ellipsisIcon: {
  106. type: iconPropType,
  107. default: () => More
  108. },
  109. /**
  110. * @description Tooltip theme, built-in theme: `dark` / `light` when menu is collapsed
  111. */
  112. popperEffect: {
  113. type: definePropType(String),
  114. default: "dark"
  115. },
  116. /**
  117. * @description custom class name for all popup menus
  118. */
  119. popperClass: String,
  120. /**
  121. * @description custom style for all popup menus
  122. */
  123. popperStyle: { type: definePropType([String, Object]) },
  124. /**
  125. * @description control timeout for all menus before showing
  126. */
  127. showTimeout: {
  128. type: Number,
  129. default: 300
  130. },
  131. /**
  132. * @description control timeout for all menus before hiding
  133. */
  134. hideTimeout: {
  135. type: Number,
  136. default: 300
  137. },
  138. /**
  139. * @description when menu inactive and `persistent` is `false` , dropdown menu will be destroyed
  140. */
  141. persistent: {
  142. type: Boolean,
  143. default: true
  144. }
  145. });
  146. const checkIndexPath = (indexPath) => isArray(indexPath) && indexPath.every((path) => isString(path));
  147. const menuEmits = {
  148. close: (index, indexPath) => isString(index) && checkIndexPath(indexPath),
  149. open: (index, indexPath) => isString(index) && checkIndexPath(indexPath),
  150. select: (index, indexPath, item, routerResult) => isString(index) && checkIndexPath(indexPath) && isObject(item) && (isUndefined$1(routerResult) || routerResult instanceof Promise)
  151. };
  152. const DEFAULT_MORE_ITEM_WIDTH = 64;
  153. var menu_default = defineComponent({
  154. name: "ElMenu",
  155. props: menuProps,
  156. emits: menuEmits,
  157. setup(props, { emit, slots, expose }) {
  158. const instance = getCurrentInstance();
  159. const router = instance.appContext.config.globalProperties.$router;
  160. const menu = ref();
  161. const subMenu = ref();
  162. const nsMenu = useNamespace("menu");
  163. const nsSubMenu = useNamespace("sub-menu");
  164. let moreItemWidth = DEFAULT_MORE_ITEM_WIDTH;
  165. const sliceIndex = ref(-1);
  166. const openedMenus = ref(props.defaultOpeneds && !props.collapse ? props.defaultOpeneds.slice(0) : []);
  167. const activeIndex = ref(props.defaultActive);
  168. const items = ref({});
  169. const subMenus = ref({});
  170. const isMenuPopup = computed(() => props.mode === "horizontal" || props.mode === "vertical" && props.collapse);
  171. const initMenu = () => {
  172. const activeItem = activeIndex.value && items.value[activeIndex.value];
  173. if (!activeItem || props.mode === "horizontal" || props.collapse) return;
  174. activeItem.indexPath.forEach((index) => {
  175. const subMenu = subMenus.value[index];
  176. subMenu && openMenu(index, subMenu.indexPath);
  177. });
  178. };
  179. const openMenu = (index, indexPath) => {
  180. if (openedMenus.value.includes(index)) return;
  181. if (props.uniqueOpened) openedMenus.value = openedMenus.value.filter((index) => indexPath.includes(index));
  182. openedMenus.value.push(index);
  183. emit("open", index, indexPath);
  184. };
  185. const close = (index) => {
  186. const i = openedMenus.value.indexOf(index);
  187. if (i !== -1) openedMenus.value.splice(i, 1);
  188. };
  189. const closeMenu = (index, indexPath) => {
  190. close(index);
  191. emit("close", index, indexPath);
  192. };
  193. const handleSubMenuClick = ({ index, indexPath }) => {
  194. openedMenus.value.includes(index) ? closeMenu(index, indexPath) : openMenu(index, indexPath);
  195. };
  196. const handleMenuItemClick = (menuItem) => {
  197. if (props.mode === "horizontal" || props.collapse) openedMenus.value = [];
  198. const { index, indexPath } = menuItem;
  199. if (isNil(index) || isNil(indexPath)) return;
  200. if (props.router && router) {
  201. const route = menuItem.route || index;
  202. const routerResult = router.push(route).then((res) => {
  203. if (!res) activeIndex.value = index;
  204. return res;
  205. });
  206. emit("select", index, indexPath, {
  207. index,
  208. indexPath,
  209. route
  210. }, routerResult);
  211. } else {
  212. activeIndex.value = index;
  213. emit("select", index, indexPath, {
  214. index,
  215. indexPath
  216. });
  217. }
  218. };
  219. const updateActiveIndex = (val) => {
  220. const itemsInData = items.value;
  221. activeIndex.value = (itemsInData[val] || activeIndex.value && itemsInData[activeIndex.value] || itemsInData[props.defaultActive])?.index ?? val;
  222. };
  223. const calcMenuItemWidth = (menuItem) => {
  224. const computedStyle = getComputedStyle(menuItem);
  225. const marginLeft = Number.parseInt(computedStyle.marginLeft, 10);
  226. const marginRight = Number.parseInt(computedStyle.marginRight, 10);
  227. return menuItem.offsetWidth + marginLeft + marginRight || 0;
  228. };
  229. const calcSliceIndex = () => {
  230. if (!menu.value) return -1;
  231. const items = Array.from(menu.value.childNodes).filter((item) => item.nodeName !== "#comment" && (item.nodeName !== "#text" || item.nodeValue));
  232. const computedMenuStyle = getComputedStyle(menu.value);
  233. const paddingLeft = Number.parseInt(computedMenuStyle.paddingLeft, 10);
  234. const paddingRight = Number.parseInt(computedMenuStyle.paddingRight, 10);
  235. const menuWidth = menu.value.clientWidth - paddingLeft - paddingRight;
  236. let calcWidth = 0;
  237. let sliceIndex = 0;
  238. items.forEach((item, index) => {
  239. calcWidth += calcMenuItemWidth(item);
  240. if (calcWidth <= menuWidth - moreItemWidth) sliceIndex = index + 1;
  241. });
  242. return sliceIndex === items.length ? -1 : sliceIndex;
  243. };
  244. const getIndexPath = (index) => subMenus.value[index].indexPath;
  245. const debounce = (fn, wait = 33.34) => {
  246. let timer;
  247. return () => {
  248. timer && clearTimeout(timer);
  249. timer = setTimeout(() => {
  250. fn();
  251. }, wait);
  252. };
  253. };
  254. let isFirstTimeRender = true;
  255. const handleResize = () => {
  256. const el = unrefElement(subMenu);
  257. if (el) moreItemWidth = calcMenuItemWidth(el) || DEFAULT_MORE_ITEM_WIDTH;
  258. if (sliceIndex.value === calcSliceIndex()) return;
  259. const callback = () => {
  260. sliceIndex.value = -1;
  261. nextTick(() => {
  262. sliceIndex.value = calcSliceIndex();
  263. });
  264. };
  265. isFirstTimeRender ? callback() : debounce(callback)();
  266. isFirstTimeRender = false;
  267. };
  268. watch(() => props.defaultActive, (currentActive) => {
  269. if (!items.value[currentActive]) activeIndex.value = "";
  270. updateActiveIndex(currentActive);
  271. });
  272. watch(() => props.collapse, (value) => {
  273. if (value) openedMenus.value = [];
  274. });
  275. watch(items.value, initMenu);
  276. let resizeStopper;
  277. watchEffect(() => {
  278. if (props.mode === "horizontal" && props.ellipsis) resizeStopper = useResizeObserver(menu, handleResize).stop;
  279. else resizeStopper?.();
  280. });
  281. const mouseInChild = ref(false);
  282. {
  283. const addSubMenu = (item) => {
  284. subMenus.value[item.index] = item;
  285. };
  286. const removeSubMenu = (item) => {
  287. delete subMenus.value[item.index];
  288. };
  289. const addMenuItem = (item) => {
  290. items.value[item.index] = item;
  291. };
  292. const removeMenuItem = (item) => {
  293. delete items.value[item.index];
  294. };
  295. provide(MENU_INJECTION_KEY, reactive({
  296. props,
  297. openedMenus,
  298. items,
  299. subMenus,
  300. activeIndex,
  301. isMenuPopup,
  302. addMenuItem,
  303. removeMenuItem,
  304. addSubMenu,
  305. removeSubMenu,
  306. openMenu,
  307. closeMenu,
  308. handleMenuItemClick,
  309. handleSubMenuClick
  310. }));
  311. provide(`${SUB_MENU_INJECTION_KEY}${instance.uid}`, {
  312. addSubMenu,
  313. removeSubMenu,
  314. mouseInChild,
  315. level: 0
  316. });
  317. }
  318. onMounted(() => {
  319. if (props.mode === "horizontal") new Menu(instance.vnode.el, nsMenu.namespace.value);
  320. });
  321. {
  322. const open = (index) => {
  323. const { indexPath } = subMenus.value[index];
  324. indexPath.forEach((i) => openMenu(i, indexPath));
  325. };
  326. expose({
  327. open,
  328. close,
  329. updateActiveIndex,
  330. handleResize
  331. });
  332. }
  333. const ulStyle = useMenuCssVar(props, 0);
  334. return () => {
  335. let slot = slots.default?.() ?? [];
  336. const vShowMore = [];
  337. if (props.mode === "horizontal" && menu.value) {
  338. const originalSlot = flattedChildren(slot).filter((vnode) => {
  339. return vnode?.shapeFlag !== 8;
  340. });
  341. const slotDefault = sliceIndex.value === -1 ? originalSlot : originalSlot.slice(0, sliceIndex.value);
  342. const slotMore = sliceIndex.value === -1 ? [] : originalSlot.slice(sliceIndex.value);
  343. if (slotMore?.length && props.ellipsis) {
  344. slot = slotDefault;
  345. vShowMore.push(h(sub_menu_default, {
  346. ref: subMenu,
  347. index: "sub-menu-more",
  348. class: nsSubMenu.e("hide-arrow"),
  349. popperOffset: props.popperOffset
  350. }, {
  351. title: () => h(ElIcon, { class: nsSubMenu.e("icon-more") }, { default: () => h(props.ellipsisIcon) }),
  352. default: () => slotMore
  353. }));
  354. }
  355. }
  356. const directives = props.closeOnClickOutside ? [[ClickOutside, () => {
  357. if (!openedMenus.value.length) return;
  358. if (!mouseInChild.value) {
  359. openedMenus.value.forEach((openedMenu) => emit("close", openedMenu, getIndexPath(openedMenu)));
  360. openedMenus.value = [];
  361. }
  362. }]] : [];
  363. const vMenu = withDirectives(h("ul", {
  364. key: String(props.collapse),
  365. role: "menubar",
  366. ref: menu,
  367. style: ulStyle.value,
  368. class: {
  369. [nsMenu.b()]: true,
  370. [nsMenu.m(props.mode)]: true,
  371. [nsMenu.m("collapse")]: props.collapse
  372. }
  373. }, [...slot, ...vShowMore]), directives);
  374. if (props.collapseTransition && props.mode === "vertical") return h(menu_collapse_transition_default, () => vMenu);
  375. return vMenu;
  376. };
  377. }
  378. });
  379. //#endregion
  380. export { menu_default as default, menuEmits, menuProps };
  381. //# sourceMappingURL=menu.mjs.map