basecoat-css 0.3.8 → 0.3.10-beta.1

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/js/select.js CHANGED
@@ -3,23 +3,31 @@
3
3
  const trigger = selectComponent.querySelector(':scope > button');
4
4
  const selectedLabel = trigger.querySelector(':scope > span');
5
5
  const popover = selectComponent.querySelector(':scope > [data-popover]');
6
- const listbox = popover.querySelector('[role="listbox"]');
6
+ const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
7
7
  const input = selectComponent.querySelector(':scope > input[type="hidden"]');
8
8
  const filter = selectComponent.querySelector('header input[type="text"]');
9
+
9
10
  if (!trigger || !popover || !listbox || !input) {
10
11
  const missing = [];
11
12
  if (!trigger) missing.push('trigger');
12
13
  if (!popover) missing.push('popover');
13
14
  if (!listbox) missing.push('listbox');
14
- if (!input) missing.push('input');
15
+ if (!input) missing.push('input');
15
16
  console.error(`Select component initialisation failed. Missing element(s): ${missing.join(', ')}`, selectComponent);
16
17
  return;
17
18
  }
18
-
19
+
19
20
  const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
20
21
  const options = allOptions.filter(opt => opt.getAttribute('aria-disabled') !== 'true');
21
22
  let visibleOptions = [...options];
22
23
  let activeIndex = -1;
24
+ const isMultiple = listbox.getAttribute('aria-multiselectable') === 'true';
25
+ let selectedValues = isMultiple ? new Set() : null;
26
+ let placeholder = null;
27
+
28
+ if (isMultiple) {
29
+ placeholder = selectComponent.dataset.placeholder || '';
30
+ }
23
31
 
