@blocklet/ui-react
Version:
Some useful front-end web components that can be used in Blocklets.
162 lines (142 loc) • 5.09 kB
JavaScript
export const mapRecursive = (array, fn, childrenKey = 'children') => {
return array.map((item) => {
if (Array.isArray(item[childrenKey])) {
return fn({
...item,
[childrenKey]: mapRecursive(item[childrenKey], fn, childrenKey),
});
}
return fn(item);
});
};
// 展平有层级结构的 array
export const flatRecursive = (array, childrenKey = 'children') => {
const result = [];
mapRecursive(array, (item) => result.push(item), childrenKey);
return result;
};
// 对有层级结构的 array 元素计数
export const countRecursive = (array, childrenKey = 'children') => {
let counter = 0;
mapRecursive(array, () => counter++, childrenKey);
return counter;
};
// 对有层级结构的 array 进行 filter 处理
// 因为是 DFS 遍历, 可以借助 context.filteredChildren 在过滤/保留子结的同时保持父子结构 (即使父结点不满足筛选条件)
export const filterRecursive = (array, predicate, childrenKey = 'children') => {
return array
.map((item) => ({ ...item }))
.filter((item) => {
const children = item[childrenKey];
if (Array.isArray(children)) {
const filtered = filterRecursive(children, predicate, childrenKey);
item[childrenKey] = filtered?.length ? filtered : undefined;
}
const context = { filteredChildren: item[childrenKey], isLeaf: !children?.length };
return predicate(item, context);
});
};
// "http://", "https://" 2 种情况
export const isUrl = (str) => {
return /^https?:\/\//.test(str);
};
// 链接是否是 mailto 协议
export const isMailProtocol = (str) => {
return /^mailto:/i.test(str.trim());
};
/**
* @description 检测是否是 Iconify 格式的字符串
* @deprecated
*/
export const isIconifyString = (str) => {
return /^[\w-]+:[\w-]+$/.test(str);
};
/**
* 检测 path 是否匹配当前 location, path 只考虑 "/" 开头的相对路径
*/
export const matchPath = (path) => {
if (!path || !path?.startsWith('/')) {
return false;
}
const ensureTrailingSlash = (str) => (str.endsWith('/') ? str : `${str}/`);
const pathname = ensureTrailingSlash(window.location.pathname);
const normalizedPath = ensureTrailingSlash(new URL(path, window.location.origin).pathname);
return pathname.startsWith(normalizedPath);
};
/**
* 从一组 paths 中, 找到匹配当前 location 的 path, 返回序号
*/
export const matchPaths = (paths = []) => {
const matched = paths.map((item, index) => ({ path: item, index })).filter((item) => matchPath(item.path));
if (!matched?.length) {
return -1;
}
// 多个 path 都匹配时, 取一个最具体 (最长的) path
const mostSpecific = matched.slice(1).reduce((prev, cur) => {
return prev.path.length >= cur.path.length ? prev : cur;
}, matched[0]);
return mostSpecific.index;
};
/** 导航列表分列 */
export const splitNavColumns = (items, options = {}) => {
const { columns = 1, breakInside = false, groupHeight = 48, itemHeight = 24, childrenKey = 'items' } = options;
// 分组数
const groups = items.length;
// 高度预估
const totalHeight = items.reduce((height, group) => {
return height + groupHeight + (group[childrenKey]?.length || 0) * itemHeight;
}, 0);
const targetHeight = Math.ceil(totalHeight / columns);
// 使用贪心策略进行分列
const result = [[]];
let currentColumn = 0;
let currentHeight = 0;
// 允许的高度偏差范围(有利于得到高度相差不大的列)
const heightVariance = targetHeight * 0.2;
// 是否应该分列
const shouldBreakColumn = (nextHeight) => {
return (
currentHeight > targetHeight - heightVariance &&
currentColumn < columns - 1 &&
currentHeight + nextHeight > targetHeight + heightVariance
);
};
items.forEach((group) => {
// 当前分组的预估高度
const groupTotalHeight = groupHeight + (group[childrenKey]?.length || 0) * itemHeight;
// 允许截断分组,可以在任何子项处换列,尽可能利用当前列的剩余空间
if (breakInside && shouldBreakColumn(groupHeight)) {
currentColumn++;
currentHeight = 0;
result[currentColumn] = [];
}
// 不允许截断分组,只能在分组边界换列,优先分列,列不够用再考虑充分利用剩余空间
if (!breakInside && currentHeight > 0 && (groups <= columns || shouldBreakColumn(groupTotalHeight))) {
currentColumn++;
currentHeight = 0;
result[currentColumn] = [];
}
// 添加分组标题
result[currentColumn].push({
...group,
group: true,
});
currentHeight += groupHeight;
// 添加子项
if (group[childrenKey]) {
group[childrenKey].forEach((child) => {
if (breakInside && shouldBreakColumn(itemHeight)) {
currentColumn++;
currentHeight = 0;
result[currentColumn] = [];
}
result[currentColumn].push({
...child,
group: false,
});
currentHeight += itemHeight;
});
}
});
return result;
};