shineout
Version:
Shein 前端组件库
395 lines (323 loc) • 11 kB
text/typescript
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))
}
}