el-menu實現橫向溢出截取的示例代碼
antd的menu組件,會在subMenu超出的情況下對超出的subMenu進行截取。 但是element的menu組件不會對溢出進行截取
於是我想對element的menu再次進行封裝,讓它能夠支持寬度溢出截取。
思考
查看瞭antd的源碼,還是比較復雜的,他會對每一個subMenu進行一份拷貝,然後隱藏在對應subMenu的後邊,然後依賴於resize-observer-polyfill對menu和subMenu進行監聽,然後計算超出的subMenu下標。代碼量還是比較多的,看到最後有點迷糊。
後來我進行瞭一些思考,需求大概如下
- 通過resize-observer-polyfill對頁面變化進行監聽
- 計算寬度是否溢出,以及subMenu下標lastVisbileIndex是多少
- 渲染出溢出的subMenu集合
- 底層還是使用el-menu
代碼部分
<template> <el-menu class="sweet-menu" v-bind="$attrs" v-on="$listeners" > <!-- 傳入的menu --> <slot /> <!-- ...按鈕 --> <sub-menu v-if="ellipsis" :list="overflowedElements" /> </el-menu> </template>
首先確定template部分 僅僅是需要將傳入的參數透傳給el-menu,然後通過默認插槽的形式接收傳入的子元素。最後渲染出溢出部分的展示開關。
//subMenu組件 export default { props: { list: {}, }, render(h) { return h('template', [ h('el-submenu', { attrs: { key: 'overflow-menu', index: 'overflow-menu', 'popper-append-to-body': true, }, class: { 'overflow-btn': true, }, }, [ h('span', { slot: 'title' }, '...'), ...this.list, ]), ]); }, };
subMenu組件的主要作用是渲染出傳入的list,list其實就是一段從$slots.default中拿到的VNode列表。
import ResizeObserver from 'resize-observer-polyfill'; import subMenu from './subMenu.vue'; import { setStyle, getWidth, cloneElement } from './utils'; //偏差部分 const FLOAT_PRECISION_ADJUST = 0.5; export default { name: 'SweetMenu', components: { subMenu, }, data() { return { // 所有menu寬度總和 originalTotalWidth: 0, resizeObserver: null, // 最後一個可展示menu的下標 lastVisibleIndex: undefined, // 溢出的subMenus overflowedItems: [], overflowedElements: [], // 所有menu寬度集合 menuItemSizes: [], lastChild: undefined, // 所有menu集合 ulChildrenNodes: [], // 原始slots.defaule備份 originSlots: [], }; }, computed: { ellipsis() { return this.$attrs?.mode === 'horizontal'; }, }, mounted() { if (!this.ellipsis) return; // 備份slots.default this.originSlots = this.$slots.default.map((vnode) => cloneElement(vnode)); // 拿到...按鈕 // eslint-disable-next-line prefer-destructuring this.lastChild = [].slice.call(this.$el.children, -1)[0]; // 拿到所有li this.ulChildrenNodes = [].slice.call(this.$el.children, 0, -1); // 保存每個menu的寬度 this.menuItemSizes = [].slice .call(this.ulChildrenNodes) .map((c) => getWidth(c)); // 計算menu寬度總和 this.originalTotalWidth = this.menuItemSizes.reduce( (acc, cur) => acc + cur, 0, ); // 註冊監聽事件 this.$nextTick(() => { this.setChildrenWidthAndResize(); if (this.$attrs.mode === 'horizontal') { const menuUl = this.$el; if (!menuUl) return; this.resizeObserver = new ResizeObserver((entries) => { entries.forEach(this.setChildrenWidthAndResize); }); this.resizeObserver.observe(menuUl); } }); }, methods: { setChildrenWidthAndResize() { if (this.$attrs.mode !== 'horizontal' || !this.$el) return; const { lastChild, ulChildrenNodes } = this; // ...按鈕的寬度 const overflowedIndicatorWidth = getWidth(lastChild); if (!ulChildrenNodes || ulChildrenNodes.length === 0) { return; } // 拿到所有slots.default this.$slots.default = this.originSlots.map((vnode) => cloneElement(vnode)); // 解決內容區撐開ul寬度問題 ulChildrenNodes.forEach((c) => { setStyle(c, 'display', 'none'); }); // 獲取el-menu寬度 const width = getWidth(this.$el); // 可展示menu寬度總和 let currentSumWidth = 0; // 最後一個可展示menu的下標 let lastVisibleIndex; // 如果寬度溢出 if (this.originalTotalWidth > width + FLOAT_PRECISION_ADJUST) { lastVisibleIndex = -1; this.menuItemSizes.forEach((liWidth) => { currentSumWidth += liWidth; if (currentSumWidth + overflowedIndicatorWidth <= width) { lastVisibleIndex += 1; } }); } this.lastVisibleIndex = lastVisibleIndex; // 過濾menu相關dom this.overflowedItems = [].slice .call(ulChildrenNodes) .filter((c, index) => index > lastVisibleIndex); this.overflowedElements = this.$slots.default.filter( (c, index) => index > lastVisibleIndex, ); // 展示所有li ulChildrenNodes.forEach((c) => { setStyle(c, 'display', 'inline-block'); }); // 對溢出li隱藏 this.overflowedItems.forEach((c) => { setStyle(c, 'display', 'none'); }); // 判斷是否需要顯示... setStyle( this.lastChild, 'display', lastVisibleIndex === undefined ? 'none' : 'inline-block', ); // 去除隱藏的menu 解決hover時 被隱藏的menu彈窗同時出現問題 this.$slots.default = this.$slots.default.filter((vnode, index) => index <= lastVisibleIndex); }, }, };
在js部分,主要是對subMenu寬度進行瞭判斷,通過menuItemSizes
保存所有subMenu的寬度,然後拿到this.$el
也就是容器ul的寬度。通過遞增的方式,判斷是否溢出,然後記錄lastVisibleIndex
。這裡需要註意的就是記得要加上最後一個subMenu的寬度
。
然後是一些css樣式的處理
.sweet-menu { overflow: hidden; position: relative; white-space: nowrap; width: 100%; ::v-deep & > .el-menu-item { position: relative; } ::v-deep .overflow-btn { .el-submenu__icon-arrow { display: none; } } ::v-deep .sweet-icon { margin-right: 0.5rem; } }
這裡我們隻是對horizontal
模式進行瞭處理,vertical
模式還是兼容的,所以隻需要像使用el-menu
的方式進行使用 就可以瞭
//utils.js部分 import classNames from 'classnames'; const camelizeRE = /-(\w)/g; const camelize = (str) => str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')); export function isEmptyElement(c) { return !(c.tag || (c.text && c.text.trim() !== '')); } const filterEmpty = (children = []) => children.filter((c) => !isEmptyElement(c)); // eslint-disable-next-line default-param-last const parseStyleText = (cssText = '', camel) => { const res = {}; const listDelimiter = /;(?![^(]*\))/g; const propertyDelimiter = /:(.+)/; cssText.split(listDelimiter).forEach((item) => { if (item) { const tmp = item.split(propertyDelimiter); if (tmp.length > 1) { const k = camel ? camelize(tmp[0].trim()) : tmp[0].trim(); res[k] = tmp[1].trim(); } } }); return res; }; function cloneVNodes(vnodes, deep) { const len = vnodes.length; const res = new Array(len); // eslint-disable-next-line no-plusplus for (let i = 0; i < len; i++) { // eslint-disable-next-line no-use-before-define res[i] = cloneVNode(vnodes[i], deep); } return res; } const cloneVNode = (vnode, deep) => { const { componentOptions } = vnode; const { data } = vnode; let listeners = {}; if (componentOptions && componentOptions.listeners) { listeners = { ...componentOptions.listeners }; } let on = {}; if (data && data.on) { on = { ...data.on }; } const cloned = new vnode.constructor( vnode.tag, data ? { ...data, on } : data, vnode.children, vnode.text, vnode.elm, vnode.context, componentOptions ? { ...componentOptions, listeners } : componentOptions, vnode.asyncFactory, ); cloned.ns = vnode.ns; cloned.isStatic = vnode.isStatic; cloned.key = vnode.key; cloned.isComment = vnode.isComment; cloned.fnContext = vnode.fnContext; cloned.fnOptions = vnode.fnOptions; cloned.fnScopeId = vnode.fnScopeId; cloned.isCloned = true; if (deep) { if (vnode.children) { cloned.children = cloneVNodes(vnode.children, true); } if (componentOptions && componentOptions.children) { componentOptions.children = cloneVNodes(componentOptions.children, true); } } return cloned; }; // eslint-disable-next-line default-param-last const cloneElement = (n, nodeProps = {}, deep) => { let ele = n; if (Array.isArray(n)) { // eslint-disable-next-line prefer-destructuring ele = filterEmpty(n)[0]; } if (!ele) { return null; } const node = cloneVNode(ele, deep); // // 函數式組件不支持clone https://github.com/vueComponent/ant-design-vue/pull/1947 // warning( // !(node.fnOptions && node.fnOptions.functional), // ); const { props = {}, key, on = {}, nativeOn = {}, children, directives = [], } = nodeProps; const data = node.data || {}; let cls = {}; let style = {}; const { attrs = {}, ref, domProps = {}, style: tempStyle = {}, class: tempCls = {}, scopedSlots = {}, } = nodeProps; if (typeof data.style === 'string') { style = parseStyleText(data.style); } else { style = { ...data.style, ...style }; } if (typeof tempStyle === 'string') { style = { ...style, ...parseStyleText(style) }; } else { style = { ...style, ...tempStyle }; } if (typeof data.class === 'string' && data.class.trim() !== '') { data.class.split(' ').forEach((c) => { cls[c.trim()] = true; }); } else if (Array.isArray(data.class)) { classNames(data.class) .split(' ') .forEach((c) => { cls[c.trim()] = true; }); } else { cls = { ...data.class, ...cls }; } if (typeof tempCls === 'string' && tempCls.trim() !== '') { tempCls.split(' ').forEach((c) => { cls[c.trim()] = true; }); } else { cls = { ...cls, ...tempCls }; } node.data = { ...data, style, attrs: { ...data.attrs, ...attrs }, class: cls, domProps: { ...data.domProps, ...domProps }, scopedSlots: { ...data.scopedSlots, ...scopedSlots }, directives: [...(data.directives || []), ...directives], }; if (node.componentOptions) { node.componentOptions.propsData = node.componentOptions.propsData || {}; node.componentOptions.listeners = node.componentOptions.listeners || {}; node.componentOptions.propsData = { ...node.componentOptions.propsData, ...props }; node.componentOptions.listeners = { ...node.componentOptions.listeners, ...on }; if (children) { node.componentOptions.children = children; } } else { if (children) { node.children = children; } node.data.on = { ...(node.data.on || {}), ...on }; } node.data.on = { ...(node.data.on || {}), ...nativeOn }; if (key !== undefined) { node.key = key; node.data.key = key; } if (typeof ref === 'string') { node.data.ref = ref; } return node; }; const getWidth = (elem) => { let width = elem && typeof elem.getBoundingClientRect === 'function' && elem.getBoundingClientRect().width; if (width) { width = +width.toFixed(6); } return width || 0; }; const setStyle = (elem, styleProperty, value) => { if (elem && typeof elem.style === 'object') { elem.style[styleProperty] = value; } }; export { cloneElement, setStyle, getWidth, };
總結
到此這篇關於el-menu實現橫向溢出截取的文章就介紹到這瞭,更多相關el-menu橫向溢出截取內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!