tyrell-components 1.0.0-RC7 → 1.0.0-RC8
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/tyrell.js +1 -1
- package/lib/components/button.d.ts +9 -0
- package/lib/components/button.d.ts.map +1 -1
- package/lib/components/button.js +34 -1
- package/lib/components/button.js.map +1 -1
- package/lib/components/copy.js +1 -1
- package/lib/components/date-picker.js +1 -1
- package/lib/components/dropdown.d.ts +35 -13
- package/lib/components/dropdown.d.ts.map +1 -1
- package/lib/components/dropdown.js +130 -42
- package/lib/components/dropdown.js.map +1 -1
- package/lib/components/file-upload.d.ts +121 -0
- package/lib/components/file-upload.d.ts.map +1 -0
- package/lib/components/file-upload.js +441 -0
- package/lib/components/file-upload.js.map +1 -0
- package/lib/components/input.js +3 -3
- package/lib/components/input.js.map +1 -1
- package/lib/components/multiselect.d.ts +22 -22
- package/lib/components/multiselect.d.ts.map +1 -1
- package/lib/components/multiselect.js +123 -108
- package/lib/components/multiselect.js.map +1 -1
- package/lib/components/textarea.js +1 -1
- package/lib/components/wizard.js +9 -9
- package/lib/components/wizard.js.map +1 -1
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/styles/button.d.ts +1 -1
- package/lib/styles/button.d.ts.map +1 -1
- package/lib/styles/button.js +41 -0
- package/lib/styles/button.js.map +1 -1
- package/lib/styles/checkbox.d.ts +1 -1
- package/lib/styles/checkbox.d.ts.map +1 -1
- package/lib/styles/date-picker.d.ts +1 -1
- package/lib/styles/date-picker.d.ts.map +1 -1
- package/lib/styles/date-picker.js +7 -4
- package/lib/styles/date-picker.js.map +1 -1
- package/lib/styles/dropdown.d.ts +1 -1
- package/lib/styles/dropdown.d.ts.map +1 -1
- package/lib/styles/dropdown.js +104 -6
- package/lib/styles/dropdown.js.map +1 -1
- package/lib/styles/file-upload.d.ts +2 -0
- package/lib/styles/file-upload.d.ts.map +1 -0
- package/lib/styles/file-upload.js +241 -0
- package/lib/styles/file-upload.js.map +1 -0
- package/lib/styles/input.d.ts +1 -1
- package/lib/styles/input.d.ts.map +1 -1
- package/lib/styles/input.js +2 -2
- package/lib/styles/multiselect.d.ts +1 -1
- package/lib/styles/multiselect.d.ts.map +1 -1
- package/lib/styles/multiselect.js +147 -96
- package/lib/styles/multiselect.js.map +1 -1
- package/lib/styles/radio.d.ts +1 -1
- package/lib/styles/radio.d.ts.map +1 -1
- package/lib/styles/step.d.ts +1 -1
- package/lib/styles/step.d.ts.map +1 -1
- package/lib/styles/step.js +3 -3
- package/lib/styles/switch.d.ts +1 -1
- package/lib/styles/switch.d.ts.map +1 -1
- package/lib/styles/tag.d.ts +1 -1
- package/lib/styles/tag.d.ts.map +1 -1
- package/lib/styles/tag.js +1 -12
- package/lib/styles/tag.js.map +1 -1
- package/lib/styles/textarea.d.ts +1 -1
- package/lib/styles/textarea.js +1 -1
- package/lib/styles/wizard.d.ts +10 -15
- package/lib/styles/wizard.d.ts.map +1 -1
- package/lib/styles/wizard.js +149 -98
- package/lib/styles/wizard.js.map +1 -1
- package/lib/utils/loader-registry.d.ts +35 -0
- package/lib/utils/loader-registry.d.ts.map +1 -0
- package/lib/utils/loader-registry.js +43 -0
- package/lib/utils/loader-registry.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +5 -1
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { ensureStyles } from '../utils/styles.js';
|
|
44
44
|
import { multiselectStyles } from '../styles/multiselect.js';
|
|
45
|
+
import { getLoaderSvg } from '../utils/loader-registry.js';
|
|
45
46
|
import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
|
|
46
47
|
import { isMobileTouch } from '../utils/mobile.js';
|
|
47
48
|
import { TyComponent } from '../base/ty-component.js';
|
|
@@ -90,19 +91,11 @@ const CHEVRON_DOWN_SVG = `<svg viewBox="0 0 20 20" fill="currentColor">
|
|
|
90
91
|
* Ty Multiselect Component
|
|
91
92
|
*/
|
|
92
93
|
export class TyMultiselect extends TyComponent {
|
|
93
|
-
// Debug: Log when expandedSection changes
|
|
94
|
-
set expandedSection(value) {
|
|
95
|
-
this._state.expandedSection = value;
|
|
96
|
-
}
|
|
97
|
-
get expandedSection() {
|
|
98
|
-
return this._state.expandedSection;
|
|
99
|
-
}
|
|
100
94
|
constructor() {
|
|
101
95
|
super(); // TyComponent handles attachInternals() and attachShadow()
|
|
102
96
|
// ============================================================================
|
|
103
97
|
// INTERNAL STATE
|
|
104
98
|
// ============================================================================
|
|
105
|
-
this._value = '';
|
|
106
99
|
this._name = '';
|
|
107
100
|
this._placeholder = 'Select options...';
|
|
108
101
|
this._label = '';
|
|
@@ -110,12 +103,11 @@ export class TyMultiselect extends TyComponent {
|
|
|
110
103
|
this._readonly = false;
|
|
111
104
|
this._required = false;
|
|
112
105
|
this._externalSearch = false;
|
|
106
|
+
this._loading = false;
|
|
113
107
|
this._scrollLockId = null;
|
|
114
108
|
this._size = 'md';
|
|
115
|
-
this._flavor = 'neutral';
|
|
116
109
|
this._selectedLabel = 'Selected';
|
|
117
110
|
this._availableLabel = 'Available';
|
|
118
|
-
this._noSelectionMessage = 'No items selected';
|
|
119
111
|
this._noOptionsMessage = 'No options available';
|
|
120
112
|
// Component state
|
|
121
113
|
this._state = {
|
|
@@ -124,12 +116,10 @@ export class TyMultiselect extends TyComponent {
|
|
|
124
116
|
highlightedIndex: -1,
|
|
125
117
|
filteredTags: [],
|
|
126
118
|
selectedValues: [],
|
|
127
|
-
mode: 'desktop'
|
|
128
|
-
expandedSection: 'selected' // Will be corrected by toggleSection call
|
|
119
|
+
mode: 'desktop' // Updated dynamically on render via syncMode()
|
|
129
120
|
};
|
|
130
121
|
// Event handler references for cleanup
|
|
131
122
|
this._stubClickHandler = null;
|
|
132
|
-
this._outsideClickHandler = null;
|
|
133
123
|
this._tagClickHandler = null;
|
|
134
124
|
this._tagDismissHandler = null;
|
|
135
125
|
this._searchInputHandler = null;
|
|
@@ -140,6 +130,9 @@ export class TyMultiselect extends TyComponent {
|
|
|
140
130
|
this._searchDebounceTimer = null;
|
|
141
131
|
// Custom scrollbar for options list
|
|
142
132
|
this._optionsScrollbar = null;
|
|
133
|
+
// MutationObserver for light-DOM children — re-syncs selected tags' visual
|
|
134
|
+
// state when consumers swap tag children (external-search refresh pattern).
|
|
135
|
+
this._childObserver = null;
|
|
143
136
|
const shadow = this.shadowRoot;
|
|
144
137
|
ensureStyles(shadow, { css: multiselectStyles, id: 'ty-multiselect' });
|
|
145
138
|
// DON'T render here - wait for onConnect() to initialize values first
|
|
@@ -166,6 +159,16 @@ export class TyMultiselect extends TyComponent {
|
|
|
166
159
|
this.initializeState();
|
|
167
160
|
// Visual updates happen automatically via onPropertiesChanged
|
|
168
161
|
});
|
|
162
|
+
// Observe light-DOM children — re-sync selected state when consumers swap
|
|
163
|
+
// tag children (external-search refresh). syncSelectedTags is idempotent
|
|
164
|
+
// (only acts on tags whose desired-vs-actual selected state differs), so
|
|
165
|
+
// spurious firings caused by our own re-slot work are no-ops.
|
|
166
|
+
this._childObserver = new MutationObserver(() => {
|
|
167
|
+
if (this._state.selectedValues.length > 0) {
|
|
168
|
+
this.syncSelectedTags(this._state.selectedValues);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
this._childObserver.observe(this, { childList: true });
|
|
169
172
|
}
|
|
170
173
|
/**
|
|
171
174
|
* Called when component is disconnected from DOM
|
|
@@ -192,6 +195,11 @@ export class TyMultiselect extends TyComponent {
|
|
|
192
195
|
}
|
|
193
196
|
// Cleanup custom scrollbar
|
|
194
197
|
this._destroyOptionsScrollbar();
|
|
198
|
+
// Disconnect children observer
|
|
199
|
+
if (this._childObserver) {
|
|
200
|
+
this._childObserver.disconnect();
|
|
201
|
+
this._childObserver = null;
|
|
202
|
+
}
|
|
195
203
|
}
|
|
196
204
|
/**
|
|
197
205
|
* Called when properties change
|
|
@@ -201,7 +209,6 @@ export class TyMultiselect extends TyComponent {
|
|
|
201
209
|
for (const { name, newValue } of changes) {
|
|
202
210
|
switch (name) {
|
|
203
211
|
case 'value':
|
|
204
|
-
this._value = newValue || '';
|
|
205
212
|
const selectedValues = this.parseValue(newValue);
|
|
206
213
|
this._state.selectedValues = selectedValues;
|
|
207
214
|
// CRITICAL: Only sync tags if we're connected and tags exist
|
|
@@ -240,9 +247,6 @@ export class TyMultiselect extends TyComponent {
|
|
|
240
247
|
case 'size':
|
|
241
248
|
this._size = newValue;
|
|
242
249
|
break;
|
|
243
|
-
case 'flavor':
|
|
244
|
-
this._flavor = newValue;
|
|
245
|
-
break;
|
|
246
250
|
case 'debounce':
|
|
247
251
|
this._debounce = newValue;
|
|
248
252
|
break;
|
|
@@ -252,15 +256,40 @@ export class TyMultiselect extends TyComponent {
|
|
|
252
256
|
case 'available-label':
|
|
253
257
|
this._availableLabel = newValue || 'Available';
|
|
254
258
|
break;
|
|
255
|
-
case 'no-selection-message':
|
|
256
|
-
this._noSelectionMessage = newValue || 'No items selected';
|
|
257
|
-
break;
|
|
258
259
|
case 'no-options-message':
|
|
259
260
|
this._noOptionsMessage = newValue || 'No options available';
|
|
260
261
|
break;
|
|
262
|
+
case 'loading':
|
|
263
|
+
this._loading = newValue;
|
|
264
|
+
this.applyLoadingState();
|
|
265
|
+
break;
|
|
261
266
|
}
|
|
262
267
|
}
|
|
263
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Toggle the loading visual state on the open popup.
|
|
271
|
+
* Replaces the available-options area with a centered spinner; search input stays usable.
|
|
272
|
+
* Pulls the latest registered loader SVG on each call so registry changes
|
|
273
|
+
* take effect on the next loading toggle.
|
|
274
|
+
*/
|
|
275
|
+
applyLoadingState() {
|
|
276
|
+
const shadow = this.shadowRoot;
|
|
277
|
+
if (!shadow)
|
|
278
|
+
return;
|
|
279
|
+
const svg = this._loading ? getLoaderSvg() : null;
|
|
280
|
+
shadow.querySelectorAll('.dropdown-options-wrapper').forEach((wrapper) => {
|
|
281
|
+
wrapper.classList.toggle('loading', this._loading);
|
|
282
|
+
if (this._loading) {
|
|
283
|
+
wrapper.setAttribute('aria-busy', 'true');
|
|
284
|
+
const spinner = wrapper.querySelector('.dropdown-loading-spinner');
|
|
285
|
+
if (spinner && svg)
|
|
286
|
+
spinner.innerHTML = svg;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
wrapper.removeAttribute('aria-busy');
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
264
293
|
/**
|
|
265
294
|
* Get the form value for this component
|
|
266
295
|
* Returns FormData with multiple entries (HTMX standard)
|
|
@@ -419,7 +448,6 @@ export class TyMultiselect extends TyComponent {
|
|
|
419
448
|
const valueStr = newValues.join(',');
|
|
420
449
|
// Only update if changed
|
|
421
450
|
if (JSON.stringify(newValues.sort()) !== JSON.stringify(oldValues.sort())) {
|
|
422
|
-
const currentPropertyValue = this.getProperty('value');
|
|
423
451
|
// Use TyComponent's property system - this will trigger:
|
|
424
452
|
// 1. onPropertiesChanged() → syncs tags via syncSelectedTags()
|
|
425
453
|
// 2. onPropertiesChanged() → updates placeholder via updateSelectionDisplay()
|
|
@@ -561,6 +589,8 @@ export class TyMultiselect extends TyComponent {
|
|
|
561
589
|
if (searchInput) {
|
|
562
590
|
setTimeout(() => searchInput.focus(), 100);
|
|
563
591
|
}
|
|
592
|
+
// Lifecycle event (also fires empty search if external-search)
|
|
593
|
+
this.fireOpenEvent();
|
|
564
594
|
}
|
|
565
595
|
/**
|
|
566
596
|
* Close dropdown dialog (desktop mode)
|
|
@@ -619,6 +649,8 @@ export class TyMultiselect extends TyComponent {
|
|
|
619
649
|
this.updateTagVisibility(allTags, allTags);
|
|
620
650
|
this.clearHighlights(allTags);
|
|
621
651
|
}
|
|
652
|
+
// Lifecycle event
|
|
653
|
+
this.fireCloseEvent();
|
|
622
654
|
}
|
|
623
655
|
/**
|
|
624
656
|
* Open mobile modal (mobile mode)
|
|
@@ -651,13 +683,12 @@ export class TyMultiselect extends TyComponent {
|
|
|
651
683
|
// Small delay to ensure dialog is ready
|
|
652
684
|
setTimeout(() => searchInput.focus(), 100);
|
|
653
685
|
}
|
|
654
|
-
// Initialize sections (available expanded by default)
|
|
655
|
-
this._state.expandedSection = 'available';
|
|
656
|
-
this.syncSectionStates();
|
|
657
686
|
// Update state after slots are ready
|
|
658
687
|
requestAnimationFrame(() => {
|
|
659
688
|
this.updateMobileSelectedState();
|
|
660
689
|
});
|
|
690
|
+
// Lifecycle event (also fires empty search if external-search)
|
|
691
|
+
this.fireOpenEvent();
|
|
661
692
|
}
|
|
662
693
|
/**
|
|
663
694
|
* Close mobile modal (mobile mode)
|
|
@@ -706,6 +737,8 @@ export class TyMultiselect extends TyComponent {
|
|
|
706
737
|
const allTags = this.getTagElements();
|
|
707
738
|
allTags.forEach(el => el.removeAttribute('hidden'));
|
|
708
739
|
}
|
|
740
|
+
// Lifecycle event
|
|
741
|
+
this.fireCloseEvent();
|
|
709
742
|
}
|
|
710
743
|
// ============================================================================
|
|
711
744
|
// EVENT HANDLERS (Phase 5 & 6)
|
|
@@ -722,18 +755,6 @@ export class TyMultiselect extends TyComponent {
|
|
|
722
755
|
}
|
|
723
756
|
this.openDropdown();
|
|
724
757
|
}
|
|
725
|
-
handleOutsideClick(e) {
|
|
726
|
-
if (!this._state.open)
|
|
727
|
-
return;
|
|
728
|
-
const target = e.target;
|
|
729
|
-
// Check if click is inside this component (handles shadow DOM retargeting)
|
|
730
|
-
// Also check composedPath for clicks that originated inside shadow DOM
|
|
731
|
-
const path = e.composedPath();
|
|
732
|
-
const clickedInside = path.includes(this);
|
|
733
|
-
if (!clickedInside) {
|
|
734
|
-
this.closeDropdown();
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
758
|
handleTagClick(e) {
|
|
738
759
|
const target = e.target;
|
|
739
760
|
// Find the ty-tag element
|
|
@@ -799,8 +820,10 @@ export class TyMultiselect extends TyComponent {
|
|
|
799
820
|
this._state.highlightedIndex = -1;
|
|
800
821
|
// Update visibility
|
|
801
822
|
this.updateTagVisibility(filtered, allTags);
|
|
802
|
-
// Hide options area if no results
|
|
823
|
+
// Hide options area if no results (desktop)
|
|
803
824
|
this.updateOptionsVisibility(filtered.length > 0);
|
|
825
|
+
// Refresh mobile count + empty-state to reflect filtered visibility
|
|
826
|
+
this.updateMobileSelectedState();
|
|
804
827
|
// Clear highlights
|
|
805
828
|
this.clearHighlights(allTags);
|
|
806
829
|
}
|
|
@@ -978,6 +1001,28 @@ export class TyMultiselect extends TyComponent {
|
|
|
978
1001
|
composed: true
|
|
979
1002
|
}));
|
|
980
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Dispatch lifecycle events for popup open/close.
|
|
1006
|
+
* On open with external-search, also fire a `search` event with an empty
|
|
1007
|
+
* query so consumers have a clean hook to reset/refetch the option list.
|
|
1008
|
+
*/
|
|
1009
|
+
fireOpenEvent() {
|
|
1010
|
+
this.dispatchEvent(new CustomEvent('open', {
|
|
1011
|
+
detail: { mode: this._state.mode, element: this },
|
|
1012
|
+
bubbles: true,
|
|
1013
|
+
composed: true
|
|
1014
|
+
}));
|
|
1015
|
+
if (this._externalSearch) {
|
|
1016
|
+
this.fireSearchEvent('');
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
fireCloseEvent() {
|
|
1020
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
1021
|
+
detail: { mode: this._state.mode, element: this },
|
|
1022
|
+
bubbles: true,
|
|
1023
|
+
composed: true
|
|
1024
|
+
}));
|
|
1025
|
+
}
|
|
981
1026
|
// ============================================================================
|
|
982
1027
|
// CHANGE EVENT DISPATCHING (Phase 5)
|
|
983
1028
|
// ============================================================================
|
|
@@ -1007,6 +1052,8 @@ export class TyMultiselect extends TyComponent {
|
|
|
1007
1052
|
else {
|
|
1008
1053
|
this.renderDesktop();
|
|
1009
1054
|
}
|
|
1055
|
+
// Loading wrapper is rendered dynamically — re-apply each render
|
|
1056
|
+
this.applyLoadingState();
|
|
1010
1057
|
}
|
|
1011
1058
|
/**
|
|
1012
1059
|
* Setup event listeners
|
|
@@ -1068,14 +1115,14 @@ export class TyMultiselect extends TyComponent {
|
|
|
1068
1115
|
if (!shadow.querySelector('.multiselect-container')) {
|
|
1069
1116
|
const stubClasses = this.buildStubClasses();
|
|
1070
1117
|
const labelHtml = this._label ? `
|
|
1071
|
-
<label class="
|
|
1118
|
+
<label class="ty-field-label">
|
|
1072
1119
|
${this._label}
|
|
1073
1120
|
${this._required ? `<span class="required-icon">${REQUIRED_ICON_SVG}</span>` : ''}
|
|
1074
1121
|
</label>
|
|
1075
1122
|
` : '';
|
|
1076
1123
|
const searchPlaceholder = 'Search...';
|
|
1077
1124
|
shadow.innerHTML = `
|
|
1078
|
-
<div class="multiselect-container dropdown-mode-
|
|
1125
|
+
<div class="multiselect-container dropdown-mode-desktop">
|
|
1079
1126
|
${labelHtml}
|
|
1080
1127
|
<div class="dropdown-wrapper">
|
|
1081
1128
|
<div class="dropdown-stub multiselect-stub ${stubClasses}"
|
|
@@ -1105,6 +1152,12 @@ export class TyMultiselect extends TyComponent {
|
|
|
1105
1152
|
<div class="dropdown-options">
|
|
1106
1153
|
<slot id="options-slot"></slot>
|
|
1107
1154
|
</div>
|
|
1155
|
+
<div class="dropdown-loading" aria-hidden="true">
|
|
1156
|
+
<slot name="loading">
|
|
1157
|
+
<span class="dropdown-loading-spinner"></span>
|
|
1158
|
+
<span class="dropdown-loading-text">Searching…</span>
|
|
1159
|
+
</slot>
|
|
1160
|
+
</div>
|
|
1108
1161
|
</div>
|
|
1109
1162
|
</dialog>
|
|
1110
1163
|
</div>
|
|
@@ -1128,7 +1181,7 @@ export class TyMultiselect extends TyComponent {
|
|
|
1128
1181
|
if (!shadow.querySelector('.multiselect-container')) {
|
|
1129
1182
|
const stubClasses = this.buildStubClasses();
|
|
1130
1183
|
const labelHtml = this._label ? `
|
|
1131
|
-
<label class="
|
|
1184
|
+
<label class="ty-field-label">
|
|
1132
1185
|
${this._label}
|
|
1133
1186
|
${this._required ? `<span class="required-icon">${REQUIRED_ICON_SVG}</span>` : ''}
|
|
1134
1187
|
</label>
|
|
@@ -1179,32 +1232,36 @@ export class TyMultiselect extends TyComponent {
|
|
|
1179
1232
|
<!-- HEADER (matches dropdown.ts) -->
|
|
1180
1233
|
${searchHeaderHtml}
|
|
1181
1234
|
|
|
1182
|
-
<!-- BODY
|
|
1235
|
+
<!-- BODY: pinned selected strip + filter list -->
|
|
1183
1236
|
<div class="mobile-body">
|
|
1184
|
-
|
|
1185
|
-
<!-- SELECTED
|
|
1186
|
-
<div class="mobile-selected-section" data-
|
|
1237
|
+
|
|
1238
|
+
<!-- SELECTED STRIP (pinned, collapses when empty) -->
|
|
1239
|
+
<div class="mobile-selected-section" data-empty="true">
|
|
1187
1240
|
<div class="section-header">
|
|
1188
1241
|
<span class="section-title">${this._selectedLabel} <span class="section-count">(0)</span></span>
|
|
1189
|
-
<span class="section-chevron">${CHEVRON_DOWN_SVG}</span>
|
|
1190
1242
|
</div>
|
|
1191
1243
|
<div class="section-content">
|
|
1192
1244
|
<slot id="mobile-slot" name="selected"></slot>
|
|
1193
|
-
<div class="empty-state">${this._noSelectionMessage}</div>
|
|
1194
1245
|
</div>
|
|
1195
1246
|
</div>
|
|
1196
|
-
|
|
1197
|
-
<!-- AVAILABLE
|
|
1198
|
-
<div class="mobile-available-section" data-
|
|
1247
|
+
|
|
1248
|
+
<!-- AVAILABLE LIST (always visible, takes remaining space) -->
|
|
1249
|
+
<div class="mobile-available-section" data-empty="false">
|
|
1199
1250
|
<div class="section-header">
|
|
1200
1251
|
<span class="section-title">${this._availableLabel}</span>
|
|
1201
1252
|
</div>
|
|
1202
|
-
<div class="section-content">
|
|
1253
|
+
<div class="section-content dropdown-options-wrapper">
|
|
1203
1254
|
<slot id="options-slot"></slot>
|
|
1204
1255
|
<div class="empty-state">${this._noOptionsMessage}</div>
|
|
1256
|
+
<div class="dropdown-loading" aria-hidden="true">
|
|
1257
|
+
<slot name="loading">
|
|
1258
|
+
<span class="dropdown-loading-spinner"></span>
|
|
1259
|
+
<span class="dropdown-loading-text">Searching…</span>
|
|
1260
|
+
</slot>
|
|
1261
|
+
</div>
|
|
1205
1262
|
</div>
|
|
1206
1263
|
</div>
|
|
1207
|
-
|
|
1264
|
+
|
|
1208
1265
|
</div>
|
|
1209
1266
|
</div>
|
|
1210
1267
|
</dialog>
|
|
@@ -1228,9 +1285,6 @@ export class TyMultiselect extends TyComponent {
|
|
|
1228
1285
|
const searchInput = shadow.querySelector('.mobile-search-input');
|
|
1229
1286
|
const closeButton = shadow.querySelector('.mobile-close-button');
|
|
1230
1287
|
const dialog = shadow.querySelector('.mobile-dialog');
|
|
1231
|
-
// Get both section headers
|
|
1232
|
-
const selectedHeader = shadow.querySelector('.mobile-selected-section .section-header');
|
|
1233
|
-
const availableHeader = shadow.querySelector('.mobile-available-section .section-header');
|
|
1234
1288
|
if (stub) {
|
|
1235
1289
|
stub.addEventListener('click', (e) => this.handleMobileStubClick(e));
|
|
1236
1290
|
}
|
|
@@ -1242,13 +1296,6 @@ export class TyMultiselect extends TyComponent {
|
|
|
1242
1296
|
if (searchInput) {
|
|
1243
1297
|
searchInput.addEventListener('input', (e) => this.handleSearchInput(e));
|
|
1244
1298
|
}
|
|
1245
|
-
// Toggle section expansion on header click
|
|
1246
|
-
if (selectedHeader) {
|
|
1247
|
-
selectedHeader.addEventListener('click', () => this.toggleSection('selected'));
|
|
1248
|
-
}
|
|
1249
|
-
if (availableHeader) {
|
|
1250
|
-
availableHeader.addEventListener('click', () => this.toggleSection('available'));
|
|
1251
|
-
}
|
|
1252
1299
|
// Close button click
|
|
1253
1300
|
if (closeButton) {
|
|
1254
1301
|
closeButton.addEventListener('click', () => this.closeMobileModal());
|
|
@@ -1294,43 +1341,6 @@ export class TyMultiselect extends TyComponent {
|
|
|
1294
1341
|
// It already handles mobile mode for auto-close
|
|
1295
1342
|
this.handleTagClick(e);
|
|
1296
1343
|
}
|
|
1297
|
-
/**
|
|
1298
|
-
* Toggle section expansion (mobile only)
|
|
1299
|
-
* Clicking expanded section collapses it and expands the other
|
|
1300
|
-
* Clicking collapsed section expands it and collapses the other
|
|
1301
|
-
*/
|
|
1302
|
-
toggleSection(section) {
|
|
1303
|
-
const shadow = this.shadowRoot;
|
|
1304
|
-
const selectedSection = shadow.querySelector('.mobile-selected-section');
|
|
1305
|
-
const availableSection = shadow.querySelector('.mobile-available-section');
|
|
1306
|
-
if (!selectedSection || !availableSection)
|
|
1307
|
-
return;
|
|
1308
|
-
// If clicking already expanded section, collapse it and expand the other
|
|
1309
|
-
if (this._state.expandedSection === section) {
|
|
1310
|
-
const otherSection = section === 'selected' ? 'available' : 'selected';
|
|
1311
|
-
this._state.expandedSection = otherSection;
|
|
1312
|
-
}
|
|
1313
|
-
else {
|
|
1314
|
-
// Expand clicked section
|
|
1315
|
-
this._state.expandedSection = section;
|
|
1316
|
-
}
|
|
1317
|
-
// Update DOM attributes
|
|
1318
|
-
selectedSection.setAttribute('data-expanded', String(this._state.expandedSection === 'selected'));
|
|
1319
|
-
availableSection.setAttribute('data-expanded', String(this._state.expandedSection === 'available'));
|
|
1320
|
-
}
|
|
1321
|
-
/**
|
|
1322
|
-
* Sync section states to DOM without toggle logic
|
|
1323
|
-
*/
|
|
1324
|
-
syncSectionStates() {
|
|
1325
|
-
const shadow = this.shadowRoot;
|
|
1326
|
-
const selectedSection = shadow.querySelector('.mobile-selected-section');
|
|
1327
|
-
const availableSection = shadow.querySelector('.mobile-available-section');
|
|
1328
|
-
if (!selectedSection || !availableSection)
|
|
1329
|
-
return;
|
|
1330
|
-
// Update DOM attributes to match current state
|
|
1331
|
-
selectedSection.setAttribute('data-expanded', String(this._state.expandedSection === 'selected'));
|
|
1332
|
-
availableSection.setAttribute('data-expanded', String(this._state.expandedSection === 'available'));
|
|
1333
|
-
}
|
|
1334
1344
|
/**
|
|
1335
1345
|
* Update mobile selected section state (collapsed view, empty states, etc.)
|
|
1336
1346
|
*/
|
|
@@ -1351,14 +1361,13 @@ export class TyMultiselect extends TyComponent {
|
|
|
1351
1361
|
}
|
|
1352
1362
|
}
|
|
1353
1363
|
if (availableSection) {
|
|
1354
|
-
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
availableSection.setAttribute('data-empty', String(!hasAvailable));
|
|
1364
|
+
// Count *visible* available tags — tags hidden by search filtering count as 0
|
|
1365
|
+
const visibleAvailable = this.getTagElements().filter(tag => !tag.hasAttribute('selected') && !tag.hasAttribute('hidden')).length;
|
|
1366
|
+
availableSection.setAttribute('data-empty', String(visibleAvailable === 0));
|
|
1358
1367
|
// Update available header count
|
|
1359
1368
|
const availableTitleSpan = shadow.querySelector('.mobile-available-section .section-title');
|
|
1360
1369
|
if (availableTitleSpan) {
|
|
1361
|
-
availableTitleSpan.textContent = `${this._availableLabel} (${
|
|
1370
|
+
availableTitleSpan.textContent = `${this._availableLabel} (${visibleAvailable})`;
|
|
1362
1371
|
}
|
|
1363
1372
|
}
|
|
1364
1373
|
}
|
|
@@ -1415,6 +1424,12 @@ export class TyMultiselect extends TyComponent {
|
|
|
1415
1424
|
set disabled(value) {
|
|
1416
1425
|
this.setProperty('disabled', value);
|
|
1417
1426
|
}
|
|
1427
|
+
get loading() {
|
|
1428
|
+
return this.getProperty('loading');
|
|
1429
|
+
}
|
|
1430
|
+
set loading(value) {
|
|
1431
|
+
this.setProperty('loading', value);
|
|
1432
|
+
}
|
|
1418
1433
|
get readonly() {
|
|
1419
1434
|
return this.getProperty('readonly');
|
|
1420
1435
|
}
|
|
@@ -1562,15 +1577,15 @@ TyMultiselect.properties = {
|
|
|
1562
1577
|
visual: true,
|
|
1563
1578
|
default: 'Available'
|
|
1564
1579
|
},
|
|
1565
|
-
'no-selection-message': {
|
|
1566
|
-
type: 'string',
|
|
1567
|
-
visual: true,
|
|
1568
|
-
default: 'No items selected'
|
|
1569
|
-
},
|
|
1570
1580
|
'no-options-message': {
|
|
1571
1581
|
type: 'string',
|
|
1572
1582
|
visual: true,
|
|
1573
1583
|
default: 'No options available'
|
|
1584
|
+
},
|
|
1585
|
+
loading: {
|
|
1586
|
+
type: 'boolean',
|
|
1587
|
+
visual: true,
|
|
1588
|
+
default: false
|
|
1574
1589
|
}
|
|
1575
1590
|
};
|
|
1576
1591
|
// Register the custom element
|