24
32
  const setActiveOption = (index) => {
25
33
  if (activeIndex > -1 && options[activeIndex]) {
@@ -46,23 +54,92 @@
46
54
  return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
47
55
  };
48
56
 
49
- const updateValue = (option, triggerEvent = true) => {
50
- if (option) {
57
+ const syncMultipleInputs = () => {
58
+ if (!isMultiple) return;
59
+ const values = Array.from(selectedValues);
60
+ const inputs = Array.from(selectComponent.querySelectorAll(':scope > input[type="hidden"]'));
61
+ inputs.slice(1).forEach(inp => inp.remove());
62
+
63
+ if (values.length === 0) {
64
+ input.value = '';
65
+ } else {
66
+ input.value = values[0];
67
+ let insertAfter = input;
68
+ for (let i = 1; i < values.length; i++) {
69
+ const clone = input.cloneNode(true);
70
+ clone.removeAttribute('id');
71
+ clone.value = values[i];
72
+ insertAfter.after(clone);
73
+ insertAfter = clone;
74
+ }
75
+ }
76
+ };
77
+
78
+ const updateMultipleLabel = () => {
79
+ if (!isMultiple) return;
80
+ const selected = options.filter(opt => selectedValues.has(opt.dataset.value));
81
+ if (selected.length === 0) {
82
+ selectedLabel.textContent = placeholder;
83
+ selectedLabel.classList.add('text-muted-foreground');
84
+ } else {
85
+ selectedLabel.textContent = selected.map(opt => opt.dataset.label || opt.textContent.trim()).join(', ');
86
+ selectedLabel.classList.remove('text-muted-foreground');
87
+ }
88
+ };
89
+
90
+ const updateValue = (optionOrOptions, triggerEvent = true) => {
91
+ let value;
92
+
93
+ if (isMultiple) {
94
+ const opts = Array.isArray(optionOrOptions) ? optionOrOptions : [];
95
+ selectedValues = new Set(opts.map(opt => opt.dataset.value));
96
+ options.forEach(opt => {
97
+ if (selectedValues.has(opt.dataset.value)) {
98
+ opt.setAttribute('aria-selected', 'true');
99
+ } else {
100
+ opt.removeAttribute('aria-selected');
101
+ }
102
+ });
103
+ updateMultipleLabel();
104
+ syncMultipleInputs();
105
+ value = Array.from(selectedValues);
106
+ } else {
107
+ const option = optionOrOptions;
108
+ if (!option) return;
51
109
  selectedLabel.innerHTML = option.innerHTML;
52
110
  input.value = option.dataset.value;
53
- listbox.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute('aria-selected');
54
- option.setAttribute('aria-selected', 'true');
55
-
56
- if (triggerEvent) {
57
- const event = new CustomEvent('change', {
58
- detail: { value: option.dataset.value },
59
- bubbles: true
60
- });
61
- selectComponent.dispatchEvent(event);
62
- }
111
+ options.forEach(opt => {
112
+ if (opt === option) {
113
+ opt.setAttribute('aria-selected', 'true');
114
+ } else {
115
+ opt.removeAttribute('aria-selected');
116
+ }
117
+ });
118
+ value = option.dataset.value;
119
+ }
120
+
121
+ if (triggerEvent) {
122
+ selectComponent.dispatchEvent(new CustomEvent('change', {
123
+ detail: { value },
124
+ bubbles: true
125
+ }));
63
126
  }
64
127
  };
65
128
 
129
+ const toggleMultipleValue = (value, triggerEvent = true) => {
130
+ if (!isMultiple || value == null) return;
131
+
132
+ const newValues = new Set(selectedValues);
133
+ if (newValues.has(value)) {
134
+ newValues.delete(value);
135
+ } else {
136
+ newValues.add(value);
137
+ }
138
+
139
+ const selectedOptions = options.filter(opt => newValues.has(opt.dataset.value));
140
+ updateValue(selectedOptions, triggerEvent);
141
+ };
142
+
66
143
  const closePopover = (focusOnTrigger = true) => {
67
144
  if (popover.getAttribute('aria-hidden') === 'true') return;
68
145
 
@@ -101,7 +178,27 @@
101
178
 
102
179
  const selectByValue = (value) => {
103
180
  const option = options.find(opt => opt.dataset.value === value);
104
- selectOption(option);
181
+ if (isMultiple) {
182
+ if (value != null && selectedValues.has(value)) return;
183
+ if (option && value != null) {
184
+ const newValues = new Set(selectedValues);
185
+ newValues.add(value);
186
+ const selectedOptions = options.filter(opt => newValues.has(opt.dataset.value));
187
+ updateValue(selectedOptions);
188
+ }
189
+ } else {
190
+ selectOption(option);
191
+ }
192
+ };
193
+
194
+ const selectAll = () => {
195
+ if (!isMultiple) return;
196
+ updateValue(options.filter(opt => opt.dataset.value != null));
197
+ };
198
+
199
+ const selectNone = () => {
200
+ if (!isMultiple) return;
201
+ updateValue([]);
105
202
  };
106
203
 
107
204
  if (filter) {
@@ -137,13 +234,32 @@
137
234
  filter.addEventListener('input', filterOptions);
138
235
  }
139
236
 
140
- let initialOption = options.find(opt => opt.dataset.value === input.value);
141
-
142
- if (!initialOption) {
143
- initialOption = options.find(opt => opt.dataset.value !== undefined) ?? options[0];
144
- }
237
+ if (isMultiple) {
238
+ const validValues = new Set(options.map(opt => opt.dataset.value).filter(v => v != null));
239
+ const inputs = Array.from(selectComponent.querySelectorAll(':scope > input[type="hidden"]'));
240
+ const initialValues = inputs
241
+ .map(inp => inp.value)
242
+ .filter(v => v != null && validValues.has(v));
243
+
244
+ let initialOptions;
245
+ if (initialValues.length > 0) {
246
+ initialOptions = options.filter(opt => initialValues.includes(opt.dataset.value));
247
+ } else {
248
+ initialOptions = options.filter(opt => opt.getAttribute('aria-selected') === 'true');
249
+ }
250
+
251
+ updateValue(initialOptions, false);
252
+ } else {
253
+ let initialOption = options.find(opt => opt.dataset.value === input.value);
145
254
 
146
- updateValue(initialOption, false);
255
+ if (!initialOption) {
256
+ initialOption = options.find(opt => opt.dataset.value !== undefined) ?? options[0];
257
+ }
258
+
259
+ if (initialOption) {
260
+ updateValue(initialOption, false);
261
+ }
262
+ }
147
263
 
148
264
  const handleKeyNavigation = (event) => {
149
265
  const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
@@ -169,7 +285,11 @@
169
285
 
170
286
  if (event.key === 'Enter') {
171
287
  if (activeIndex > -1) {
172
- selectOption(options[activeIndex]);
288
+ if (isMultiple) {
289
+ toggleMultipleValue(options[activeIndex].dataset.value);
290
+ } else {
291
+ selectOption(options[activeIndex]);
292
+ }
173
293
  }
174
294
  return;
175
295
  }
@@ -268,7 +388,17 @@
268
388
  listbox.addEventListener('click', (event) => {
269
389
  const clickedOption = event.target.closest('[role="option"]');
270
390
  if (clickedOption) {
271
- selectOption(clickedOption);
391
+ if (isMultiple) {
392
+ toggleMultipleValue(clickedOption.dataset.value);
393
+ setActiveOption(options.indexOf(clickedOption));
394
+ if (filter) {
395
+ filter.focus();
396
+ } else {
397
+ trigger.focus();
398
+ }
399
+ } else {
400
+ selectOption(clickedOption);
401
+ }
272
402
  }
273
403
  });
274
404
 
@@ -285,8 +415,12 @@
285
415
  });
286
416
 
287
417
  popover.setAttribute('aria-hidden', 'true');
288
-
418
+
289
419
  selectComponent.selectByValue = selectByValue;
420
+ if (isMultiple) {
421
+ selectComponent.selectAll = selectAll;
422
+ selectComponent.selectNone = selectNone;
423
+ }
290
424
  selectComponent.dataset.selectInitialized = true;
291
425
  selectComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
292
426
  };
@@ -1 +1 @@
1
- (()=>{const e=e=>{const t=e.querySelector(":scope > button"),i=t.querySelector(":scope > span"),a=e.querySelector(":scope > [data-popover]"),r=a.querySelector('[role="listbox"]'),n=e.querySelector(':scope > input[type="hidden"]'),s=e.querySelector('header input[type="text"]');if(!(t&&a&&r&&n)){const i=[];return t||i.push("trigger"),a||i.push("popover"),r||i.push("listbox"),n||i.push("input"),void console.error(`Select component initialisation failed. Missing element(s): ${i.join(", ")}`,e)}const o=Array.from(r.querySelectorAll('[role="option"]')),d=o.filter((e=>"true"!==e.getAttribute("aria-disabled")));let c=[...d],l=-1;const u=e=>{if(l>-1&&d[l]&&d[l].classList.remove("active"),l=e,l>-1){const e=d[l];e.classList.add("active"),e.id?t.setAttribute("aria-activedescendant",e.id):t.removeAttribute("aria-activedescendant")}else t.removeAttribute("aria-activedescendant")},v=()=>{const e=getComputedStyle(a);return parseFloat(e.transitionDuration)>0||parseFloat(e.transitionDelay)>0},p=(t,a=!0)=>{if(t&&(i.innerHTML=t.innerHTML,n.value=t.dataset.value,r.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute("aria-selected"),t.setAttribute("aria-selected","true"),a)){const i=new CustomEvent("change",{detail:{value:t.dataset.value},bubbles:!0});e.dispatchEvent(i)}},f=(e=!0)=>{if("true"!==a.getAttribute("aria-hidden")){if(s){const e=()=>{s.value="",c=[...d],o.forEach((e=>e.setAttribute("aria-hidden","false")))};v()?a.addEventListener("transitionend",e,{once:!0}):e()}e&&t.focus(),a.setAttribute("aria-hidden","true"),t.setAttribute("aria-expanded","false"),u(-1)}},b=e=>{if(!e)return;const t=n.value,i=e.dataset.value;null!=i&&i!==t&&p(e),f()};if(s){const e=()=>{const e=s.value.trim().toLowerCase();u(-1),c=[],o.forEach((t=>{if(t.hasAttribute("data-force"))return t.setAttribute("aria-hidden","false"),void(d.includes(t)&&c.push(t));const i=(t.dataset.filter||t.textContent).trim().toLowerCase(),a=(t.dataset.keywords||"").toLowerCase().split(/[\s,]+/).filter(Boolean).some((t=>t.includes(e))),r=i.includes(e)||a;t.setAttribute("aria-hidden",String(!r)),r&&d.includes(t)&&c.push(t)}))};s.addEventListener("input",e)}let h=d.find((e=>e.dataset.value===n.value));h||(h=d.find((e=>void 0!==e.dataset.value))??d[0]),p(h,!1);const E=e=>{const i="false"===a.getAttribute("aria-hidden");if(!["ArrowDown","ArrowUp","Enter","Home","End","Escape"].includes(e.key))return;if(!i)return void("Enter"!==e.key&&"Escape"!==e.key&&(e.preventDefault(),t.click()));if(e.preventDefault(),"Escape"===e.key)return void f();if("Enter"===e.key)return void(l>-1&&b(d[l]));if(0===c.length)return;const r=l>-1?c.indexOf(d[l]):-1;let n=r;switch(e.key){case"ArrowDown":r<c.length-1&&(n=r+1);break;case"ArrowUp":r>0?n=r-1:-1===r&&(n=0);break;case"Home":n=0;break;case"End":n=c.length-1}if(n!==r){const e=c[n];u(d.indexOf(e)),e.scrollIntoView({block:"nearest",behavior:"smooth"})}};r.addEventListener("mousemove",(e=>{const t=e.target.closest('[role="option"]');if(t&&c.includes(t)){const e=d.indexOf(t);e!==l&&u(e)}})),r.addEventListener("mouseleave",(()=>{const e=r.querySelector('[role="option"][aria-selected="true"]');u(e?d.indexOf(e):-1)})),t.addEventListener("keydown",E),s&&s.addEventListener("keydown",E);t.addEventListener("click",(()=>{"true"===t.getAttribute("aria-expanded")?f():(()=>{document.dispatchEvent(new CustomEvent("basecoat:popover",{detail:{source:e}})),s&&(v()?a.addEventListener("transitionend",(()=>{s.focus()}),{once:!0}):s.focus()),a.setAttribute("aria-hidden","false"),t.setAttribute("aria-expanded","true");const i=r.querySelector('[role="option"][aria-selected="true"]');i&&(u(d.indexOf(i)),i.scrollIntoView({block:"nearest"}))})()})),r.addEventListener("click",(e=>{const t=e.target.closest('[role="option"]');t&&b(t)})),document.addEventListener("click",(t=>{e.contains(t.target)||f(!1)})),document.addEventListener("basecoat:popover",(t=>{t.detail.source!==e&&f(!1)})),a.setAttribute("aria-hidden","true"),e.selectByValue=e=>{const t=d.find((t=>t.dataset.value===e));b(t)},e.dataset.selectInitialized=!0,e.dispatchEvent(new CustomEvent("basecoat:initialized"))};window.basecoat&&window.basecoat.register("select","div.select:not([data-select-initialized])",e)})();
1
+ (()=>{const e=e=>{const t=e.querySelector(":scope > button"),a=t.querySelector(":scope > span"),r=e.querySelector(":scope > [data-popover]"),n=r?r.querySelector('[role="listbox"]'):null,i=e.querySelector(':scope > input[type="hidden"]'),s=e.querySelector('header input[type="text"]');if(!(t&&r&&n&&i)){const a=[];return t||a.push("trigger"),r||a.push("popover"),n||a.push("listbox"),i||a.push("input"),void console.error(`Select component initialisation failed. Missing element(s): ${a.join(", ")}`,e)}const o=Array.from(n.querySelectorAll('[role="option"]')),l=o.filter((e=>"true"!==e.getAttribute("aria-disabled")));let d=[...l],u=-1;const c="true"===n.getAttribute("aria-multiselectable");let f=c?new Set:null,v=null;c&&(v=e.dataset.placeholder||"");const p=e=>{if(u>-1&&l[u]&&l[u].classList.remove("active"),u=e,u>-1){const e=l[u];e.classList.add("active"),e.id?t.setAttribute("aria-activedescendant",e.id):t.removeAttribute("aria-activedescendant")}else t.removeAttribute("aria-activedescendant")},h=()=>{const e=getComputedStyle(r);return parseFloat(e.transitionDuration)>0||parseFloat(e.transitionDelay)>0},b=(t,r=!0)=>{let n;if(c){const r=Array.isArray(t)?t:[];f=new Set(r.map((e=>e.dataset.value))),l.forEach((e=>{f.has(e.dataset.value)?e.setAttribute("aria-selected","true"):e.removeAttribute("aria-selected")})),(()=>{if(!c)return;const e=l.filter((e=>f.has(e.dataset.value)));0===e.length?(a.textContent=v,a.classList.add("text-muted-foreground")):(a.textContent=e.map((e=>e.dataset.label||e.textContent.trim())).join(", "),a.classList.remove("text-muted-foreground"))})(),(()=>{if(!c)return;const t=Array.from(f);if(Array.from(e.querySelectorAll(':scope > input[type="hidden"]')).slice(1).forEach((e=>e.remove())),0===t.length)i.value="";else{i.value=t[0];let e=i;for(let a=1;a<t.length;a++){const r=i.cloneNode(!0);r.removeAttribute("id"),r.value=t[a],e.after(r),e=r}}})(),n=Array.from(f)}else{const e=t;if(!e)return;a.innerHTML=e.innerHTML,i.value=e.dataset.value,l.forEach((t=>{t===e?t.setAttribute("aria-selected","true"):t.removeAttribute("aria-selected")})),n=e.dataset.value}r&&e.dispatchEvent(new CustomEvent("change",{detail:{value:n},bubbles:!0}))},m=(e,t=!0)=>{if(!c||null==e)return;const a=new Set(f);a.has(e)?a.delete(e):a.add(e);const r=l.filter((e=>a.has(e.dataset.value)));b(r,t)},A=(e=!0)=>{if("true"!==r.getAttribute("aria-hidden")){if(s){const e=()=>{s.value="",d=[...l],o.forEach((e=>e.setAttribute("aria-hidden","false")))};h()?r.addEventListener("transitionend",e,{once:!0}):e()}e&&t.focus(),r.setAttribute("aria-hidden","true"),t.setAttribute("aria-expanded","false"),p(-1)}},y=e=>{if(!e)return;const t=i.value,a=e.dataset.value;null!=a&&a!==t&&b(e),A()},E=()=>{c&&b(l.filter((e=>null!=e.dataset.value)))},w=()=>{c&&b([])};if(s){const e=()=>{const e=s.value.trim().toLowerCase();p(-1),d=[],o.forEach((t=>{if(t.hasAttribute("data-force"))return t.setAttribute("aria-hidden","false"),void(l.includes(t)&&d.push(t));const a=(t.dataset.filter||t.textContent).trim().toLowerCase(),r=(t.dataset.keywords||"").toLowerCase().split(/[\s,]+/).filter(Boolean).some((t=>t.includes(e))),n=a.includes(e)||r;t.setAttribute("aria-hidden",String(!n)),n&&l.includes(t)&&d.push(t)}))};s.addEventListener("input",e)}if(c){const t=new Set(l.map((e=>e.dataset.value)).filter((e=>null!=e))),a=Array.from(e.querySelectorAll(':scope > input[type="hidden"]')).map((e=>e.value)).filter((e=>null!=e&&t.has(e)));let r;r=a.length>0?l.filter((e=>a.includes(e.dataset.value))):l.filter((e=>"true"===e.getAttribute("aria-selected"))),b(r,!1)}else{let e=l.find((e=>e.dataset.value===i.value));e||(e=l.find((e=>void 0!==e.dataset.value))??l[0]),e&&b(e,!1)}const g=e=>{const a="false"===r.getAttribute("aria-hidden");if(!["ArrowDown","ArrowUp","Enter","Home","End","Escape"].includes(e.key))return;if(!a)return void("Enter"!==e.key&&"Escape"!==e.key&&(e.preventDefault(),t.click()));if(e.preventDefault(),"Escape"===e.key)return void A();if("Enter"===e.key)return void(u>-1&&(c?m(l[u].dataset.value):y(l[u])));if(0===d.length)return;const n=u>-1?d.indexOf(l[u]):-1;let i=n;switch(e.key){case"ArrowDown":n<d.length-1&&(i=n+1);break;case"ArrowUp":n>0?i=n-1:-1===n&&(i=0);break;case"Home":i=0;break;case"End":i=d.length-1}if(i!==n){const e=d[i];p(l.indexOf(e)),e.scrollIntoView({block:"nearest",behavior:"smooth"})}};n.addEventListener("mousemove",(e=>{const t=e.target.closest('[role="option"]');if(t&&d.includes(t)){const e=l.indexOf(t);e!==u&&p(e)}})),n.addEventListener("mouseleave",(()=>{const e=n.querySelector('[role="option"][aria-selected="true"]');p(e?l.indexOf(e):-1)})),t.addEventListener("keydown",g),s&&s.addEventListener("keydown",g);t.addEventListener("click",(()=>{"true"===t.getAttribute("aria-expanded")?A():(()=>{document.dispatchEvent(new CustomEvent("basecoat:popover",{detail:{source:e}})),s&&(h()?r.addEventListener("transitionend",(()=>{s.focus()}),{once:!0}):s.focus()),r.setAttribute("aria-hidden","false"),t.setAttribute("aria-expanded","true");const a=n.querySelector('[role="option"][aria-selected="true"]');a&&(p(l.indexOf(a)),a.scrollIntoView({block:"nearest"}))})()})),n.addEventListener("click",(e=>{const a=e.target.closest('[role="option"]');a&&(c?(m(a.dataset.value),p(l.indexOf(a)),s?s.focus():t.focus()):y(a))})),document.addEventListener("click",(t=>{e.contains(t.target)||A(!1)})),document.addEventListener("basecoat:popover",(t=>{t.detail.source!==e&&A(!1)})),r.setAttribute("aria-hidden","true"),e.selectByValue=e=>{const t=l.find((t=>t.dataset.value===e));if(c){if(null!=e&&f.has(e))return;if(t&&null!=e){const t=new Set(f);t.add(e);const a=l.filter((e=>t.has(e.dataset.value)));b(a)}}else y(t)},c&&(e.selectAll=E,e.selectNone=w),e.dataset.selectInitialized=!0,e.dispatchEvent(new CustomEvent("basecoat:initialized"))};window.basecoat&&window.basecoat.register("select","div.select:not([data-select-initialized])",e)})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basecoat-css",
3
- "version": "0.3.8",
3
+ "version": "0.3.10-beta.1",
4
4
  "description": "Tailwind CSS for Basecoat components",
5
5
  "author": {
6
6
  "name": "hunvreus",