basecoat-cli 0.3.9 → 0.3.10-beta.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/assets/jinja/carousel.html.jinja +111 -0
- package/dist/assets/jinja/select.html.jinja +35 -15
- package/dist/assets/js/all.js +185 -50
- package/dist/assets/js/all.min.js +1 -1
- package/dist/assets/js/carousel.js +192 -0
- package/dist/assets/js/carousel.min.js +1 -0
- package/dist/assets/js/select.js +185 -50
- package/dist/assets/js/select.min.js +1 -1
- package/dist/assets/nunjucks/carousel.njk +111 -0
- package/dist/assets/nunjucks/select.njk +36 -16
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{#
|
|
2
|
+
Renders a carousel component with navigation controls and optional indicators.
|
|
3
|
+
|
|
4
|
+
@param id {string} [optional] - Unique identifier for the carousel component. Auto-generated if not provided.
|
|
5
|
+
@param slides {array} - An array of objects representing carousel slides.
|
|
6
|
+
Each object should have:
|
|
7
|
+
- content {string}: HTML content for the slide.
|
|
8
|
+
- attrs {object} [optional]: Additional HTML attributes for the slide item.
|
|
9
|
+
@param loop {boolean} [optional] [default=false] - Enable continuous looping.
|
|
10
|
+
@param autoplay {number} [optional] [default=0] - Auto-advance delay in milliseconds (0 = disabled).
|
|
11
|
+
@param align {string} [optional] [default='start'] - Slide alignment ('start' or 'center').
|
|
12
|
+
@param orientation {string} [optional] [default='horizontal'] - Carousel orientation ('horizontal' or 'vertical').
|
|
13
|
+
@param show_controls {boolean} [optional] [default=true] - Show previous/next navigation buttons.
|
|
14
|
+
@param show_indicators {boolean} [optional] [default=true] - Show slide indicator dots.
|
|
15
|
+
@param main_attrs {object} [optional] - Additional HTML attributes for the main container.
|
|
16
|
+
@param viewport_attrs {object} [optional] - Additional HTML attributes for the viewport container.
|
|
17
|
+
#}
|
|
18
|
+
{% macro carousel(
|
|
19
|
+
id=None,
|
|
20
|
+
slides=[],
|
|
21
|
+
loop=false,
|
|
22
|
+
autoplay=0,
|
|
23
|
+
align='start',
|
|
24
|
+
orientation='horizontal',
|
|
25
|
+
show_controls=true,
|
|
26
|
+
show_indicators=true,
|
|
27
|
+
main_attrs={},
|
|
28
|
+
viewport_attrs={}
|
|
29
|
+
)
|
|
30
|
+
%}
|
|
31
|
+
{% set id = id or ("carousel-" + (range(100000, 999999) | random | string)) %}
|
|
32
|
+
<div
|
|
33
|
+
class="carousel {{ main_attrs.class }}"
|
|
34
|
+
id="{{ id }}"
|
|
35
|
+
data-carousel-loop="{{ 'true' if loop else 'false' }}"
|
|
36
|
+
{% if autoplay > 0 %}data-carousel-autoplay="{{ autoplay }}"{% endif %}
|
|
37
|
+
data-orientation="{{ orientation }}"
|
|
38
|
+
role="region"
|
|
39
|
+
aria-roledescription="carousel"
|
|
40
|
+
aria-label="Carousel"
|
|
41
|
+
tabindex="0"
|
|
42
|
+
{% for key, value in main_attrs.items() %}
|
|
43
|
+
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
|
44
|
+
{% endfor %}
|
|
45
|
+
>
|
|
46
|
+
<div
|
|
47
|
+
class="carousel-viewport {{ viewport_attrs.class }}"
|
|
48
|
+
{% for key, value in viewport_attrs.items() %}
|
|
49
|
+
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
|
50
|
+
{% endfor %}
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
class="carousel-slides"
|
|
54
|
+
data-orientation="{{ orientation }}"
|
|
55
|
+
>
|
|
56
|
+
{% for slide in slides %}
|
|
57
|
+
<div
|
|
58
|
+
class="carousel-item"
|
|
59
|
+
role="group"
|
|
60
|
+
aria-roledescription="slide"
|
|
61
|
+
aria-label="{{ loop.index }} of {{ slides | length }}"
|
|
62
|
+
{% if align == 'center' %}data-align="center"{% endif %}
|
|
63
|
+
{% if slide.attrs %}
|
|
64
|
+
{% for key, value in slide.attrs.items() %}
|
|
65
|
+
{{ key }}="{{ value }}"
|
|
66
|
+
{% endfor %}
|
|
67
|
+
{% endif %}
|
|
68
|
+
>
|
|
69
|
+
{{ slide.content | safe }}
|
|
70
|
+
</div>
|
|
71
|
+
{% endfor %}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{% if show_controls %}
|
|
76
|
+
<div class="carousel-controls">
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
class="carousel-prev"
|
|
80
|
+
aria-label="Previous slide"
|
|
81
|
+
>
|
|
82
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
83
|
+
<path d="m15 18-6-6 6-6"/>
|
|
84
|
+
</svg>
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="carousel-next"
|
|
89
|
+
aria-label="Next slide"
|
|
90
|
+
>
|
|
91
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
92
|
+
<path d="m9 18 6-6-6-6"/>
|
|
93
|
+
</svg>
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
{% endif %}
|
|
97
|
+
|
|
98
|
+
{% if show_indicators %}
|
|
99
|
+
<div class="carousel-indicators" role="tablist" aria-label="Slides">
|
|
100
|
+
{% for slide in slides %}
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
role="tab"
|
|
104
|
+
aria-label="Slide {{ loop.index }}"
|
|
105
|
+
{% if loop.index == 1 %}aria-current="true"{% else %}aria-current="false"{% endif %}
|
|
106
|
+
></button>
|
|
107
|
+
{% endfor %}
|
|
108
|
+
</div>
|
|
109
|
+
{% endif %}
|
|
110
|
+
</div>
|
|
111
|
+
{% endmacro %}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
Renders a select or combobox component.
|
|
3
3
|
|
|
4
4
|
@param id {string} [optional] - Unique identifier for the select component.
|
|
5
|
-
@param selected {string} [optional] - The initially selected value.
|
|
5
|
+
@param selected {string|array} [optional] - The initially selected value(s).
|
|
6
6
|
@param name {string} [optional] - The name attribute for the hidden input storing the selected value.
|
|
7
|
+
@param multiple {boolean} [optional] [default=false] - Enables multiple selection mode.
|
|
8
|
+
@param placeholder {string} [optional] - Placeholder text shown when no options are selected (multiple mode only).
|
|
7
9
|
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
|
8
10
|
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
|
|
9
11
|
@param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
|
|
@@ -17,6 +19,8 @@
|
|
|
17
19
|
selected=None,
|
|
18
20
|
name=None,
|
|
19
21
|
items=[],
|
|
22
|
+
multiple=false,
|
|
23
|
+
placeholder=None,
|
|
20
24
|
main_attrs={},
|
|
21
25
|
trigger_attrs={},
|
|
22
26
|
popover_attrs={},
|
|
@@ -27,8 +31,15 @@
|
|
|
27
31
|
) %}
|
|
28
32
|
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
|
|
29
33
|
|
|
34
|
+
{% if selected is defined and selected is iterable and selected is not string %}
|
|
35
|
+
{% set selectedArray = selected %}
|
|
36
|
+
{% elif selected is defined %}
|
|
37
|
+
{% set selectedArray = [selected] %}
|
|
38
|
+
{% else %}
|
|
39
|
+
{% set selectedArray = [] %}
|
|
40
|
+
{% endif %}
|
|
30
41
|
{% set first_option = namespace(item=None) %}
|
|
31
|
-
{% set
|
|
42
|
+
{% set selected_options = namespace(items=[]) %}
|
|
32
43
|
|
|
33
44
|
{% if items %}
|
|
34
45
|
{% for item in items %}
|
|
@@ -37,33 +48,41 @@
|
|
|
37
48
|
{% if not first_option.item %}
|
|
38
49
|
{% set first_option.item = sub_item %}
|
|
39
50
|
{% endif %}
|
|
40
|
-
{% if
|
|
41
|
-
{% set
|
|
51
|
+
{% if sub_item.value in selectedArray %}
|
|
52
|
+
{% set selected_options.items = selected_options.items + [sub_item] %}
|
|
42
53
|
{% endif %}
|
|
43
54
|
{% endfor %}
|
|
44
55
|
{% else %}
|
|
45
56
|
{% if not first_option.item %}
|
|
46
57
|
{% set first_option.item = item %}
|
|
47
58
|
{% endif %}
|
|
48
|
-
{% if
|
|
49
|
-
{% set
|
|
59
|
+
{% if item.value in selectedArray %}
|
|
60
|
+
{% set selected_options.items = selected_options.items + [item] %}
|
|
50
61
|
{% endif %}
|
|
51
62
|
{% endif %}
|
|
52
63
|
{% endfor %}
|
|
53
64
|
{% endif %}
|
|
54
65
|
|
|
55
|
-
{% set default_option =
|
|
66
|
+
{% set default_option = (selected_options.items[0] if selected_options.items else first_option.item) %}
|
|
67
|
+
{% set labels = [] %}
|
|
68
|
+
{% if multiple and selected_options.items %}
|
|
69
|
+
{% for opt in selected_options.items %}
|
|
70
|
+
{% set labels = labels + [opt.label] %}
|
|
71
|
+
{% endfor %}
|
|
72
|
+
{% endif %}
|
|
73
|
+
{% set default_label = (', '.join(labels) if (multiple and selected_options.items) else ((placeholder or '') if (multiple and not selected_options.items) else (default_option.label if default_option else ''))) %}
|
|
56
74
|
|
|
57
75
|
<div
|
|
58
76
|
id="{{ id }}"
|
|
59
77
|
class="select {{ main_attrs.class }}"
|
|
78
|
+
{% if multiple and placeholder %}data-placeholder="{{ placeholder }}"{% endif %}
|
|
60
79
|
{% for key, value in main_attrs.items() %}
|
|
61
80
|
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
|
62
81
|
{% endfor %}
|
|
63
82
|
>
|
|
64
83
|
<button
|
|
65
84
|
type="button"
|
|
66
|
-
class="btn-outline
|
|
85
|
+
class="btn-outline {{ trigger_attrs.class }}"
|
|
67
86
|
id="{{ id }}-trigger"
|
|
68
87
|
aria-haspopup="listbox"
|
|
69
88
|
aria-expanded="false"
|
|
@@ -72,7 +91,7 @@
|
|
|
72
91
|
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
|
73
92
|
{% endfor %}
|
|
74
93
|
>
|
|
75
|
-
<span class="truncate">{{
|
|
94
|
+
<span class="truncate">{{ default_label }}</span>
|
|
76
95
|
{% if is_combobox %}
|
|
77
96
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down-icon lucide-chevrons-up-down text-muted-foreground opacity-50 shrink-0"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
|
78
97
|
{% else %}
|
|
@@ -110,12 +129,13 @@
|
|
|
110
129
|
id="{{ id }}-listbox"
|
|
111
130
|
aria-orientation="vertical"
|
|
112
131
|
aria-labelledby="{{ id }}-trigger"
|
|
132
|
+
{% if multiple %}aria-multiselectable="true"{% endif %}
|
|
113
133
|
{% for key, value in listbox_attrs.items() %}
|
|
114
134
|
{{ key }}="{{ value }}"
|
|
115
135
|
{% endfor %}
|
|
116
136
|
>
|
|
117
137
|
{% if items %}
|
|
118
|
-
{{ render_select_items(items,
|
|
138
|
+
{{ render_select_items(items, selectedArray, id ~ "-items" if id else "items") }}
|
|
119
139
|
{% else %}
|
|
120
140
|
{{ caller() if caller }}
|
|
121
141
|
{% endif %}
|
|
@@ -124,7 +144,7 @@
|
|
|
124
144
|
<input
|
|
125
145
|
type="hidden"
|
|
126
146
|
name="{{ name or id ~ '-value' }}"
|
|
127
|
-
value="{{ (default_option.value if default_option) or '' }}"
|
|
147
|
+
value="{% if multiple %}{{ selectedArray | tojson }}{% else %}{{ (default_option.value if default_option) or '' }}{% endif %}"
|
|
128
148
|
{% for key, value in input_attrs.items() %}
|
|
129
149
|
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
|
130
150
|
{% endfor %}
|
|
@@ -138,7 +158,7 @@
|
|
|
138
158
|
@param items {array} - The array of items to render.
|
|
139
159
|
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
|
140
160
|
#}
|
|
141
|
-
{% macro render_select_items(items,
|
|
161
|
+
{% macro render_select_items(items, selectedArray, parent_id_prefix="items") %}
|
|
142
162
|
{% for item in items %}
|
|
143
163
|
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
|
144
164
|
{% if item.type == "group" %}
|
|
@@ -153,7 +173,7 @@
|
|
|
153
173
|
{% endif %}
|
|
154
174
|
>
|
|
155
175
|
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
|
|
156
|
-
{{ render_select_items(item['items'],
|
|
176
|
+
{{ render_select_items(item['items'], selectedArray, item_id) if item['items'] }}
|
|
157
177
|
</div>
|
|
158
178
|
{% elif item.type == "separator" %}
|
|
159
179
|
<hr role="separator" />
|
|
@@ -161,8 +181,8 @@
|
|
|
161
181
|
<div
|
|
162
182
|
id="{{ item_id }}"
|
|
163
183
|
role="option"
|
|
164
|
-
data-value="{{ item.value }}"
|
|
165
|
-
{% if
|
|
184
|
+
{% if item.value is defined and item.value is not none %}data-value="{{ item.value }}"{% endif %}
|
|
185
|
+
{% if item.value in selectedArray %}aria-selected="true"{% endif %}
|
|
166
186
|
{% if item.attrs %}
|
|
167
187
|
{% for key, value in item.attrs.items() %}
|
|
168
188
|
{{ key }}="{{ value }}"
|
package/dist/assets/js/all.js
CHANGED
|
@@ -508,31 +508,37 @@
|
|
|
508
508
|
const trigger = selectComponent.querySelector(':scope > button');
|
|
509
509
|
const selectedLabel = trigger.querySelector(':scope > span');
|
|
510
510
|
const popover = selectComponent.querySelector(':scope > [data-popover]');
|
|
511
|
-
const listbox = popover.querySelector('[role="listbox"]');
|
|
511
|
+
const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
|
|
512
512
|
const input = selectComponent.querySelector(':scope > input[type="hidden"]');
|
|
513
513
|
const filter = selectComponent.querySelector('header input[type="text"]');
|
|
514
|
+
|
|
514
515
|
if (!trigger || !popover || !listbox || !input) {
|
|
515
516
|
const missing = [];
|
|
516
517
|
if (!trigger) missing.push('trigger');
|
|
517
518
|
if (!popover) missing.push('popover');
|
|
518
519
|
if (!listbox) missing.push('listbox');
|
|
519
|
-
if (!input)
|
|
520
|
+
if (!input) missing.push('input');
|
|
520
521
|
console.error(`Select component initialisation failed. Missing element(s): ${missing.join(', ')}`, selectComponent);
|
|
521
522
|
return;
|
|
522
523
|
}
|
|
523
|
-
|
|
524
|
+
|
|
524
525
|
const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
|
|
525
526
|
const options = allOptions.filter(opt => opt.getAttribute('aria-disabled') !== 'true');
|
|
526
527
|
let visibleOptions = [...options];
|
|
527
528
|
let activeIndex = -1;
|
|
529
|
+
const isMultiple = listbox.getAttribute('aria-multiselectable') === 'true';
|
|
530
|
+
const selectedOptions = isMultiple ? new Set() : null;
|
|
531
|
+
const placeholder = isMultiple ? (selectComponent.dataset.placeholder || '') : null;
|
|
532
|
+
|
|
533
|
+
const getValue = (opt) => opt.dataset.value ?? opt.textContent.trim();
|
|
528
534
|
|
|
529
535
|
const setActiveOption = (index) => {
|
|
530
536
|
if (activeIndex > -1 && options[activeIndex]) {
|
|
531
537
|
options[activeIndex].classList.remove('active');
|
|
532
538
|
}
|
|
533
|
-
|
|
539
|
+
|
|
534
540
|
activeIndex = index;
|
|
535
|
-
|
|
541
|
+
|
|
536
542
|
if (activeIndex > -1) {
|
|
537
543
|
const activeOption = options[activeIndex];
|
|
538
544
|
activeOption.classList.add('active');
|
|
@@ -551,68 +557,118 @@
|
|
|
551
557
|
return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
|
|
552
558
|
};
|
|
553
559
|
|
|
554
|
-
const updateValue = (
|
|
555
|
-
|
|
560
|
+
const updateValue = (optionOrOptions, triggerEvent = true) => {
|
|
561
|
+
let value;
|
|
562
|
+
|
|
563
|
+
if (isMultiple) {
|
|
564
|
+
const opts = Array.isArray(optionOrOptions) ? optionOrOptions : [];
|
|
565
|
+
selectedOptions.clear();
|
|
566
|
+
opts.forEach(opt => selectedOptions.add(opt));
|
|
567
|
+
|
|
568
|
+
// Get selected options in DOM order
|
|
569
|
+
const selected = options.filter(opt => selectedOptions.has(opt));
|
|
570
|
+
if (selected.length === 0) {
|
|
571
|
+
selectedLabel.textContent = placeholder;
|
|
572
|
+
selectedLabel.classList.add('text-muted-foreground');
|
|
573
|
+
} else {
|
|
574
|
+
selectedLabel.textContent = selected.map(opt => opt.dataset.label || opt.textContent.trim()).join(', ');
|
|
575
|
+
selectedLabel.classList.remove('text-muted-foreground');
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
value = selected.map(getValue);
|
|
579
|
+
input.value = JSON.stringify(value);
|
|
580
|
+
} else {
|
|
581
|
+
const option = optionOrOptions;
|
|
582
|
+
if (!option) return;
|
|
556
583
|
selectedLabel.innerHTML = option.innerHTML;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
584
|
+
value = getValue(option);
|
|
585
|
+
input.value = value;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
options.forEach(opt => {
|
|
589
|
+
const isSelected = isMultiple ? selectedOptions.has(opt) : opt === optionOrOptions;
|
|
590
|
+
if (isSelected) {
|
|
591
|
+
opt.setAttribute('aria-selected', 'true');
|
|
592
|
+
} else {
|
|
593
|
+
opt.removeAttribute('aria-selected');
|
|
567
594
|
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
if (triggerEvent) {
|
|
598
|
+
selectComponent.dispatchEvent(new CustomEvent('change', {
|
|
599
|
+
detail: { value },
|
|
600
|
+
bubbles: true
|
|
601
|
+
}));
|
|
568
602
|
}
|
|
569
603
|
};
|
|
570
604
|
|
|
571
605
|
const closePopover = (focusOnTrigger = true) => {
|
|
572
606
|
if (popover.getAttribute('aria-hidden') === 'true') return;
|
|
573
|
-
|
|
607
|
+
|
|
574
608
|
if (filter) {
|
|
575
609
|
const resetFilter = () => {
|
|
576
610
|
filter.value = '';
|
|
577
611
|
visibleOptions = [...options];
|
|
578
612
|
allOptions.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
|
|
579
613
|
};
|
|
580
|
-
|
|
614
|
+
|
|
581
615
|
if (hasTransition()) {
|
|
582
616
|
popover.addEventListener('transitionend', resetFilter, { once: true });
|
|
583
617
|
} else {
|
|
584
618
|
resetFilter();
|
|
585
619
|
}
|
|
586
620
|
}
|
|
587
|
-
|
|
621
|
+
|
|
588
622
|
if (focusOnTrigger) trigger.focus();
|
|
589
623
|
popover.setAttribute('aria-hidden', 'true');
|
|
590
624
|
trigger.setAttribute('aria-expanded', 'false');
|
|
591
625
|
setActiveOption(-1);
|
|
592
|
-
}
|
|
626
|
+
};
|
|
593
627
|
|
|
594
|
-
const
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
628
|
+
const toggleMultipleValue = (option) => {
|
|
629
|
+
if (selectedOptions.has(option)) {
|
|
630
|
+
selectedOptions.delete(option);
|
|
631
|
+
} else {
|
|
632
|
+
selectedOptions.add(option);
|
|
633
|
+
}
|
|
634
|
+
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
|
635
|
+
};
|
|
599
636
|
|
|
600
|
-
|
|
601
|
-
|
|
637
|
+
const select = (value) => {
|
|
638
|
+
if (isMultiple) {
|
|
639
|
+
const option = options.find(opt => getValue(opt) === value && !selectedOptions.has(opt));
|
|
640
|
+
if (!option) return;
|
|
641
|
+
selectedOptions.add(option);
|
|
642
|
+
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
|
643
|
+
} else {
|
|
644
|
+
const option = options.find(opt => getValue(opt) === value);
|
|
645
|
+
if (!option) return;
|
|
646
|
+
if (input.value !== value) {
|
|
647
|
+
updateValue(option);
|
|
648
|
+
}
|
|
649
|
+
closePopover();
|
|
602
650
|
}
|
|
603
|
-
|
|
604
|
-
closePopover();
|
|
605
651
|
};
|
|
606
652
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
653
|
+
const deselect = (value) => {
|
|
654
|
+
if (!isMultiple) return;
|
|
655
|
+
const option = options.find(opt => getValue(opt) === value && selectedOptions.has(opt));
|
|
656
|
+
if (!option) return;
|
|
657
|
+
selectedOptions.delete(option);
|
|
658
|
+
updateValue(options.filter(opt => selectedOptions.has(opt)));
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const toggle = (value) => {
|
|
662
|
+
if (!isMultiple) return;
|
|
663
|
+
const option = options.find(opt => getValue(opt) === value);
|
|
664
|
+
if (!option) return;
|
|
665
|
+
toggleMultipleValue(option);
|
|
610
666
|
};
|
|
611
667
|
|
|
612
668
|
if (filter) {
|
|
613
669
|
const filterOptions = () => {
|
|
614
670
|
const searchTerm = filter.value.trim().toLowerCase();
|
|
615
|
-
|
|
671
|
+
|
|
616
672
|
setActiveOption(-1);
|
|
617
673
|
|
|
618
674
|
visibleOptions = [];
|
|
@@ -638,17 +694,37 @@
|
|
|
638
694
|
}
|
|
639
695
|
});
|
|
640
696
|
};
|
|
641
|
-
|
|
697
|
+
|
|
642
698
|
filter.addEventListener('input', filterOptions);
|
|
643
699
|
}
|
|
644
700
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
701
|
+
// Initialization
|
|
702
|
+
if (isMultiple) {
|
|
703
|
+
const ariaSelected = options.filter(opt => opt.getAttribute('aria-selected') === 'true');
|
|
704
|
+
try {
|
|
705
|
+
const parsed = JSON.parse(input.value || '[]');
|
|
706
|
+
const validValues = new Set(options.map(getValue));
|
|
707
|
+
const initialValues = Array.isArray(parsed) ? parsed.filter(v => validValues.has(v)) : [];
|
|
708
|
+
|
|
709
|
+
const initialOptions = [];
|
|
710
|
+
if (initialValues.length > 0) {
|
|
711
|
+
// Match values to options in order, allowing duplicates
|
|
712
|
+
initialValues.forEach(val => {
|
|
713
|
+
const opt = options.find(o => getValue(o) === val && !initialOptions.includes(o));
|
|
714
|
+
if (opt) initialOptions.push(opt);
|
|
715
|
+
});
|
|
716
|
+
} else {
|
|
717
|
+
initialOptions.push(...ariaSelected);
|
|
718
|
+
}
|
|
650
719
|
|
|
651
|
-
|
|
720
|
+
updateValue(initialOptions, false);
|
|
721
|
+
} catch (e) {
|
|
722
|
+
updateValue(ariaSelected, false);
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
const initialOption = options.find(opt => getValue(opt) === input.value) || options[0];
|
|
726
|
+
if (initialOption) updateValue(initialOption, false);
|
|
727
|
+
}
|
|
652
728
|
|
|
653
729
|
const handleKeyNavigation = (event) => {
|
|
654
730
|
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
|
|
@@ -664,17 +740,25 @@
|
|
|
664
740
|
}
|
|
665
741
|
return;
|
|
666
742
|
}
|
|
667
|
-
|
|
743
|
+
|
|
668
744
|
event.preventDefault();
|
|
669
745
|
|
|
670
746
|
if (event.key === 'Escape') {
|
|
671
747
|
closePopover();
|
|
672
748
|
return;
|
|
673
749
|
}
|
|
674
|
-
|
|
750
|
+
|
|
675
751
|
if (event.key === 'Enter') {
|
|
676
752
|
if (activeIndex > -1) {
|
|
677
|
-
|
|
753
|
+
const option = options[activeIndex];
|
|
754
|
+
if (isMultiple) {
|
|
755
|
+
toggleMultipleValue(option);
|
|
756
|
+
} else {
|
|
757
|
+
if (input.value !== getValue(option)) {
|
|
758
|
+
updateValue(option);
|
|
759
|
+
}
|
|
760
|
+
closePopover();
|
|
761
|
+
}
|
|
678
762
|
}
|
|
679
763
|
return;
|
|
680
764
|
}
|
|
@@ -740,7 +824,7 @@
|
|
|
740
824
|
document.dispatchEvent(new CustomEvent('basecoat:popover', {
|
|
741
825
|
detail: { source: selectComponent }
|
|
742
826
|
}));
|
|
743
|
-
|
|
827
|
+
|
|
744
828
|
if (filter) {
|
|
745
829
|
if (hasTransition()) {
|
|
746
830
|
popover.addEventListener('transitionend', () => {
|
|
@@ -753,7 +837,7 @@
|
|
|
753
837
|
|
|
754
838
|
popover.setAttribute('aria-hidden', 'false');
|
|
755
839
|
trigger.setAttribute('aria-expanded', 'true');
|
|
756
|
-
|
|
840
|
+
|
|
757
841
|
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
|
|
758
842
|
if (selectedOption) {
|
|
759
843
|
setActiveOption(options.indexOf(selectedOption));
|
|
@@ -772,8 +856,24 @@
|
|
|
772
856
|
|
|
773
857
|
listbox.addEventListener('click', (event) => {
|
|
774
858
|
const clickedOption = event.target.closest('[role="option"]');
|
|
775
|
-
if (clickedOption)
|
|
776
|
-
|
|
859
|
+
if (!clickedOption) return;
|
|
860
|
+
|
|
861
|
+
const option = options.find(opt => opt === clickedOption);
|
|
862
|
+
if (!option) return;
|
|
863
|
+
|
|
864
|
+
if (isMultiple) {
|
|
865
|
+
toggleMultipleValue(option);
|
|
866
|
+
setActiveOption(options.indexOf(option));
|
|
867
|
+
if (filter) {
|
|
868
|
+
filter.focus();
|
|
869
|
+
} else {
|
|
870
|
+
trigger.focus();
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
873
|
+
if (input.value !== getValue(option)) {
|
|
874
|
+
updateValue(option);
|
|
875
|
+
}
|
|
876
|
+
closePopover();
|
|
777
877
|
}
|
|
778
878
|
});
|
|
779
879
|
|
|
@@ -790,8 +890,43 @@
|
|
|
790
890
|
});
|
|
791
891
|
|
|
792
892
|
popover.setAttribute('aria-hidden', 'true');
|
|
793
|
-
|
|
794
|
-
|
|
893
|
+
|
|
894
|
+
// Public API
|
|
895
|
+
Object.defineProperty(selectComponent, 'value', {
|
|
896
|
+
get: () => {
|
|
897
|
+
if (isMultiple) {
|
|
898
|
+
return options.filter(opt => selectedOptions.has(opt)).map(getValue);
|
|
899
|
+
} else {
|
|
900
|
+
return input.value;
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
set: (val) => {
|
|
904
|
+
if (isMultiple) {
|
|
905
|
+
const values = Array.isArray(val) ? val : (val != null ? [val] : []);
|
|
906
|
+
const opts = [];
|
|
907
|
+
values.forEach(v => {
|
|
908
|
+
const opt = options.find(o => getValue(o) === v && !opts.includes(o));
|
|
909
|
+
if (opt) opts.push(opt);
|
|
910
|
+
});
|
|
911
|
+
updateValue(opts);
|
|
912
|
+
} else {
|
|
913
|
+
const option = options.find(opt => getValue(opt) === val);
|
|
914
|
+
if (option) {
|
|
915
|
+
updateValue(option);
|
|
916
|
+
closePopover();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
selectComponent.select = select;
|
|
923
|
+
selectComponent.selectByValue = select; // Backward compatibility alias
|
|
924
|
+
if (isMultiple) {
|
|
925
|
+
selectComponent.deselect = deselect;
|
|
926
|
+
selectComponent.toggle = toggle;
|
|
927
|
+
selectComponent.selectAll = () => updateValue(options);
|
|
928
|
+
selectComponent.selectNone = () => updateValue([]);
|
|
929
|
+
}
|
|
795
930
|
selectComponent.dataset.selectInitialized = true;
|
|
796
931
|
selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
|
|
797
932
|
};
|