secure-ui-components 0.2.3 → 0.2.4

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.
@@ -35,592 +35,4 @@
35
35
  *
36
36
  * @module secure-select
37
37
  * @license MIT
38
- */
39
- import { SecureBaseComponent } from '../../core/base-component.js';
40
- /**
41
- * Secure Select Web Component
42
- *
43
- * Provides a security-hardened select dropdown with progressive enhancement.
44
- * The component works as a standard form select without JavaScript and
45
- * enhances with security features when JavaScript is available.
46
- *
47
- * @extends SecureBaseComponent
48
- */
49
- export class SecureSelect extends SecureBaseComponent {
50
- /**
51
- * Select element reference
52
- * @private
53
- */
54
- #selectElement = null;
55
- /**
56
- * Label element reference
57
- * @private
58
- */
59
- #labelElement = null;
60
- /**
61
- * Error container element reference
62
- * @private
63
- */
64
- #errorContainer = null;
65
- /**
66
- * Unique ID for this select instance
67
- * @private
68
- */
69
- #instanceId = `secure-select-${Math.random().toString(36).substring(2, 11)}`;
70
- /**
71
- * Valid option values
72
- * @private
73
- */
74
- #validOptions = new Set();
75
- /**
76
- * Flag to track if options have been transferred from light DOM
77
- * @private
78
- */
79
- #optionsTransferred = false;
80
- /**
81
- * Whether this is a multi-select instance
82
- * @private
83
- */
84
- #isMultiple = false;
85
- /**
86
- * Observed attributes for this component
87
- *
88
- * @static
89
- */
90
- static get observedAttributes() {
91
- return [
92
- ...super.observedAttributes,
93
- 'name',
94
- 'label',
95
- 'required',
96
- 'multiple',
97
- 'size',
98
- 'value'
99
- ];
100
- }
101
- /**
102
- * Constructor
103
- */
104
- constructor() {
105
- super();
106
- }
107
- /**
108
- * Render the select component
109
- *
110
- * Security Note: We use a native <select> element wrapped in our web component
111
- * to ensure progressive enhancement. The native select works without JavaScript,
112
- * and we enhance it with security features when JS is available.
113
- *
114
- * @protected
115
- */
116
- render() {
117
- const fragment = document.createDocumentFragment();
118
- const container = document.createElement('div');
119
- container.className = 'select-container';
120
- container.setAttribute('part', 'container');
121
- // Check if this is a multi-select
122
- this.#isMultiple = this.hasAttribute('multiple');
123
- // Create label
124
- const label = this.getAttribute('label');
125
- if (label) {
126
- this.#labelElement = document.createElement('label');
127
- this.#labelElement.htmlFor = this.#instanceId;
128
- this.#labelElement.textContent = this.sanitizeValue(label);
129
- this.#labelElement.setAttribute('part', 'label');
130
- container.appendChild(this.#labelElement);
131
- }
132
- // Create select wrapper for progressive enhancement
133
- const selectWrapper = document.createElement('div');
134
- selectWrapper.className = 'select-wrapper';
135
- selectWrapper.setAttribute('part', 'wrapper');
136
- // Create the actual select element
137
- this.#selectElement = document.createElement('select');
138
- this.#selectElement.id = this.#instanceId;
139
- this.#selectElement.className = 'select-field';
140
- this.#selectElement.setAttribute('part', 'select');
141
- // Apply attributes from web component to native select
142
- this.#applySelectAttributes();
143
- // Set up event listeners
144
- this.#attachEventListeners();
145
- // Defer transferring options to allow light DOM to be fully parsed
146
- // This handles the case where the component is created before its children
147
- queueMicrotask(() => {
148
- this.#transferOptions();
149
- });
150
- selectWrapper.appendChild(this.#selectElement);
151
- container.appendChild(selectWrapper);
152
- // Create error container
153
- // role="alert" already implies aria-live="assertive" — do not override with polite
154
- this.#errorContainer = document.createElement('div');
155
- this.#errorContainer.className = 'error-container hidden';
156
- this.#errorContainer.setAttribute('role', 'alert');
157
- this.#errorContainer.setAttribute('part', 'error');
158
- this.#errorContainer.id = `${this.#instanceId}-error`;
159
- container.appendChild(this.#errorContainer);
160
- // Add component styles (CSP-compliant via adoptedStyleSheets)
161
- this.addComponentStyles(this.#getComponentStyles());
162
- fragment.appendChild(container);
163
- return fragment;
164
- }
165
- /**
166
- * Apply attributes from the web component to the native select
167
- *
168
- * Security Note: This is where we enforce tier-specific security controls
169
- * like validation rules.
170
- *
171
- * @private
172
- */
173
- #applySelectAttributes() {
174
- const config = this.config;
175
- // Name attribute (required for form submission)
176
- const name = this.getAttribute('name');
177
- if (name) {
178
- this.#selectElement.name = this.sanitizeValue(name);
179
- }
180
- // Accessible name fallback when no visible label is provided
181
- if (!this.getAttribute('label') && name) {
182
- this.#selectElement.setAttribute('aria-label', this.sanitizeValue(name));
183
- }
184
- // Link select to its error container for screen readers
185
- this.#selectElement.setAttribute('aria-describedby', `${this.#instanceId}-error`);
186
- // Required attribute
187
- if (this.hasAttribute('required') || config.validation.required) {
188
- this.#selectElement.required = true;
189
- this.#selectElement.setAttribute('aria-required', 'true');
190
- }
191
- // Multiple selection
192
- if (this.hasAttribute('multiple')) {
193
- this.#selectElement.multiple = true;
194
- }
195
- // Size attribute
196
- const size = this.getAttribute('size');
197
- if (size) {
198
- this.#selectElement.size = parseInt(size, 10);
199
- }
200
- // Disabled state
201
- if (this.hasAttribute('disabled')) {
202
- this.#selectElement.disabled = true;
203
- }
204
- // Autocomplete control
205
- if (!config.storage.allowAutocomplete) {
206
- this.#selectElement.autocomplete = 'off';
207
- }
208
- }
209
- /**
210
- * Transfer option elements from light DOM to select element
211
- *
212
- * Security Note: We sanitize all option values and text to prevent XSS
213
- *
214
- * @private
215
- */
216
- #transferOptions() {
217
- // Only transfer once to avoid clearing programmatically-added options
218
- if (this.#optionsTransferred)
219
- return;
220
- this.#optionsTransferred = true;
221
- // Get option elements from light DOM (original content)
222
- const options = Array.from(this.querySelectorAll('option'));
223
- // If no light DOM options, nothing to transfer
224
- if (options.length === 0)
225
- return;
226
- // Track selected values (supports multiple selected attributes)
227
- const selectedValues = [];
228
- // Transfer each option to the select element
229
- options.forEach((option) => {
230
- const newOption = document.createElement('option');
231
- // Sanitize value
232
- const value = option.getAttribute('value') || '';
233
- newOption.value = this.sanitizeValue(value);
234
- this.#validOptions.add(newOption.value);
235
- // Sanitize text content
236
- newOption.textContent = this.sanitizeValue(option.textContent || '');
237
- // Copy other attributes
238
- if (option.hasAttribute('selected')) {
239
- newOption.selected = true;
240
- selectedValues.push(newOption.value);
241
- }
242
- if (option.hasAttribute('disabled')) {
243
- newOption.disabled = true;
244
- }
245
- this.#selectElement.appendChild(newOption);
246
- });
247
- // Set initial value - attribute takes precedence over selected option
248
- if (!this.#isMultiple) {
249
- const initialValue = this.getAttribute('value');
250
- if (initialValue) {
251
- this.#selectElement.value = initialValue;
252
- }
253
- else if (selectedValues.length > 0) {
254
- this.#selectElement.value = selectedValues[0];
255
- }
256
- }
257
- // For multiple, the selected attributes on individual options already applied
258
- }
259
- /**
260
- * Attach event listeners to the select
261
- *
262
- * @private
263
- */
264
- #attachEventListeners() {
265
- // Focus event - audit logging + telemetry
266
- this.#selectElement.addEventListener('focus', () => {
267
- this.recordTelemetryFocus();
268
- this.audit('select_focused', {
269
- name: this.#selectElement.name
270
- });
271
- });
272
- // Change event - validation, audit logging + telemetry
273
- this.#selectElement.addEventListener('change', (e) => {
274
- this.recordTelemetryInput(e);
275
- this.#handleChange(e);
276
- });
277
- // Blur event - final validation + telemetry
278
- this.#selectElement.addEventListener('blur', () => {
279
- this.recordTelemetryBlur();
280
- this.#validateAndShowErrors();
281
- this.audit('select_blurred', {
282
- name: this.#selectElement.name,
283
- hasValue: this.#isMultiple
284
- ? this.#selectElement.selectedOptions.length > 0
285
- : this.#selectElement.value.length > 0
286
- });
287
- });
288
- }
289
- /**
290
- * Handle change events
291
- *
292
- * Security Note: We validate that the selected value is in the list of valid options
293
- * to prevent value injection attacks.
294
- *
295
- * @private
296
- */
297
- #handleChange(_event) {
298
- if (this.#isMultiple) {
299
- // Multi-select: validate all selected values
300
- const selectedValues = Array.from(this.#selectElement.selectedOptions).map(opt => opt.value);
301
- const invalidValues = selectedValues.filter(v => v && !this.#validOptions.has(v));
302
- if (invalidValues.length > 0) {
303
- this.#showError('Invalid option selected');
304
- this.audit('invalid_option_detected', {
305
- name: this.#selectElement.name,
306
- attemptedValues: invalidValues
307
- });
308
- return;
309
- }
310
- // Clear previous errors
311
- this.#clearErrors();
312
- // Log the change
313
- this.audit('select_changed', {
314
- name: this.#selectElement.name,
315
- values: selectedValues
316
- });
317
- // Dispatch custom event for parent forms
318
- this.dispatchEvent(new CustomEvent('secure-select', {
319
- detail: {
320
- name: this.#selectElement.name,
321
- value: selectedValues,
322
- tier: this.securityTier
323
- },
324
- bubbles: true,
325
- composed: true
326
- }));
327
- }
328
- else {
329
- // Single select: validate the selected value
330
- const selectedValue = this.#selectElement.value;
331
- if (selectedValue && !this.#validOptions.has(selectedValue)) {
332
- this.#showError('Invalid option selected');
333
- this.audit('invalid_option_detected', {
334
- name: this.#selectElement.name,
335
- attemptedValue: selectedValue
336
- });
337
- // Reset to empty value
338
- this.#selectElement.value = '';
339
- return;
340
- }
341
- // Clear previous errors
342
- this.#clearErrors();
343
- // Log the change
344
- this.audit('select_changed', {
345
- name: this.#selectElement.name,
346
- value: selectedValue
347
- });
348
- // Dispatch custom event for parent forms
349
- this.dispatchEvent(new CustomEvent('secure-select', {
350
- detail: {
351
- name: this.#selectElement.name,
352
- value: selectedValue,
353
- tier: this.securityTier
354
- },
355
- bubbles: true,
356
- composed: true
357
- }));
358
- }
359
- }
360
- /**
361
- * Validate the select and show error messages
362
- *
363
- * @private
364
- */
365
- #validateAndShowErrors() {
366
- // Check rate limit first
367
- const rateLimitCheck = this.checkRateLimit();
368
- if (!rateLimitCheck.allowed) {
369
- this.#showError(`Too many attempts. Please wait ${Math.ceil(rateLimitCheck.retryAfter / 1000)} seconds.`);
370
- return;
371
- }
372
- const required = this.hasAttribute('required') || this.config.validation.required;
373
- if (this.#isMultiple) {
374
- // Multi-select: check if at least one option is selected
375
- const selectedValues = Array.from(this.#selectElement.selectedOptions)
376
- .map(opt => opt.value)
377
- .filter(v => v !== '');
378
- if (required && selectedValues.length === 0) {
379
- this.#showError('Please select at least one option');
380
- return;
381
- }
382
- // Validate all selected values are in valid options
383
- const invalidValues = selectedValues.filter(v => !this.#validOptions.has(v));
384
- if (invalidValues.length > 0) {
385
- this.#showError('Invalid option selected');
386
- return;
387
- }
388
- }
389
- else {
390
- // Single select: check required and valid option
391
- if (required && !this.#selectElement.value) {
392
- this.#showError('Please select an option');
393
- return;
394
- }
395
- const selectedValue = this.#selectElement.value;
396
- if (selectedValue && !this.#validOptions.has(selectedValue)) {
397
- this.#showError('Invalid option selected');
398
- return;
399
- }
400
- }
401
- }
402
- /**
403
- * Show error message
404
- *
405
- * @private
406
- */
407
- #showError(message) {
408
- this.#errorContainer.textContent = message;
409
- // Force reflow so browser registers the hidden state with content,
410
- // then remove hidden to trigger the CSS transition
411
- void this.#errorContainer.offsetHeight;
412
- this.#errorContainer.classList.remove('hidden');
413
- this.#selectElement.classList.add('error');
414
- this.#selectElement.setAttribute('aria-invalid', 'true');
415
- }
416
- /**
417
- * Clear error messages
418
- *
419
- * @private
420
- */
421
- #clearErrors() {
422
- // Start the hide animation first, clear text only after transition ends
423
- this.#errorContainer.classList.add('hidden');
424
- this.#errorContainer.addEventListener('transitionend', () => {
425
- if (this.#errorContainer.classList.contains('hidden')) {
426
- this.#errorContainer.textContent = '';
427
- }
428
- }, { once: true });
429
- this.#selectElement.classList.remove('error');
430
- this.#selectElement.removeAttribute('aria-invalid');
431
- }
432
- /**
433
- * Get component-specific styles
434
- *
435
- * @private
436
- */
437
- #getComponentStyles() {
438
- return new URL('./secure-select.css', import.meta.url).href;
439
- }
440
- /**
441
- * Handle attribute changes
442
- *
443
- * @protected
444
- */
445
- handleAttributeChange(name, _oldValue, newValue) {
446
- if (!this.#selectElement)
447
- return;
448
- switch (name) {
449
- case 'disabled':
450
- this.#selectElement.disabled = this.hasAttribute('disabled');
451
- break;
452
- case 'value':
453
- if (newValue !== this.#selectElement.value) {
454
- this.#selectElement.value = newValue || '';
455
- }
456
- break;
457
- }
458
- }
459
- /**
460
- * Get the current value
461
- *
462
- * @public
463
- */
464
- get value() {
465
- if (!this.#selectElement)
466
- return '';
467
- // Multi-select: return comma-separated selected values
468
- if (this.#isMultiple) {
469
- return Array.from(this.#selectElement.selectedOptions)
470
- .map(opt => opt.value)
471
- .filter(v => v !== '')
472
- .join(', ');
473
- }
474
- return this.#selectElement.value;
475
- }
476
- /**
477
- * Set the value
478
- *
479
- * @public
480
- */
481
- set value(value) {
482
- if (!this.#selectElement)
483
- return;
484
- if (this.#isMultiple) {
485
- // Multi-select: accept comma-separated values
486
- const values = value.split(',').map(v => v.trim()).filter(v => v !== '');
487
- // Deselect all first
488
- Array.from(this.#selectElement.options).forEach(opt => { opt.selected = false; });
489
- // Select matching valid options
490
- values.forEach(v => {
491
- if (this.#validOptions.has(v)) {
492
- const opt = Array.from(this.#selectElement.options).find(o => o.value === v);
493
- if (opt)
494
- opt.selected = true;
495
- }
496
- });
497
- }
498
- else {
499
- if (this.#validOptions.has(value)) {
500
- this.#selectElement.value = value;
501
- }
502
- }
503
- }
504
- /**
505
- * Get the select name
506
- *
507
- * @public
508
- */
509
- get name() {
510
- return this.#selectElement ? this.#selectElement.name : '';
511
- }
512
- /**
513
- * Get selected options (for multiple select)
514
- *
515
- * @public
516
- */
517
- get selectedOptions() {
518
- if (!this.#selectElement)
519
- return [];
520
- return Array.from(this.#selectElement.selectedOptions).map(opt => opt.value);
521
- }
522
- /**
523
- * Check if the select is valid
524
- *
525
- * @public
526
- */
527
- get valid() {
528
- const required = this.hasAttribute('required') || this.config.validation.required;
529
- if (this.#isMultiple) {
530
- const selectedValues = Array.from(this.#selectElement.selectedOptions)
531
- .map(opt => opt.value)
532
- .filter(v => v !== '');
533
- if (required && selectedValues.length === 0) {
534
- return false;
535
- }
536
- // All selected values must be valid
537
- return selectedValues.every(v => this.#validOptions.has(v));
538
- }
539
- // Single select
540
- if (required && !this.#selectElement.value) {
541
- return false;
542
- }
543
- const selectedValue = this.#selectElement.value;
544
- if (selectedValue && !this.#validOptions.has(selectedValue)) {
545
- return false;
546
- }
547
- return true;
548
- }
549
- /**
550
- * Focus the select
551
- *
552
- * @public
553
- */
554
- focus() {
555
- if (this.#selectElement) {
556
- this.#selectElement.focus();
557
- }
558
- }
559
- /**
560
- * Blur the select
561
- *
562
- * @public
563
- */
564
- blur() {
565
- if (this.#selectElement) {
566
- this.#selectElement.blur();
567
- }
568
- }
569
- /**
570
- * Add an option programmatically
571
- *
572
- * @public
573
- */
574
- addOption(value, text, selected = false) {
575
- if (!this.#selectElement)
576
- return;
577
- const option = document.createElement('option');
578
- option.value = this.sanitizeValue(value);
579
- option.textContent = this.sanitizeValue(text);
580
- option.selected = selected;
581
- this.#validOptions.add(option.value);
582
- this.#selectElement.appendChild(option);
583
- }
584
- /**
585
- * Remove an option by value
586
- *
587
- * @public
588
- */
589
- removeOption(value) {
590
- if (!this.#selectElement)
591
- return;
592
- const options = Array.from(this.#selectElement.options);
593
- const option = options.find(opt => opt.value === value);
594
- if (option) {
595
- this.#selectElement.removeChild(option);
596
- this.#validOptions.delete(value);
597
- }
598
- }
599
- /**
600
- * Clear all options
601
- *
602
- * @public
603
- */
604
- clearOptions() {
605
- if (!this.#selectElement)
606
- return;
607
- this.#selectElement.innerHTML = '';
608
- this.#validOptions.clear();
609
- }
610
- /**
611
- * Cleanup on disconnect
612
- *
613
- * Note: We intentionally do NOT clear #validOptions here.
614
- * When <secure-select> is inside a <secure-form>, the form moves its children
615
- * into a <form> element, which triggers disconnect/reconnect. Clearing
616
- * #validOptions on disconnect would leave the set empty after reconnect,
617
- * causing all subsequent selections to be rejected as "invalid option".
618
- */
619
- disconnectedCallback() {
620
- super.disconnectedCallback();
621
- }
622
- }
623
- // Define the custom element
624
- customElements.define('secure-select', SecureSelect);
625
- export default SecureSelect;
626
- //# sourceMappingURL=secure-select.js.map
38
+ */import{SecureBaseComponent as n}from"../../core/base-component.js";class a extends n{#e=null;#a=null;#t=null;#n=`secure-select-${Math.random().toString(36).substring(2,11)}`;#i=new Set;#l=!1;#s=!1;static get observedAttributes(){return[...super.observedAttributes,"name","label","required","multiple","size","value"]}constructor(){super()}render(){const t=document.createDocumentFragment(),i=document.createElement("div");i.className="select-container",i.setAttribute("part","container"),this.#s=this.hasAttribute("multiple");const e=this.getAttribute("label");e&&(this.#a=document.createElement("label"),this.#a.htmlFor=this.#n,this.#a.textContent=this.sanitizeValue(e),this.#a.setAttribute("part","label"),i.appendChild(this.#a));const s=document.createElement("div");return s.className="select-wrapper",s.setAttribute("part","wrapper"),this.#e=document.createElement("select"),this.#e.id=this.#n,this.#e.className="select-field",this.#e.setAttribute("part","select"),this.#u(),this.#d(),queueMicrotask(()=>{this.#o()}),s.appendChild(this.#e),i.appendChild(s),this.#t=document.createElement("div"),this.#t.className="error-container hidden",this.#t.setAttribute("role","alert"),this.#t.setAttribute("part","error"),this.#t.id=`${this.#n}-error`,i.appendChild(this.#t),this.addComponentStyles(this.#f()),t.appendChild(i),t}#u(){const t=this.config,i=this.getAttribute("name");i&&(this.#e.name=this.sanitizeValue(i)),!this.getAttribute("label")&&i&&this.#e.setAttribute("aria-label",this.sanitizeValue(i)),this.#e.setAttribute("aria-describedby",`${this.#n}-error`),(this.hasAttribute("required")||t.validation.required)&&(this.#e.required=!0,this.#e.setAttribute("aria-required","true")),this.hasAttribute("multiple")&&(this.#e.multiple=!0);const e=this.getAttribute("size");e&&(this.#e.size=parseInt(e,10)),this.hasAttribute("disabled")&&(this.#e.disabled=!0),t.storage.allowAutocomplete||(this.#e.autocomplete="off")}#o(){if(this.#l)return;this.#l=!0;const t=Array.from(this.querySelectorAll("option"));if(t.length===0)return;const i=[];if(t.forEach(e=>{const s=document.createElement("option"),r=e.getAttribute("value")||"";s.value=this.sanitizeValue(r),this.#i.add(s.value),s.textContent=this.sanitizeValue(e.textContent||""),e.hasAttribute("selected")&&(s.selected=!0,i.push(s.value)),e.hasAttribute("disabled")&&(s.disabled=!0),this.#e.appendChild(s)}),!this.#s){const e=this.getAttribute("value");e?this.#e.value=e:i.length>0&&(this.#e.value=i[0])}}#d(){this.#e.addEventListener("focus",()=>{this.recordTelemetryFocus(),this.audit("select_focused",{name:this.#e.name})}),this.#e.addEventListener("change",t=>{this.recordTelemetryInput(t),this.#c(t)}),this.#e.addEventListener("blur",()=>{this.recordTelemetryBlur(),this.#m(),this.audit("select_blurred",{name:this.#e.name,hasValue:this.#s?this.#e.selectedOptions.length>0:this.#e.value.length>0})})}#c(t){if(this.#s){const i=Array.from(this.#e.selectedOptions).map(s=>s.value),e=i.filter(s=>s&&!this.#i.has(s));if(e.length>0){this.#r("Invalid option selected"),this.audit("invalid_option_detected",{name:this.#e.name,attemptedValues:e});return}this.#h(),this.audit("select_changed",{name:this.#e.name,values:i}),this.dispatchEvent(new CustomEvent("secure-select",{detail:{name:this.#e.name,value:i,tier:this.securityTier},bubbles:!0,composed:!0}))}else{const i=this.#e.value;if(i&&!this.#i.has(i)){this.#r("Invalid option selected"),this.audit("invalid_option_detected",{name:this.#e.name,attemptedValue:i}),this.#e.value="";return}this.#h(),this.audit("select_changed",{name:this.#e.name,value:i}),this.dispatchEvent(new CustomEvent("secure-select",{detail:{name:this.#e.name,value:i,tier:this.securityTier},bubbles:!0,composed:!0}))}}#m(){const t=this.checkRateLimit();if(!t.allowed){this.#r(`Too many attempts. Please wait ${Math.ceil(t.retryAfter/1e3)} seconds.`);return}const i=this.hasAttribute("required")||this.config.validation.required;if(this.#s){const e=Array.from(this.#e.selectedOptions).map(r=>r.value).filter(r=>r!=="");if(i&&e.length===0){this.#r("Please select at least one option");return}if(e.filter(r=>!this.#i.has(r)).length>0){this.#r("Invalid option selected");return}}else{if(i&&!this.#e.value){this.#r("Please select an option");return}const e=this.#e.value;if(e&&!this.#i.has(e)){this.#r("Invalid option selected");return}}}#r(t){this.#t.textContent=t,this.#t.offsetHeight,this.#t.classList.remove("hidden"),this.#e.classList.add("error"),this.#e.setAttribute("aria-invalid","true")}#h(){this.#t.classList.add("hidden"),this.#t.addEventListener("transitionend",()=>{this.#t.classList.contains("hidden")&&(this.#t.textContent="")},{once:!0}),this.#e.classList.remove("error"),this.#e.removeAttribute("aria-invalid")}#f(){return new URL("./secure-select.css",import.meta.url).href}handleAttributeChange(t,i,e){if(this.#e)switch(t){case"disabled":this.#e.disabled=this.hasAttribute("disabled");break;case"value":e!==this.#e.value&&(this.#e.value=e||"");break}}get value(){return this.#e?this.#s?Array.from(this.#e.selectedOptions).map(t=>t.value).filter(t=>t!=="").join(", "):this.#e.value:""}set value(t){if(this.#e)if(this.#s){const i=t.split(",").map(e=>e.trim()).filter(e=>e!=="");Array.from(this.#e.options).forEach(e=>{e.selected=!1}),i.forEach(e=>{if(this.#i.has(e)){const s=Array.from(this.#e.options).find(r=>r.value===e);s&&(s.selected=!0)}})}else this.#i.has(t)&&(this.#e.value=t)}get name(){return this.#e?this.#e.name:""}get selectedOptions(){return this.#e?Array.from(this.#e.selectedOptions).map(t=>t.value):[]}get valid(){const t=this.hasAttribute("required")||this.config.validation.required;if(this.#s){const e=Array.from(this.#e.selectedOptions).map(s=>s.value).filter(s=>s!=="");return t&&e.length===0?!1:e.every(s=>this.#i.has(s))}if(t&&!this.#e.value)return!1;const i=this.#e.value;return!(i&&!this.#i.has(i))}focus(){this.#e&&this.#e.focus()}blur(){this.#e&&this.#e.blur()}addOption(t,i,e=!1){if(!this.#e)return;const s=document.createElement("option");s.value=this.sanitizeValue(t),s.textContent=this.sanitizeValue(i),s.selected=e,this.#i.add(s.value),this.#e.appendChild(s)}removeOption(t){if(!this.#e)return;const e=Array.from(this.#e.options).find(s=>s.value===t);e&&(this.#e.removeChild(e),this.#i.delete(t))}clearOptions(){this.#e&&(this.#e.innerHTML="",this.#i.clear())}disconnectedCallback(){super.disconnectedCallback()}}customElements.define("secure-select",a);var u=a;export{a as SecureSelect,u as default};