shineout
Version:
Shein 前端组件库
326 lines (268 loc) • 8.51 kB
text/typescript
import deepEqual from 'deep-eql'
import shallowEqual from '../utils/shallowEqual'
import { CHANGE_TOPIC, ChangeType, WITH_OUT_DISPATCH } from './types'
import { ObjectType } from '../@types/common'
import { ListDatumOptions } from './Props'
export default class<Item, Value> {
distinct: ListDatumOptions<Item, Value>['distinct']
prediction: ListDatumOptions<Item, Value>['prediction']
onChange: ListDatumOptions<Item, Value>['onChange']
limit: ListDatumOptions<Item, Value>['limit']
separator?: ListDatumOptions<Item, Value>['separator']
$events: ObjectType<Function[]>
$cachedDisabled: ListDatumOptions<Item, Value>['disabled'] | {}
$cachedFlatten: Map<any, any>
valueMap: Map<any, boolean>
disabled: (...args: any) => boolean
format: (...args: any) => unknown
$values: any[]
$cachedValue: Value
updateLock: boolean
constructor(args: ListDatumOptions<Item, Value> = {}) {
const { format, onChange, separator, value, prediction, distinct, disabled, limit } = args
this.distinct = distinct
this.limit = limit
this.separator = separator
this.initFormat(format)
this.$events = {}
this.$cachedDisabled = {}
this.$cachedFlatten = new Map()
this.setDisabled(disabled)
if (prediction) this.prediction = prediction
this.setValue(value, WITH_OUT_DISPATCH)
this.onChange = onChange
}
get length() {
return this.$values.length
}
// should clean $cachedFlatten when data changed
cleanDataCache() {
this.$cachedFlatten = new Map()
}
get values() {
return this.$values
}
set values(values) {
this.$values = values
this.resetValueMap()
this.dispatch(CHANGE_TOPIC)
if (this.onChange) {
this.onChange(this.getValue())
}
}
resetValueMap() {
const map = new Map()
for (let i = 0; i < this.$values.length; i++) {
map.set(this.$values[i], true)
}
this.valueMap = map
}
setDisabled(disabled: ListDatumOptions<Item, Value>['disabled']) {
if (this.$cachedDisabled === disabled) return
this.$cachedDisabled = disabled
this.disabled = (data: Item, ...obj) => {
switch (typeof disabled) {
case 'boolean':
return disabled
case 'function':
return disabled(data, ...obj)
default:
return false
}
}
}
handleChange(values: any[], ...args: any) {
this.$values = values
this.resetValueMap()
this.dispatch(CHANGE_TOPIC)
if (this.onChange) {
this.onChange(this.getValue(), ...args)
}
}
flattenTreeData(data: Item[], childrenKey: keyof Item) {
const keys = data.map(v => this.format(v)).map(v => (typeof v === 'object' ? JSON.stringify(v) : v))
const key = keys.join()
if (keys.length !== 0) {
const cached = this.$cachedFlatten.get(key)
if (cached) return cached
}
const flatten: Item[] = []
const deepAdd = (items: Item[]) => {
items.forEach(item => {
const exist = flatten.find(raw =>
this.prediction ? this.prediction(raw as any, item) : this.format(raw) === this.format(item)
)
if (!exist) flatten.push(item)
if (item[childrenKey]) deepAdd((item[childrenKey] as unknown) as Item[])
})
}
deepAdd(data)
if (keys.length) this.$cachedFlatten.set(key, flatten)
return flatten
}
setLock(lock: boolean) {
this.updateLock = lock
}
add(data: Item | Item[], _?: any, childrenKey?: keyof Item, unshift?: boolean) {
if (data === undefined || data === null) return
// clear value
if (this.limit === 1) this.$values = []
this.resetValueMap()
let raws = Array.isArray(data) ? data : [data]
if (childrenKey && this.limit !== 1) {
raws = this.flattenTreeData(raws, childrenKey)
}
raws = raws.filter(v => {
const disabled = this.disabled(v)
if (disabled) return false
if (this.distinct) return !this.check(v)
return true
})
const values = []
for (const r of raws) {
const v = this.format(r)
if (v !== undefined) values.push(v)
}
this.handleChange(unshift ? values.concat(this.values) : this.values.concat(values), data, true)
}
set(value: Item) {
this.$values = []
this.resetValueMap()
this.add(value)
}
check(raw: Item) {
if (this.prediction) {
for (let i = 0, count = this.values.length; i < count; i++) {
if (this.prediction(this.values[i], raw)) return true
}
return false
}
return !!this.valueMap.get(this.format(raw))
}
getDataByValue(data: Item[], value: Value extends (infer U)[] ? U : Value) {
if (this.prediction) {
for (let i = 0, count = data.length; i < count; i++) {
if (this.prediction(value, data[i])) return data[i]
}
return null
}
return data.find(d => value === this.format(d))
}
clear() {
this.values = []
}
dispatch(name: string, ...args: any) {
const event = this.$events[name]
if (!event) return
event.forEach(fn => fn(...args))
}
initFormat(f: ListDatumOptions<Item, Value>['format']) {
switch (typeof f) {
case 'string':
this.format = value => value[f]
break
case 'function':
this.format = value => f(value)
break
default:
this.format = a => a
break
}
}
defaultPrediction(value: unknown, data: Item) {
return value === this.format(data)
}
remove(value: Item | Item[] | { IS_NOT_MATCHED_VALUE: boolean; value: any }, _?: unknown, childrenKey?: string) {
if (value === undefined || value === null) return
let raws: any = Array.isArray(value) ? value : [value]
if (childrenKey) {
raws = this.flattenTreeData(raws, childrenKey as any)
}
raws = raws.filter((r: any) => !this.disabled(r))
const values = []
if (!this.prediction) {
const rowValueMap = new Map()
for (let i = 0; i < raws.length; i++) {
if (raws[i].IS_NOT_MATCHED_VALUE) {
rowValueMap.set(raws[i].value, true)
} else {
rowValueMap.set(this.format(raws[i]), true)
}
}
for (let i = 0; i < this.values.length; i++) {
const val = this.values[i]
if (!rowValueMap.get(val)) {
values.push(val)
}
}
} else {
const { prediction } = this
outer: for (const val of this.values) {
for (let j = 0; j < raws.length; j++) {
if ((raws[j].IS_NOT_MATCHED_VALUE && val === raws[j].value) || prediction(val, raws[j])) {
raws.splice(j, 1)
continue outer
}
}
values.push(val)
}
}
// this.values = values
this.handleChange(values, value, false)
}
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)
}
getValue() {
let value: any = this.values
// eslint-disable-next-line
if (this.limit === 1) value = this.values[0]
else if (this.separator) value = this.values.join(this.separator)
this.$cachedValue = value
return value
}
resetValue(values: any[], cached: boolean) {
this.$values = values
this.resetValueMap()
if (this.onChange && !cached) {
this.onChange(this.getValue())
}
this.dispatch(CHANGE_TOPIC)
this.dispatch('set-value')
}
formatValue(values: Value | undefined) {
if (this.limit === 1 && !Array.isArray(values)) {
return [values]
}
if (!values) return []
if (Array.isArray(values)) {
return values
}
if (typeof values === 'string') {
if (this.separator) {
return values.split(this.separator).map(s => s.trim())
}
console.warn('Select separator parameter is empty.')
return [values]
}
console.error(new Error('Select values is not valid.'))
return []
}
setValue(values: Value = ([] as unknown) as Value, type?: ChangeType) {
if (deepEqual(values, this.$values)) return
if (type === WITH_OUT_DISPATCH) {
this.$values = this.formatValue(values)
this.resetValueMap()
} else {
this.resetValue(this.formatValue(values), shallowEqual(this.$cachedValue, values, {}))
}
this.$cachedValue = this.getValue()
}
}