这篇教程教你:当桌面端导航栏溢出时,自动添加一个「More」菜单项。
引言
UI/UX 文献都建议导航菜单项越少越好。但做 Web 的人都知道,总有些客户坚持要在导航栏里塞一堆菜单项,结果为了在各种断点上避免溢出,样式调起来简直是噩梦。
其实有另一种办法:与其写一堆媒体查询,不如集成这段小脚本。只要你的菜单在窗口缩放时发生溢出,脚本就会自动在导航栏末尾创建一个「More」项,并把溢出的菜单项收进它的子菜单里。来看看怎么实现。
DOM 结构
按照下面的 DOM 树搭建:
一定要按上图把 menu-wrapper、menu-inner、menu-crop 这三个类加到正确的元素上。
menu-wrapper
menu-wrapper 元素需要 100% 宽度。如果你想要盒装(boxed)导航栏,再加上 max-width:
menu-inner
menu-inner 块需要启用 flexbox,并使用 flex-direction: row:
JavaScript
把下面的 JavaScript 代码复制/粘贴到需要生效的地方。举个例子,如果你的菜单是全局菜单、所有页面都显示,可以把代码粘贴到 Bricks Settings → Custom Code 标签页 → Body (footer) scripts:
window.addEventListener('load', () => {
// Variables
const wrapper = document.querySelector('.menu-wrapper');
if (!wrapper) {
return console.log('Resizable Desktop Menu - There is no menu-wrapper class on this page. Please double check you correctly assigned the classes to each element');
}
const inner = wrapper.querySelector('.menu-inner');
if (!inner) {
return console.log('Resizable Desktop Menu - There is no menu-inner class on this page. Please double check you correctly assigned the classes to each element');
}
const menu = inner.querySelector('.menu-crop');
if (!menu) {
return console.log('Resizable Desktop Menu - There is no menu-crop class on this page. Please double check you correctly assigned the classes to each element');
}
const menuUl = menu.querySelector('ul.bricks-nav-menu');
const menuItems = menu.querySelectorAll("ul.bricks-nav-menu > li.menu-item").length;
let hiddenItems = [];
let moreItem;
let moreIsVisible = false;
// debounce so calculation doesn't happen every millisecond when resizing
const debounce = (fn, threshold) => {
var timeout;
threshold = threshold || 200;
return function debounced() {
clearTimeout(timeout);
var args = arguments;
var _this = this;
function delayed() {
fn.apply(_this, args);
}
timeout = setTimeout(delayed, threshold);
};
};
// Check if overflow
const isOverflown = (element) => {
return element.scrollWidth > element.clientWidth;
}
// Create elements function
const createElement = (el, options, append) => {
let element = document.createElement(el);
if (!options) return element;
let entries = Object.entries(options)
let data = entries.map(([key, val] = entry) => {
element.setAttribute(key, val);
});
if (!append) return element
return append.appendChild(element);
}
// Create More Item
const createMoreMenu = (menuUl) => {
// Create Elemets
const tag = createElement('li', {
'id': 'moreItem',
'class': 'bricks-menu-item'
}, menuUl);
const active = createElement('a', {}, tag);
const text = document.createTextNode("More");
active.appendChild(text);
const qty = createElement('span', {
'id': 'moreQty'
}, active);
const ulTag = createElement('ul', {
'id': 'moreSubMenu',
'role': 'menu',
'class': 'sub-menu'
}, tag);
moreIsVisible = true;
}
// Remove Item
const removeItem = () => {
// Stop the function is NOT overflown
if (!isOverflown(inner) || hiddenItems.length == menuItems) return;
// Declare Variables
let menuLi = menu.querySelectorAll("ul.bricks-nav-menu > li.menu-item");
let lastItem = menuLi[menuLi.length - 1];
// Add node to hidden array
hiddenItems.push(lastItem);
// Remove node from the DOM
if (lastItem) menuUl.removeChild(lastItem);
// Insert the More Button
if (!moreIsVisible) createMoreMenu(menuUl);
// Append the item to the sub menu
let subMenu = menu.querySelector('#moreSubMenu');
if (subMenu) subMenu.insertBefore(lastItem, subMenu.firstChild);
// Update the Qty Number
let subMenuQty = menu.querySelector('#moreQty');
subMenuQty.innerHTML = hiddenItems.length;
// Keep removing items if the inner container is overflown
removeItem();
}
// Add Item
const addItem = () => {
if (hiddenItems.length < 1) return;
// Remove the More Item
if (moreIsVisible) {
let moreItem = menuUl.querySelector("#moreItem");
moreItem.remove();
moreIsVisible = false;
}
// Readd all hidden items as visible
if (hiddenItems.length > 0) {
hiddenItems.slice().reverse().forEach(item => {
menuUl.insertBefore(item, moreItem);
});
// reset the hidden array
hiddenItems = [];
}
// Remove item is the inner container is overflown
if (isOverflown(inner)) removeItem();
}
// Init
if (isOverflown(inner)) removeItem();
// Resize
window.addEventListener('resize', debounce(() => {
isOverflown(inner) ? removeItem() : addItem();
}, 0))
})
CSS
把下面的 CSS 片段粘贴到脚本运行的地方。同样,全局菜单可以放在 Bricks Settings → Custom Code 标签页 → Custom CSS:
.menu-inner > * {
white-space: nowrap;
}
#moreItem a {
display: flex;
align-items: flex-start;
flex-wrap: nowrap;
flex-direction: row;
}
span#moreQty {
margin-left: 0.3rem;
background-color: var(--bricks-color-primary, blue);
padding: 0.3rem 0.45rem;
font-size: 9px;
color: #fff;
border-radius: 50%;
vertical-align: super;
font-weight: 800;
width: 15px;
height: 15px;
display: flex;
align-items: center;
justify-content: center;
}
参考资源
debounce 函数(防抖)
https://www.freecodecamp.org/news/javascript-debounce-example/
JS 对象中的键值对
https://javascript.info/keys-values-entries
理解 offsetWidth、clientWidth、scrollWidth
结论
脚本的核心逻辑:用 scrollWidth > clientWidth 判断 menu-inner 是否溢出;溢出就从菜单末尾逐个摘掉菜单项收进 #moreSubMenu 子菜单,并创建带数量徽章(#moreQty)的「More」项;窗口拉宽不再溢出时再按逆序放回。resize 事件用防抖(默认阈值 200ms)避免频繁计算。搭配 CSS 里的 white-space: nowrap 防止菜单项换行即可。