@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 +39 -9
- package/dist/index.cjs +58 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +58 -69
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.umd.js +58 -69
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/dist/types/src/components/enhanced-select.d.ts +0 -2
- package/package.json +1 -1
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[];
|
|
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]
|
|
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]
|
|
2376
|
-
|
|
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
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
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
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
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
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
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
|
-
|
|
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() {
|