这篇文章主要介绍了el-menu如何实现横向溢出截取的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇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-lastconst 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-lastconst 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如何实现横向溢出截取”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注编程网行业资讯频道。