@keenmate/web-multiselect 1.0.0-rc04 → 1.0.0-rc06

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
@@ -3,7 +3,7 @@
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
4
  [![npm version](https://img.shields.io/npm/v/@keenmate/web-multiselect.svg)](https://www.npmjs.com/package/@keenmate/web-multiselect)
5
5
 
6
- A lightweight, accessible multiselect web component with typeahead search, rich content support, and excellent keyboard navigation.
6
+ A lightweight, accessible multiselect web component with typeahead search, RTL language support, rich content, and excellent keyboard navigation.
7
7
 
8
8
  ## Features
9
9
 
@@ -17,6 +17,7 @@ A lightweight, accessible multiselect web component with typeahead search, rich
17
17
  - 📦 **Grouped Options** - Organize options into collapsible groups
18
18
  - 🎉 **Smart Positioning** - Uses Floating UI for intelligent dropdown placement
19
19
  - 🌍 **i18n Support** - Customizable callbacks for pluralization and localization
20
+ - 🌐 **RTL Support** - Full right-to-left language support (Arabic, Hebrew, Persian, Urdu, etc.)
20
21
  - ✨ **Modern** - Web Component with Shadow DOM, TypeScript, bundled with Vite
21
22
  - 🌐 **Framework Agnostic** - Works with any framework or vanilla JS
22
23
 
@@ -87,7 +88,7 @@ multiselect.setSelected(['js', 'ts']);
87
88
  | `show-checkboxes` | `boolean` | `true` | Show checkboxes next to options |
88
89
  | `close-on-select` | `boolean` | `false` | Close dropdown after selecting |
89
90
  | `dropdown-min-width` | `string` | - | Min width for dropdown (e.g., '20rem') |
90
- | `pills-display-mode` | `'pills' \| 'count' \| 'compact'` | `'pills'` | How to display selected items |
91
+ | `pills-display-mode` | `'pills' \| 'count' \| 'compact' \| 'partial'` | `'pills'` | How to display selected items |
91
92
  | `pills-threshold` | `number` | - | Auto-switch mode when exceeded (see pills-threshold-mode) |
92
93
  | `pills-threshold-mode` | `'count' \| 'partial'` | `'count'` | Mode after threshold: 'count' shows badge, 'partial' shows limited pills + more badge |
93
94
  | `pills-max-visible` | `number` | `3` | Max pills shown in partial mode |
@@ -101,6 +102,20 @@ multiselect.setSelected(['js', 'ts']);
101
102
  | `empty-message` | `string` | `'No results found'` | Message when no options found |
102
103
  | `loading-message` | `string` | `'Loading...'` | Message while loading async data |
103
104
  | `min-search-length` | `number` | `0` | Minimum search length for async |
105
+ | `sticky-actions` | `boolean` | `true` | Keep Select All/Clear All buttons fixed at top while scrolling |
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
+ | `allow-add-new` | `boolean` | `false` | Allow adding new options not in the list |
110
+ | `value-member` | `string` | - | Property name for value/ID extraction from custom objects |
111
+ | `display-value-member` | `string` | - | Property name for display text extraction from custom objects |
112
+ | `search-value-member` | `string` | - | Property name for search text extraction from custom objects |
113
+ | `icon-member` | `string` | - | Property name for icon extraction from custom objects |
114
+ | `subtitle-member` | `string` | - | Property name for subtitle extraction from custom objects |
115
+ | `group-member` | `string` | - | Property name for group extraction from custom objects |
116
+ | `disabled-member` | `string` | - | Property name for disabled state extraction from custom objects |
117
+ | `name` | `string` | - | HTML form field name for form integration (creates hidden input) |
118
+ | `value-format` | `'json' \| 'csv' \| 'array'` | `'json'` | Format for form value serialization |
104
119
  | `initial-values` | `string` (JSON array) | - | Pre-selected values |
105
120
 
106
121
  ## Properties
@@ -131,6 +146,12 @@ multiselect.onChange = (selectedOptions) => {
131
146
  console.log('Changed:', selectedOptions);
132
147
  };
133
148
 
149
+ // Pill display customization (show different text in pills vs dropdown)
150
+ multiselect.getPillDisplayCallback = (item) => {
151
+ // Show shorter text in pills (e.g., just name instead of "name (email)")
152
+ return item.name; // Dropdown might show "John Doe (john@example.com)"
153
+ };
154
+
134
155
  // Pill tooltip customization
135
156
  multiselect.getPillTooltipCallback = (item) => {
136
157
  return `${item.label} - ${item.subtitle}`;
@@ -143,14 +164,51 @@ multiselect.getCountPillCallback = (count, moreCount) => {
143
164
  }
144
165
  return `${count} selected`; // Count mode display
145
166
  };
167
+
168
+ // Data extraction - Member properties (for simple property names)
169
+ multiselect.valueMember = 'id';
170
+ multiselect.displayValueMember = 'name';
171
+ multiselect.iconMember = 'icon';
172
+ multiselect.subtitleMember = 'description';
173
+ multiselect.groupMember = 'category';
174
+ multiselect.disabledMember = 'isDisabled';
175
+
176
+ // Data extraction - Callback functions (for complex logic)
177
+ multiselect.getValueCallback = (item) => item.id || item.value;
178
+ multiselect.getDisplayValueCallback = (item) => item.label || item.name;
179
+ multiselect.getSearchValueCallback = (item) => `${item.name} ${item.tags.join(' ')}`;
180
+ multiselect.getIconCallback = (item) => item.icon || '📄';
181
+ multiselect.getSubtitleCallback = (item) => `${item.price} - ${item.stock} in stock`;
182
+ multiselect.getGroupCallback = (item) => item.category;
183
+ multiselect.getDisabledCallback = (item) => item.stock === 0;
184
+
185
+ // Form integration
186
+ multiselect.name = 'selected_items';
187
+ multiselect.valueFormat = 'json'; // 'json' | 'csv' | 'array'
188
+ multiselect.getValueFormatCallback = (values) => values.join('|'); // Custom format
189
+
190
+ // Read-only properties
191
+ const selectedValue = multiselect.selectedValue; // string | number | array | null
192
+ const selectedItem = multiselect.selectedItem; // First selected item object
193
+
194
+ // Add new option callback
195
+ multiselect.addNewCallback = async (value) => {
196
+ // Validate and create new option
197
+ const newOption = await fetch('/api/options', {
198
+ method: 'POST',
199
+ body: JSON.stringify({ name: value })
200
+ }).then(r => r.json());
201
+ return newOption;
202
+ };
146
203
  ```
147
204
 
148
205
  ## Methods
149
206
 
150
207
  | Method | Description |
151
208
  |--------|-------------|
152
- | `getSelected()` | Get currently selected options |
153
- | `setSelected(values: string[])` | Set selected values |
209
+ | `getSelected()` | Get currently selected options as array of option objects |
210
+ | `setSelected(values: (string \| number)[])` | Set selected values by ID/value |
211
+ | `getValue()` | Get selected value(s) - returns single value in single-select mode, array in multi-select mode |
154
212
  | `destroy()` | Clean up and destroy instance |
155
213
 
156
214
  ## Events
@@ -272,7 +330,7 @@ Perfect for different use cases and space constraints:
272
330
 
273
331
  ### Pills Positioning
274
332
 
275
- Control where selected item badges appear:
333
+ Control where selected item badges appear relative to the input:
276
334
 
277
335
  ```html
278
336
  <!-- Pills below input (default) -->
@@ -281,13 +339,15 @@ Control where selected item badges appear:
281
339
  <!-- Pills above input -->
282
340
  <multi-select pills-position="top"></multi-select>
283
341
 
284
- <!-- Pills to the left (RTL) -->
342
+ <!-- Pills to the left of input -->
285
343
  <multi-select pills-position="left"></multi-select>
286
344
 
287
- <!-- Pills to the right (LTR) -->
345
+ <!-- Pills to the right of input -->
288
346
  <multi-select pills-position="right"></multi-select>
289
347
  ```
290
348
 
349
+ **Note:** In RTL mode, left/right positions are automatically mirrored - `pills-position="left"` will appear on the physical right side in RTL languages.
350
+
291
351
  ### Pill Tooltips
292
352
 
293
353
  Enable tooltips on selected item pills with customizable placement and delay:
@@ -342,6 +402,287 @@ Customize count pill text for proper pluralization and localization:
342
402
  </script>
343
403
  ```
344
404
 
405
+ ### Right-to-Left (RTL) Language Support
406
+
407
+ Full RTL support for Arabic, Hebrew, Persian, Urdu, and other right-to-left languages with automatic detection and complete UI mirroring:
408
+
409
+ ```html
410
+ <!-- Automatic RTL detection from dir attribute -->
411
+ <multi-select dir="rtl" search-placeholder="ابحث..."></multi-select>
412
+
413
+ <!-- RTL inherited from parent element -->
414
+ <div dir="rtl">
415
+ <multi-select search-placeholder="חיפוש..."></multi-select>
416
+ </div>
417
+
418
+ <!-- RTL on page level -->
419
+ <html dir="rtl">
420
+ <!-- All multi-selects will auto-detect RTL -->
421
+ </html>
422
+ ```
423
+
424
+ **RTL Features:**
425
+ - ✅ **Auto-detection** - Detects `dir="rtl"` on component or any ancestor element
426
+ - ✅ **Complete UI mirroring** - Toggle icon, text alignment, pills, dropdown, badges
427
+ - ✅ **Logical positioning** - `pills-position="left"` becomes physically right in RTL
428
+ - ✅ **Pills remove buttons** - Flip to left side in RTL mode
429
+ - ✅ **Text direction** - All text content properly right-aligned
430
+ - ✅ **No configuration needed** - Just set `dir="rtl"` attribute
431
+
432
+ ### Flexible Data Handling
433
+
434
+ The component supports **any data structure** through a member/callback pattern, allowing you to work with custom objects, tuple arrays, or existing API responses without transformation.
435
+
436
+ #### Member Properties (Simple Property Names)
437
+
438
+ For objects with consistent property names, use member attributes:
439
+
440
+ ```html
441
+ <multi-select
442
+ id="products"
443
+ value-member="productId"
444
+ display-value-member="productName"
445
+ icon-member="icon"
446
+ subtitle-member="description"
447
+ group-member="category">
448
+ </multi-select>
449
+
450
+ <script type="module">
451
+ const select = document.getElementById('products');
452
+ select.options = [
453
+ {
454
+ productId: 'p1',
455
+ productName: 'Laptop',
456
+ icon: '💻',
457
+ description: 'High-performance laptop',
458
+ category: 'Electronics'
459
+ },
460
+ {
461
+ productId: 'p2',
462
+ productName: 'Mouse',
463
+ icon: '🖱️',
464
+ description: 'Wireless mouse',
465
+ category: 'Electronics'
466
+ }
467
+ ];
468
+ </script>
469
+ ```
470
+
471
+ #### Callback Functions (Complex Logic)
472
+
473
+ For complex data extraction or conditional logic, use callbacks:
474
+
475
+ ```javascript
476
+ const select = document.querySelector('multi-select');
477
+
478
+ // Custom value extraction
479
+ select.getValueCallback = (item) => item.id || item.code || item.value;
480
+
481
+ // Combine multiple fields for display
482
+ select.getDisplayValueCallback = (item) => {
483
+ return `${item.firstName} ${item.lastName}`;
484
+ };
485
+
486
+ // Include multiple fields in search
487
+ select.getSearchValueCallback = (item) => {
488
+ return `${item.name} ${item.sku} ${item.tags.join(' ')}`;
489
+ };
490
+
491
+ // Conditional icons
492
+ select.getIconCallback = (item) => {
493
+ return item.inStock ? '✅' : '❌';
494
+ };
495
+
496
+ // Dynamic subtitles
497
+ select.getSubtitleCallback = (item) => {
498
+ return `$${item.price} - ${item.stock} in stock`;
499
+ };
500
+
501
+ // Disable based on conditions
502
+ select.getDisabledCallback = (item) => {
503
+ return item.stock === 0 || item.discontinued;
504
+ };
505
+
506
+ // Customize pill display (show different text in pills vs dropdown)
507
+ select.getPillDisplayCallback = (item) => {
508
+ // Pills show just the name for space efficiency
509
+ return item.name;
510
+ // While dropdown can show full details: "Laptop - $999 - Electronics"
511
+ };
512
+ ```
513
+
514
+ #### Tuple Array Auto-Detection
515
+
516
+ The component automatically detects `[key, value]` tuple arrays:
517
+
518
+ ```javascript
519
+ select.options = [
520
+ ['js', 'JavaScript'],
521
+ ['ts', 'TypeScript'],
522
+ ['py', 'Python']
523
+ ];
524
+ // First element becomes value, second becomes display text
525
+ ```
526
+
527
+ #### Priority Order
528
+
529
+ When multiple extraction methods are defined, the component uses this priority:
530
+
531
+ 1. **Callbacks** (highest priority) - `getValueCallback`, `getDisplayValueCallback`, etc.
532
+ 2. **Member properties** - `valueMember`, `displayValueMember`, etc.
533
+ 3. **Default properties** (lowest priority) - Falls back to `value`, `label`, `name`, etc.
534
+
535
+ #### TypeScript Support
536
+
537
+ The component is fully typed with generics:
538
+
539
+ ```typescript
540
+ import type { MultiSelectElement } from '@keenmate/web-multiselect';
541
+
542
+ interface Product {
543
+ id: string;
544
+ name: string;
545
+ price: number;
546
+ category: string;
547
+ }
548
+
549
+ const select = document.querySelector<MultiSelectElement<Product>>('multi-select');
550
+ select.options = [
551
+ { id: 'p1', name: 'Laptop', price: 999, category: 'Electronics' }
552
+ ];
553
+ ```
554
+
555
+ ### Form Integration
556
+
557
+ The component seamlessly integrates with standard HTML forms by automatically creating hidden inputs in the light DOM (outside Shadow DOM) so FormData can access them.
558
+
559
+ #### Basic Form Integration
560
+
561
+ ```html
562
+ <form id="userForm" action="/submit" method="POST">
563
+ <label>Select Skills:</label>
564
+ <multi-select
565
+ name="skills"
566
+ value-format="json"
567
+ multiple="true">
568
+ </multi-select>
569
+
570
+ <button type="submit">Submit</button>
571
+ </form>
572
+
573
+ <script type="module">
574
+ import '@keenmate/web-multiselect';
575
+
576
+ const form = document.getElementById('userForm');
577
+ const select = form.querySelector('multi-select');
578
+
579
+ select.options = [
580
+ { value: 'js', label: 'JavaScript' },
581
+ { value: 'ts', label: 'TypeScript' },
582
+ { value: 'py', label: 'Python' }
583
+ ];
584
+
585
+ form.addEventListener('submit', (e) => {
586
+ e.preventDefault();
587
+ const formData = new FormData(form);
588
+
589
+ // Access the value
590
+ const skills = formData.get('skills');
591
+ console.log('Selected skills:', skills);
592
+ // Output: ["js","ts"] (JSON string)
593
+ });
594
+ </script>
595
+ ```
596
+
597
+ #### Value Formats
598
+
599
+ Choose how selected values are serialized in forms:
600
+
601
+ **JSON Format** (default):
602
+ ```html
603
+ <multi-select name="items" value-format="json"></multi-select>
604
+ <!-- FormData result: items = ["item1","item2","item3"] -->
605
+ ```
606
+
607
+ **CSV Format**:
608
+ ```html
609
+ <multi-select name="items" value-format="csv"></multi-select>
610
+ <!-- FormData result: items = "item1,item2,item3" -->
611
+ ```
612
+
613
+ **Array Format** (multiple inputs):
614
+ ```html
615
+ <multi-select name="items" value-format="array"></multi-select>
616
+ <!-- FormData result:
617
+ items[] = "item1"
618
+ items[] = "item2"
619
+ items[] = "item3"
620
+ -->
621
+ ```
622
+
623
+ #### Custom Value Formatting
624
+
625
+ For advanced use cases, provide a custom formatting function:
626
+
627
+ ```javascript
628
+ const select = document.querySelector('multi-select');
629
+
630
+ select.name = 'product_ids';
631
+ select.getValueFormatCallback = (values) => {
632
+ // Custom format: pipe-separated with prefix
633
+ return values.map(v => `ID:${v}`).join('|');
634
+ };
635
+
636
+ // When submitted, FormData will have:
637
+ // product_ids = "ID:123|ID:456|ID:789"
638
+ ```
639
+
640
+ #### Using getValue() for JavaScript Submissions
641
+
642
+ For JavaScript-based form submissions (AJAX, fetch), use `getValue()`:
643
+
644
+ ```javascript
645
+ // Single-select mode
646
+ const select = document.querySelector('multi-select[multiple="false"]');
647
+ const selectedId = select.getValue();
648
+ // Returns: "js" or null
649
+
650
+ // Multi-select mode
651
+ const multiSelect = document.querySelector('multi-select[multiple="true"]');
652
+ const selectedIds = multiSelect.getValue();
653
+ // Returns: ["js", "ts", "py"] or []
654
+
655
+ // Submit with fetch
656
+ const response = await fetch('/api/update', {
657
+ method: 'POST',
658
+ headers: { 'Content-Type': 'application/json' },
659
+ body: JSON.stringify({
660
+ skills: multiSelect.getValue()
661
+ })
662
+ });
663
+ ```
664
+
665
+ #### Working with Numeric Values
666
+
667
+ The component handles both string and numeric values correctly:
668
+
669
+ ```javascript
670
+ select.options = [
671
+ { value: 1, label: 'Option 1' },
672
+ { value: 2, label: 'Option 2' },
673
+ { value: 3, label: 'Option 3' }
674
+ ];
675
+
676
+ // getValue() preserves types
677
+ const values = select.getValue();
678
+ // Returns: [1, 2, 3] (numbers, not strings)
679
+
680
+ // FormData serialization
681
+ // JSON format: [1,2,3]
682
+ // CSV format: 1,2,3
683
+ // Array format: items[]=1, items[]=2, items[]=3
684
+ ```
685
+
345
686
  ### Disabled Options
346
687
 
347
688
  ```javascript
@@ -409,7 +750,32 @@ You can customize the component using CSS variables even with just a `<script>`
409
750
 
410
751
  ### Available CSS Variables
411
752
 
412
- All 211 SCSS variables are exposed as CSS custom properties with fallbacks. Below are the most commonly customized variables organized by category. For the complete list, see [_multiselect.scss](./src/scss/_multiselect.scss).
753
+ The component exposes **150+ CSS custom properties** defined at the `:host` level, making them inspectable and overridable. Below are the **50+ most commonly customized variables** organized by category.
754
+
755
+ #### Inspecting Variables in DevTools
756
+
757
+ All CSS custom properties are now defined at the `:host` level in the compiled CSS, making them visible in browser DevTools:
758
+
759
+ 1. Open DevTools (F12) and select the `<multi-select>` element
760
+ 2. In the **Styles** panel, look for the `:host` selector
761
+ 3. You'll see all 150+ variables with their default values
762
+ 4. Edit values live to preview changes instantly
763
+
764
+ **CSS variables work with Shadow DOM** because they inherit through the shadow boundary. This means you can customize the component from outside:
765
+
766
+ ```html
767
+ <style>
768
+ /* These variables will penetrate into the Shadow DOM */
769
+ multi-select {
770
+ --ml-accent-color: #10b981; /* Changes primary color */
771
+ --ml-input-border-radius: 0.5rem; /* Rounds input corners */
772
+ }
773
+ </style>
774
+ ```
775
+
776
+ For the complete list of all available CSS variables, see:
777
+ - [_css-variables.scss](./src/scss/_css-variables.scss) - All 150+ CSS custom properties at `:host` level
778
+ - [_variables.scss](./src/scss/_variables.scss) - Foundation SCSS variables (colors, spacing, typography)
413
779
 
414
780
  #### Colors
415
781