UNPKG

shineout

Version:

Shein 前端组件库

395 lines (323 loc) 11 kB
import { CHANGE_TOPIC } from './types' import { KeygenResult, ObjectType, KeygenType, UnMatchedValue } from '../@types/common' const IS_NOT_MATCHED_VALUE = 'IS_NOT_MATCHED_VALUE' export const CheckedMode = { // 只返回全选数据,包含父节点和子节点 Full: 0, // 返回全部选择字节点和部分选中的父节点 Half: 1, // 只返回选中子节点 Child: 2, // 如果父节点下所有子节点全部选中,只返回父节点 Shallow: 3, // 所选即所得 Freedom: 4, } type CheckedStatus = 0 | 1 | 2 // check status stack const checkStatusStack = (stack: CheckedStatus[], defaultStatus: CheckedStatus) => { if (!stack || stack.length <= 0) return defaultStatus if (stack.filter(d => d === 0).length === stack.length) return 0 const s = stack.filter(d => d === 0 || d === 2) if (s.length <= 0) return defaultStatus return 2 } export type TreeModeType = 0 | 1 | 2 | 3 | 4 export interface TreePathType { children: KeygenResult[] path: (number | string)[] isDisabled: boolean indexPath: number[] index: number } type Value = KeygenResult[] export interface TreeDatumOptions<Item> { data?: Item[] keygen?: KeygenType<Item> | ((data: Item, parentKey: KeygenResult) => KeygenResult) value?: Value /** * @en mode 0: Returns only the fully selected node including the parent node. 1: Returns all selected nodes and semi-selected nodes. 2: Return only the selected child nodes. 3: If the parent node is full selected, only return the parent node. 4: What you choose is what you get. * @cn 选中值模式,未设置值为单选 0: 只返回完全选中的节点,包含父节点 1: 返回全部选中的节点和半选中的父节点 2: 只返回选中的子节点 3: 如果父节点选中,只返回父节点 4: 所选即所得 */ mode?: TreeModeType disabled?: ((data: Item, ...rest: any) => boolean) | boolean childrenKey: string unmatch?: boolean loader?: (key: KeygenResult, data: Item) => void onChange?: (value: any, ...rest: any) => void } export default class<Item> { keygen?: TreeDatumOptions<Item>['keygen'] mode: TreeDatumOptions<Item>['mode'] unmatch: TreeDatumOptions<Item>['unmatch'] disabled?: ((data: Item, ...rest: any) => boolean) childrenKey: TreeDatumOptions<Item>['childrenKey'] valueMap: Map<KeygenResult, CheckedStatus> unmatchedValueMap: Map<any, any> events: Map<KeygenResult, Function> $events: ObjectType<Function[]> value?: Value data?: Item[] cachedValue?: unknown[] pathMap: Map<KeygenResult, TreePathType> dataMap: Map<KeygenResult, Item> constructor(options: TreeDatumOptions<Item> = { data: [], childrenKey: '' }) { const { data, value, keygen, mode, disabled, childrenKey = 'children', unmatch } = options this.keygen = keygen this.mode = mode this.valueMap = new Map() this.unmatchedValueMap = new Map() this.unmatch = unmatch this.events = new Map() this.$events = {} this.childrenKey = childrenKey this.updateDisabled(disabled) this.setValue(value) this.setData(data) } updateDisabled(dis: TreeDatumOptions<Item>['disabled']) { if (typeof dis === 'function') { this.disabled = dis } else { this.disabled = () => false } } bind(id: number | string, update: Function) { this.events.set(id, update) } unbind(id: KeygenResult) { this.events.delete(id) } setUnmatedValue() { this.unmatchedValueMap = new Map() if (!this.value || !this.data) return this.value.forEach(v => { const data = this.getDataById(v) const unmatched = this.isUnMatch(data) if (unmatched) this.unmatchedValueMap.set(v, true) else this.unmatchedValueMap.delete(v) }) } // eslint-disable-next-line class-methods-use-this isUnMatch(data: ObjectType | null): data is UnMatchedValue { return data && data[IS_NOT_MATCHED_VALUE] } setValue(value?: Value) { this.value = value if (value && value !== this.cachedValue) { this.initValue() } this.setUnmatedValue() } getValue() { const value: KeygenResult[] = [] this.valueMap.forEach((checked, id) => { switch (this.mode) { case CheckedMode.Full: case CheckedMode.Freedom: if (checked === 1) value.push(id) break case CheckedMode.Half: if (checked >= 1) value.push(id) break case CheckedMode.Child: if (checked === 1) { const info = this.pathMap.get(id) if (info && info.children.length === 0) value.push(id) } break case CheckedMode.Shallow: if (checked === 1) { const parentChecked = (() => { const info = this.pathMap.get(id) if (!info) return false const { path } = info const pid = path[path.length - 1] if (!pid && pid !== 0) return false return this.valueMap.get(pid) === 1 })() if (!parentChecked) value.push(id) } break default: } }) this.unmatchedValueMap.forEach((unmatch, id) => { if (unmatch && this.unmatch) value.push(id) }) this.cachedValue = value return value as Value } setValueMap(id: KeygenResult, checked: CheckedStatus) { this.valueMap.set(id, checked) const update = this.events.get(id) if (update) update() } set(id: KeygenResult, checked: CheckedStatus, direction?: 'asc' | 'desc') { // self if (!this.isDisabled(id)) this.setValueMap(id, checked) const data = this.getDataById(id) if (data && (data as ObjectType)[IS_NOT_MATCHED_VALUE]) { if (checked) this.unmatchedValueMap.set(id, true) else this.unmatchedValueMap.delete(id) return null } if (CheckedMode.Freedom === this.mode) { // Free mode will return zero return 0 } const { path, children } = this.pathMap.get(id)! const childrenStack: any = [] // children if (direction !== 'asc') { children.forEach(cid => { // push status to stack childrenStack.push(this.set(cid, checked, 'desc')) }) } // Exclude disabled let current = this.valueMap.get(id)! // check all children status const status = checkStatusStack(childrenStack, current) if (status !== current) { this.setValueMap(id, status) current = status } // parent if (direction !== 'desc' && path.length > 0) { const parentId = path[path.length - 1] let parentChecked = current this.pathMap.get(parentId)!.children.forEach(cid => { if (parentChecked !== this.valueMap.get(cid)) { parentChecked = 2 } }) this.set(parentId, parentChecked, 'asc') } return current } isDisabled(id: KeygenResult) { const node = this.pathMap.get(id) if (node) return node.isDisabled return false } get(id: KeygenResult) { return this.valueMap.get(id) } getDataById(id: KeygenResult) { const oroginData = this.dataMap.get(id) if (oroginData) return oroginData if (!this.unmatch) return null return { [IS_NOT_MATCHED_VALUE]: true, value: id } } getPath(id: KeygenResult) { return this.pathMap.get(id) } getChecked(id: KeygenResult) { const value = this.get(id) let checked: boolean | 'indeterminate' = value === 1 if (value === 2) checked = 'indeterminate' return checked } getKey(data: Item, id: KeygenResult = '', index?: number): KeygenResult { if (typeof this.keygen === 'function') return this.keygen(data, id as number) if (this.keygen) return (data[this.keygen as keyof Item] as unknown) as KeygenResult return id + (id ? ',' : '') + index } initValue(ids?: KeygenResult[], forceCheck?: boolean) { if (!this.data || !this.value) return undefined if (!ids) { ids = [] this.pathMap.forEach((val, id) => { if (val.path.length === 0) ids!.push(id) }) } let checked: CheckedStatus ids.forEach(id => { const { children } = this.pathMap.get(id)! if (forceCheck) { this.setValueMap(id, 1) this.initValue(children, forceCheck) return } let childChecked: CheckedStatus = this.value!.indexOf(id) >= 0 ? 1 : 0 if (childChecked === 1 && this.mode !== CheckedMode.Half && this.mode !== CheckedMode.Freedom) { this.initValue(children, true) } else if (children.length > 0) { // 保持迭代 const res: CheckedStatus = this.initValue(children)! childChecked = this.mode === CheckedMode.Freedom ? childChecked : res } else { childChecked = this.value!.indexOf(id) >= 0 ? 1 : 0 } this.setValueMap(id, childChecked) if (checked === undefined) checked = childChecked else if (checked !== childChecked) checked = 2 }) return checked! } initData(data: Item[], path: KeygenResult[], disabled?: boolean, index: number[] = []) { const ids: KeygenResult[] = [] data.forEach((d, i) => { const id = this.getKey(d, path[path.length - 1], i) if (this.dataMap.get(id)) { console.error(`There is already a key "${id}" exists. The key must be unique.`) return } this.dataMap.set(id, d) let isDisabled = !!disabled if (!isDisabled && typeof this.disabled === 'function') { isDisabled = this.disabled(d, i) } const indexPath = [...index, i] ids.push(id) let children: KeygenResult[] = [] if (Array.isArray((d as any)[this.childrenKey])) { children = this.initData( (d as any)[this.childrenKey], [...path, id], // exclude Freedom this.mode === CheckedMode.Freedom ? disabled : isDisabled, indexPath ) } this.pathMap.set(id, { children, path, isDisabled, indexPath, index: i, }) }) return ids } setData(data?: Item[], dispatch?: boolean) { const prevValue: any[] = this.value || [] this.cachedValue = [] this.pathMap = new Map() this.dataMap = new Map() this.valueMap = new Map() this.unmatchedValueMap = new Map() this.data = data if (!data) return this.initData(data, []) this.initValue() this.setValue(prevValue as Value) if (dispatch) this.dispatch(CHANGE_TOPIC) } subscribe(name: string, fn: Function) { if (!this.$events[name]) this.$events[name] = [] const events = this.$events[name] if (events.includes(fn)) return events.push(fn) } unsubscribe(name: string, fn: Function) { if (!this.$events[name]) return this.$events[name] = this.$events[name].filter(e => e !== fn) } dispatch(name: string, ...args: any) { const event = this.$events[name] if (!event) return event.forEach(fn => fn(...args)) } }