最近遇到一个业务需求,需要面包屑不换行展示;如果展示不下,则只展示第一个和最后两个面包屑,其余收至 ... 省略号按钮并悬浮展示
接到需求后先翻翻项目使用的蚂蚁组件能否满足需求,然而并不能;
接着搜索 responsive breadcrumb、dynamic breadcrumb 也没找到符合要求的组件,所以只能自己写一个了
思考流程
静下来想了下思路 🤔 :本质是要通过 面包屑宽度 和 父容器宽度 决定展示策略
假设遇到以下场景:
父容器为灰色背景部分,面包屑列表比父容器长,一行放不下,按照咱们的需求此时应该只展示第一个和最后两个面包屑,其余省略展示,即:
最终效果为:
对应的流程为:
相关代码
流程捋顺了,咱们来写计算面包屑展示的方法:
// 省略号按钮的宽度
const MORE_BTN_WIDTH = 40
/**
* items: 每个面包屑宽度的数据列表
* parentWidth: 父容器宽度
*/
function calcBreadcrumbDisplay(items, parentWidth) {
// 计算面包屑总宽度
const totalWidth = items.reduce((acc, curr) => acc + curr.width, 0)
if (totalWidth <= parentWidth) return items
// 不超过 3 个面包屑,直接返回面包屑宽度的计算结果
if (items.length <= 3) return calcItemWidth(items, totalWidth, parentWidth)
// 分出展示组和剩余组
const firstOne = items.shift()
const lastOne = item.pop()
const lastTwo = item.pop()
const displayItems = [firstOne, lastTwo, lastOne]
const displayWidth = displayItems.reduce((acc, curr) => acc + curr.width, 0)
// 如果展示组加上省略按钮不能在父容器内完整展示
if (displayWidth + MORE_BTN_WIDTH >= parentWidth) {
// 添加 omit 标记,组件内会根据该标记将其收至省略列表
items.forEach(item => (item.omit = true))
calcItemWidth(displayItems, totalWidth, parentWidth - MORE_BTN_WIDTH)
return [firstOne, ...items, lastTwo, lastOne]
}
// 父容器剩余的宽度
const parentRemindWidth = parentWidth - displayWidth - MORE_BTN_WIDTH
// 减去展示区宽度后,面包屑整体剩余的宽度
let breadcrumbRestWidth = totalWidth - displayWidth
// 计算剩余组哪些面包屑需要省略,哪些可以展示
for (const item of items) {
breadcrumbRestWidth -= item.width
item.omit = true
// 如果父容器剩余的空间能放得下剩下的面包屑,则结束循环
if (breadcrumbRestWidth <= parentRemindWidth) break
}
return [firstOne, ...items, lastTwo, lastOne]
}
calcItemWidth 的逻辑如下:
// 面包屑最小展示宽度
const MIN_DISPLAY_WIDTH = 80
/**
* items: 每个面包屑宽度的数据列表
* itemsWidth: 面包屑整体宽度
* parentWidth: 父容器宽度
*/
function calcItemWidth(items, itemsWidth, parentWidth) {
for (const item of items) {
// 计算面包屑能削减的宽度
const reduceWidth = item.width - MIN_DISPLAY_WIDTH
// 如果面包屑宽度小于最小宽度,忽略,跳过计算
if (reduceWidth <= 0) continue
// 计算此时面包屑列表与父容器相差的宽度
const gap = itemsWidth - parentEleWidth
// 如果列表与父容器相差的宽度比面包屑能削减的宽度大
if (gap > reduceWidth) {
// 添加 idealWidth 标记,组件内会根据该标记设置宽度样式
item.idealWidth = MIN_DISPLAY_WIDTH
itemsWidth -= reduceWidth
} else { // 否则将面包屑 idealWidth 设为能满足列表摆放的最小宽度并结束循环
item.idealWidth = item.width - gap
break
}
}
return items
}
面包屑组件的逻辑是:
根据传进来的 items 渲染一个 cloneList 列表,获取每个面包屑的实际宽度,再将数据传入 calcBreadcrumbDisplay 获取计算后的数据,最后展示计算后的面包屑并隐藏 cloneList
大致代码如下:
imoprt { useRef, useEffect, useState } from 'react'
function Breadcrumb({ items }) {
const breadcrumbRef = useRef(null)
const cloneList = useRef(null)
const [calcedBreadcrumbList, setCalcedBreadcrumbList] = useState([])
useEffect(() => {
// 展示 clone-list 列表
cloneListRef.current.style.display = 'block'
calcBreadcrumbList()
}, [items])
function calcBreadcrumbList() {
const parent = breadcrumbRef.current.parentElement
const children = cloneListRef.current.children
// 返回新数组,防止操作数据导致 items 被同步更改
const breadcrumbList = items.concat()
let breadcrumbItemDatas = []
for (let i = 0; i < children.length; i ++) {
breadcrumbItemDatas.push({ width: children[i].clientWidth })
}
breadcrumbItemDatas = calcBreadcrumb(breadcrumbItemDatas, parent.clientWidth)
// 如果有需要省略的面包屑
if (breadcrumbItemDatas.some(item => item.omit)) {
// 将需要省略的面包屑移进 breadcrumbList 的 omitList 里
}
setCalcedBreadcrumbList(breadcrumbList)
// 隐藏 clone-list 列表
cloneListRef.current.style.display = 'none'
}
return (
<div ref={breadcrumbRef}>
// 初次渲染时通过 clone-list 获取面包屑元素宽度信息
// 在计算结束后隐藏 clone-list 列表
<div className="clone-list" ref={cloneList}>
{items.map(item => (
<div className="item" key={item.key}>{item.name}</div>
))}
</div>
{calcedBreadcrumbList.map(item => {
if (Array.isArray(item.omitList)) {
// 展示省略按钮和下拉省略列表
}
// 否则直接展示
return (
<div
className="item"
key={item.key}
style={item.idealWidth && { width: idealWidth }}
>
{item.name}
</div>
)
})}
</div>
)
}
使用效果
npm 地址
对了,npm 地址 在这里,欢迎大家使用和反馈哦 😁