mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
* feat(components): [cascader] Hide radio in single mode * feat(components): [cascader] Renamed prop to \`selectOnClick\` * feat(components): [cascader] Update doc * feat(components): [cascader] Adjust how data is fetched * feat(components): [cascader] update type * Update packages/components/cascader-panel/src/node-content.ts Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> * feat(components): [cascader] Merge and fix v-model bug * feat(components): [cascader] Renamed to checkOnClickNode * feat(components): [cascader] Renamed to checkOnClickNode * feat(components): [cascader] merge * feat(components): [cascader] Add a check * feat(components): [cascader] Add test case * feat(components): [cascader] Fix type error * feat(components): [cascader] Add checkOnClickNode, showRadio props * feat(components): [cascader] Update doc * Update packages/components/cascader/src/cascader.ts Done, thanks! Co-authored-by: btea <2356281422@qq.com> * fix: showRadio defaults checkOnClickNode * test: showRadio fallback behavior * refactor: rename prop and support checkOnClickNode for multiple mode * doc:update * Update packages/components/cascader-panel/src/config.ts Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> * docs: tweak props * docs: update doc * chore: update doc & fix toggle check * Update docs/en-US/component/cascader.md Co-authored-by: btea <2356281422@qq.com> * Update docs/en-US/component/cascader.md Co-authored-by: btea <2356281422@qq.com> * Update docs/en-US/component/cascader.md Co-authored-by: btea <2356281422@qq.com> * Update cascader-panel.test.tsx --------- Co-authored-by: WANGXIAOYU1995 <109521682+WANGXIAOYU1995@users.noreply.github.com> Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> Co-authored-by: btea <2356281422@qq.com> Co-authored-by: Dsaquel <291874700n@gmail.com>
236 lines
5.8 KiB
TypeScript
236 lines
5.8 KiB
TypeScript
import { isArray, isEmpty, isFunction, isUndefined } from '@element-plus/utils'
|
|
|
|
import type { VNode } from 'vue'
|
|
|
|
export type CascaderNodeValue = string | number
|
|
export type CascaderNodePathValue = CascaderNodeValue[]
|
|
export type CascaderValue =
|
|
| CascaderNodeValue
|
|
| CascaderNodePathValue
|
|
| (CascaderNodeValue | CascaderNodePathValue)[]
|
|
export type CascaderConfig = Required<CascaderProps>
|
|
export type ExpandTrigger = 'click' | 'hover'
|
|
export type isDisabled = (data: CascaderOption, node: Node) => boolean
|
|
export type isLeaf = (data: CascaderOption, node: Node) => boolean
|
|
export type Resolve = (dataList?: CascaderOption[]) => void
|
|
export type LazyLoad = (node: Node, resolve: Resolve) => void
|
|
export interface RenderLabelProps {
|
|
node: Node
|
|
data: CascaderOption
|
|
}
|
|
export type RenderLabel = (props: RenderLabelProps) => VNode | VNode[]
|
|
export interface CascaderOption extends Record<string, unknown> {
|
|
label?: string
|
|
value?: CascaderNodeValue
|
|
children?: CascaderOption[]
|
|
disabled?: boolean
|
|
leaf?: boolean
|
|
}
|
|
|
|
export interface CascaderProps {
|
|
expandTrigger?: ExpandTrigger
|
|
multiple?: boolean
|
|
checkStrictly?: boolean
|
|
emitPath?: boolean
|
|
lazy?: boolean
|
|
lazyLoad?: LazyLoad
|
|
value?: string
|
|
label?: string
|
|
children?: string
|
|
disabled?: string | isDisabled
|
|
leaf?: string | isLeaf
|
|
hoverThreshold?: number
|
|
checkOnClickNode?: boolean
|
|
showPrefix?: boolean
|
|
}
|
|
|
|
export type Nullable<T> = null | T
|
|
|
|
type ChildrenData = CascaderOption[] | undefined
|
|
|
|
let uid = 0
|
|
|
|
const calculatePathNodes = (node: Node) => {
|
|
const nodes = [node]
|
|
let { parent } = node
|
|
|
|
while (parent) {
|
|
nodes.unshift(parent)
|
|
parent = parent.parent
|
|
}
|
|
|
|
return nodes
|
|
}
|
|
|
|
class Node {
|
|
readonly uid: number = uid++
|
|
readonly level: number
|
|
readonly value: CascaderNodeValue
|
|
readonly label: string
|
|
readonly pathNodes: Node[]
|
|
readonly pathValues: CascaderNodePathValue
|
|
readonly pathLabels: string[]
|
|
|
|
childrenData: ChildrenData
|
|
children: Node[]
|
|
text: string
|
|
loaded: boolean
|
|
/**
|
|
* Is it checked
|
|
*
|
|
* @default false
|
|
*/
|
|
checked = false
|
|
/**
|
|
* Used to indicate the intermediate state of unchecked and fully checked child nodes
|
|
*
|
|
* @default false
|
|
*/
|
|
indeterminate = false
|
|
/**
|
|
* Loading Status
|
|
*
|
|
* @default false
|
|
*/
|
|
loading = false
|
|
|
|
constructor(
|
|
readonly data: CascaderOption,
|
|
readonly config: CascaderConfig,
|
|
readonly parent?: Node,
|
|
readonly root = false
|
|
) {
|
|
const { value: valueKey, label: labelKey, children: childrenKey } = config
|
|
|
|
const childrenData = data[childrenKey] as ChildrenData
|
|
const pathNodes = calculatePathNodes(this)
|
|
|
|
this.level = root ? 0 : parent ? parent.level + 1 : 1
|
|
this.value = data[valueKey] as CascaderNodeValue
|
|
this.label = data[labelKey] as string
|
|
this.pathNodes = pathNodes
|
|
this.pathValues = pathNodes.map((node) => node.value)
|
|
this.pathLabels = pathNodes.map((node) => node.label)
|
|
this.childrenData = childrenData
|
|
this.children = (childrenData || []).map(
|
|
(child) => new Node(child, config, this)
|
|
)
|
|
this.loaded = !config.lazy || this.isLeaf || !isEmpty(childrenData)
|
|
this.text = ''
|
|
}
|
|
|
|
get isDisabled(): boolean {
|
|
const { data, parent, config } = this
|
|
const { disabled, checkStrictly } = config
|
|
const isDisabled = isFunction(disabled)
|
|
? disabled(data, this)
|
|
: !!data[disabled]
|
|
return isDisabled || (!checkStrictly && !!parent?.isDisabled)
|
|
}
|
|
|
|
get isLeaf(): boolean {
|
|
const { data, config, childrenData, loaded } = this
|
|
const { lazy, leaf } = config
|
|
const isLeaf = isFunction(leaf) ? leaf(data, this) : data[leaf]
|
|
|
|
return isUndefined(isLeaf)
|
|
? lazy && !loaded
|
|
? false
|
|
: !(isArray(childrenData) && childrenData.length)
|
|
: !!isLeaf
|
|
}
|
|
|
|
get valueByOption() {
|
|
return this.config.emitPath ? this.pathValues : this.value
|
|
}
|
|
|
|
appendChild(childData: CascaderOption) {
|
|
const { childrenData, children } = this
|
|
const node = new Node(childData, this.config, this)
|
|
|
|
if (isArray(childrenData)) {
|
|
childrenData.push(childData)
|
|
} else {
|
|
this.childrenData = [childData]
|
|
}
|
|
|
|
children.push(node)
|
|
|
|
return node
|
|
}
|
|
|
|
calcText(allLevels: boolean, separator: string) {
|
|
const text = allLevels ? this.pathLabels.join(separator) : this.label
|
|
this.text = text
|
|
return text
|
|
}
|
|
|
|
broadcast(checked: boolean) {
|
|
this.children.forEach((child) => {
|
|
if (child) {
|
|
// bottom up
|
|
child.broadcast(checked)
|
|
child.onParentCheck?.(checked)
|
|
}
|
|
})
|
|
}
|
|
|
|
emit() {
|
|
const { parent } = this
|
|
if (parent) {
|
|
parent.onChildCheck?.()
|
|
parent.emit()
|
|
}
|
|
}
|
|
|
|
onParentCheck(checked: boolean) {
|
|
if (!this.isDisabled) {
|
|
this.setCheckState(checked)
|
|
}
|
|
}
|
|
|
|
onChildCheck() {
|
|
const { children } = this
|
|
const validChildren = children.filter((child) => !child.isDisabled)
|
|
const checked = validChildren.length
|
|
? validChildren.every((child) => child.checked)
|
|
: false
|
|
|
|
this.setCheckState(checked)
|
|
}
|
|
|
|
setCheckState(checked: boolean) {
|
|
const totalNum = this.children.length
|
|
const checkedNum = this.children.reduce((c, p) => {
|
|
const num = p.checked ? 1 : p.indeterminate ? 0.5 : 0
|
|
return c + num
|
|
}, 0)
|
|
|
|
this.checked =
|
|
this.loaded &&
|
|
this.children
|
|
.filter((child) => !child.isDisabled)
|
|
.every((child) => child.loaded && child.checked) &&
|
|
checked
|
|
this.indeterminate =
|
|
this.loaded && checkedNum !== totalNum && checkedNum > 0
|
|
}
|
|
|
|
doCheck(checked: boolean) {
|
|
if (this.checked === checked) return
|
|
|
|
const { checkStrictly, multiple } = this.config
|
|
|
|
if (checkStrictly || !multiple) {
|
|
this.checked = checked
|
|
} else {
|
|
// bottom up to unify the calculation of the indeterminate state
|
|
this.broadcast(checked)
|
|
this.setCheckState(checked)
|
|
this.emit()
|
|
}
|
|
}
|
|
}
|
|
|
|
export default Node
|