eyzy-tree
Version:
React tree component
340 lines (268 loc) • 8.84 kB
text/typescript
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 = []
}
}