@keenmate/web-multiselect 1.12.0-rc02 → 1.12.0-rc03
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 +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/multiselect.js +376 -350
- package/dist/multiselect.umd.js +13 -13
- package/docs/accessibility.md +84 -0
- package/docs/examples.md +1180 -0
- package/docs/theming.md +388 -0
- package/docs/usage.md +307 -0
- package/package.json +5 -1
package/docs/usage.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Usage & API reference
|
|
2
|
+
|
|
3
|
+
This document covers the public surface of `<web-multiselect>` — declarative HTML usage, the full attribute / property / method / event tables, and the data shape.
|
|
4
|
+
|
|
5
|
+
## Declarative (no JavaScript)
|
|
6
|
+
|
|
7
|
+
Perfect for simple forms — just use standard HTML `<option>` and `<optgroup>` elements:
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<!-- Simple choice -->
|
|
11
|
+
<web-multiselect multiple="false">
|
|
12
|
+
<option value="yes">Yes</option>
|
|
13
|
+
<option value="no">No</option>
|
|
14
|
+
<option value="maybe" selected>Maybe</option>
|
|
15
|
+
</web-multiselect>
|
|
16
|
+
|
|
17
|
+
<!-- With icons -->
|
|
18
|
+
<web-multiselect>
|
|
19
|
+
<option value="apple" data-icon="🍎">Apple</option>
|
|
20
|
+
<option value="banana" data-icon="🍌" selected>Banana</option>
|
|
21
|
+
<option value="orange" data-icon="🍊">Orange</option>
|
|
22
|
+
</web-multiselect>
|
|
23
|
+
|
|
24
|
+
<!-- With groups -->
|
|
25
|
+
<web-multiselect>
|
|
26
|
+
<optgroup label="Frontend">
|
|
27
|
+
<option value="js" data-icon="🟨">JavaScript</option>
|
|
28
|
+
<option value="ts" data-icon="🔷">TypeScript</option>
|
|
29
|
+
</optgroup>
|
|
30
|
+
<optgroup label="Backend">
|
|
31
|
+
<option value="python" data-icon="🐍" selected>Python</option>
|
|
32
|
+
<option value="java" data-icon="☕">Java</option>
|
|
33
|
+
</optgroup>
|
|
34
|
+
</web-multiselect>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Programmatic (with JavaScript)
|
|
38
|
+
|
|
39
|
+
For dynamic data and advanced features:
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<!-- Multi-select -->
|
|
43
|
+
<web-multiselect
|
|
44
|
+
id="my-select"
|
|
45
|
+
search-placeholder="Search options..."
|
|
46
|
+
initial-values='["js","ts"]'>
|
|
47
|
+
</web-multiselect>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Import the component (includes styles)
|
|
52
|
+
import '@keenmate/web-multiselect';
|
|
53
|
+
|
|
54
|
+
// Or import styles separately if needed
|
|
55
|
+
import '@keenmate/web-multiselect/style.css';
|
|
56
|
+
|
|
57
|
+
const multiselect = document.querySelector('web-multiselect');
|
|
58
|
+
|
|
59
|
+
// Set options programmatically
|
|
60
|
+
multiselect.options = [
|
|
61
|
+
{ value: 'js', label: 'JavaScript', icon: '🟨' },
|
|
62
|
+
{ value: 'ts', label: 'TypeScript', icon: '🔷' },
|
|
63
|
+
{ value: 'py', label: 'Python', icon: '🐍' }
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Listen for events
|
|
67
|
+
multiselect.addEventListener('change', (e) => {
|
|
68
|
+
console.log('Selected:', e.detail.selectedOptions);
|
|
69
|
+
console.log('Values:', e.detail.selectedValues);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Public API
|
|
73
|
+
const selected = multiselect.getSelected();
|
|
74
|
+
multiselect.setSelected(['js', 'ts']);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Attributes
|
|
78
|
+
|
|
79
|
+
| Attribute | Type | Default | Description |
|
|
80
|
+
|-----------|------|---------|-------------|
|
|
81
|
+
| `multiple` | `boolean` | `true` | Allow multiple selections |
|
|
82
|
+
| `search-placeholder` | `string` | `'Search...'` | Placeholder text for search input |
|
|
83
|
+
| `search-hint` | `string` | - | Hint text shown above input when focused |
|
|
84
|
+
| `allow-groups` | `boolean` | `true` | Enable option grouping |
|
|
85
|
+
| `show-checkboxes` | `boolean` | `true` | Show checkboxes next to options |
|
|
86
|
+
| `close-on-select` | `boolean` | `false` | Close dropdown after selecting |
|
|
87
|
+
| `dropdown-min-width` | `string` | - | Min width for dropdown (e.g., `'20rem'`) |
|
|
88
|
+
| `badges-display-mode` | `'pills' \| 'count' \| 'compact' \| 'partial' \| 'none'` | `'pills'` | How to display selected items. `compact`: first item + count. `none`: no display |
|
|
89
|
+
| `badges-threshold` | `number` | - | Auto-switch mode when exceeded (see `badges-threshold-mode`) |
|
|
90
|
+
| `badges-threshold-mode` | `'count' \| 'partial'` | `'count'` | Mode after threshold: `count` shows badge, `partial` shows limited badges + more badge |
|
|
91
|
+
| `badges-max-visible` | `number` | `3` | Max badges shown in partial mode |
|
|
92
|
+
| `badges-position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Position of badges container |
|
|
93
|
+
| `show-counter` | `boolean` | `false` | Show `[3]` badge next to toggle icon |
|
|
94
|
+
| `enable-badge-tooltips` | `boolean` | `false` | Enable tooltips on selected badges |
|
|
95
|
+
| `badge-tooltip-placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Tooltip placement relative to badge |
|
|
96
|
+
| `badge-tooltip-delay` | `number` | `100` | Delay in ms before showing tooltip |
|
|
97
|
+
| `badge-tooltip-offset` | `number` | `8` | Distance in pixels between badge and tooltip |
|
|
98
|
+
| `max-height` | `string` | `'20rem'` | Maximum height of dropdown |
|
|
99
|
+
| `empty-message` | `string` | `'No results found'` | Message when no options found |
|
|
100
|
+
| `loading-message` | `string` | `'Loading...'` | Message while loading async data |
|
|
101
|
+
| `min-search-length` | `number` | `0` | Minimum search length for async |
|
|
102
|
+
| `keep-options-on-search` | `boolean` | `true` | Keep initial options visible when `searchCallback` is active (hybrid search) |
|
|
103
|
+
| `should-keep-search-on-close` | `boolean` | `true` | Preserve search text and filtered results when dropdown closes |
|
|
104
|
+
| `sticky-actions` | `boolean` | `true` | Keep action buttons fixed at top while scrolling |
|
|
105
|
+
| `actions-layout` | `'nowrap' \| 'wrap'` | `'nowrap'` | Layout mode for action buttons: `nowrap` (single row) or `wrap` (multi-row) |
|
|
106
|
+
| `lock-placement` | `boolean` | `true` | Lock dropdown placement after first open to prevent flipping |
|
|
107
|
+
| `enable-search` | `boolean` | `true` | Enable/disable search functionality |
|
|
108
|
+
| `search-input-mode` | `'normal' \| 'readonly' \| 'hidden'` | `'normal'` | Search input display mode |
|
|
109
|
+
| `search-mode` | `'filter' \| 'navigate'` | `'filter'` | Search behavior: `filter` hides non-matches, `navigate` jumps to matches |
|
|
110
|
+
| `allow-add-new` | `boolean` | `false` | Allow adding new options not in the list |
|
|
111
|
+
| `value-member` | `string` | - | Property name for value/ID extraction from custom objects |
|
|
112
|
+
| `display-value-member` | `string` | - | Property name for display text extraction from custom objects |
|
|
113
|
+
| `search-value-member` | `string` | - | Property name for search text extraction from custom objects |
|
|
114
|
+
| `icon-member` | `string` | - | Property name for icon extraction from custom objects |
|
|
115
|
+
| `subtitle-member` | `string` | - | Property name for subtitle extraction from custom objects |
|
|
116
|
+
| `group-member` | `string` | - | Property name for group extraction from custom objects |
|
|
117
|
+
| `disabled-member` | `string` | - | Property name for disabled state extraction from custom objects |
|
|
118
|
+
| `name` | `string` | - | HTML form field name for form integration (creates hidden input) |
|
|
119
|
+
| `value-format` | `'json' \| 'csv' \| 'array'` | `'json'` | Format for form value serialization |
|
|
120
|
+
| `initial-values` | `string` (JSON array) | - | Pre-selected values |
|
|
121
|
+
| `enable-virtual-scroll` | `boolean` | `false` | Enable virtual scrolling for large datasets |
|
|
122
|
+
| `virtual-scroll-threshold` | `number` | `100` | Minimum items before virtual scroll activates |
|
|
123
|
+
| `option-height` | `number` | `50` | Fixed height for each option in pixels (required for virtual scroll) |
|
|
124
|
+
| `virtual-scroll-buffer` | `number` | `10` | Buffer size — extra items rendered above/below viewport |
|
|
125
|
+
|
|
126
|
+
## Properties
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Get/set options
|
|
130
|
+
multiselect.options = [
|
|
131
|
+
{ value: 'js', label: 'JavaScript' },
|
|
132
|
+
{ value: 'ts', label: 'TypeScript' }
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// Async data loading
|
|
136
|
+
multiselect.onSearch = async (searchTerm) => {
|
|
137
|
+
const response = await fetch(`/api/search?q=${searchTerm}`);
|
|
138
|
+
return await response.json();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Pre-process search terms before calling searchCallback
|
|
142
|
+
multiselect.beforeSearchCallback = (searchTerm) => {
|
|
143
|
+
// Remove accents: "café" → "cafe"
|
|
144
|
+
const normalized = searchTerm.normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
145
|
+
|
|
146
|
+
// Block search if too short (return null to prevent search)
|
|
147
|
+
if (normalized.length < 2) return null;
|
|
148
|
+
|
|
149
|
+
return normalized; // Return transformed term
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Event callbacks
|
|
153
|
+
multiselect.onSelect = (option) => {
|
|
154
|
+
console.log('Selected:', option);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
multiselect.onDeselect = (option) => {
|
|
158
|
+
console.log('Deselected:', option);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
multiselect.onChange = (selectedOptions) => {
|
|
162
|
+
console.log('Changed:', selectedOptions);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Badge display customization (show different text in badges vs dropdown)
|
|
166
|
+
multiselect.getBadgeDisplayCallback = (item) => {
|
|
167
|
+
// Show shorter text in badges (e.g., just name instead of "name (email)")
|
|
168
|
+
return item.name; // Dropdown might show "John Doe (john@example.com)"
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Badge tooltip customization
|
|
172
|
+
multiselect.getBadgeTooltipCallback = (item) => {
|
|
173
|
+
return `${item.label} - ${item.subtitle}`;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Action buttons (Select All, Clear All, custom actions)
|
|
177
|
+
multiselect.actionButtons = [
|
|
178
|
+
{
|
|
179
|
+
action: 'select-all',
|
|
180
|
+
text: 'Select All',
|
|
181
|
+
tooltip: 'Select all items',
|
|
182
|
+
cssClass: 'my-custom-class',
|
|
183
|
+
isVisibleCallback: (multiselect) => multiselect.getSelected().length < 5 // Hide if 5+ selected
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
action: 'clear-all',
|
|
187
|
+
text: 'Clear All',
|
|
188
|
+
tooltip: 'Clear selection',
|
|
189
|
+
isVisible: true, // Static visibility
|
|
190
|
+
isDisabled: false // Static disabled state
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
action: 'custom',
|
|
194
|
+
text: 'Invert',
|
|
195
|
+
tooltip: 'Invert selection',
|
|
196
|
+
onClick: (multiselect) => {
|
|
197
|
+
// Custom action - invert selection
|
|
198
|
+
const allValues = multiselect.options.map(opt => opt.value);
|
|
199
|
+
const selectedValues = multiselect.getValue();
|
|
200
|
+
const inverted = allValues.filter(v => !selectedValues.includes(v));
|
|
201
|
+
multiselect.setSelected(inverted);
|
|
202
|
+
},
|
|
203
|
+
// Dynamic callbacks (take priority over static properties)
|
|
204
|
+
isDisabledCallback: (multiselect) => multiselect.getSelected().length === 0,
|
|
205
|
+
getTextCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'Invert' : 'Select Items First',
|
|
206
|
+
getClassCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'active' : 'inactive'
|
|
207
|
+
}
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
// Counter i18n/pluralization
|
|
211
|
+
multiselect.getCounterCallback = (count, moreCount) => {
|
|
212
|
+
if (moreCount !== undefined) {
|
|
213
|
+
return `+${moreCount} more`; // Partial mode badge
|
|
214
|
+
}
|
|
215
|
+
return `${count} selected`; // Count mode display
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Data extraction - Member properties (for simple property names)
|
|
219
|
+
multiselect.valueMember = 'id';
|
|
220
|
+
multiselect.displayValueMember = 'name';
|
|
221
|
+
multiselect.iconMember = 'icon';
|
|
222
|
+
multiselect.subtitleMember = 'description';
|
|
223
|
+
multiselect.groupMember = 'category';
|
|
224
|
+
multiselect.disabledMember = 'isDisabled';
|
|
225
|
+
|
|
226
|
+
// Data extraction - Callback functions (for complex logic)
|
|
227
|
+
multiselect.getValueCallback = (item) => item.id || item.value;
|
|
228
|
+
multiselect.getDisplayValueCallback = (item) => item.label || item.name;
|
|
229
|
+
multiselect.getSearchValueCallback = (item) => `${item.name} ${item.tags.join(' ')}`;
|
|
230
|
+
multiselect.getIconCallback = (item) => item.icon || '📄';
|
|
231
|
+
multiselect.getSubtitleCallback = (item) => `${item.price} - ${item.stock} in stock`;
|
|
232
|
+
multiselect.getGroupCallback = (item) => item.category;
|
|
233
|
+
multiselect.getDisabledCallback = (item) => item.stock === 0;
|
|
234
|
+
|
|
235
|
+
// Custom rendering - Full HTML control
|
|
236
|
+
multiselect.renderGroupLabelContentCallback = (groupName) => {
|
|
237
|
+
return `<strong>📦 ${groupName.toUpperCase()}</strong>`;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
multiselect.renderOptionContentCallback = (item, context) => {
|
|
241
|
+
return `<strong>${item.name}</strong> <span class="badge">${item.status}</span>`;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
multiselect.renderBadgeContentCallback = (item, context) => {
|
|
245
|
+
return context.isInPopover
|
|
246
|
+
? `${item.icon} ${item.name} - ${item.description}`
|
|
247
|
+
: `${item.icon} ${item.name}`;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
multiselect.renderSelectedContentCallback = (item) => {
|
|
251
|
+
// Customize selected item text in single-select mode (plain text only)
|
|
252
|
+
return item.firstName;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Form integration
|
|
256
|
+
multiselect.name = 'selected_items';
|
|
257
|
+
multiselect.valueFormat = 'json'; // 'json' | 'csv' | 'array'
|
|
258
|
+
multiselect.getValueFormatCallback = (values) => values.join('|'); // Custom format
|
|
259
|
+
|
|
260
|
+
// Read-only properties
|
|
261
|
+
const selectedValue = multiselect.selectedValue; // string | number | array | null
|
|
262
|
+
const selectedItem = multiselect.selectedItem; // First selected item object
|
|
263
|
+
|
|
264
|
+
// Add new option callback
|
|
265
|
+
multiselect.addNewCallback = async (value) => {
|
|
266
|
+
const newOption = await fetch('/api/options', {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
body: JSON.stringify({ name: value })
|
|
269
|
+
}).then(r => r.json());
|
|
270
|
+
return newOption;
|
|
271
|
+
};
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Methods
|
|
275
|
+
|
|
276
|
+
| Method | Description |
|
|
277
|
+
|--------|-------------|
|
|
278
|
+
| `getSelected()` | Get currently selected options as array of option objects |
|
|
279
|
+
| `setSelected(values: (string \| number)[])` | Set selected values by ID/value |
|
|
280
|
+
| `getValue()` | Get selected value(s) — returns single value in single-select mode, array in multi-select mode |
|
|
281
|
+
| `destroy()` | Clean up and destroy instance |
|
|
282
|
+
|
|
283
|
+
## Events
|
|
284
|
+
|
|
285
|
+
| Event | Detail | Description |
|
|
286
|
+
|-------|--------|-------------|
|
|
287
|
+
| `select` | `{ option, selectedOptions }` | Fired when an option is selected |
|
|
288
|
+
| `deselect` | `{ option, selectedOptions }` | Fired when an option is deselected |
|
|
289
|
+
| `change` | `{ selectedOptions, selectedValues }` | Fired when selection changes |
|
|
290
|
+
|
|
291
|
+
## Option structure
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
interface MultiSelectOption {
|
|
295
|
+
value: string; // Required: Unique identifier
|
|
296
|
+
label: string; // Required: Display text
|
|
297
|
+
icon?: string; // Optional: Icon or emoji
|
|
298
|
+
subtitle?: string; // Optional: Subtitle/description
|
|
299
|
+
group?: string; // Optional: Group name
|
|
300
|
+
disabled?: boolean; // Optional: Disable selection
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The component also accepts:
|
|
305
|
+
|
|
306
|
+
- **Tuple arrays** like `[['js', 'JavaScript'], ['ts', 'TypeScript']]` — first element becomes value, second becomes display text.
|
|
307
|
+
- **Arbitrary custom objects** via member attributes (`value-member`, `display-value-member`, etc.) or callbacks (`getValueCallback`, `getDisplayValueCallback`, etc.). See [examples.md → Flexible Data Handling](./examples.md#flexible-data-handling).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keenmate/web-multiselect",
|
|
3
|
-
"version": "1.12.0-
|
|
3
|
+
"version": "1.12.0-rc03",
|
|
4
4
|
"description": "Lightweight multiselect web component with typeahead search, rich content support, and excellent keyboard navigation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/multiselect.umd.js",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
32
|
"src/css",
|
|
33
|
+
"docs/usage.md",
|
|
34
|
+
"docs/theming.md",
|
|
35
|
+
"docs/examples.md",
|
|
36
|
+
"docs/accessibility.md",
|
|
33
37
|
"component-variables.manifest.json"
|
|
34
38
|
],
|
|
35
39
|
"scripts": {
|