UNPKG

eyzy-tree

Version:
340 lines (268 loc) 8.84 kB
import { TreeNode } from '../types/Node' import { TreeComponent } from '../types/Tree' import { State } from '../types/State' import { Core, PromiseCallback, PromiseNodes, Resource, InsertOptions } from '../types/Core' import { callFetcher, isCallable, isString, remove, has, isLeaf, toArray } from '../utils' import { parseNode } from '../utils/parser' import { recurseDown, walkBreadth, FlatMap, flatMap } from '../utils/traveler' import { find } from '../utils/find' function parseOpts(opts?: InsertOptions): InsertOptions { try { return Object.assign({}, {loading: true}, opts) } catch(e) { return { loading: true } } } export default class CoreTree implements Core { private state: State private tree: TreeComponent constructor(tree: TreeComponent, state: State) { this.state = state this.tree = tree } flatMap = (collection: TreeNode[], ignoreCollapsed?: boolean): FlatMap => { return flatMap(collection, ignoreCollapsed) } find<T>(target: TreeNode[], multiple: boolean, query: any): T | null { return find(target, walkBreadth, multiple, query) } set = (node: TreeNode, key: string, value: any): void => { // TODO: selected, checked should be dublicated in the tree state this.state.set(node.id, key, value) this.tree.updateState() } updateKeys = (nodes: TreeNode[], targetNodes?: TreeNode[]): void => { const tree = this.tree const state = tree.getState() const checked = tree.checked const cascadeCheck: boolean = true !== tree.props.noCascade let lastSelected: string if (targetNodes) { targetNodes.forEach((node: TreeNode) => { if (node.selected) { lastSelected = node.id } }) } nodes.forEach((parentNode: TreeNode) => { const parentDepth: number = parentNode.depth || 0 recurseDown(parentNode, (obj: TreeNode, depth: number) => { if (obj.id !== parentNode.id) { obj.depth = parentDepth + depth } if (cascadeCheck && obj.parent && obj.parent.checked) { obj.checked = true } if (obj.checked && !has(checked, obj.id)) { checked.push(obj.id) } if (obj.selected) { tree.selected.push(obj.id) if (!lastSelected) { lastSelected = obj.id } } }) if (lastSelected) { tree.selected = tree.selected.filter((id: string) => { if (id !== lastSelected) { state.set(id, 'selected', false) } return id === lastSelected }) } if (cascadeCheck) { checked.forEach((id: string) => { const node: TreeNode | null = state.byId(id) if (node && isLeaf(node)) { tree.refreshDefinite(id, true, false) } }) } }) } clearKeys = (node: TreeNode, includeSelf: boolean = false): void => { const selected: string[] = this.tree.selected const checked: string[] = this.tree.checked const indeterminate: string[] = this.tree.indeterminate recurseDown(node, (child: TreeNode) => { if (child.selected) { remove(selected, child.id) } if (child.checked) { remove(checked, child.id) } remove(indeterminate, child.id) }, includeSelf) } load = (node: TreeNode, resource: Resource, showLoading?: boolean): PromiseNodes => { const result = callFetcher(node, resource) if (showLoading) { this.set(node, 'loading', true) } return result.then((items: any) => { this.state.set(node.id, { isBatch: false, loading: false }) return parseNode(items, node) }) } beside = (targetNode: TreeNode, resource: Resource, shift: number): PromiseNodes => { const insertIndex: number | null = this.state.getIndex(targetNode) if (null === insertIndex) { return Promise.resolve([]) } const parent: TreeNode | null = targetNode.parent const insert = (nodes: Resource) => { return this.insert(parent, nodes, { expand: parent ? parent.expanded : false, loading: false, index: insertIndex + shift }) } if (isCallable(resource)) { return this.load(targetNode, resource, false).then((nodes: TreeNode[]) => { return insert(nodes) }) } else { return insert(resource) } } insert = (parent: TreeNode | null, resource: Resource, opts: InsertOptions): PromiseNodes => { opts = parseOpts(opts) const tree = this.tree const state = tree.getState() const insert = (nodes: TreeNode[]) => { const index: number = undefined !== opts.index ? opts.index : (parent && parent.child ? parent.child.length : 0) const child = state.insertAt( parent, nodes, index ) if (parent) { const updatedItem = state.set(parent.id, { child }) if (updatedItem) { // it must be called before checking 'selectOnExpand' this.updateKeys([updatedItem], nodes) if (opts.expand && !updatedItem.expanded) { this.tree.expand(updatedItem) } } } else { this.updateKeys(nodes) } tree.$emit('Add', parent, nodes) tree.updateState() return nodes .map((node: TreeNode) => state.byId(node.id)) .filter(Boolean) as TreeNode[] } if (parent && isCallable(resource)) { return this.load(parent, resource as PromiseCallback, opts.loading).then(insert) } else { return Promise.resolve( insert(parseNode(resource)) ) } } remove = (node: TreeNode): TreeNode | null => { const tree = this.tree const id = node.id if (tree.props.checkable && node.checked) { this.state.set(id, 'checked', false) tree.checked = tree.checked.filter((checkedId: string) => id !== checkedId) tree.refreshDefinite(id, false, false) } const removedNode: TreeNode | null = this.state.remove(id) if (removedNode) { removedNode.parent = null this.clearKeys(removedNode) tree.updateState() tree.$emit('Remove', removedNode) } return removedNode } data = (node: TreeNode, key: any, value?: any): any => { if (!key && !value) { return node.data } if (undefined === value && isString(key)) { return node.data[key] } let data if (!isString(key)) { data = Object.assign({}, node.data, key) } else { node.data[key] = value data = node.data } this.state.set(node.id, 'data', data) this.tree.updateState() return node } hasClass = (node: TreeNode, className: string): boolean => { return !!node.className && new RegExp(className).test(node.className) } removeClass(node: TreeNode, classNames: string | string[]): TreeNode { const className: string = (node.className || "") .split(' ') .filter((klazz: string) => !has(toArray(classNames), klazz)) .join(' ') this.set(node, 'className', className) return node } addClass(node: TreeNode, classNames: string | string[]): TreeNode { const className: string[] = node.className ? node.className.split(' ') : [] toArray(classNames).forEach((klazz: string) => { if (!has(className, "" + klazz)) { className.push(klazz) } }) this.set(node, 'className', className.join(' ')) return node } uncheckAll = () => { const tree = this.tree if (!tree.props.checkable) { return } const state = tree.getState() const nodes: TreeNode[] | null = this.find( state.get(), true, [{ checked: true }, { indeterminate: true }] ) if (nodes) { nodes.forEach((node: TreeNode) => { this.tree.$emit('UnCheck', state.set(node.id, { checked: false, indeterminate: false })) }) } tree.updateState() tree.checked = [] tree.indeterminate = [] } unselectAll = () => { const tree = this.tree const state = tree.getState() const nodes: TreeNode[] | null = this.find(state.get(), true, { selected: true }) if (nodes) { nodes.forEach((node: TreeNode) => { this.tree.$emit('UnSelect', state.set(node.id, { selected: false })) }) } tree.updateState() tree.selected = [] } }