@lumen-design/cascader 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Cascader.svelte +191 -0
- package/dist/index.js +3 -0
- package/dist/types.js +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import { createDropdownTransition } from '../../utils'
|
|
4
|
+
import Icon from '@lumen-design/icon'
|
|
5
|
+
import { ChevronRight, ChevronDown, X, Check } from 'lucide'
|
|
6
|
+
import type { CascaderOption, CascaderProps as Props } from './types'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
modelValue = $bindable([]),
|
|
10
|
+
options = [],
|
|
11
|
+
placeholder = '请选择',
|
|
12
|
+
disabled = false,
|
|
13
|
+
clearable = false,
|
|
14
|
+
separator = ' / ',
|
|
15
|
+
expandTrigger = 'click',
|
|
16
|
+
size = 'default',
|
|
17
|
+
showMenuArrow = true,
|
|
18
|
+
class: cls = '',
|
|
19
|
+
...attrs
|
|
20
|
+
}: Props = $props()
|
|
21
|
+
|
|
22
|
+
let visible = $state(false)
|
|
23
|
+
let reduceMotion = $state(false)
|
|
24
|
+
let inputRef: HTMLElement | null = $state(null)
|
|
25
|
+
let menus = $state<CascaderOption[][]>([])
|
|
26
|
+
let activeOptions = $state<CascaderOption[]>([])
|
|
27
|
+
|
|
28
|
+
$effect(() => {
|
|
29
|
+
menus = [options]
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const getLabelsFromValue = (values: (string | number)[], opts: CascaderOption[]): string[] => {
|
|
33
|
+
const labels: string[] = []
|
|
34
|
+
let current = opts
|
|
35
|
+
for (const val of values) {
|
|
36
|
+
const opt = current.find((o) => o.value === val)
|
|
37
|
+
if (opt) {
|
|
38
|
+
labels.push(opt.label)
|
|
39
|
+
current = opt.children || []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return labels
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const displayValue = $derived(activeOptions.length > 0 ? activeOptions.map((o) => o.label).join(separator) : modelValue.length > 0 ? getLabelsFromValue(modelValue, options).join(separator) : '')
|
|
46
|
+
|
|
47
|
+
const initMenus = (): void => {
|
|
48
|
+
menus = [options]
|
|
49
|
+
activeOptions = []
|
|
50
|
+
if (modelValue.length > 0) {
|
|
51
|
+
let current = options
|
|
52
|
+
for (const val of modelValue) {
|
|
53
|
+
const opt = current.find((o) => o.value === val)
|
|
54
|
+
if (opt) {
|
|
55
|
+
activeOptions = [...activeOptions, opt]
|
|
56
|
+
if (opt.children?.length) {
|
|
57
|
+
menus = [...menus, opt.children]
|
|
58
|
+
current = opt.children
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleToggle = (): void => {
|
|
66
|
+
if (disabled) return
|
|
67
|
+
visible = !visible
|
|
68
|
+
if (visible) initMenus()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handleOptionClick = (option: CascaderOption, level: number): void => {
|
|
72
|
+
if (option.disabled) return
|
|
73
|
+
activeOptions = [...activeOptions.slice(0, level), option]
|
|
74
|
+
menus = [...menus.slice(0, level + 1)]
|
|
75
|
+
if (option.children?.length) {
|
|
76
|
+
menus = [...menus, option.children]
|
|
77
|
+
} else {
|
|
78
|
+
modelValue = activeOptions.map((o) => o.value)
|
|
79
|
+
visible = false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleOptionHover = (option: CascaderOption, level: number): void => {
|
|
84
|
+
if (expandTrigger !== 'hover' || option.disabled) return
|
|
85
|
+
activeOptions = [...activeOptions.slice(0, level), option]
|
|
86
|
+
menus = [...menus.slice(0, level + 1)]
|
|
87
|
+
if (option.children?.length) menus = [...menus, option.children]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleClear = (e: MouseEvent): void => {
|
|
91
|
+
e.stopPropagation()
|
|
92
|
+
modelValue = []
|
|
93
|
+
activeOptions = []
|
|
94
|
+
menus = [options]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleClickOutside = (e: MouseEvent): void => {
|
|
98
|
+
if (inputRef && !inputRef.contains(e.target as Node)) visible = false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const handleKeydown = (e: KeyboardEvent): void => {
|
|
102
|
+
if (e.key === 'Escape') visible = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const dropdownTransition = $derived(createDropdownTransition(reduceMotion))
|
|
106
|
+
|
|
107
|
+
$effect(() => {
|
|
108
|
+
if (visible) document.addEventListener('click', handleClickOutside)
|
|
109
|
+
return () => document.removeEventListener('click', handleClickOutside)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
$effect(() => {
|
|
113
|
+
if (typeof window !== 'undefined') {
|
|
114
|
+
reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
onMount(() => {
|
|
119
|
+
if (typeof window !== 'undefined') {
|
|
120
|
+
reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const isActive = (option: CascaderOption, level: number): boolean => activeOptions[level]?.value === option.value
|
|
125
|
+
|
|
126
|
+
const classes = $derived(['lm-cascader', disabled && 'is-disabled', size !== 'default' && `lm-cascader--${size}`, cls].filter(Boolean).join(' '))
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<div bind:this={inputRef} class={classes} {...attrs}>
|
|
130
|
+
<div
|
|
131
|
+
class="lm-cascader__input"
|
|
132
|
+
class:is-focus={visible}
|
|
133
|
+
role="combobox"
|
|
134
|
+
tabindex={disabled ? -1 : 0}
|
|
135
|
+
aria-expanded={visible}
|
|
136
|
+
aria-controls="cascader-dropdown"
|
|
137
|
+
aria-haspopup="listbox"
|
|
138
|
+
onclick={handleToggle}
|
|
139
|
+
onkeydown={handleKeydown}
|
|
140
|
+
>
|
|
141
|
+
<span class="lm-cascader__value" class:is-placeholder={!displayValue}>
|
|
142
|
+
{displayValue || placeholder}
|
|
143
|
+
</span>
|
|
144
|
+
{#if clearable && modelValue.length > 0 && !disabled}
|
|
145
|
+
<button type="button" class="lm-cascader__clear" onclick={handleClear} aria-label="清除">
|
|
146
|
+
<Icon icon={X} size={14} />
|
|
147
|
+
</button>
|
|
148
|
+
{:else}
|
|
149
|
+
<span class="lm-cascader__arrow" class:is-open={visible}>
|
|
150
|
+
<Icon icon={ChevronDown} size={14} />
|
|
151
|
+
</span>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{#if visible}
|
|
156
|
+
<div
|
|
157
|
+
class="lm-cascader__dropdown"
|
|
158
|
+
role="listbox"
|
|
159
|
+
id="cascader-dropdown"
|
|
160
|
+
transition:dropdownTransition
|
|
161
|
+
>
|
|
162
|
+
{#if showMenuArrow}
|
|
163
|
+
<div class="lm-cascader__menu-arrow" aria-hidden="true"></div>
|
|
164
|
+
{/if}
|
|
165
|
+
{#each menus as menu, level}
|
|
166
|
+
<ul class="lm-cascader__menu" role="group">
|
|
167
|
+
{#each menu as option (option.value)}
|
|
168
|
+
<li
|
|
169
|
+
class="lm-cascader__option"
|
|
170
|
+
class:is-active={isActive(option, level)}
|
|
171
|
+
class:is-disabled={option.disabled}
|
|
172
|
+
role="option"
|
|
173
|
+
aria-selected={isActive(option, level)}
|
|
174
|
+
onmousedown={() => handleOptionClick(option, level)}
|
|
175
|
+
onmouseenter={() => handleOptionHover(option, level)}
|
|
176
|
+
>
|
|
177
|
+
<span class="lm-cascader__option-label">{option.label}</span>
|
|
178
|
+
<span class="lm-cascader__option-suffix" aria-hidden="true">
|
|
179
|
+
{#if option.children?.length}
|
|
180
|
+
<Icon icon={ChevronRight} size={14} />
|
|
181
|
+
{:else if isActive(option, level) && level === menus.length - 1}
|
|
182
|
+
<Icon icon={Check} size={14} />
|
|
183
|
+
{/if}
|
|
184
|
+
</span>
|
|
185
|
+
</li>
|
|
186
|
+
{/each}
|
|
187
|
+
</ul>
|
|
188
|
+
{/each}
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
</div>
|
package/dist/index.js
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lumen-design/cascader",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Cascader component for Lumen UI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"svelte": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "svelte-package -i src -o dist --types",
|
|
21
|
+
"build:watch": "svelte-package -i src -o dist --types -w"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@lumen-design/icon": "0.0.2",
|
|
25
|
+
"lucide": "^0.563.0",
|
|
26
|
+
"@lumen-design/utils": "0.0.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@sveltejs/package": "^2.5.7",
|
|
30
|
+
"svelte": "5.48.2"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"svelte": "^5.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|