menu.mjs 12 KB

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