UNPKG

zent

Version:

一套前端设计语言和基于React的实现

340 lines (292 loc) 7.06 kB
import { IPublicCascaderItem, ICascaderItem, CascaderValue } from './types'; import { isPathEqual } from './path-fns'; import { getNodeDepth } from './node-fns'; interface IBuildStackFrame<T> { node: T; children?: IPublicCascaderItem[]; } interface IInsertStackFrame<T> { parent: T | null; children: T[]; node?: IPublicCascaderItem; } interface IReduceNodeDfsFrame { node: ICascaderItem; phase: 'recurse' | 'visit'; } export function clone<T extends IPublicCascaderItem>( from: IPublicCascaderItem[], cloneNode: (node: IPublicCascaderItem, parent: T) => T ): T[] { const stack: IBuildStackFrame<T>[] = from.map(n => ({ node: cloneNode(n, null), children: n.children, })); const trees = stack.map(n => n.node); while (stack.length > 0) { const state = stack.pop(); if (!state) { continue; } const { node, children } = state; children?.forEach(n => { const m = cloneNode(n, node); stack.push({ node: m, children: n.children }); node.children.push(m); }); } return trees; } export function insertPath<T extends IPublicCascaderItem>( trees: T[], path: IPublicCascaderItem[], createNode: (node: IPublicCascaderItem, parent: T | null) => T ): T[] { path = path.slice(); const stack: IInsertStackFrame<T>[] = [ { parent: null, children: trees, node: path.shift(), }, ]; while (stack.length > 0) { const frame = stack.pop(); if (!frame) { continue; } const { children, node } = frame; // done if (!node) { break; } const nval = node.value; let matchedNode = children.find(n => n.value === nval); if (!matchedNode) { matchedNode = createNode(node, frame.parent); children.push(matchedNode); } stack.push({ parent: matchedNode, children: matchedNode.children as T[], node: path.shift(), }); } return trees; } /** * A forsest */ export class Forest { private trees: ICascaderItem[]; constructor(from: IPublicCascaderItem[]) { this.trees = this.build(from); } private build(from: IPublicCascaderItem[]) { return clone(from, createNode); } /** * Like Array.prototype.reduce but works on tree paths. */ reducePath<T>( callback: ( accumulator: T, path: ICascaderItem[], terminate: () => void ) => T, initialValue: T ): T { const stack = reverse(this.trees); const path = []; let acc = initialValue; let earlyExit = false; const terminate = () => { earlyExit = true; }; while (stack.length > 0) { const node = stack.pop(); if (!node) { continue; } const depth = getNodeDepth(node); while (depth <= path.length) { path.pop(); } path.push(node); if (node.children.length > 0) { reversePush(stack, node.children); } else { acc = callback(acc, path.slice(), terminate); if (earlyExit) { break; } } } return acc; } /** * Like Array.prototype.reduce but work on tree nodes. * * Nodes are reduced in pre-order. */ reduceNode<T>( callback: (accumulator: T, node: ICascaderItem, terminate: () => void) => T, initialValue: T ): T { const stack = reverse(this.trees); let acc = initialValue; let earlyExit = false; const terminate = () => { earlyExit = true; }; while (stack.length > 0) { const node = stack.pop(); if (!node) { continue; } acc = callback(acc, node, terminate); if (earlyExit) { break; } if (node.children.length > 0) { reversePush(stack, node.children); } } return acc; } /** * Same as reduceNode, but in post-order */ reduceNodeDfs<T>( callback: (accumulator: T, node: ICascaderItem, terminate: () => void) => T, initialValue: T ): T { const stack: IReduceNodeDfsFrame[] = this.trees.map(n => ({ node: n, phase: 'recurse', })); let acc = initialValue; let earlyExit = false; const terminate = () => { earlyExit = true; }; while (stack.length > 0) { const frame = stack.pop(); if (!frame) { continue; } const { node, phase } = frame; if (phase === 'recurse') { stack.push({ node, phase: 'visit', }); node.children.forEach(node => { stack.push({ node, phase: 'recurse', }); }); } else if (phase === 'visit') { acc = callback(acc, node, terminate); if (earlyExit) { break; } } } return acc; } /** * Sort paths using orders in `this.trees`. * * Does not mutate `paths`. */ sort(paths: Array<ICascaderItem[]>): Array<ICascaderItem[]> { return this.reducePath((acc, path) => { if (paths.some(x => isPathEqual(x, path))) { acc.push(path); } return acc; }, [] as ICascaderItem[][]); } clone(): Forest { return new Forest(this.trees); } /** * Insert a path into tree. * * Modifies tree in place. */ insertPath(path: IPublicCascaderItem[]): this { insertPath(this.trees, path, createNode); return this; } getTrees(): ICascaderItem[] { return this.trees; } /** * Find first matching path with values */ getPathByValue(values: CascaderValue[]): ICascaderItem[] { return this.reducePath<ICascaderItem[]>((found, path, terminate) => { if (values.length > path.length || values.length === 0) { return found; } const size = values.length; let i: number; for (i = 0; i < size; i++) { if (path[i].value !== values[i]) { return found; } } terminate(); return i === path.length ? path : path.slice(0, size); }, []); } /** * Returns all paths from root to leaf that contains `startNode` * * An optional `predicate` function can be used to filter results, * return `true` to keep it, `false` to drop it. */ getPaths( startNode: ICascaderItem, predicate?: (path: ICascaderItem[]) => boolean ) { const depth = getNodeDepth(startNode); const idx = depth - 1; const { value } = startNode; return this.reducePath((acc, path) => { // Paths may have different lengths if ( path.length > idx && path[idx].value === value && (!predicate || predicate(path)) ) { acc.push(path); } return acc; }, []); } } function reverse<T>(arr: T[]): T[] { const ret = arr.slice(); ret.reverse(); return ret; } function reversePush<T>(arr: T[], from: T[]): T[] { for (let i = from.length - 1; i >= 0; i--) { arr.push(from[i]); } return arr; } function createNode( node: IPublicCascaderItem, parent: ICascaderItem | null ): ICascaderItem { return { ...node, parent, children: [], }; }