@duskmoon-dev/el-cascader 0.5.0

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.
@@ -0,0 +1,1056 @@
1
+ // src/el-dm-cascader.ts
2
+ import { BaseElement, css } from "@duskmoon-dev/el-core";
3
+ var chevronDownIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`;
4
+ var chevronRightIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`;
5
+ var checkIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`;
6
+ var closeIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`;
7
+ var searchIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`;
8
+ var loadingIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="spinner"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>`;
9
+ var styles = css`
10
+ :host {
11
+ display: inline-block;
12
+ width: 100%;
13
+ }
14
+
15
+ .cascader {
16
+ position: relative;
17
+ width: 100%;
18
+ }
19
+
20
+ /* Trigger Button */
21
+ .cascader-trigger {
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 0.5rem;
25
+ width: 100%;
26
+ min-height: 2.75rem;
27
+ padding: 0.5rem 0.75rem;
28
+ font-size: var(--font-size-md, 1rem);
29
+ line-height: 1.5;
30
+ color: var(--color-on-surface);
31
+ background-color: var(--color-surface);
32
+ border: 1px solid var(--color-outline);
33
+ border-radius: var(--radius-md, 0.5rem);
34
+ cursor: pointer;
35
+ transition: border-color 150ms ease, box-shadow 150ms ease;
36
+ }
37
+
38
+ .cascader-trigger:hover:not(:disabled) {
39
+ border-color: var(--color-on-surface-variant);
40
+ }
41
+
42
+ .cascader-trigger:focus {
43
+ outline: none;
44
+ border-color: var(--color-primary);
45
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-primary) 15%, transparent);
46
+ }
47
+
48
+ .cascader-trigger:disabled {
49
+ cursor: not-allowed;
50
+ opacity: 0.5;
51
+ background-color: var(--color-surface-container);
52
+ }
53
+
54
+ /* Value Display */
55
+ .cascader-value {
56
+ flex: 1;
57
+ overflow: hidden;
58
+ text-overflow: ellipsis;
59
+ white-space: nowrap;
60
+ text-align: left;
61
+ }
62
+
63
+ .cascader-placeholder {
64
+ color: var(--color-on-surface-variant);
65
+ opacity: 0.7;
66
+ }
67
+
68
+ /* Tags Container (for multiple mode) */
69
+ .cascader-tags {
70
+ display: flex;
71
+ flex-wrap: wrap;
72
+ gap: 0.25rem;
73
+ flex: 1;
74
+ min-width: 0;
75
+ }
76
+
77
+ .cascader-tag {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: 0.25rem;
81
+ max-width: 100%;
82
+ padding: 0.125rem 0.25rem 0.125rem 0.5rem;
83
+ font-size: var(--font-size-sm, 0.875rem);
84
+ line-height: 1.25rem;
85
+ background-color: var(--color-surface-container-high, #e8e8e8);
86
+ color: var(--color-on-surface);
87
+ border-radius: 1rem;
88
+ }
89
+
90
+ .cascader-tag-text {
91
+ overflow: hidden;
92
+ text-overflow: ellipsis;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ .cascader-tag-remove {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ width: 16px;
101
+ height: 16px;
102
+ padding: 0;
103
+ color: inherit;
104
+ background-color: transparent;
105
+ border-radius: 50%;
106
+ cursor: pointer;
107
+ opacity: 0.7;
108
+ transition: opacity 150ms ease, background-color 150ms ease;
109
+ }
110
+
111
+ .cascader-tag-remove svg {
112
+ width: 10px;
113
+ height: 10px;
114
+ display: block;
115
+ }
116
+
117
+ .cascader-tag-remove:hover {
118
+ opacity: 1;
119
+ background-color: color-mix(in oklch, currentColor 15%, transparent);
120
+ }
121
+
122
+ .cascader-tag-overflow {
123
+ padding: 0.125rem 0.5rem;
124
+ background-color: var(--color-surface-container);
125
+ color: var(--color-on-surface-variant);
126
+ }
127
+
128
+ /* Icons */
129
+ .cascader-arrow {
130
+ display: inline-flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ width: 20px;
134
+ height: 20px;
135
+ flex-shrink: 0;
136
+ color: var(--color-on-surface-variant);
137
+ transition: transform 150ms ease;
138
+ }
139
+
140
+ .cascader-arrow svg {
141
+ width: 16px;
142
+ height: 16px;
143
+ display: block;
144
+ }
145
+
146
+ .cascader.open .cascader-arrow {
147
+ transform: rotate(180deg);
148
+ }
149
+
150
+ .cascader-clear {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ width: 20px;
155
+ height: 20px;
156
+ padding: 0;
157
+ color: var(--color-on-surface-variant);
158
+ background-color: transparent;
159
+ border-radius: 50%;
160
+ cursor: pointer;
161
+ flex-shrink: 0;
162
+ transition: background-color 150ms ease;
163
+ }
164
+
165
+ .cascader-clear svg {
166
+ width: 14px;
167
+ height: 14px;
168
+ display: block;
169
+ }
170
+
171
+ .cascader-clear:hover {
172
+ background-color: var(--color-surface-container-high);
173
+ }
174
+
175
+ /* Dropdown - uses Popover API (top-layer requires position: fixed) */
176
+ .cascader-dropdown {
177
+ position: fixed;
178
+ margin: 0;
179
+ padding: 0;
180
+ border: 1px solid var(--color-outline-variant);
181
+ border-radius: var(--radius-md, 0.5rem);
182
+ background-color: var(--color-surface);
183
+ box-shadow: var(--shadow-md, 0 4px 6px -1px rgba(0, 0, 0, 0.1));
184
+ overflow: hidden;
185
+ display: none;
186
+ flex-direction: column;
187
+ z-index: 1000;
188
+ }
189
+
190
+ .cascader-dropdown:popover-open {
191
+ display: flex;
192
+ }
193
+
194
+ /* Search */
195
+ .cascader-search {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 0.5rem;
199
+ padding: 0.5rem;
200
+ border-bottom: 1px solid var(--color-outline-variant);
201
+ }
202
+
203
+ .cascader-search-icon {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ width: 16px;
208
+ height: 16px;
209
+ color: var(--color-on-surface-variant);
210
+ flex-shrink: 0;
211
+ }
212
+
213
+ .cascader-search-icon svg {
214
+ width: 14px;
215
+ height: 14px;
216
+ display: block;
217
+ }
218
+
219
+ .cascader-search-input {
220
+ flex: 1;
221
+ padding: 0.375rem 0.5rem;
222
+ font-size: var(--font-size-sm, 0.875rem);
223
+ color: var(--color-on-surface);
224
+ background-color: var(--color-surface-container);
225
+ border: none;
226
+ border-radius: var(--radius-sm, 0.25rem);
227
+ outline: none;
228
+ }
229
+
230
+ .cascader-search-input:focus {
231
+ background-color: var(--color-surface-container-high);
232
+ }
233
+
234
+ .cascader-search-input::placeholder {
235
+ color: var(--color-on-surface-variant);
236
+ opacity: 0.7;
237
+ }
238
+
239
+ /* Panels Container */
240
+ .cascader-panels {
241
+ display: flex;
242
+ max-height: 18rem;
243
+ }
244
+
245
+ /* Panel */
246
+ .cascader-panel {
247
+ display: flex;
248
+ flex-direction: column;
249
+ min-width: 10rem;
250
+ max-width: 14rem;
251
+ max-height: 18rem;
252
+ overflow-y: auto;
253
+ border-right: 1px solid var(--color-outline-variant);
254
+ }
255
+
256
+ .cascader-panel:last-child {
257
+ border-right: none;
258
+ }
259
+
260
+ .cascader-panel-options {
261
+ padding: 0.25rem;
262
+ }
263
+
264
+ /* Option */
265
+ .cascader-option {
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 0.5rem;
269
+ width: 100%;
270
+ padding: 0.5rem 0.75rem;
271
+ font-size: var(--font-size-sm, 0.875rem);
272
+ color: var(--color-on-surface);
273
+ background-color: transparent;
274
+ border: none;
275
+ border-radius: var(--radius-sm, 0.25rem);
276
+ cursor: pointer;
277
+ text-align: left;
278
+ transition: background-color 150ms ease;
279
+ }
280
+
281
+ .cascader-option:hover:not(.disabled) {
282
+ background-color: var(--color-surface-container);
283
+ }
284
+
285
+ .cascader-option.active {
286
+ background-color: var(--color-surface-container-high);
287
+ }
288
+
289
+ .cascader-option.selected {
290
+ background-color: var(--color-primary-container, #e8def8);
291
+ color: var(--color-on-primary-container, #1d1b20);
292
+ }
293
+
294
+ .cascader-option.disabled {
295
+ opacity: 0.5;
296
+ cursor: not-allowed;
297
+ }
298
+
299
+ .cascader-option-checkbox {
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ width: 1rem;
304
+ height: 1rem;
305
+ background-color: transparent;
306
+ border: 2px solid var(--color-on-surface-variant);
307
+ border-radius: 0.125rem;
308
+ flex-shrink: 0;
309
+ }
310
+
311
+ .cascader-option.selected .cascader-option-checkbox {
312
+ background-color: var(--color-primary);
313
+ border-color: var(--color-primary);
314
+ color: var(--color-on-primary, white);
315
+ }
316
+
317
+ .cascader-option-label {
318
+ flex: 1;
319
+ overflow: hidden;
320
+ text-overflow: ellipsis;
321
+ white-space: nowrap;
322
+ }
323
+
324
+ .cascader-option-arrow {
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ color: var(--color-on-surface-variant);
329
+ flex-shrink: 0;
330
+ }
331
+
332
+ .cascader-option-loading {
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ flex-shrink: 0;
337
+ }
338
+
339
+ .cascader-option-loading .spinner {
340
+ animation: spin 1s linear infinite;
341
+ }
342
+
343
+ @keyframes spin {
344
+ from { transform: rotate(0deg); }
345
+ to { transform: rotate(360deg); }
346
+ }
347
+
348
+ /* Search Results */
349
+ .cascader-search-results {
350
+ padding: 0.25rem;
351
+ max-height: 18rem;
352
+ overflow-y: auto;
353
+ }
354
+
355
+ .cascader-search-result {
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 0.5rem;
359
+ width: 100%;
360
+ padding: 0.5rem 0.75rem;
361
+ font-size: var(--font-size-sm, 0.875rem);
362
+ color: var(--color-on-surface);
363
+ background-color: transparent;
364
+ border: none;
365
+ border-radius: var(--radius-sm, 0.25rem);
366
+ cursor: pointer;
367
+ text-align: left;
368
+ transition: background-color 150ms ease;
369
+ }
370
+
371
+ .cascader-search-result:hover {
372
+ background-color: var(--color-surface-container);
373
+ }
374
+
375
+ .cascader-search-result.selected {
376
+ background-color: var(--color-primary-container, #e8def8);
377
+ color: var(--color-on-primary-container, #1d1b20);
378
+ }
379
+
380
+ .cascader-search-result-path {
381
+ flex: 1;
382
+ overflow: hidden;
383
+ text-overflow: ellipsis;
384
+ white-space: nowrap;
385
+ }
386
+
387
+ .cascader-search-result-separator {
388
+ color: var(--color-on-surface-variant);
389
+ margin: 0 0.25rem;
390
+ }
391
+
392
+ /* Empty State */
393
+ .cascader-empty {
394
+ padding: 1.5rem;
395
+ text-align: center;
396
+ color: var(--color-on-surface-variant);
397
+ font-size: var(--font-size-sm, 0.875rem);
398
+ }
399
+
400
+ /* Size Variants */
401
+ :host([size="sm"]) .cascader-trigger {
402
+ min-height: 2.25rem;
403
+ padding: 0.375rem 0.5rem;
404
+ font-size: var(--font-size-sm, 0.875rem);
405
+ border-radius: var(--radius-sm, 0.375rem);
406
+ }
407
+
408
+ :host([size="lg"]) .cascader-trigger {
409
+ min-height: 3.25rem;
410
+ padding: 0.625rem 1rem;
411
+ font-size: var(--font-size-lg, 1.125rem);
412
+ border-radius: var(--radius-lg, 0.625rem);
413
+ }
414
+
415
+ /* Validation States */
416
+ :host([validation-state="invalid"]) .cascader-trigger {
417
+ border-color: var(--color-error);
418
+ }
419
+
420
+ :host([validation-state="invalid"]) .cascader-trigger:focus {
421
+ border-color: var(--color-error);
422
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-error) 15%, transparent);
423
+ }
424
+
425
+ :host([validation-state="valid"]) .cascader-trigger {
426
+ border-color: var(--color-success);
427
+ }
428
+
429
+ /* Disabled State */
430
+ :host([disabled]) {
431
+ pointer-events: none;
432
+ }
433
+
434
+ :host([disabled]) .cascader-trigger {
435
+ cursor: not-allowed;
436
+ opacity: 0.5;
437
+ background-color: var(--color-surface-container);
438
+ }
439
+ `;
440
+
441
+ class ElDmCascader extends BaseElement {
442
+ static properties = {
443
+ value: { type: String, reflect: true, default: "" },
444
+ placeholder: { type: String, default: "Select..." },
445
+ disabled: { type: Boolean, reflect: true, default: false },
446
+ multiple: { type: Boolean, reflect: true, default: false },
447
+ searchable: { type: Boolean, reflect: true, default: false },
448
+ clearable: { type: Boolean, reflect: true, default: false },
449
+ changeOnSelect: { type: Boolean, reflect: true, attribute: "change-on-select", default: false },
450
+ expandTrigger: { type: String, attribute: "expand-trigger", default: "click" },
451
+ separator: { type: String, default: " / " },
452
+ showAllLevels: { type: Boolean, reflect: true, attribute: "show-all-levels", default: true },
453
+ showCheckedStrategy: { type: String, attribute: "show-checked-strategy", default: "all" },
454
+ size: { type: String, reflect: true, default: "md" },
455
+ validationState: { type: String, reflect: true, attribute: "validation-state" },
456
+ options: { type: String, default: "" }
457
+ };
458
+ _isOpen = false;
459
+ _searchValue = "";
460
+ _activePath = [];
461
+ _selectedPaths = [];
462
+ _loadingKeys = new Set;
463
+ _options = [];
464
+ _loadDataFn = null;
465
+ _handleOutsideClick = this._onOutsideClick.bind(this);
466
+ _handleKeyDown = this._onKeyDown.bind(this);
467
+ _handleScroll = this._onScroll.bind(this);
468
+ _handleResize = this._onResize.bind(this);
469
+ constructor() {
470
+ super();
471
+ this.attachStyles(styles);
472
+ }
473
+ connectedCallback() {
474
+ super.connectedCallback();
475
+ document.addEventListener("click", this._handleOutsideClick);
476
+ document.addEventListener("keydown", this._handleKeyDown);
477
+ this._parseOptionsFromAttribute();
478
+ this._parseValue();
479
+ this._setupEventDelegation();
480
+ }
481
+ _parseOptionsFromAttribute() {
482
+ if (this.options) {
483
+ try {
484
+ const parsed = JSON.parse(this.options);
485
+ if (Array.isArray(parsed)) {
486
+ this._options = parsed;
487
+ }
488
+ } catch {}
489
+ }
490
+ }
491
+ disconnectedCallback() {
492
+ super.disconnectedCallback();
493
+ document.removeEventListener("click", this._handleOutsideClick);
494
+ document.removeEventListener("keydown", this._handleKeyDown);
495
+ this._removeScrollListeners();
496
+ }
497
+ _addScrollListeners() {
498
+ window.addEventListener("scroll", this._handleScroll, true);
499
+ window.addEventListener("resize", this._handleResize);
500
+ }
501
+ _removeScrollListeners() {
502
+ window.removeEventListener("scroll", this._handleScroll, true);
503
+ window.removeEventListener("resize", this._handleResize);
504
+ }
505
+ _onScroll() {
506
+ if (this._isOpen) {
507
+ const dropdown = this.shadowRoot?.querySelector(".cascader-dropdown");
508
+ const trigger = this.shadowRoot?.querySelector(".cascader-trigger");
509
+ if (dropdown && trigger) {
510
+ this._positionDropdown(dropdown, trigger);
511
+ }
512
+ }
513
+ }
514
+ _onResize() {
515
+ if (this._isOpen) {
516
+ this._close();
517
+ }
518
+ }
519
+ setOptions(options) {
520
+ this._options = options;
521
+ this.update();
522
+ }
523
+ setLoadData(fn) {
524
+ this._loadDataFn = fn;
525
+ }
526
+ _parseValue() {
527
+ if (!this.value) {
528
+ this._selectedPaths = [];
529
+ return;
530
+ }
531
+ try {
532
+ const parsed = JSON.parse(this.value);
533
+ if (this.multiple) {
534
+ this._selectedPaths = Array.isArray(parsed[0]) ? parsed : [parsed];
535
+ } else {
536
+ this._selectedPaths = Array.isArray(parsed) ? [parsed] : [];
537
+ }
538
+ } catch {
539
+ this._selectedPaths = [];
540
+ }
541
+ }
542
+ _getPanels() {
543
+ const panels = [this._options];
544
+ let currentOptions = this._options;
545
+ for (const value of this._activePath) {
546
+ const option = currentOptions.find((o) => o.value === value);
547
+ if (option?.children && option.children.length > 0) {
548
+ panels.push(option.children);
549
+ currentOptions = option.children;
550
+ } else {
551
+ break;
552
+ }
553
+ }
554
+ return panels;
555
+ }
556
+ _getDisplayLabel() {
557
+ if (this._selectedPaths.length === 0) {
558
+ return "";
559
+ }
560
+ const path = this._selectedPaths[0];
561
+ const labels = this._getPathLabels(path);
562
+ if (this.showAllLevels) {
563
+ return labels.join(this.separator);
564
+ }
565
+ return labels[labels.length - 1] || "";
566
+ }
567
+ _getPathLabels(path) {
568
+ const labels = [];
569
+ let currentOptions = this._options;
570
+ for (const value of path) {
571
+ const option = currentOptions.find((o) => o.value === value);
572
+ if (option) {
573
+ labels.push(option.label);
574
+ currentOptions = option.children || [];
575
+ }
576
+ }
577
+ return labels;
578
+ }
579
+ _findOptionByPath(path) {
580
+ let currentOptions = this._options;
581
+ let option;
582
+ for (const value of path) {
583
+ option = currentOptions.find((o) => o.value === value);
584
+ if (option?.children) {
585
+ currentOptions = option.children;
586
+ }
587
+ }
588
+ return option;
589
+ }
590
+ _isLeaf(option) {
591
+ if (option.leaf === true)
592
+ return true;
593
+ if (option.leaf === false)
594
+ return false;
595
+ return !option.children || option.children.length === 0;
596
+ }
597
+ _searchOptions() {
598
+ const results = [];
599
+ const search = this._searchValue.toLowerCase();
600
+ const searchRecursive = (options, path, pathValues) => {
601
+ for (const option of options) {
602
+ const newPath = [...path, option];
603
+ const newPathValues = [...pathValues, option.value];
604
+ if (option.label.toLowerCase().includes(search)) {
605
+ if (this._isLeaf(option) || this.changeOnSelect) {
606
+ results.push({
607
+ path: newPath,
608
+ pathLabels: newPath.map((o) => o.label),
609
+ pathValues: newPathValues
610
+ });
611
+ }
612
+ }
613
+ if (option.children) {
614
+ searchRecursive(option.children, newPath, newPathValues);
615
+ }
616
+ }
617
+ };
618
+ searchRecursive(this._options, [], []);
619
+ return results;
620
+ }
621
+ _onOutsideClick(e) {
622
+ if (!this.contains(e.target)) {
623
+ this._close();
624
+ }
625
+ }
626
+ _onKeyDown(e) {
627
+ if (!this._isOpen) {
628
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
629
+ if (document.activeElement === this || this.contains(document.activeElement)) {
630
+ e.preventDefault();
631
+ this._open();
632
+ }
633
+ }
634
+ return;
635
+ }
636
+ switch (e.key) {
637
+ case "Escape":
638
+ e.preventDefault();
639
+ this._close();
640
+ break;
641
+ case "ArrowLeft":
642
+ e.preventDefault();
643
+ if (this._activePath.length > 0) {
644
+ this._activePath = this._activePath.slice(0, -1);
645
+ this.update();
646
+ }
647
+ break;
648
+ case "ArrowRight":
649
+ break;
650
+ }
651
+ }
652
+ _open() {
653
+ if (this.disabled)
654
+ return;
655
+ this._isOpen = true;
656
+ this._activePath = this._selectedPaths[0] ? [...this._selectedPaths[0]] : [];
657
+ this.update();
658
+ this._addScrollListeners();
659
+ requestAnimationFrame(() => {
660
+ const dropdown = this.shadowRoot?.querySelector(".cascader-dropdown");
661
+ const trigger = this.shadowRoot?.querySelector(".cascader-trigger");
662
+ if (dropdown && trigger) {
663
+ const triggerRect = trigger.getBoundingClientRect();
664
+ dropdown.style.top = `${triggerRect.bottom + 4}px`;
665
+ dropdown.style.left = `${triggerRect.left}px`;
666
+ dropdown.style.minWidth = `${triggerRect.width}px`;
667
+ dropdown.showPopover();
668
+ requestAnimationFrame(() => {
669
+ this._positionDropdown(dropdown, trigger);
670
+ });
671
+ }
672
+ const searchInput = this.shadowRoot?.querySelector(".cascader-search-input");
673
+ if (searchInput) {
674
+ searchInput.focus();
675
+ }
676
+ });
677
+ }
678
+ _close() {
679
+ this._isOpen = false;
680
+ this._searchValue = "";
681
+ this._activePath = [];
682
+ this._removeScrollListeners();
683
+ const dropdown = this.shadowRoot?.querySelector(".cascader-dropdown");
684
+ if (dropdown) {
685
+ try {
686
+ dropdown.hidePopover();
687
+ } catch {}
688
+ }
689
+ this.update();
690
+ }
691
+ _positionDropdown(dropdown, trigger) {
692
+ const triggerRect = trigger.getBoundingClientRect();
693
+ const viewportHeight = window.innerHeight;
694
+ const viewportWidth = window.innerWidth;
695
+ let top = triggerRect.bottom + 4;
696
+ let left = triggerRect.left;
697
+ const dropdownRect = dropdown.getBoundingClientRect();
698
+ if (top + dropdownRect.height > viewportHeight && triggerRect.top > dropdownRect.height) {
699
+ top = triggerRect.top - dropdownRect.height - 4;
700
+ }
701
+ if (left + dropdownRect.width > viewportWidth) {
702
+ left = viewportWidth - dropdownRect.width - 8;
703
+ }
704
+ if (left < 8) {
705
+ left = 8;
706
+ }
707
+ dropdown.style.top = `${top}px`;
708
+ dropdown.style.left = `${left}px`;
709
+ dropdown.style.minWidth = `${triggerRect.width}px`;
710
+ }
711
+ _toggle() {
712
+ if (this._isOpen) {
713
+ this._close();
714
+ } else {
715
+ this._open();
716
+ }
717
+ }
718
+ async _handleOptionClick(value, level) {
719
+ this._activePath = this._activePath.slice(0, level);
720
+ this._activePath.push(value);
721
+ const option = this._findOptionByPath(this._activePath);
722
+ if (!option) {
723
+ return;
724
+ }
725
+ if (this._loadDataFn && !option.children && !option.leaf) {
726
+ this._loadingKeys.add(value);
727
+ this.update();
728
+ try {
729
+ const children = await this._loadDataFn(option);
730
+ option.children = children;
731
+ } finally {
732
+ this._loadingKeys.delete(value);
733
+ }
734
+ }
735
+ this.emit("expand", { option, level });
736
+ const isLeaf = this._isLeaf(option);
737
+ if (isLeaf || this.changeOnSelect) {
738
+ this._selectPath([...this._activePath]);
739
+ if (isLeaf && !this.multiple) {
740
+ this._close();
741
+ }
742
+ }
743
+ this.update();
744
+ }
745
+ _handleOptionHover(value, level) {
746
+ if (this.expandTrigger !== "hover")
747
+ return;
748
+ this._activePath = this._activePath.slice(0, level);
749
+ this._activePath.push(value);
750
+ this.update();
751
+ const option = this._findOptionByPath(this._activePath);
752
+ if (option && this._loadDataFn && !option.children && !option.leaf) {
753
+ this._handleOptionClick(value, level);
754
+ }
755
+ }
756
+ _selectPath(path) {
757
+ if (this.multiple) {
758
+ const pathStr = JSON.stringify(path);
759
+ const index = this._selectedPaths.findIndex((p) => JSON.stringify(p) === pathStr);
760
+ if (index >= 0) {
761
+ this._selectedPaths.splice(index, 1);
762
+ } else {
763
+ this._selectedPaths.push(path);
764
+ }
765
+ this.value = JSON.stringify(this._selectedPaths);
766
+ } else {
767
+ this._selectedPaths = [path];
768
+ this.value = JSON.stringify(path);
769
+ }
770
+ this._emitChange();
771
+ }
772
+ _selectSearchResult(result) {
773
+ this._selectPath(result.pathValues);
774
+ if (!this.multiple) {
775
+ this._close();
776
+ }
777
+ this.update();
778
+ }
779
+ _removeTag(pathIndex) {
780
+ this._selectedPaths.splice(pathIndex, 1);
781
+ this.value = this._selectedPaths.length > 0 ? JSON.stringify(this._selectedPaths) : "";
782
+ this._emitChange();
783
+ this.update();
784
+ }
785
+ _handleSearch(e) {
786
+ const input = e.target;
787
+ this._searchValue = input.value;
788
+ this.emit("search", { searchValue: this._searchValue });
789
+ this.update();
790
+ }
791
+ _handleClear(e) {
792
+ e.stopPropagation();
793
+ this.value = "";
794
+ this._selectedPaths = [];
795
+ this.emit("clear", {});
796
+ this._emitChange();
797
+ this.update();
798
+ }
799
+ _emitChange() {
800
+ const selectedOptions = this._selectedPaths.map((path) => this._findOptionByPath(path)).filter((o) => o !== undefined);
801
+ this.emit("change", {
802
+ value: this.value,
803
+ selectedOptions,
804
+ path: this._selectedPaths[0] || []
805
+ });
806
+ }
807
+ render() {
808
+ return `
809
+ <div class="cascader ${this._isOpen ? "open" : ""}">
810
+ ${this._renderTrigger()}
811
+ ${this._renderDropdown()}
812
+ </div>
813
+ `;
814
+ }
815
+ _renderTrigger() {
816
+ const hasValue = this._selectedPaths.length > 0;
817
+ const showClear = this.clearable && hasValue && !this.disabled;
818
+ return `
819
+ <button
820
+ type="button"
821
+ class="cascader-trigger"
822
+ aria-haspopup="listbox"
823
+ aria-expanded="${this._isOpen}"
824
+ ${this.disabled ? "disabled" : ""}
825
+ data-action="toggle"
826
+ >
827
+ ${this.multiple && hasValue ? this._renderTags() : this._renderValue()}
828
+ ${showClear ? `<span class="cascader-clear" role="button" tabindex="-1" data-action="clear">${closeIcon}</span>` : ""}
829
+ <span class="cascader-arrow">${chevronDownIcon}</span>
830
+ </button>
831
+ `;
832
+ }
833
+ _renderValue() {
834
+ const displayLabel = this._getDisplayLabel();
835
+ if (!displayLabel) {
836
+ return `<span class="cascader-value cascader-placeholder">${this.placeholder}</span>`;
837
+ }
838
+ return `<span class="cascader-value">${this._escapeHtml(displayLabel)}</span>`;
839
+ }
840
+ _renderTags() {
841
+ const tagsHtml = this._selectedPaths.map((path, index) => {
842
+ const labels = this._getPathLabels(path);
843
+ const displayLabel = this.showAllLevels ? labels.join(this.separator) : labels[labels.length - 1];
844
+ return `
845
+ <span class="cascader-tag">
846
+ <span class="cascader-tag-text">${this._escapeHtml(displayLabel)}</span>
847
+ <span class="cascader-tag-remove" role="button" tabindex="-1" data-action="remove-tag" data-index="${index}">${closeIcon}</span>
848
+ </span>
849
+ `;
850
+ }).join("");
851
+ return `<div class="cascader-tags">${tagsHtml || `<span class="cascader-placeholder">${this.placeholder}</span>`}</div>`;
852
+ }
853
+ _renderDropdown() {
854
+ const showSearch = this.searchable && this._searchValue;
855
+ return `
856
+ <div class="cascader-dropdown" role="listbox" popover="manual">
857
+ ${this.searchable ? this._renderSearch() : ""}
858
+ ${showSearch ? this._renderSearchResults() : this._renderPanels()}
859
+ </div>
860
+ `;
861
+ }
862
+ _renderSearch() {
863
+ return `
864
+ <div class="cascader-search">
865
+ <span class="cascader-search-icon">${searchIcon}</span>
866
+ <input
867
+ type="text"
868
+ class="cascader-search-input"
869
+ placeholder="Search..."
870
+ value="${this._escapeHtml(this._searchValue)}"
871
+ data-action="search"
872
+ />
873
+ </div>
874
+ `;
875
+ }
876
+ _renderPanels() {
877
+ const panels = this._getPanels();
878
+ if (panels.length === 0 || panels[0].length === 0) {
879
+ return `<div class="cascader-empty">No options available</div>`;
880
+ }
881
+ return `
882
+ <div class="cascader-panels">
883
+ ${panels.map((options, level) => this._renderPanel(options, level)).join("")}
884
+ </div>
885
+ `;
886
+ }
887
+ _renderPanel(options, level) {
888
+ const selectedValue = this._activePath[level];
889
+ const selectedPathValues = this._selectedPaths.flatMap((p) => p);
890
+ const optionsHtml = options.map((option) => {
891
+ const isActive = option.value === selectedValue;
892
+ const isSelected = this.multiple ? selectedPathValues.includes(option.value) : JSON.stringify(this._selectedPaths[0]) === JSON.stringify([...this._activePath.slice(0, level), option.value]);
893
+ const isLoading = this._loadingKeys.has(option.value);
894
+ const hasChildren = !this._isLeaf(option);
895
+ const classes = [
896
+ "cascader-option",
897
+ isActive ? "active" : "",
898
+ isSelected ? "selected" : "",
899
+ option.disabled ? "disabled" : ""
900
+ ].filter(Boolean).join(" ");
901
+ return `
902
+ <button
903
+ type="button"
904
+ class="${classes}"
905
+ data-action="option"
906
+ data-value="${this._escapeHtml(option.value)}"
907
+ data-level="${level}"
908
+ ${option.disabled ? "disabled" : ""}
909
+ >
910
+ ${this.multiple ? `<span class="cascader-option-checkbox">${isSelected ? checkIcon : ""}</span>` : ""}
911
+ <span class="cascader-option-label">${this._escapeHtml(option.label)}</span>
912
+ ${isLoading ? `<span class="cascader-option-loading">${loadingIcon}</span>` : ""}
913
+ ${hasChildren && !isLoading ? `<span class="cascader-option-arrow">${chevronRightIcon}</span>` : ""}
914
+ </button>
915
+ `;
916
+ }).join("");
917
+ return `
918
+ <div class="cascader-panel">
919
+ <div class="cascader-panel-options">${optionsHtml}</div>
920
+ </div>
921
+ `;
922
+ }
923
+ _renderSearchResults() {
924
+ const results = this._searchOptions();
925
+ if (results.length === 0) {
926
+ return `<div class="cascader-empty">No results found</div>`;
927
+ }
928
+ const selectedPathStrs = this._selectedPaths.map((p) => JSON.stringify(p));
929
+ const resultsHtml = results.map((result) => {
930
+ const isSelected = selectedPathStrs.includes(JSON.stringify(result.pathValues));
931
+ const classes = ["cascader-search-result", isSelected ? "selected" : ""].filter(Boolean).join(" ");
932
+ const pathHtml = result.pathLabels.map((label, i) => {
933
+ const separator = i < result.pathLabels.length - 1 ? `<span class="cascader-search-result-separator">${this.separator}</span>` : "";
934
+ return `<span>${this._escapeHtml(label)}</span>${separator}`;
935
+ }).join("");
936
+ return `
937
+ <button
938
+ type="button"
939
+ class="${classes}"
940
+ data-action="search-result"
941
+ data-path="${this._escapeHtml(JSON.stringify(result.pathValues))}"
942
+ >
943
+ ${this.multiple ? `<span class="cascader-option-checkbox">${isSelected ? checkIcon : ""}</span>` : ""}
944
+ <span class="cascader-search-result-path">${pathHtml}</span>
945
+ </button>
946
+ `;
947
+ }).join("");
948
+ return `<div class="cascader-search-results">${resultsHtml}</div>`;
949
+ }
950
+ _escapeHtml(str) {
951
+ const div = document.createElement("div");
952
+ div.textContent = str;
953
+ return div.innerHTML;
954
+ }
955
+ update() {
956
+ const searchInput = this.shadowRoot?.querySelector(".cascader-search-input");
957
+ const hadFocus = searchInput && this.shadowRoot?.activeElement === searchInput;
958
+ const cursorPosition = hadFocus ? searchInput.selectionStart : null;
959
+ super.update();
960
+ if (this._isOpen) {
961
+ const dropdown = this.shadowRoot?.querySelector(".cascader-dropdown");
962
+ const trigger = this.shadowRoot?.querySelector(".cascader-trigger");
963
+ if (dropdown && trigger) {
964
+ try {
965
+ dropdown.showPopover();
966
+ this._positionDropdown(dropdown, trigger);
967
+ } catch {}
968
+ }
969
+ if (hadFocus) {
970
+ const newSearchInput = this.shadowRoot?.querySelector(".cascader-search-input");
971
+ if (newSearchInput) {
972
+ newSearchInput.focus();
973
+ if (cursorPosition !== null) {
974
+ newSearchInput.setSelectionRange(cursorPosition, cursorPosition);
975
+ }
976
+ }
977
+ }
978
+ }
979
+ }
980
+ _setupEventDelegation() {
981
+ this.shadowRoot?.addEventListener("click", (e) => {
982
+ const target = e.target;
983
+ const trigger = target.closest('[data-action="toggle"]');
984
+ if (trigger && !target.closest('[data-action="clear"]') && !target.closest('[data-action="remove-tag"]')) {
985
+ this._toggle();
986
+ return;
987
+ }
988
+ if (target.closest('[data-action="clear"]')) {
989
+ this._handleClear(e);
990
+ return;
991
+ }
992
+ const removeTag = target.closest('[data-action="remove-tag"]');
993
+ if (removeTag) {
994
+ e.stopPropagation();
995
+ const index = parseInt(removeTag.getAttribute("data-index") || "0", 10);
996
+ this._removeTag(index);
997
+ return;
998
+ }
999
+ const option = target.closest('[data-action="option"]');
1000
+ if (option) {
1001
+ const value = option.getAttribute("data-value");
1002
+ const level = parseInt(option.getAttribute("data-level") || "0", 10);
1003
+ if (value) {
1004
+ this._handleOptionClick(value, level);
1005
+ }
1006
+ return;
1007
+ }
1008
+ const searchResult = target.closest('[data-action="search-result"]');
1009
+ if (searchResult) {
1010
+ const pathStr = searchResult.getAttribute("data-path");
1011
+ if (pathStr) {
1012
+ try {
1013
+ const pathValues = JSON.parse(pathStr);
1014
+ const result = {
1015
+ pathValues,
1016
+ path: [],
1017
+ pathLabels: this._getPathLabels(pathValues)
1018
+ };
1019
+ this._selectSearchResult(result);
1020
+ } catch {}
1021
+ }
1022
+ return;
1023
+ }
1024
+ });
1025
+ this.shadowRoot?.addEventListener("input", (e) => {
1026
+ const target = e.target;
1027
+ if (target.matches('[data-action="search"]')) {
1028
+ this._handleSearch(e);
1029
+ }
1030
+ });
1031
+ this.shadowRoot?.addEventListener("mouseenter", (e) => {
1032
+ if (this.expandTrigger !== "hover")
1033
+ return;
1034
+ const target = e.target;
1035
+ const option = target.closest('[data-action="option"]');
1036
+ if (option) {
1037
+ const value = option.getAttribute("data-value");
1038
+ const level = parseInt(option.getAttribute("data-level") || "0", 10);
1039
+ if (value) {
1040
+ this._handleOptionHover(value, level);
1041
+ }
1042
+ }
1043
+ }, true);
1044
+ }
1045
+ }
1046
+ function register() {
1047
+ if (!customElements.get("el-dm-cascader")) {
1048
+ customElements.define("el-dm-cascader", ElDmCascader);
1049
+ }
1050
+ }
1051
+
1052
+ // src/register.ts
1053
+ register();
1054
+
1055
+ //# debugId=B49288E17D9672F264756E2164756E21
1056
+ //# sourceMappingURL=register.js.map