@smilodon/core 1.1.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.umd.js CHANGED
@@ -952,6 +952,404 @@
952
952
  selectConfig.resetConfig();
953
953
  }
954
954
 
955
+ /**
956
+ * Custom Option Component Pool
957
+ *
958
+ * Manages lifecycle and recycling of custom option components for optimal performance.
959
+ * Uses object pooling pattern to minimize allocation/deallocation overhead.
960
+ */
961
+ /**
962
+ * Manages a pool of reusable custom option component instances
963
+ */
964
+ class CustomOptionPool {
965
+ constructor(maxPoolSize = 50) {
966
+ this._pool = new Map();
967
+ this._activeComponents = new Map();
968
+ this._maxPoolSize = maxPoolSize;
969
+ }
970
+ /**
971
+ * Get or create a component instance for the given index
972
+ *
973
+ * @param factory - Factory function to create new instances
974
+ * @param item - The data item
975
+ * @param index - The option index
976
+ * @param context - Context for mounting
977
+ * @param container - DOM container for mounting
978
+ * @returns Component instance
979
+ */
980
+ acquire(factory, item, index, context, container) {
981
+ const factoryKey = this._getFactoryKey(factory);
982
+ // Try to find an available component in the pool
983
+ const pooled = this._findAvailableComponent(factoryKey);
984
+ let component;
985
+ if (pooled) {
986
+ // Reuse pooled component
987
+ component = pooled.instance;
988
+ pooled.inUse = true;
989
+ pooled.lastUsedIndex = index;
990
+ console.log(`[CustomOptionPool] Reusing component for index ${index}`);
991
+ }
992
+ else {
993
+ // Create new component
994
+ try {
995
+ component = factory(item, index);
996
+ console.log(`[CustomOptionPool] Created new component for index ${index}`);
997
+ // Add to pool if under limit
998
+ const pool = this._pool.get(factoryKey) || [];
999
+ if (pool.length < this._maxPoolSize) {
1000
+ pool.push({
1001
+ instance: component,
1002
+ inUse: true,
1003
+ lastUsedIndex: index
1004
+ });
1005
+ this._pool.set(factoryKey, pool);
1006
+ }
1007
+ }
1008
+ catch (error) {
1009
+ console.error(`[CustomOptionPool] Failed to create component:`, error);
1010
+ throw error;
1011
+ }
1012
+ }
1013
+ // Mount the component
1014
+ try {
1015
+ component.mountOption(container, context);
1016
+ this._activeComponents.set(index, component);
1017
+ }
1018
+ catch (error) {
1019
+ console.error(`[CustomOptionPool] Failed to mount component at index ${index}:`, error);
1020
+ throw error;
1021
+ }
1022
+ return component;
1023
+ }
1024
+ /**
1025
+ * Release a component back to the pool
1026
+ *
1027
+ * @param index - The index of the component to release
1028
+ */
1029
+ release(index) {
1030
+ const component = this._activeComponents.get(index);
1031
+ if (!component)
1032
+ return;
1033
+ try {
1034
+ component.unmountOption();
1035
+ }
1036
+ catch (error) {
1037
+ console.error(`[CustomOptionPool] Failed to unmount component at index ${index}:`, error);
1038
+ }
1039
+ this._activeComponents.delete(index);
1040
+ // Mark as available in pool
1041
+ for (const pool of this._pool.values()) {
1042
+ const pooled = pool.find(p => p.instance === component);
1043
+ if (pooled) {
1044
+ pooled.inUse = false;
1045
+ console.log(`[CustomOptionPool] Released component from index ${index}`);
1046
+ break;
1047
+ }
1048
+ }
1049
+ }
1050
+ /**
1051
+ * Release all active components
1052
+ */
1053
+ releaseAll() {
1054
+ console.log(`[CustomOptionPool] Releasing ${this._activeComponents.size} active components`);
1055
+ const indices = Array.from(this._activeComponents.keys());
1056
+ indices.forEach(index => this.release(index));
1057
+ }
1058
+ /**
1059
+ * Update selection state for a component
1060
+ *
1061
+ * @param index - The index of the component
1062
+ * @param selected - Whether it's selected
1063
+ */
1064
+ updateSelection(index, selected) {
1065
+ const component = this._activeComponents.get(index);
1066
+ if (component) {
1067
+ component.updateSelected(selected);
1068
+ }
1069
+ }
1070
+ /**
1071
+ * Update focused state for a component
1072
+ *
1073
+ * @param index - The index of the component
1074
+ * @param focused - Whether it has keyboard focus
1075
+ */
1076
+ updateFocused(index, focused) {
1077
+ const component = this._activeComponents.get(index);
1078
+ if (component && component.updateFocused) {
1079
+ component.updateFocused(focused);
1080
+ }
1081
+ }
1082
+ /**
1083
+ * Get active component at index
1084
+ *
1085
+ * @param index - The option index
1086
+ * @returns The component instance or undefined
1087
+ */
1088
+ getComponent(index) {
1089
+ return this._activeComponents.get(index);
1090
+ }
1091
+ /**
1092
+ * Clear the entire pool
1093
+ */
1094
+ clear() {
1095
+ this.releaseAll();
1096
+ this._pool.clear();
1097
+ console.log('[CustomOptionPool] Pool cleared');
1098
+ }
1099
+ /**
1100
+ * Get pool statistics for debugging
1101
+ */
1102
+ getStats() {
1103
+ let totalPooled = 0;
1104
+ let availableComponents = 0;
1105
+ for (const pool of this._pool.values()) {
1106
+ totalPooled += pool.length;
1107
+ availableComponents += pool.filter(p => !p.inUse).length;
1108
+ }
1109
+ return {
1110
+ totalPooled,
1111
+ activeComponents: this._activeComponents.size,
1112
+ availableComponents
1113
+ };
1114
+ }
1115
+ /**
1116
+ * Find an available component in the pool
1117
+ */
1118
+ _findAvailableComponent(factoryKey) {
1119
+ const pool = this._pool.get(factoryKey);
1120
+ if (!pool)
1121
+ return undefined;
1122
+ return pool.find(p => !p.inUse);
1123
+ }
1124
+ /**
1125
+ * Generate a unique key for a factory function
1126
+ */
1127
+ _getFactoryKey(factory) {
1128
+ // Use function name or create a symbol
1129
+ return factory.name || `factory_${factory.toString().slice(0, 50)}`;
1130
+ }
1131
+ }
1132
+
1133
+ /**
1134
+ * Option Renderer
1135
+ *
1136
+ * Unified renderer that handles both lightweight (label/value) and
1137
+ * custom component rendering with consistent performance characteristics.
1138
+ */
1139
+ /**
1140
+ * Manages rendering of both lightweight and custom component options
1141
+ */
1142
+ class OptionRenderer {
1143
+ constructor(config) {
1144
+ this._mountedElements = new Map();
1145
+ this._config = config;
1146
+ this._pool = new CustomOptionPool(config.maxPoolSize);
1147
+ }
1148
+ /**
1149
+ * Render an option (lightweight or custom component)
1150
+ *
1151
+ * @param item - The data item
1152
+ * @param index - The option index
1153
+ * @param isSelected - Whether the option is selected
1154
+ * @param isFocused - Whether the option has keyboard focus
1155
+ * @param uniqueId - Unique ID for the select instance
1156
+ * @returns The rendered DOM element
1157
+ */
1158
+ render(item, index, isSelected, isFocused, uniqueId) {
1159
+ const extendedItem = item;
1160
+ const value = this._config.getValue(item);
1161
+ const label = this._config.getLabel(item);
1162
+ const isDisabled = this._config.getDisabled ? this._config.getDisabled(item) : false;
1163
+ // Determine if this is a custom component or lightweight option
1164
+ const hasCustomComponent = extendedItem.optionComponent && typeof extendedItem.optionComponent === 'function';
1165
+ if (hasCustomComponent) {
1166
+ return this._renderCustomComponent(item, index, value, label, isSelected, isFocused, isDisabled, uniqueId, extendedItem.optionComponent);
1167
+ }
1168
+ else {
1169
+ return this._renderLightweightOption(item, index, value, label, isSelected, isFocused, isDisabled, uniqueId);
1170
+ }
1171
+ }
1172
+ /**
1173
+ * Update selection state for an option
1174
+ *
1175
+ * @param index - The option index
1176
+ * @param selected - Whether it's selected
1177
+ */
1178
+ updateSelection(index, selected) {
1179
+ const element = this._mountedElements.get(index);
1180
+ if (!element)
1181
+ return;
1182
+ // Check if this is a custom component
1183
+ const component = this._pool.getComponent(index);
1184
+ if (component) {
1185
+ component.updateSelected(selected);
1186
+ }
1187
+ else {
1188
+ // Update lightweight option
1189
+ if (selected) {
1190
+ element.classList.add('selected');
1191
+ element.setAttribute('aria-selected', 'true');
1192
+ }
1193
+ else {
1194
+ element.classList.remove('selected');
1195
+ element.setAttribute('aria-selected', 'false');
1196
+ }
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Update focused state for an option
1201
+ *
1202
+ * @param index - The option index
1203
+ * @param focused - Whether it has keyboard focus
1204
+ */
1205
+ updateFocused(index, focused) {
1206
+ const element = this._mountedElements.get(index);
1207
+ if (!element)
1208
+ return;
1209
+ // Check if this is a custom component
1210
+ const component = this._pool.getComponent(index);
1211
+ if (component) {
1212
+ if (component.updateFocused) {
1213
+ component.updateFocused(focused);
1214
+ }
1215
+ // Also update the element's focused class for styling
1216
+ element.classList.toggle('focused', focused);
1217
+ }
1218
+ else {
1219
+ // Update lightweight option
1220
+ element.classList.toggle('focused', focused);
1221
+ }
1222
+ }
1223
+ /**
1224
+ * Unmount and cleanup an option
1225
+ *
1226
+ * @param index - The option index
1227
+ */
1228
+ unmount(index) {
1229
+ // Release custom component if exists
1230
+ this._pool.release(index);
1231
+ // Remove from mounted elements
1232
+ this._mountedElements.delete(index);
1233
+ }
1234
+ /**
1235
+ * Unmount all options
1236
+ */
1237
+ unmountAll() {
1238
+ this._pool.releaseAll();
1239
+ this._mountedElements.clear();
1240
+ }
1241
+ /**
1242
+ * Get pool statistics
1243
+ */
1244
+ getStats() {
1245
+ return this._pool.getStats();
1246
+ }
1247
+ /**
1248
+ * Render a lightweight option (traditional label/value)
1249
+ */
1250
+ _renderLightweightOption(item, index, value, label, isSelected, isFocused, isDisabled, uniqueId) {
1251
+ const option = document.createElement('div');
1252
+ option.className = 'option';
1253
+ if (isSelected)
1254
+ option.classList.add('selected');
1255
+ if (isFocused)
1256
+ option.classList.add('focused');
1257
+ if (isDisabled)
1258
+ option.classList.add('disabled');
1259
+ option.id = `${uniqueId}-option-${index}`;
1260
+ option.textContent = label;
1261
+ option.dataset.value = String(value);
1262
+ option.dataset.index = String(index);
1263
+ option.dataset.mode = 'lightweight';
1264
+ option.setAttribute('role', 'option');
1265
+ option.setAttribute('aria-selected', String(isSelected));
1266
+ if (isDisabled) {
1267
+ option.setAttribute('aria-disabled', 'true');
1268
+ }
1269
+ // Click handler
1270
+ if (!isDisabled) {
1271
+ option.addEventListener('click', () => {
1272
+ this._config.onSelect(index);
1273
+ });
1274
+ }
1275
+ this._mountedElements.set(index, option);
1276
+ console.log(`[OptionRenderer] Rendered lightweight option ${index}: ${label}`);
1277
+ return option;
1278
+ }
1279
+ /**
1280
+ * Render a custom component option
1281
+ */
1282
+ _renderCustomComponent(item, index, value, label, isSelected, isFocused, isDisabled, uniqueId, factory) {
1283
+ // Create wrapper container for the custom component
1284
+ const wrapper = document.createElement('div');
1285
+ wrapper.className = 'option option-custom';
1286
+ if (isSelected)
1287
+ wrapper.classList.add('selected');
1288
+ if (isFocused)
1289
+ wrapper.classList.add('focused');
1290
+ if (isDisabled)
1291
+ wrapper.classList.add('disabled');
1292
+ wrapper.id = `${uniqueId}-option-${index}`;
1293
+ wrapper.dataset.value = String(value);
1294
+ wrapper.dataset.index = String(index);
1295
+ wrapper.dataset.mode = 'component';
1296
+ wrapper.setAttribute('role', 'option');
1297
+ wrapper.setAttribute('aria-selected', String(isSelected));
1298
+ wrapper.setAttribute('aria-label', label); // Accessibility fallback
1299
+ if (isDisabled) {
1300
+ wrapper.setAttribute('aria-disabled', 'true');
1301
+ }
1302
+ // Create context for the custom component
1303
+ const context = {
1304
+ item,
1305
+ index,
1306
+ value,
1307
+ label,
1308
+ isSelected,
1309
+ isFocused,
1310
+ isDisabled,
1311
+ onSelect: (idx) => {
1312
+ if (!isDisabled) {
1313
+ this._config.onSelect(idx);
1314
+ }
1315
+ },
1316
+ onCustomEvent: (eventName, data) => {
1317
+ if (this._config.onCustomEvent) {
1318
+ this._config.onCustomEvent(index, eventName, data);
1319
+ }
1320
+ }
1321
+ };
1322
+ // Acquire component from pool and mount it
1323
+ try {
1324
+ const component = this._pool.acquire(factory, item, index, context, wrapper);
1325
+ // Get the component's root element and attach click handler to wrapper
1326
+ const componentElement = component.getElement();
1327
+ if (!isDisabled) {
1328
+ // Use event delegation on the wrapper
1329
+ wrapper.addEventListener('click', (e) => {
1330
+ // Only trigger if clicking within the component
1331
+ if (wrapper.contains(e.target)) {
1332
+ this._config.onSelect(index);
1333
+ }
1334
+ });
1335
+ }
1336
+ console.log(`[OptionRenderer] Rendered custom component option ${index}: ${label}`);
1337
+ }
1338
+ catch (error) {
1339
+ console.error(`[OptionRenderer] Failed to render custom component at index ${index}:`, error);
1340
+ // Fallback to lightweight rendering on error
1341
+ wrapper.innerHTML = '';
1342
+ wrapper.textContent = label;
1343
+ wrapper.classList.add('component-error');
1344
+ if (this._config.onError) {
1345
+ this._config.onError(index, error);
1346
+ }
1347
+ }
1348
+ this._mountedElements.set(index, wrapper);
1349
+ return wrapper;
1350
+ }
1351
+ }
1352
+
955
1353
  /**
956
1354
  * Enhanced Select Component
957
1355
  * Implements all advanced features: infinite scroll, load more, busy state,
@@ -1010,6 +1408,9 @@
1010
1408
  // Initialize styles BEFORE assembling DOM (order matters in shadow DOM)
1011
1409
  this._initializeStyles();
1012
1410
  console.log('[EnhancedSelect] Styles initialized');
1411
+ // Initialize option renderer
1412
+ this._initializeOptionRenderer();
1413
+ console.log('[EnhancedSelect] Option renderer initialized');
1013
1414
  this._assembleDOM();
1014
1415
  console.log('[EnhancedSelect] DOM assembled');
1015
1416
  this._attachEventListeners();
@@ -1046,6 +1447,11 @@
1046
1447
  clearTimeout(this._typeTimeout);
1047
1448
  if (this._searchTimeout)
1048
1449
  clearTimeout(this._searchTimeout);
1450
+ // Cleanup option renderer
1451
+ if (this._optionRenderer) {
1452
+ this._optionRenderer.unmountAll();
1453
+ console.log('[EnhancedSelect] Option renderer cleaned up');
1454
+ }
1049
1455
  // Cleanup arrow click listener
1050
1456
  if (this._boundArrowClick && this._arrowContainer) {
1051
1457
  this._arrowContainer.removeEventListener('click', this._boundArrowClick);
@@ -1585,6 +1991,40 @@
1585
1991
  }, { threshold: 0.1 });
1586
1992
  }
1587
1993
  }
1994
+ _initializeOptionRenderer() {
1995
+ const getValue = this._config.serverSide.getValueFromItem || ((item) => item?.value ?? item);
1996
+ const getLabel = this._config.serverSide.getLabelFromItem || ((item) => item?.label ?? String(item));
1997
+ const getDisabled = (item) => item?.disabled ?? false;
1998
+ const rendererConfig = {
1999
+ enableRecycling: true,
2000
+ maxPoolSize: 100,
2001
+ getValue,
2002
+ getLabel,
2003
+ getDisabled,
2004
+ onSelect: (index) => {
2005
+ this._selectOption(index);
2006
+ },
2007
+ onCustomEvent: (index, eventName, data) => {
2008
+ console.log(`[EnhancedSelect] Custom event from option ${index}: ${eventName}`, data);
2009
+ // Emit as a generic event since these aren't in the standard event map
2010
+ this.dispatchEvent(new CustomEvent('option:custom-event', {
2011
+ detail: { index, eventName, data },
2012
+ bubbles: true,
2013
+ composed: true
2014
+ }));
2015
+ },
2016
+ onError: (index, error) => {
2017
+ console.error(`[EnhancedSelect] Error in option ${index}:`, error);
2018
+ this.dispatchEvent(new CustomEvent('option:mount-error', {
2019
+ detail: { index, error },
2020
+ bubbles: true,
2021
+ composed: true
2022
+ }));
2023
+ }
2024
+ };
2025
+ this._optionRenderer = new OptionRenderer(rendererConfig);
2026
+ console.log('[EnhancedSelect] Option renderer initialized with config:', rendererConfig);
2027
+ }
1588
2028
  async _loadInitialSelectedItems() {
1589
2029
  if (!this._config.serverSide.fetchSelectedItems || !this._config.serverSide.initialSelectedValues) {
1590
2030
  return;
@@ -2305,6 +2745,11 @@
2305
2745
  if (this._loadMoreTrigger && this._intersectionObserver) {
2306
2746
  this._intersectionObserver.unobserve(this._loadMoreTrigger);
2307
2747
  }
2748
+ // Cleanup all rendered options (including custom components)
2749
+ if (this._optionRenderer) {
2750
+ this._optionRenderer.unmountAll();
2751
+ console.log('[EnhancedSelect] Unmounted all option components');
2752
+ }
2308
2753
  // Clear options container
2309
2754
  console.log('[EnhancedSelect] Clearing options container, previous children:', this._optionsContainer.children.length);
2310
2755
  this._optionsContainer.innerHTML = '';
@@ -2421,28 +2866,23 @@
2421
2866
  console.log('[EnhancedSelect] _renderOptions complete, optionsContainer children:', this._optionsContainer.children.length);
2422
2867
  }
2423
2868
  _renderSingleOption(item, index, getValue, getLabel) {
2424
- const option = document.createElement('div');
2425
- option.className = 'option';
2426
- option.id = `${this._uniqueId}-option-${index}`;
2427
- const value = getValue(item);
2428
- const label = getLabel(item);
2429
- console.log('[EnhancedSelect] Rendering option', index, ':', { value, label });
2430
- option.textContent = label;
2431
- option.dataset.value = String(value);
2432
- option.dataset.index = String(index); // Also useful for debugging/selectors
2433
- // Check if selected using selectedItems map
2434
- const isSelected = this._state.selectedIndices.has(index);
2435
- if (isSelected) {
2436
- option.classList.add('selected');
2437
- option.setAttribute('aria-selected', 'true');
2438
- }
2439
- else {
2440
- option.setAttribute('aria-selected', 'false');
2869
+ if (!this._optionRenderer) {
2870
+ console.error('[EnhancedSelect] Option renderer not initialized');
2871
+ return;
2441
2872
  }
2442
- option.addEventListener('click', () => {
2443
- this._selectOption(index);
2873
+ // Check if selected
2874
+ const isSelected = this._state.selectedIndices.has(index);
2875
+ const isFocused = this._state.activeIndex === index;
2876
+ console.log('[EnhancedSelect] Rendering option', index, ':', {
2877
+ value: getValue(item),
2878
+ label: getLabel(item),
2879
+ isSelected,
2880
+ isFocused,
2881
+ hasCustomComponent: !!item.optionComponent
2444
2882
  });
2445
- this._optionsContainer.appendChild(option);
2883
+ // Use the OptionRenderer to render both lightweight and custom component options
2884
+ const optionElement = this._optionRenderer.render(item, index, isSelected, isFocused, this._uniqueId);
2885
+ this._optionsContainer.appendChild(optionElement);
2446
2886
  console.log('[EnhancedSelect] Option', index, 'appended to optionsContainer');
2447
2887
  }
2448
2888
  _addLoadMoreTrigger() {
@@ -2480,920 +2920,6 @@
2480
2920
  customElements.define('enhanced-select', EnhancedSelect);
2481
2921
  }
2482
2922
 
2483
- /**
2484
- * Angular-Optimized Enhanced Select Component
2485
- *
2486
- * This is a specialized variant that uses light DOM instead of shadow DOM
2487
- * to ensure perfect compatibility with Angular's rendering pipeline and
2488
- * view encapsulation system.
2489
- *
2490
- * Key differences from standard EnhancedSelect:
2491
- * - Uses light DOM with scoped CSS classes
2492
- * - Integrates seamlessly with Angular's change detection
2493
- * - Maintains all core features (virtualization, accessibility, performance)
2494
- * - Uses unique class prefixes to avoid style conflicts
2495
- *
2496
- * @performance Optimized for Angular's zone.js and rendering cycle
2497
- * @accessibility Full WCAG 2.1 AAA compliance maintained
2498
- */
2499
- /**
2500
- * Angular-Enhanced Select Web Component
2501
- * Uses light DOM for Angular compatibility
2502
- */
2503
- class AngularEnhancedSelect extends HTMLElement {
2504
- constructor() {
2505
- super();
2506
- this._pageCache = {};
2507
- this._typeBuffer = '';
2508
- this._hasError = false;
2509
- this._errorMessage = '';
2510
- this._boundArrowClick = null;
2511
- this._isReady = false;
2512
- // Unique class prefix to avoid conflicts
2513
- this.PREFIX = 'smilodon-ang-';
2514
- console.log('[AngularEnhancedSelect] Constructor called');
2515
- this._uniqueId = `angular-select-${Math.random().toString(36).substr(2, 9)}`;
2516
- // Merge global config
2517
- this._config = selectConfig.getConfig();
2518
- // Initialize state
2519
- this._state = {
2520
- isOpen: false,
2521
- isBusy: false,
2522
- isSearching: false,
2523
- currentPage: this._config.infiniteScroll.initialPage || 1,
2524
- totalPages: 1,
2525
- selectedIndices: new Set(),
2526
- selectedItems: new Map(),
2527
- activeIndex: -1,
2528
- searchQuery: '',
2529
- loadedItems: [],
2530
- groupedItems: [],
2531
- preserveScrollPosition: false,
2532
- lastScrollPosition: 0,
2533
- lastNotifiedQuery: null,
2534
- lastNotifiedResultCount: 0,
2535
- isExpanded: false,
2536
- };
2537
- // Create DOM structure in light DOM
2538
- this._initializeStyles();
2539
- this._container = this._createContainer();
2540
- this._inputContainer = this._createInputContainer();
2541
- this._input = this._createInput();
2542
- this._arrowContainer = this._createArrowContainer();
2543
- this._dropdown = this._createDropdown();
2544
- this._optionsContainer = this._createOptionsContainer();
2545
- this._liveRegion = this._createLiveRegion();
2546
- this._assembleDOM();
2547
- this._attachEventListeners();
2548
- this._initializeObservers();
2549
- }
2550
- connectedCallback() {
2551
- console.log('[AngularEnhancedSelect] connectedCallback called');
2552
- // Ensure host has proper layout
2553
- if (!this.style.display) {
2554
- this.style.display = 'block';
2555
- }
2556
- if (!this.style.position) {
2557
- this.style.position = 'relative';
2558
- }
2559
- // Load initial data if server-side is enabled
2560
- if (this._config.serverSide.enabled && this._config.serverSide.initialSelectedValues) {
2561
- this._loadInitialSelectedItems();
2562
- }
2563
- // Mark element as ready
2564
- this._isReady = true;
2565
- console.log('[AngularEnhancedSelect] Element is now ready');
2566
- }
2567
- disconnectedCallback() {
2568
- // Cleanup observers
2569
- this._resizeObserver?.disconnect();
2570
- this._intersectionObserver?.disconnect();
2571
- if (this._busyTimeout)
2572
- clearTimeout(this._busyTimeout);
2573
- if (this._typeTimeout)
2574
- clearTimeout(this._typeTimeout);
2575
- if (this._searchTimeout)
2576
- clearTimeout(this._searchTimeout);
2577
- // Cleanup arrow click listener
2578
- if (this._boundArrowClick && this._arrowContainer) {
2579
- this._arrowContainer.removeEventListener('click', this._boundArrowClick);
2580
- }
2581
- // Remove style element
2582
- if (this._styleElement && this._styleElement.parentNode) {
2583
- this._styleElement.parentNode.removeChild(this._styleElement);
2584
- }
2585
- }
2586
- _initializeStyles() {
2587
- // Check if styles already exist for this component type
2588
- const existingStyle = document.head.querySelector('style[data-component="angular-enhanced-select-shared"]');
2589
- if (existingStyle) {
2590
- // Styles already injected, reuse the existing one
2591
- this._styleElement = existingStyle;
2592
- return;
2593
- }
2594
- // Create scoped styles for this component type (shared across all instances)
2595
- this._styleElement = document.createElement('style');
2596
- this._styleElement.setAttribute('data-component', 'angular-enhanced-select-shared');
2597
- const p = this.PREFIX; // shorthand
2598
- this._styleElement.textContent = `
2599
- /* Host styles - applied to <angular-enhanced-select> */
2600
- angular-enhanced-select {
2601
- display: block;
2602
- position: relative;
2603
- width: 100%;
2604
- min-height: 44px;
2605
- box-sizing: border-box;
2606
- }
2607
-
2608
- /* Container */
2609
- .${p}container {
2610
- position: relative;
2611
- width: 100%;
2612
- min-height: 44px;
2613
- box-sizing: border-box;
2614
- }
2615
-
2616
- /* Input Container */
2617
- .${p}input-container {
2618
- position: relative;
2619
- width: 100%;
2620
- display: flex;
2621
- align-items: center;
2622
- flex-wrap: wrap;
2623
- gap: 6px;
2624
- padding: 6px 52px 6px 8px;
2625
- min-height: 44px;
2626
- background: white;
2627
- border: 1px solid #d1d5db;
2628
- border-radius: 6px;
2629
- box-sizing: border-box;
2630
- transition: all 0.2s ease;
2631
- }
2632
-
2633
- .${p}input-container:focus-within {
2634
- border-color: #667eea;
2635
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
2636
- }
2637
-
2638
- /* Gradient separator before arrow */
2639
- .${p}input-container::after {
2640
- content: '';
2641
- position: absolute;
2642
- top: 50%;
2643
- right: 40px;
2644
- transform: translateY(-50%);
2645
- width: 1px;
2646
- height: 60%;
2647
- background: linear-gradient(
2648
- to bottom,
2649
- transparent 0%,
2650
- rgba(0, 0, 0, 0.1) 20%,
2651
- rgba(0, 0, 0, 0.1) 80%,
2652
- transparent 100%
2653
- );
2654
- pointer-events: none;
2655
- z-index: 1;
2656
- }
2657
-
2658
- /* Dropdown Arrow Container */
2659
- .${p}arrow-container {
2660
- position: absolute;
2661
- top: 0;
2662
- right: 0;
2663
- bottom: 0;
2664
- width: 40px;
2665
- display: flex;
2666
- align-items: center;
2667
- justify-content: center;
2668
- cursor: pointer;
2669
- transition: background-color 0.2s ease;
2670
- border-radius: 0 4px 4px 0;
2671
- z-index: 2;
2672
- }
2673
-
2674
- .${p}arrow-container:hover {
2675
- background-color: rgba(102, 126, 234, 0.08);
2676
- }
2677
-
2678
- .${p}arrow {
2679
- width: 16px;
2680
- height: 16px;
2681
- color: #667eea;
2682
- transition: transform 0.2s ease, color 0.2s ease;
2683
- transform: translateY(0);
2684
- }
2685
-
2686
- .${p}arrow-container:hover .${p}arrow {
2687
- color: #667eea;
2688
- }
2689
-
2690
- .${p}arrow.${p}open {
2691
- transform: rotate(180deg);
2692
- }
2693
-
2694
- /* Input */
2695
- .${p}input {
2696
- flex: 1;
2697
- min-width: 120px;
2698
- padding: 4px;
2699
- border: none;
2700
- font-size: 14px;
2701
- line-height: 1.5;
2702
- color: #1f2937;
2703
- background: transparent;
2704
- box-sizing: border-box;
2705
- outline: none;
2706
- }
2707
-
2708
- .${p}input::placeholder {
2709
- color: #9ca3af;
2710
- }
2711
-
2712
- /* Selection Badges */
2713
- .${p}badge {
2714
- display: inline-flex;
2715
- align-items: center;
2716
- gap: 4px;
2717
- padding: 4px 8px;
2718
- margin: 2px;
2719
- background: #667eea;
2720
- color: white;
2721
- border-radius: 4px;
2722
- font-size: 13px;
2723
- line-height: 1;
2724
- }
2725
-
2726
- .${p}badge-remove {
2727
- display: inline-flex;
2728
- align-items: center;
2729
- justify-content: center;
2730
- width: 16px;
2731
- height: 16px;
2732
- padding: 0;
2733
- margin-left: 4px;
2734
- background: rgba(255, 255, 255, 0.3);
2735
- border: none;
2736
- border-radius: 50%;
2737
- color: white;
2738
- font-size: 16px;
2739
- line-height: 1;
2740
- cursor: pointer;
2741
- transition: background 0.2s;
2742
- }
2743
-
2744
- .${p}badge-remove:hover {
2745
- background: rgba(255, 255, 255, 0.5);
2746
- }
2747
-
2748
- /* Dropdown */
2749
- .${p}dropdown {
2750
- position: absolute;
2751
- scroll-behavior: smooth;
2752
- top: 100%;
2753
- left: 0;
2754
- right: 0;
2755
- margin-top: 4px;
2756
- max-height: 300px;
2757
- overflow: hidden;
2758
- background: white;
2759
- border: 1px solid #ccc;
2760
- border-radius: 4px;
2761
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
2762
- z-index: 1000;
2763
- box-sizing: border-box;
2764
- }
2765
-
2766
- .${p}dropdown[style*="display: none"] {
2767
- display: none !important;
2768
- }
2769
-
2770
- /* Options Container */
2771
- .${p}options-container {
2772
- position: relative;
2773
- max-height: 300px;
2774
- overflow: auto;
2775
- transition: opacity 0.2s ease-in-out;
2776
- }
2777
-
2778
- /* Option */
2779
- .${p}option {
2780
- padding: 8px 12px;
2781
- cursor: pointer;
2782
- color: inherit;
2783
- transition: background-color 0.15s ease;
2784
- user-select: none;
2785
- }
2786
-
2787
- .${p}option:hover {
2788
- background-color: #f3f4f6;
2789
- }
2790
-
2791
- .${p}option.${p}selected {
2792
- background-color: #e0e7ff;
2793
- color: #4338ca;
2794
- font-weight: 500;
2795
- }
2796
-
2797
- .${p}option.${p}active {
2798
- background-color: #f3f4f6;
2799
- }
2800
-
2801
- /* Load More */
2802
- .${p}load-more-container {
2803
- padding: 12px;
2804
- text-align: center;
2805
- border-top: 1px solid #e0e0e0;
2806
- }
2807
-
2808
- .${p}load-more-button {
2809
- padding: 8px 16px;
2810
- border: 1px solid #1976d2;
2811
- background: white;
2812
- color: #1976d2;
2813
- border-radius: 4px;
2814
- cursor: pointer;
2815
- font-size: 14px;
2816
- transition: all 0.2s ease;
2817
- }
2818
-
2819
- .${p}load-more-button:hover {
2820
- background: #1976d2;
2821
- color: white;
2822
- }
2823
-
2824
- .${p}load-more-button:disabled {
2825
- opacity: 0.5;
2826
- cursor: not-allowed;
2827
- }
2828
-
2829
- /* Busy State */
2830
- .${p}busy-bucket {
2831
- padding: 16px;
2832
- text-align: center;
2833
- color: #666;
2834
- }
2835
-
2836
- .${p}spinner {
2837
- display: inline-block;
2838
- width: 20px;
2839
- height: 20px;
2840
- border: 2px solid #ccc;
2841
- border-top-color: #1976d2;
2842
- border-radius: 50%;
2843
- animation: ${p}spin 0.6s linear infinite;
2844
- }
2845
-
2846
- @keyframes ${p}spin {
2847
- to { transform: rotate(360deg); }
2848
- }
2849
-
2850
- /* Empty State */
2851
- .${p}empty-state {
2852
- padding: 24px;
2853
- text-align: center;
2854
- color: #999;
2855
- }
2856
-
2857
- /* Searching State */
2858
- .${p}searching-state {
2859
- padding: 24px;
2860
- text-align: center;
2861
- color: #667eea;
2862
- font-style: italic;
2863
- animation: ${p}pulse 1.5s ease-in-out infinite;
2864
- }
2865
-
2866
- @keyframes ${p}pulse {
2867
- 0%, 100% { opacity: 1; }
2868
- 50% { opacity: 0.5; }
2869
- }
2870
-
2871
- /* Error states */
2872
- .${p}input[aria-invalid="true"] {
2873
- border-color: #dc2626;
2874
- }
2875
-
2876
- .${p}input[aria-invalid="true"]:focus {
2877
- border-color: #dc2626;
2878
- box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1);
2879
- outline-color: #dc2626;
2880
- }
2881
-
2882
- /* Live Region (Screen reader only) */
2883
- .${p}live-region {
2884
- position: absolute;
2885
- left: -10000px;
2886
- width: 1px;
2887
- height: 1px;
2888
- overflow: hidden;
2889
- clip: rect(0, 0, 0, 0);
2890
- white-space: nowrap;
2891
- border-width: 0;
2892
- }
2893
-
2894
- /* Accessibility: Reduced motion */
2895
- @media (prefers-reduced-motion: reduce) {
2896
- .${p}arrow,
2897
- .${p}badge-remove,
2898
- .${p}option,
2899
- .${p}dropdown {
2900
- animation-duration: 0.01ms !important;
2901
- animation-iteration-count: 1 !important;
2902
- transition-duration: 0.01ms !important;
2903
- }
2904
- }
2905
-
2906
- /* Touch targets (WCAG 2.5.5) */
2907
- .${p}load-more-button,
2908
- .${p}option {
2909
- min-height: 44px;
2910
- }
2911
- `;
2912
- // Safely append to document head (check if document is ready and not already appended)
2913
- if (document.head && !this._styleElement.parentNode) {
2914
- try {
2915
- document.head.appendChild(this._styleElement);
2916
- }
2917
- catch (e) {
2918
- console.warn('[AngularEnhancedSelect] Could not inject styles:', e);
2919
- // Fallback: inject after a delay
2920
- setTimeout(() => {
2921
- try {
2922
- if (this._styleElement && !this._styleElement.parentNode) {
2923
- document.head.appendChild(this._styleElement);
2924
- }
2925
- }
2926
- catch (err) {
2927
- console.error('[AngularEnhancedSelect] Style injection failed:', err);
2928
- }
2929
- }, 0);
2930
- }
2931
- }
2932
- }
2933
- _createContainer() {
2934
- const container = document.createElement('div');
2935
- container.className = `${this.PREFIX}container`;
2936
- return container;
2937
- }
2938
- _createInputContainer() {
2939
- const container = document.createElement('div');
2940
- container.className = `${this.PREFIX}input-container`;
2941
- return container;
2942
- }
2943
- _createInput() {
2944
- const input = document.createElement('input');
2945
- input.type = 'text';
2946
- input.className = `${this.PREFIX}input`;
2947
- input.placeholder = this.getAttribute('placeholder') || 'Select an option...';
2948
- input.setAttribute('readonly', '');
2949
- input.setAttribute('role', 'combobox');
2950
- input.setAttribute('aria-expanded', 'false');
2951
- input.setAttribute('aria-haspopup', 'listbox');
2952
- input.setAttribute('aria-autocomplete', 'none');
2953
- input.setAttribute('aria-controls', `${this._uniqueId}-listbox`);
2954
- input.setAttribute('aria-owns', `${this._uniqueId}-listbox`);
2955
- input.tabIndex = 0;
2956
- return input;
2957
- }
2958
- _createArrowContainer() {
2959
- const container = document.createElement('div');
2960
- container.className = `${this.PREFIX}arrow-container`;
2961
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2962
- svg.setAttribute('class', `${this.PREFIX}arrow`);
2963
- svg.setAttribute('width', '16');
2964
- svg.setAttribute('height', '16');
2965
- svg.setAttribute('viewBox', '0 0 16 16');
2966
- svg.setAttribute('fill', 'none');
2967
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2968
- path.setAttribute('d', 'M4 6l4 4 4-4');
2969
- path.setAttribute('stroke', 'currentColor');
2970
- path.setAttribute('stroke-width', '2');
2971
- path.setAttribute('stroke-linecap', 'round');
2972
- path.setAttribute('stroke-linejoin', 'round');
2973
- svg.appendChild(path);
2974
- container.appendChild(svg);
2975
- return container;
2976
- }
2977
- _createDropdown() {
2978
- const dropdown = document.createElement('div');
2979
- dropdown.id = `${this._uniqueId}-listbox`;
2980
- dropdown.className = `${this.PREFIX}dropdown`;
2981
- dropdown.style.display = 'none';
2982
- dropdown.setAttribute('role', 'listbox');
2983
- return dropdown;
2984
- }
2985
- _createOptionsContainer() {
2986
- const container = document.createElement('div');
2987
- container.className = `${this.PREFIX}options-container`;
2988
- return container;
2989
- }
2990
- _createLiveRegion() {
2991
- const region = document.createElement('div');
2992
- region.className = `${this.PREFIX}live-region`;
2993
- region.setAttribute('role', 'status');
2994
- region.setAttribute('aria-live', 'polite');
2995
- region.setAttribute('aria-atomic', 'true');
2996
- return region;
2997
- }
2998
- _assembleDOM() {
2999
- // Assemble in light DOM
3000
- this._inputContainer.appendChild(this._input);
3001
- this._inputContainer.appendChild(this._arrowContainer);
3002
- this._container.appendChild(this._inputContainer);
3003
- this._dropdown.appendChild(this._optionsContainer);
3004
- this._container.appendChild(this._dropdown);
3005
- this._container.appendChild(this._liveRegion);
3006
- this.appendChild(this._container);
3007
- }
3008
- _attachEventListeners() {
3009
- // Arrow click handler
3010
- if (this._arrowContainer) {
3011
- this._boundArrowClick = (e) => {
3012
- e.stopPropagation();
3013
- e.preventDefault();
3014
- const wasOpen = this._state.isOpen;
3015
- this._state.isOpen = !this._state.isOpen;
3016
- this._updateDropdownVisibility();
3017
- this._updateArrowRotation();
3018
- if (this._state.isOpen && this._config.callbacks.onOpen) {
3019
- this._config.callbacks.onOpen();
3020
- }
3021
- else if (!this._state.isOpen && this._config.callbacks.onClose) {
3022
- this._config.callbacks.onClose();
3023
- }
3024
- if (!wasOpen && this._state.isOpen && this._state.selectedIndices.size > 0) {
3025
- setTimeout(() => this._scrollToSelected(), 50);
3026
- }
3027
- };
3028
- this._arrowContainer.addEventListener('click', this._boundArrowClick);
3029
- }
3030
- // Input focus handler
3031
- this._input.addEventListener('focus', () => {
3032
- if (!this._state.isOpen) {
3033
- this._state.isOpen = true;
3034
- this._updateDropdownVisibility();
3035
- this._updateArrowRotation();
3036
- if (this._config.callbacks.onOpen) {
3037
- this._config.callbacks.onOpen();
3038
- }
3039
- }
3040
- });
3041
- // Input keyboard handler
3042
- this._input.addEventListener('keydown', (e) => this._handleKeydown(e));
3043
- // Click outside to close
3044
- document.addEventListener('click', (e) => {
3045
- if (!this.contains(e.target) && this._state.isOpen) {
3046
- this._state.isOpen = false;
3047
- this._updateDropdownVisibility();
3048
- this._updateArrowRotation();
3049
- if (this._config.callbacks.onClose) {
3050
- this._config.callbacks.onClose();
3051
- }
3052
- }
3053
- });
3054
- // Search handler
3055
- if (this.hasAttribute('searchable')) {
3056
- this._input.removeAttribute('readonly');
3057
- this._input.addEventListener('input', (e) => {
3058
- const query = e.target.value;
3059
- this._handleSearch(query);
3060
- });
3061
- }
3062
- }
3063
- _initializeObservers() {
3064
- // Resize observer for dropdown positioning
3065
- if (typeof ResizeObserver !== 'undefined') {
3066
- this._resizeObserver = new ResizeObserver(() => {
3067
- if (this._state.isOpen) {
3068
- this._updateDropdownPosition();
3069
- }
3070
- });
3071
- this._resizeObserver.observe(this);
3072
- }
3073
- }
3074
- _updateDropdownVisibility() {
3075
- if (this._state.isOpen) {
3076
- this._dropdown.style.display = 'block';
3077
- this._input.setAttribute('aria-expanded', 'true');
3078
- this._updateDropdownPosition();
3079
- }
3080
- else {
3081
- this._dropdown.style.display = 'none';
3082
- this._input.setAttribute('aria-expanded', 'false');
3083
- }
3084
- }
3085
- _updateDropdownPosition() {
3086
- // Ensure dropdown is positioned correctly relative to input
3087
- const rect = this._inputContainer.getBoundingClientRect();
3088
- const viewportHeight = window.innerHeight;
3089
- const spaceBelow = viewportHeight - rect.bottom;
3090
- const spaceAbove = rect.top;
3091
- // Auto placement
3092
- if (spaceBelow < 300 && spaceAbove > spaceBelow) {
3093
- // Open upward
3094
- this._dropdown.style.top = 'auto';
3095
- this._dropdown.style.bottom = '100%';
3096
- this._dropdown.style.marginTop = '0';
3097
- this._dropdown.style.marginBottom = '4px';
3098
- }
3099
- else {
3100
- // Open downward (default)
3101
- this._dropdown.style.top = '100%';
3102
- this._dropdown.style.bottom = 'auto';
3103
- this._dropdown.style.marginTop = '4px';
3104
- this._dropdown.style.marginBottom = '0';
3105
- }
3106
- }
3107
- _updateArrowRotation() {
3108
- const arrow = this._arrowContainer?.querySelector(`.${this.PREFIX}arrow`);
3109
- if (arrow) {
3110
- if (this._state.isOpen) {
3111
- arrow.classList.add(`${this.PREFIX}open`);
3112
- }
3113
- else {
3114
- arrow.classList.remove(`${this.PREFIX}open`);
3115
- }
3116
- }
3117
- }
3118
- _handleKeydown(e) {
3119
- // Implement keyboard navigation
3120
- switch (e.key) {
3121
- case 'ArrowDown':
3122
- e.preventDefault();
3123
- if (!this._state.isOpen) {
3124
- this._state.isOpen = true;
3125
- this._updateDropdownVisibility();
3126
- this._updateArrowRotation();
3127
- }
3128
- else {
3129
- this._moveActive(1);
3130
- }
3131
- break;
3132
- case 'ArrowUp':
3133
- e.preventDefault();
3134
- if (this._state.isOpen) {
3135
- this._moveActive(-1);
3136
- }
3137
- break;
3138
- case 'Enter':
3139
- e.preventDefault();
3140
- if (this._state.isOpen && this._state.activeIndex >= 0) {
3141
- this._selectByIndex(this._state.activeIndex);
3142
- }
3143
- else {
3144
- this._state.isOpen = true;
3145
- this._updateDropdownVisibility();
3146
- this._updateArrowRotation();
3147
- }
3148
- break;
3149
- case 'Escape':
3150
- e.preventDefault();
3151
- if (this._state.isOpen) {
3152
- this._state.isOpen = false;
3153
- this._updateDropdownVisibility();
3154
- this._updateArrowRotation();
3155
- }
3156
- break;
3157
- case 'Tab':
3158
- if (this._state.isOpen) {
3159
- this._state.isOpen = false;
3160
- this._updateDropdownVisibility();
3161
- this._updateArrowRotation();
3162
- }
3163
- break;
3164
- }
3165
- }
3166
- _moveActive(direction) {
3167
- const options = this._optionsContainer.querySelectorAll(`.${this.PREFIX}option`);
3168
- if (options.length === 0)
3169
- return;
3170
- let newIndex = this._state.activeIndex + direction;
3171
- if (newIndex < 0)
3172
- newIndex = 0;
3173
- if (newIndex >= options.length)
3174
- newIndex = options.length - 1;
3175
- this._state.activeIndex = newIndex;
3176
- // Update visual active state
3177
- options.forEach((opt, idx) => {
3178
- if (idx === newIndex) {
3179
- opt.classList.add(`${this.PREFIX}active`);
3180
- opt.scrollIntoView({ block: 'nearest' });
3181
- }
3182
- else {
3183
- opt.classList.remove(`${this.PREFIX}active`);
3184
- }
3185
- });
3186
- }
3187
- _selectByIndex(index) {
3188
- const item = this._state.loadedItems[index];
3189
- if (!item)
3190
- return;
3191
- const isMultiple = this.hasAttribute('multiple');
3192
- if (isMultiple) {
3193
- // Toggle selection
3194
- if (this._state.selectedIndices.has(index)) {
3195
- this._state.selectedIndices.delete(index);
3196
- this._state.selectedItems.delete(index);
3197
- }
3198
- else {
3199
- this._state.selectedIndices.add(index);
3200
- this._state.selectedItems.set(index, item);
3201
- }
3202
- }
3203
- else {
3204
- // Single selection
3205
- this._state.selectedIndices.clear();
3206
- this._state.selectedItems.clear();
3207
- this._state.selectedIndices.add(index);
3208
- this._state.selectedItems.set(index, item);
3209
- // Close dropdown
3210
- this._state.isOpen = false;
3211
- this._updateDropdownVisibility();
3212
- this._updateArrowRotation();
3213
- }
3214
- this._updateInputDisplay();
3215
- this._renderOptions();
3216
- this._emitChangeEvent();
3217
- }
3218
- _updateInputDisplay() {
3219
- const selectedItems = Array.from(this._state.selectedItems.values());
3220
- const isMultiple = this.hasAttribute('multiple');
3221
- if (isMultiple) {
3222
- // Clear input, show badges
3223
- this._input.value = '';
3224
- // Remove existing badges
3225
- this._inputContainer.querySelectorAll(`.${this.PREFIX}badge`).forEach(badge => badge.remove());
3226
- // Add new badges
3227
- selectedItems.forEach((item, idx) => {
3228
- const badge = document.createElement('span');
3229
- badge.className = `${this.PREFIX}badge`;
3230
- badge.textContent = item.label;
3231
- const removeBtn = document.createElement('button');
3232
- removeBtn.className = `${this.PREFIX}badge-remove`;
3233
- removeBtn.textContent = '×';
3234
- removeBtn.addEventListener('click', (e) => {
3235
- e.stopPropagation();
3236
- const itemIndex = Array.from(this._state.selectedItems.keys())[idx];
3237
- this._state.selectedIndices.delete(itemIndex);
3238
- this._state.selectedItems.delete(itemIndex);
3239
- this._updateInputDisplay();
3240
- this._renderOptions();
3241
- this._emitChangeEvent();
3242
- });
3243
- badge.appendChild(removeBtn);
3244
- this._inputContainer.insertBefore(badge, this._input);
3245
- });
3246
- }
3247
- else {
3248
- // Single selection - show label in input
3249
- if (selectedItems.length > 0) {
3250
- this._input.value = selectedItems[0].label;
3251
- }
3252
- else {
3253
- this._input.value = '';
3254
- }
3255
- }
3256
- }
3257
- _renderOptions() {
3258
- // Clear existing options
3259
- this._optionsContainer.innerHTML = '';
3260
- const items = this._state.loadedItems;
3261
- if (items.length === 0) {
3262
- const emptyDiv = document.createElement('div');
3263
- emptyDiv.className = `${this.PREFIX}empty-state`;
3264
- emptyDiv.textContent = 'No options available';
3265
- this._optionsContainer.appendChild(emptyDiv);
3266
- return;
3267
- }
3268
- // Render options
3269
- items.forEach((item, index) => {
3270
- const optionDiv = document.createElement('div');
3271
- optionDiv.className = `${this.PREFIX}option`;
3272
- optionDiv.textContent = item.label;
3273
- optionDiv.setAttribute('role', 'option');
3274
- optionDiv.setAttribute('data-index', String(index));
3275
- if (this._state.selectedIndices.has(index)) {
3276
- optionDiv.classList.add(`${this.PREFIX}selected`);
3277
- optionDiv.setAttribute('aria-selected', 'true');
3278
- }
3279
- if (item.disabled) {
3280
- optionDiv.style.opacity = '0.5';
3281
- optionDiv.style.cursor = 'not-allowed';
3282
- }
3283
- else {
3284
- optionDiv.addEventListener('click', () => {
3285
- this._selectByIndex(index);
3286
- });
3287
- }
3288
- this._optionsContainer.appendChild(optionDiv);
3289
- });
3290
- }
3291
- _handleSearch(query) {
3292
- this._state.searchQuery = query;
3293
- // Filter items based on search
3294
- const allItems = this._state.loadedItems;
3295
- const filtered = allItems.filter(item => item.label.toLowerCase().includes(query.toLowerCase()));
3296
- // Temporarily replace loaded items with filtered
3297
- this._state.loadedItems;
3298
- this._state.loadedItems = filtered;
3299
- this._renderOptions();
3300
- // Emit search event
3301
- this.dispatchEvent(new CustomEvent('search', {
3302
- detail: { query },
3303
- bubbles: true,
3304
- composed: true,
3305
- }));
3306
- }
3307
- _emitChangeEvent() {
3308
- const selectedItems = Array.from(this._state.selectedItems.values());
3309
- const selectedValues = selectedItems.map(item => item.value);
3310
- const selectedIndices = Array.from(this._state.selectedIndices);
3311
- this.dispatchEvent(new CustomEvent('change', {
3312
- detail: { selectedItems, selectedValues, selectedIndices },
3313
- bubbles: true,
3314
- composed: true,
3315
- }));
3316
- }
3317
- _scrollToSelected() {
3318
- const firstSelected = this._optionsContainer.querySelector(`.${this.PREFIX}selected`);
3319
- if (firstSelected) {
3320
- firstSelected.scrollIntoView({ block: 'nearest' });
3321
- }
3322
- }
3323
- _loadInitialSelectedItems() {
3324
- // Placeholder for server-side data loading
3325
- }
3326
- _announce(message) {
3327
- if (this._liveRegion) {
3328
- this._liveRegion.textContent = message;
3329
- setTimeout(() => {
3330
- if (this._liveRegion)
3331
- this._liveRegion.textContent = '';
3332
- }, 1000);
3333
- }
3334
- }
3335
- // Public API methods
3336
- isReady() {
3337
- return this._isReady;
3338
- }
3339
- setItems(items) {
3340
- this._state.loadedItems = items;
3341
- this._renderOptions();
3342
- }
3343
- setGroupedItems(groups) {
3344
- this._state.groupedItems = groups;
3345
- // Flatten for now
3346
- const items = [];
3347
- groups.forEach(group => {
3348
- if (group.items) {
3349
- items.push(...group.items);
3350
- }
3351
- });
3352
- this.setItems(items);
3353
- }
3354
- setSelectedValues(values) {
3355
- this._state.selectedIndices.clear();
3356
- this._state.selectedItems.clear();
3357
- values.forEach(value => {
3358
- const index = this._state.loadedItems.findIndex(item => item.value === value);
3359
- if (index >= 0) {
3360
- this._state.selectedIndices.add(index);
3361
- this._state.selectedItems.set(index, this._state.loadedItems[index]);
3362
- }
3363
- });
3364
- this._updateInputDisplay();
3365
- this._renderOptions();
3366
- }
3367
- getSelectedValues() {
3368
- return Array.from(this._state.selectedItems.values()).map(item => item.value);
3369
- }
3370
- updateConfig(config) {
3371
- this._config = { ...this._config, ...config };
3372
- }
3373
- setError(message) {
3374
- this._hasError = true;
3375
- this._errorMessage = message;
3376
- this._input.setAttribute('aria-invalid', 'true');
3377
- this._announce(`Error: ${message}`);
3378
- }
3379
- clearError() {
3380
- this._hasError = false;
3381
- this._errorMessage = '';
3382
- this._input.removeAttribute('aria-invalid');
3383
- }
3384
- }
3385
- // Register the custom element
3386
- console.log('[AngularEnhancedSelect] Attempting to register custom element...');
3387
- console.log('[AngularEnhancedSelect] customElements available:', typeof customElements !== 'undefined');
3388
- console.log('[AngularEnhancedSelect] Already registered:', customElements?.get('angular-enhanced-select'));
3389
- if (typeof customElements !== 'undefined' && !customElements.get('angular-enhanced-select')) {
3390
- customElements.define('angular-enhanced-select', AngularEnhancedSelect);
3391
- console.log('[AngularEnhancedSelect] Successfully registered custom element');
3392
- }
3393
- else if (customElements?.get('angular-enhanced-select')) {
3394
- console.log('[AngularEnhancedSelect] Custom element already registered');
3395
- }
3396
-
3397
2923
  /**
3398
2924
  * Independent Option Component
3399
2925
  * High cohesion, low coupling - handles its own selection state and events
@@ -4656,12 +4182,13 @@
4656
4182
  }
4657
4183
  }
4658
4184
 
4659
- exports.AngularEnhancedSelect = AngularEnhancedSelect;
4660
4185
  exports.CSPFeatures = CSPFeatures;
4186
+ exports.CustomOptionPool = CustomOptionPool;
4661
4187
  exports.DOMPool = DOMPool;
4662
4188
  exports.EnhancedSelect = EnhancedSelect;
4663
4189
  exports.FenwickTree = FenwickTree;
4664
4190
  exports.NativeSelectElement = NativeSelectElement;
4191
+ exports.OptionRenderer = OptionRenderer;
4665
4192
  exports.PerformanceTelemetry = PerformanceTelemetry;
4666
4193
  exports.SelectOption = SelectOption;
4667
4194
  exports.Virtualizer = Virtualizer;