@smilodon/core 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,13 +97,6 @@ See the [main documentation](https://github.com/navidrezadoost/smilodon#readme)
97
97
  - **Sub-millisecond Search**: Fenwick tree indexing for O(log n) queries
98
98
  - **60 FPS Scrolling**: Hardware-accelerated virtualization
99
99
 
100
- ### ✨ Custom Components (NEW in v1.2.0)
101
- - **Framework Components**: Pass React, Vue, or Svelte components for option rendering
102
- - **Component Pooling**: Automatic recycling of up to 100 component instances
103
- - **Lifecycle Management**: Full mount/unmount/update lifecycle control
104
- - **Mixed Mode**: Use custom components alongside lightweight options
105
- - **See**: [Custom Option Components Guide](https://github.com/navidrezadoost/smilodon/blob/main/docs/CUSTOM-OPTION-COMPONENTS.md)
106
-
107
100
  ### 🎯 Production Ready
108
101
  - **TypeScript First**: Complete type definitions included
109
102
  - **Zero Dependencies**: 6.6 KB gzipped runtime
@@ -122,7 +115,7 @@ See the [main documentation](https://github.com/navidrezadoost/smilodon#readme)
122
115
 
123
116
  ```typescript
124
117
  interface SmilodonSelectElement extends HTMLElement {
125
- items: SelectItem[]; // Dataset (can be millions of items)
118
+ items: SelectItem[] | string[] | number[]; // Dataset (can be millions of items)
126
119
  value: any; // Current selected value
127
120
  placeholder?: string; // Placeholder text
128
121
  searchable?: boolean; // Enable search (default: true)
@@ -137,10 +130,47 @@ interface SelectItem {
137
130
  value: any; // Value (can be any type)
138
131
  disabled?: boolean; // Disable this option
139
132
  group?: string; // Optgroup name
140
- optionComponent?: CustomOptionFactory; // (v1.2.0+) Custom component for this option
141
133
  }
142
134
  ```
143
135
 
136
+ > **Flexible Input Formats:**
137
+ > - **Object arrays**: `[{ value: '1', label: 'Option 1' }, ...]`
138
+ > - **String arrays**: `['Apple', 'Banana', 'Cherry']` - automatically converted to `SelectItem` format
139
+ > - **Number arrays**: `[1, 2, 3, 5, 8]` - automatically converted to `SelectItem` format
140
+
141
+ ### Examples with Different Input Types
142
+
143
+ #### Object Array (Traditional)
144
+ ```javascript
145
+ select.items = [
146
+ { value: 'apple', label: 'Apple' },
147
+ { value: 'banana', label: 'Banana' },
148
+ { value: 'cherry', label: 'Cherry' }
149
+ ];
150
+ ```
151
+
152
+ #### String Array (Auto-converted)
153
+ ```javascript
154
+ select.items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
155
+ // Automatically becomes:
156
+ // [
157
+ // { value: 'Apple', label: 'Apple' },
158
+ // { value: 'Banana', label: 'Banana' },
159
+ // ...
160
+ // ]
161
+ ```
162
+
163
+ #### Number Array (Auto-converted)
164
+ ```javascript
165
+ select.items = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89];
166
+ // Automatically becomes:
167
+ // [
168
+ // { value: 1, label: '1' },
169
+ // { value: 2, label: '2' },
170
+ // ...
171
+ // ]
172
+ ```
173
+
144
174
  ### Events
145
175
 
146
176
  ```typescript
package/dist/index.cjs CHANGED
@@ -1535,9 +1535,6 @@ class EnhancedSelect extends HTMLElement {
1535
1535
  // Initialize styles BEFORE assembling DOM (order matters in shadow DOM)
1536
1536
  this._initializeStyles();
1537
1537
  console.log('[EnhancedSelect] Styles initialized');
1538
- // Initialize option renderer
1539
- this._initializeOptionRenderer();
1540
- console.log('[EnhancedSelect] Option renderer initialized');
1541
1538
  this._assembleDOM();
1542
1539
  console.log('[EnhancedSelect] DOM assembled');
1543
1540
  this._attachEventListeners();
@@ -1574,11 +1571,6 @@ class EnhancedSelect extends HTMLElement {
1574
1571
  clearTimeout(this._typeTimeout);
1575
1572
  if (this._searchTimeout)
1576
1573
  clearTimeout(this._searchTimeout);
1577
- // Cleanup option renderer
1578
- if (this._optionRenderer) {
1579
- this._optionRenderer.unmountAll();
1580
- console.log('[EnhancedSelect] Option renderer cleaned up');
1581
- }
1582
1574
  // Cleanup arrow click listener
1583
1575
  if (this._boundArrowClick && this._arrowContainer) {
1584
1576
  this._arrowContainer.removeEventListener('click', this._boundArrowClick);
@@ -2118,40 +2110,6 @@ class EnhancedSelect extends HTMLElement {
2118
2110
  }, { threshold: 0.1 });
2119
2111
  }
2120
2112
  }
2121
- _initializeOptionRenderer() {
2122
- const getValue = this._config.serverSide.getValueFromItem || ((item) => item?.value ?? item);
2123
- const getLabel = this._config.serverSide.getLabelFromItem || ((item) => item?.label ?? String(item));
2124
- const getDisabled = (item) => item?.disabled ?? false;
2125
- const rendererConfig = {
2126
- enableRecycling: true,
2127
- maxPoolSize: 100,
2128
- getValue,
2129
- getLabel,
2130
- getDisabled,
2131
- onSelect: (index) => {
2132
- this._selectOption(index);
2133
- },
2134
- onCustomEvent: (index, eventName, data) => {
2135
- console.log(`[EnhancedSelect] Custom event from option ${index}: ${eventName}`, data);
2136
- // Emit as a generic event since these aren't in the standard event map
2137
- this.dispatchEvent(new CustomEvent('option:custom-event', {
2138
- detail: { index, eventName, data },
2139
- bubbles: true,
2140
- composed: true
2141
- }));
2142
- },
2143
- onError: (index, error) => {
2144
- console.error(`[EnhancedSelect] Error in option ${index}:`, error);
2145
- this.dispatchEvent(new CustomEvent('option:mount-error', {
2146
- detail: { index, error },
2147
- bubbles: true,
2148
- composed: true
2149
- }));
2150
- }
2151
- };
2152
- this._optionRenderer = new OptionRenderer(rendererConfig);
2153
- console.log('[EnhancedSelect] Option renderer initialized with config:', rendererConfig);
2154
- }
2155
2113
  async _loadInitialSelectedItems() {
2156
2114
  if (!this._config.serverSide.fetchSelectedItems || !this._config.serverSide.initialSelectedValues) {
2157
2115
  return;
@@ -2367,13 +2325,31 @@ class EnhancedSelect extends HTMLElement {
2367
2325
  const options = Array.from(this._optionsContainer.children);
2368
2326
  // Clear previous active state
2369
2327
  if (this._state.activeIndex >= 0 && options[this._state.activeIndex]) {
2370
- options[this._state.activeIndex].setActive(false);
2328
+ const prevOption = options[this._state.activeIndex];
2329
+ // Check if it's a custom SelectOption or a lightweight DOM element
2330
+ if ('setActive' in prevOption && typeof prevOption.setActive === 'function') {
2331
+ prevOption.setActive(false);
2332
+ }
2333
+ else {
2334
+ // Lightweight option - remove active class
2335
+ prevOption.classList.remove('smilodon-option--active');
2336
+ prevOption.setAttribute('aria-selected', 'false');
2337
+ }
2371
2338
  }
2372
2339
  this._state.activeIndex = index;
2373
2340
  // Set new active state
2374
2341
  if (options[index]) {
2375
- options[index].setActive(true);
2376
- options[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
2342
+ const option = options[index];
2343
+ // Check if it's a custom SelectOption or a lightweight DOM element
2344
+ if ('setActive' in option && typeof option.setActive === 'function') {
2345
+ option.setActive(true);
2346
+ }
2347
+ else {
2348
+ // Lightweight option - add active class
2349
+ option.classList.add('smilodon-option--active');
2350
+ option.setAttribute('aria-selected', 'true');
2351
+ }
2352
+ option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
2377
2353
  // Announce position for screen readers
2378
2354
  const total = options.length;
2379
2355
  this._announce(`Item ${index + 1} of ${total}`);
@@ -2406,10 +2382,23 @@ class EnhancedSelect extends HTMLElement {
2406
2382
  return;
2407
2383
  }
2408
2384
  if (!this._state.selectedIndices.has(index)) {
2409
- const config = option.getConfig();
2410
- this._state.selectedIndices.add(index);
2411
- this._state.selectedItems.set(index, config.item);
2412
- option.setSelected(true);
2385
+ // Check if it's a custom SelectOption or a lightweight DOM element
2386
+ if ('getConfig' in option && typeof option.getConfig === 'function') {
2387
+ const config = option.getConfig();
2388
+ this._state.selectedIndices.add(index);
2389
+ this._state.selectedItems.set(index, config.item);
2390
+ option.setSelected(true);
2391
+ }
2392
+ else {
2393
+ // Lightweight option - get item from data attribute or state
2394
+ const item = this._state.loadedItems[index];
2395
+ if (item) {
2396
+ this._state.selectedIndices.add(index);
2397
+ this._state.selectedItems.set(index, item);
2398
+ option.classList.add('smilodon-option--selected');
2399
+ option.setAttribute('aria-selected', 'true');
2400
+ }
2401
+ }
2413
2402
  }
2414
2403
  });
2415
2404
  this._updateInputDisplay();
@@ -2872,11 +2861,6 @@ class EnhancedSelect extends HTMLElement {
2872
2861
  if (this._loadMoreTrigger && this._intersectionObserver) {
2873
2862
  this._intersectionObserver.unobserve(this._loadMoreTrigger);
2874
2863
  }
2875
- // Cleanup all rendered options (including custom components)
2876
- if (this._optionRenderer) {
2877
- this._optionRenderer.unmountAll();
2878
- console.log('[EnhancedSelect] Unmounted all option components');
2879
- }
2880
2864
  // Clear options container
2881
2865
  console.log('[EnhancedSelect] Clearing options container, previous children:', this._optionsContainer.children.length);
2882
2866
  this._optionsContainer.innerHTML = '';
@@ -2993,23 +2977,28 @@ class EnhancedSelect extends HTMLElement {
2993
2977
  console.log('[EnhancedSelect] _renderOptions complete, optionsContainer children:', this._optionsContainer.children.length);
2994
2978
  }
2995
2979
  _renderSingleOption(item, index, getValue, getLabel) {
2996
- if (!this._optionRenderer) {
2997
- console.error('[EnhancedSelect] Option renderer not initialized');
2998
- return;
2999
- }
3000
- // Check if selected
2980
+ const option = document.createElement('div');
2981
+ option.className = 'option';
2982
+ option.id = `${this._uniqueId}-option-${index}`;
2983
+ const value = getValue(item);
2984
+ const label = getLabel(item);
2985
+ console.log('[EnhancedSelect] Rendering option', index, ':', { value, label });
2986
+ option.textContent = label;
2987
+ option.dataset.value = String(value);
2988
+ option.dataset.index = String(index); // Also useful for debugging/selectors
2989
+ // Check if selected using selectedItems map
3001
2990
  const isSelected = this._state.selectedIndices.has(index);
3002
- const isFocused = this._state.activeIndex === index;
3003
- console.log('[EnhancedSelect] Rendering option', index, ':', {
3004
- value: getValue(item),
3005
- label: getLabel(item),
3006
- isSelected,
3007
- isFocused,
3008
- hasCustomComponent: !!item.optionComponent
2991
+ if (isSelected) {
2992
+ option.classList.add('selected');
2993
+ option.setAttribute('aria-selected', 'true');
2994
+ }
2995
+ else {
2996
+ option.setAttribute('aria-selected', 'false');
2997
+ }
2998
+ option.addEventListener('click', () => {
2999
+ this._selectOption(index);
3009
3000
  });
3010
- // Use the OptionRenderer to render both lightweight and custom component options
3011
- const optionElement = this._optionRenderer.render(item, index, isSelected, isFocused, this._uniqueId);
3012
- this._optionsContainer.appendChild(optionElement);
3001
+ this._optionsContainer.appendChild(option);
3013
3002
  console.log('[EnhancedSelect] Option', index, 'appended to optionsContainer');
3014
3003
  }
3015
3004
  _addLoadMoreTrigger() {