svelte-multiselect 6.0.3 → 7.0.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.
@@ -22,6 +22,7 @@ export let focusInputOnSelect = `desktop`;
22
22
  export let id = null;
23
23
  export let input = null;
24
24
  export let inputClass = ``;
25
+ export let inputmode = null;
25
26
  export let invalid = false;
26
27
  export let liActiveOptionClass = ``;
27
28
  export let liOptionClass = ``;
@@ -37,17 +38,30 @@ export let options;
37
38
  export let outerDiv = null;
38
39
  export let outerDivClass = ``;
39
40
  export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
41
+ export let pattern = null;
40
42
  export let placeholder = null;
41
43
  export let removeAllTitle = `Remove all`;
42
44
  export let removeBtnTitle = `Remove`;
43
45
  export let required = false;
44
46
  export let searchText = ``;
45
- export let selected = options?.filter((op) => op?.preselected) ?? [];
47
+ export let selected = options
48
+ ?.filter((op) => op?.preselected)
49
+ .slice(0, maxSelect ?? undefined) ?? [];
46
50
  export let selectedLabels = [];
47
51
  export let selectedValues = [];
48
52
  export let sortSelected = false;
49
53
  export let ulOptionsClass = ``;
50
54
  export let ulSelectedClass = ``;
55
+ // selected and _selected are identical except if maxSelect=1, selected will be the single item (or null)
56
+ // in _selected which will always be an array for easier component internals. selected then solves
57
+ // https://github.com/janosh/svelte-multiselect/issues/86
58
+ let _selected = (selected ?? []);
59
+ $: selected = maxSelect === 1 ? _selected[0] ?? null : _selected;
60
+ let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
61
+ $: _selectedLabels = _selected?.map(get_label) ?? [];
62
+ $: selectedLabels = maxSelect === 1 ? _selectedLabels[0] ?? null : _selectedLabels;
63
+ $: _selectedValues = _selected?.map(get_value) ?? [];
64
+ $: selectedValues = maxSelect === 1 ? _selectedValues[0] ?? null : _selectedValues;
51
65
  if (!(options?.length > 0)) {
52
66
  if (allowUserOptions) {
53
67
  options = []; // initializing as array avoids errors when component mounts
@@ -63,35 +77,32 @@ if (parseLabelsAsHtml && allowUserOptions) {
63
77
  if (maxSelect !== null && maxSelect < 1) {
64
78
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
65
79
  }
66
- if (!Array.isArray(selected)) {
67
- console.error(`selected prop must be an array, got ${selected}`);
80
+ if (!Array.isArray(_selected)) {
81
+ console.error(`internal variable _selected prop should always be an array, got ${_selected}`);
68
82
  }
69
83
  const dispatch = createEventDispatcher();
70
84
  let add_option_msg_is_active = false; // controls active state of <li>{addOptionMsg}</li>
71
85
  let window_width;
72
- let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
73
- $: selectedLabels = selected.map(get_label);
74
- $: selectedValues = selected.map(get_value);
75
86
  // formValue binds to input.form-control to prevent form submission if required
76
87
  // prop is true and no options are selected
77
- $: formValue = selectedValues.join(`,`);
88
+ $: formValue = _selectedValues.join(`,`);
78
89
  $: if (formValue)
79
90
  invalid = false; // reset error status whenever component state changes
80
91
  // options matching the current search text
81
- $: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
92
+ $: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !_selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
82
93
  );
83
94
  // raise if matchingOptions[activeIndex] does not yield a value
84
95
  if (activeIndex !== null && !matchingOptions[activeIndex]) {
85
96
  throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
86
97
  }
87
98
  // update activeOption when activeIndex changes
88
- $: activeOption = activeIndex ? matchingOptions[activeIndex] : null;
99
+ $: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
89
100
  // add an option to selected list
90
- function add(label) {
91
- if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
101
+ function add(label, event) {
102
+ if (maxSelect && maxSelect > 1 && _selected.length >= maxSelect)
92
103
  wiggle = true;
93
104
  // to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
94
- if (maxSelect === null || maxSelect === 1 || selected.length < maxSelect) {
105
+ if (maxSelect === null || maxSelect === 1 || _selected.length < maxSelect) {
95
106
  // first check if we find option in the options list
96
107
  let option = options.find((op) => get_label(op) === label);
97
108
  if (!option && // this has the side-effect of not allowing to user to add the same
@@ -123,23 +134,23 @@ function add(label) {
123
134
  }
124
135
  if (maxSelect === 1) {
125
136
  // for maxselect = 1 we always replace current option with new one
126
- selected = [option];
137
+ _selected = [option];
127
138
  }
128
139
  else {
129
- selected = [...selected, option];
140
+ _selected = [..._selected, option];
130
141
  if (sortSelected === true) {
131
- selected = selected.sort((op1, op2) => {
142
+ _selected = _selected.sort((op1, op2) => {
132
143
  const [label1, label2] = [get_label(op1), get_label(op2)];
133
144
  // coerce to string if labels are numbers
134
145
  return `${label1}`.localeCompare(`${label2}`);
135
146
  });
136
147
  }
137
148
  else if (typeof sortSelected === `function`) {
138
- selected = selected.sort(sortSelected);
149
+ _selected = _selected.sort(sortSelected);
139
150
  }
140
151
  }
141
- if (selected.length === maxSelect)
142
- close_dropdown();
152
+ if (_selected.length === maxSelect)
153
+ close_dropdown(event);
143
154
  else if (focusInputOnSelect === true ||
144
155
  (focusInputOnSelect === `desktop` && window_width > breakpoint)) {
145
156
  input?.focus();
@@ -150,10 +161,10 @@ function add(label) {
150
161
  }
151
162
  // remove an option from selected list
152
163
  function remove(label) {
153
- if (selected.length === 0)
164
+ if (_selected.length === 0)
154
165
  return;
155
- selected.splice(selectedLabels.lastIndexOf(label), 1);
156
- selected = selected; // Svelte rerender after in-place splice
166
+ _selected.splice(_selectedLabels.lastIndexOf(label), 1);
167
+ _selected = _selected; // Svelte rerender after in-place splice
157
168
  const option = options.find((option) => get_label(option) === label) ??
158
169
  // if option with label could not be found but allowUserOptions is truthy,
159
170
  // assume it was created by user and create correspondidng option object
@@ -165,24 +176,27 @@ function remove(label) {
165
176
  dispatch(`remove`, { option });
166
177
  dispatch(`change`, { option, type: `remove` });
167
178
  }
168
- function open_dropdown() {
179
+ function open_dropdown(event) {
169
180
  if (disabled)
170
181
  return;
171
182
  open = true;
172
- input?.focus();
173
- dispatch(`focus`);
183
+ if (!(event instanceof FocusEvent)) {
184
+ // avoid double-focussing input when event that opened dropdown was already input FocusEvent
185
+ input?.focus();
186
+ }
187
+ dispatch(`open`, { event });
174
188
  }
175
- function close_dropdown() {
189
+ function close_dropdown(event) {
176
190
  open = false;
177
191
  input?.blur();
178
192
  activeOption = null;
179
- dispatch(`blur`);
193
+ dispatch(`close`, { event });
180
194
  }
181
195
  // handle all keyboard events this component receives
182
196
  async function handle_keydown(event) {
183
197
  // on escape or tab out of input: dismiss options dropdown and reset search text
184
198
  if (event.key === `Escape` || event.key === `Tab`) {
185
- close_dropdown();
199
+ close_dropdown(event);
186
200
  searchText = ``;
187
201
  }
188
202
  // on enter key: toggle active option and reset search text
@@ -190,17 +204,17 @@ async function handle_keydown(event) {
190
204
  event.preventDefault(); // prevent enter key from triggering form submission
191
205
  if (activeOption) {
192
206
  const label = get_label(activeOption);
193
- selectedLabels.includes(label) ? remove(label) : add(label);
207
+ selectedLabels.includes(label) ? remove(label) : add(label, event);
194
208
  searchText = ``;
195
209
  }
196
210
  else if (allowUserOptions && searchText.length > 0) {
197
211
  // user entered text but no options match, so if allowUserOptions is truthy, we create new option
198
- add(searchText);
212
+ add(searchText, event);
199
213
  }
200
214
  // no active option and no search text means the options dropdown is closed
201
215
  // in which case enter means open it
202
216
  else
203
- open_dropdown();
217
+ open_dropdown(event);
204
218
  }
205
219
  // on up/down arrow keys: update active option
206
220
  else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
@@ -238,17 +252,17 @@ async function handle_keydown(event) {
238
252
  }
239
253
  }
240
254
  // on backspace key: remove last selected option
241
- else if (event.key === `Backspace` && selectedLabels.length > 0 && !searchText) {
242
- remove(selectedLabels.at(-1));
255
+ else if (event.key === `Backspace` && _selectedLabels.length > 0 && !searchText) {
256
+ remove(_selectedLabels.at(-1));
243
257
  }
244
258
  }
245
259
  function remove_all() {
246
- dispatch(`removeAll`, { options: selected });
247
- dispatch(`change`, { options: selected, type: `removeAll` });
248
- selected = [];
260
+ dispatch(`removeAll`, { options: _selected });
261
+ dispatch(`change`, { options: _selected, type: `removeAll` });
262
+ _selected = [];
249
263
  searchText = ``;
250
264
  }
251
- $: is_selected = (label) => selectedLabels.includes(label);
265
+ $: is_selected = (label) => _selectedLabels.includes(label);
252
266
  const if_enter_or_space = (handler) => (event) => {
253
267
  if ([`Enter`, `Space`].includes(event.code)) {
254
268
  event.preventDefault();
@@ -257,7 +271,7 @@ const if_enter_or_space = (handler) => (event) => {
257
271
  };
258
272
  function on_click_outside(event) {
259
273
  if (outerDiv && !outerDiv.contains(event.target)) {
260
- close_dropdown();
274
+ close_dropdown(event);
261
275
  }
262
276
  }
263
277
  </script>
@@ -292,7 +306,7 @@ function on_click_outside(event) {
292
306
  />
293
307
  <ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
294
308
  <ul class="selected {ulSelectedClass}">
295
- {#each selected as option, idx}
309
+ {#each _selected as option, idx}
296
310
  <li class={liSelectedClass} aria-selected="true">
297
311
  <slot name="selected" {option} {idx}>
298
312
  {#if parseLabelsAsHtml}
@@ -320,14 +334,30 @@ function on_click_outside(event) {
320
334
  {autocomplete}
321
335
  bind:value={searchText}
322
336
  on:mouseup|self|stopPropagation={open_dropdown}
323
- on:keydown={handle_keydown}
337
+ on:keydown|stopPropagation={handle_keydown}
338
+ on:focus
324
339
  on:focus={open_dropdown}
325
340
  {id}
326
341
  {name}
327
342
  {disabled}
328
- placeholder={selectedLabels.length ? `` : placeholder}
343
+ {inputmode}
344
+ {pattern}
345
+ placeholder={_selected.length == 0 ? placeholder : null}
329
346
  aria-invalid={invalid ? `true` : null}
347
+ on:blur
348
+ on:change
349
+ on:click
350
+ on:keydown
351
+ on:keyup
352
+ on:mousedown
353
+ on:mouseenter
354
+ on:mouseleave
355
+ on:touchcancel
356
+ on:touchend
357
+ on:touchmove
358
+ on:touchstart
330
359
  />
360
+ <!-- the above on:* lines forward potentially useful DOM events -->
331
361
  </li>
332
362
  </ul>
333
363
  {#if loading}
@@ -339,16 +369,16 @@ function on_click_outside(event) {
339
369
  <slot name="disabled-icon">
340
370
  <DisabledIcon width="15px" />
341
371
  </slot>
342
- {:else if selected.length > 0}
372
+ {:else if _selected.length > 0}
343
373
  {#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
344
374
  <Wiggle bind:wiggle angle={20}>
345
375
  <span style="padding: 0 3pt;">
346
- {maxSelectMsg?.(selected.length, maxSelect) ??
347
- (maxSelect > 1 ? `${selected.length}/${maxSelect}` : ``)}
376
+ {maxSelectMsg?.(_selected.length, maxSelect) ??
377
+ (maxSelect > 1 ? `${_selected.length}/${maxSelect}` : ``)}
348
378
  </span>
349
379
  </Wiggle>
350
380
  {/if}
351
- {#if maxSelect !== 1 && selected.length > 1}
381
+ {#if maxSelect !== 1 && _selected.length > 1}
352
382
  <button
353
383
  type="button"
354
384
  class="remove-all"
@@ -375,8 +405,8 @@ function on_click_outside(event) {
375
405
  {@const active = activeIndex === idx}
376
406
  <li
377
407
  on:mousedown|stopPropagation
378
- on:mouseup|stopPropagation={() => {
379
- if (!disabled) is_selected(label) ? remove(label) : add(label)
408
+ on:mouseup|stopPropagation={(event) => {
409
+ if (!disabled) is_selected(label) ? remove(label) : add(label, event)
380
410
  }}
381
411
  title={disabled
382
412
  ? disabledTitle
@@ -407,7 +437,7 @@ function on_click_outside(event) {
407
437
  {#if allowUserOptions && searchText}
408
438
  <li
409
439
  on:mousedown|stopPropagation
410
- on:mouseup|stopPropagation={() => add(searchText)}
440
+ on:mouseup|stopPropagation={(event) => add(searchText, event)}
411
441
  title={addOptionMsg}
412
442
  class:active={add_option_msg_is_active}
413
443
  on:mouseover={() => (add_option_msg_is_active = true)}
@@ -17,6 +17,7 @@ declare const __propDef: {
17
17
  id?: string | null | undefined;
18
18
  input?: HTMLInputElement | null | undefined;
19
19
  inputClass?: string | undefined;
20
+ inputmode?: string | null | undefined;
20
21
  invalid?: boolean | undefined;
21
22
  liActiveOptionClass?: string | undefined;
22
23
  liOptionClass?: string | undefined;
@@ -32,14 +33,15 @@ declare const __propDef: {
32
33
  outerDiv?: HTMLDivElement | null | undefined;
33
34
  outerDivClass?: string | undefined;
34
35
  parseLabelsAsHtml?: boolean | undefined;
36
+ pattern?: string | null | undefined;
35
37
  placeholder?: string | null | undefined;
36
38
  removeAllTitle?: string | undefined;
37
39
  removeBtnTitle?: string | undefined;
38
40
  required?: boolean | undefined;
39
41
  searchText?: string | undefined;
40
- selected?: Option[] | undefined;
41
- selectedLabels?: (string | number)[] | undefined;
42
- selectedValues?: unknown[] | undefined;
42
+ selected?: Option | Option[] | null | undefined;
43
+ selectedLabels?: string | number | (string | number)[] | null | undefined;
44
+ selectedValues?: unknown[] | unknown | null;
43
45
  sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
44
46
  ulOptionsClass?: string | undefined;
45
47
  ulSelectedClass?: string | undefined;
package/index.d.ts CHANGED
@@ -25,11 +25,27 @@ export declare type DispatchEvents = {
25
25
  options?: Option[];
26
26
  type: 'add' | 'remove' | 'removeAll';
27
27
  };
28
- focus: undefined;
29
- blur: undefined;
28
+ open: {
29
+ event: Event;
30
+ };
31
+ close: {
32
+ event: Event;
33
+ };
30
34
  };
31
35
  export declare type MultiSelectEvents = {
32
36
  [key in keyof DispatchEvents]: CustomEvent<DispatchEvents[key]>;
37
+ } & {
38
+ blur: FocusEvent;
39
+ click: MouseEvent;
40
+ focus: FocusEvent;
41
+ keydown: KeyboardEvent;
42
+ keyup: KeyboardEvent;
43
+ mouseenter: MouseEvent;
44
+ mouseleave: MouseEvent;
45
+ touchcancel: TouchEvent;
46
+ touchend: TouchEvent;
47
+ touchmove: TouchEvent;
48
+ touchstart: TouchEvent;
33
49
  };
34
50
  export declare const get_label: (op: Option) => string | number;
35
51
  export declare const get_value: (op: Option) => {};
package/package.json CHANGED
@@ -5,20 +5,20 @@
5
5
  "homepage": "https://svelte-multiselect.netlify.app",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "6.0.3",
8
+ "version": "7.0.0",
9
9
  "type": "module",
10
10
  "svelte": "index.js",
11
11
  "main": "index.js",
12
12
  "bugs": "https://github.com/janosh/svelte-multiselect/issues",
13
13
  "devDependencies": {
14
- "@playwright/test": "^1.25.2",
14
+ "@playwright/test": "^1.26.0",
15
15
  "@sveltejs/adapter-static": "^1.0.0-next.43",
16
- "@sveltejs/kit": "^1.0.0-next.481",
17
- "@sveltejs/package": "^1.0.0-next.3",
18
- "@sveltejs/vite-plugin-svelte": "^1.0.5",
19
- "@typescript-eslint/eslint-plugin": "^5.37.0",
20
- "@typescript-eslint/parser": "^5.37.0",
21
- "eslint": "^8.23.1",
16
+ "@sveltejs/kit": "^1.0.0-next.502",
17
+ "@sveltejs/package": "^1.0.0-next.5",
18
+ "@sveltejs/vite-plugin-svelte": "^1.0.8",
19
+ "@typescript-eslint/eslint-plugin": "^5.38.0",
20
+ "@typescript-eslint/parser": "^5.38.0",
21
+ "eslint": "^8.24.0",
22
22
  "eslint-plugin-svelte3": "^4.0.0",
23
23
  "hastscript": "^7.0.2",
24
24
  "jsdom": "^20.0.0",
@@ -32,11 +32,11 @@
32
32
  "svelte-github-corner": "^0.1.0",
33
33
  "svelte-preprocess": "^4.10.6",
34
34
  "svelte-toc": "^0.4.0",
35
- "svelte2tsx": "^0.5.17",
35
+ "svelte2tsx": "^0.5.18",
36
36
  "tslib": "^2.4.0",
37
37
  "typescript": "^4.8.3",
38
- "vite": "^3.1.0",
39
- "vitest": "^0.23.2"
38
+ "vite": "^3.1.3",
39
+ "vitest": "^0.23.4"
40
40
  },
41
41
  "keywords": [
42
42
  "svelte",
package/readme.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![Netlify Status](https://api.netlify.com/api/v1/badges/a45b62c3-ea45-4cfd-9912-77ec4fc8d7e8/deploy-status)](https://app.netlify.com/sites/svelte-multiselect/deploys)
10
10
  [![NPM version](https://img.shields.io/npm/v/svelte-multiselect?logo=NPM&color=purple)](https://npmjs.com/package/svelte-multiselect)
11
11
  [![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/dev/svelte?color=teal)](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
12
- [![REPL](https://img.shields.io/badge/Svelte-REPL-blue)](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
12
+ [![REPL](https://img.shields.io/badge/Svelte-REPL-blue?logo=Svelte)](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
13
13
  [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-darkblue?logo=stackblitz)](https://stackblitz.com/github/janosh/svelte-multiselect)
14
14
 
15
15
  </h4>
@@ -40,6 +40,8 @@
40
40
  - **v6.0.0** The prop `showOptions` which controls whether the list of dropdown options is currently open or closed was renamed to `open`. See [PR 103](https://github.com/janosh/svelte-multiselect/pull/103).
41
41
  - **v6.0.1** The prop `disabledTitle` which sets the title of the `<MultiSelect>` `<input>` node if in `disabled` mode was renamed to `disabledInputTitle`. See [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
42
42
  - **v6.0.1** The default margin of `1em 0` on the wrapper `div.multiselect` was removed. Instead, there is now a new CSS variable `--sms-margin`. Set it to `--sms-margin: 1em 0;` to restore the old appearance. See [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
43
+ - **6.1.0** The `dispatch` events `focus` and `blur` were renamed to `open` and `close`, respectively. These actions refer to the dropdown list, i.e. `<MultiSelect on:open={(event) => console.log(event)}>` will trigger when the dropdown list opens. The focus and blur events are now regular DOM (not Svelte `dispatch`) events emitted by the `<input>` node. See [PR 120](https://github.com/janosh/svelte-multiselect/pull/120).
44
+ - **v7.0.0** `selected` (as well `selectedLabels` and `selectedValues`) used to be arrays always. Now, if `maxSelect=1`, they will no longer be a length-1 array but simply a single a option (label/value respectively) or `null` if no option is selected. See [PR 123](https://github.com/janosh/svelte-multiselect/pull/123).
43
45
 
44
46
  ## Installation
45
47
 
@@ -97,7 +99,8 @@ import type { Option } from 'svelte-multiselect'
97
99
  allowUserOptions: boolean | 'append' = false
98
100
  ```
99
101
 
100
- Whether users are allowed to enter values not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected.
102
+ Whether users can enter values that are not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected.
103
+ If `allowUserOptions` is `true` or `'append'` then the type `object | number | string` of entered value is determined from the first option of the list to keep type homogeneity.
101
104
 
102
105
  1. ```ts
103
106
  autocomplete: string = `off`
@@ -162,6 +165,12 @@ import type { Option } from 'svelte-multiselect'
162
165
 
163
166
  Handle to the `<input>` DOM node. Only available after component mounts (`null` before then).
164
167
 
168
+ 1. ```ts
169
+ inputmode: string | null = null
170
+ ```
171
+
172
+ The `inputmode` attribute hints at the type of data the user may enter. Values like `'numeric' | 'tel' | 'email'` allow browsers to display an appropriate virtual keyboard. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode) for details.
173
+
165
174
  1. ```ts
166
175
  invalid: boolean = false
167
176
  ```
@@ -218,7 +227,7 @@ import type { Option } from 'svelte-multiselect'
218
227
  options: Option[]
219
228
  ```
220
229
 
221
- **The only required prop** (no default). Array of strings/numbers or `Option` objects to be listed in the dropdown. The only required key on objects is `label` which must also be unique. An object's `value` defaults to `label` if `undefined`. You can add arbitrary additional keys to your option objects. A few keys like `preselected` and `title` have special meaning though. See `src/lib/index.ts` for all special keys and their purpose.
230
+ **The only required prop** (no default). Array of strings/numbers or `Option` objects to be listed in the dropdown. The only required key on objects is `label` which must also be unique. An object's `value` defaults to `label` if `undefined`. You can add arbitrary additional keys to your option objects. A few keys like `preselected` and `title` have special meaning though. See type `ObjectOption` in [`src/lib/index.ts`](https://github.com/janosh/svelte-multiselect/blob/main/src/lib/index.ts) for all special keys and their purpose.
222
231
 
223
232
  1. ```ts
224
233
  outerDiv: HTMLDivElement | null = null
@@ -232,6 +241,12 @@ import type { Option } from 'svelte-multiselect'
232
241
 
233
242
  Whether option labels should be passed to [Svelte's `@html` directive](https://svelte.dev/tutorial/html-tags) or inserted into the DOM as plain text. `true` will raise an error if `allowUserOptions` is also truthy as it makes your site susceptible to [cross-site scripting (XSS) attacks](https://wikipedia.org/wiki/Cross-site_scripting).
234
243
 
244
+ 1. ```ts
245
+ pattern: string | null = null
246
+ ```
247
+
248
+ The pattern attribute specifies a regular expression which the input's value must match. If a non-null value doesn't match the `pattern` regex, the read-only `patternMismatch` property will be `true`. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Attributes/pattern) for details.
249
+
235
250
  1. ```ts
236
251
  placeholder: string | null = null
237
252
  ```
@@ -263,22 +278,25 @@ import type { Option } from 'svelte-multiselect'
263
278
  Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text.
264
279
 
265
280
  1. ```ts
266
- selected: Option[] = options?.filter((op) => op?.preselected) ?? []
281
+ selected: Option[] | Option | null =
282
+ options
283
+ ?.filter((op) => (op as ObjectOption)?.preselected)
284
+ .slice(0, maxSelect ?? undefined) ?? []
267
285
  ```
268
286
 
269
- Array of currently selected options. Can be bound to `bind:selected={[1, 2, 3]}` to control component state externally or passed as prop to set pre-selected options that will already be populated when component mounts before any user interaction.
287
+ Array of currently selected options. Supports 2-way binding `bind:selected={[1, 2, 3]}` to control component state externally or passed as prop to set pre-selected options that will already be populated when component mounts before any user interaction. If `maxSelect={1}`, selected will not be an array but a single `Option` or `null` if no options are selected.
270
288
 
271
289
  1. ```ts
272
- selectedLabels: (string | number)[] = []
290
+ selectedLabels: (string | number)[] | string | number | null = []
273
291
  ```
274
292
 
275
- Labels of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.label)` when options are objects. If options are simple strings, `selected === selectedLabels`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedLabels`.
293
+ Labels of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.label)` when options are objects. If options are simple strings, `selected === selectedLabels`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedLabels`. If `maxSelect={1}`, selectedLabels will not be an array but a single `string | number` or `null` if no options are selected.
276
294
 
277
295
  1. ```ts
278
- selectedValues: unknown[] = []
296
+ selectedValues: unknown[] | unknown | null = []
279
297
  ```
280
298
 
281
- Values of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.value)` when options are objects. If options are simple strings, `selected === selectedValues`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedValues`.
299
+ Values of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.value)` when options are objects. If options are simple strings, `selected === selectedValues`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedValues`. If `maxSelect={1}`, selectedLabels will not be an array but a single value or `null` if no options are selected.
282
300
 
283
301
  1. ```ts
284
302
  sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
@@ -286,13 +304,6 @@ import type { Option } from 'svelte-multiselect'
286
304
 
287
305
  Default behavior is to render selected items in the order they were chosen. `sortSelected={true}` uses default JS array sorting. A compare function enables custom logic for sorting selected options. See the [`/sort-selected`](https://svelte-multiselect.netlify.app/sort-selected) example.
288
306
 
289
- 1. ```ts
290
- userInputAs: 'string' | 'number' | 'object' =
291
- options.length > 0 ? (typeof options[0] as 'object' | 'string' | 'number') : 'string'
292
- ```
293
-
294
- What type new options created from user text input should be. Only relevant if `allowUserOptions=true | 'append'`. If not explicitly set, we default `userInputAs` to the type of the 1st option (if available, else `string`) to keep type homogeneity. E.g. if MultiSelect already contains at least one option and it's an object, new options from user-entered text will take the shape `{label: userText, value: userText}`. Likewise if the 1st existing option is a number of string. If MultiSelect starts out empty but you still want user-created custom options to be objects, pass `userInputAs='object'`.
295
-
296
307
  ## Slots
297
308
 
298
309
  `MultiSelect.svelte` has 3 named slots:
@@ -353,10 +364,16 @@ Example:
353
364
  Triggers when an option is either added or removed, or all options are removed at once. `type` is one of `'add' | 'remove' | 'removeAll'` and payload will be `option: Option` or `options: Option[]`, respectively.
354
365
 
355
366
  1. ```ts
356
- on:blur={() => console.log('Multiselect input lost focus')}
367
+ on:open={(event) => console.log(`Multiselect dropdown was opened by ${event}`)}
368
+ ```
369
+
370
+ Triggers when the dropdown list of options appears. Event is the DOM's `FocusEvent`,`KeyboardEvent` or `ClickEvent` that initiated this Svelte `dispatch` event.
371
+
372
+ 1. ```ts
373
+ on:close={(event) => console.log(`Multiselect dropdown was closed by ${event}`)}
357
374
  ```
358
375
 
359
- Triggers when the input field looses focus.
376
+ Triggers when the dropdown list of options disappears. Event is the DOM's `FocusEvent`, `KeyboardEvent` or `ClickEvent` that initiated this Svelte `dispatch` event.
360
377
 
361
378
  For example, here's how you might annoy your users with an alert every time one or more options are added or removed:
362
379
 
@@ -372,6 +389,15 @@ For example, here's how you might annoy your users with an alert every time one
372
389
 
373
390
  > Note: Depending on the data passed to the component the `options(s)` payload will either be objects or simple strings/numbers.
374
391
 
392
+ This component also forwards many DOM events from the `<input>` node: `blur`, `change`, `click`, `keydown`, `keyup`, `mousedown`, `mouseenter`, `mouseleave`, `touchcancel`, `touchend`, `touchmove`, `touchstart`. You can register listeners for these just like for the above [Svelte `dispatch` events](https://svelte.dev/tutorial/component-events):
393
+
394
+ ```svelte
395
+ <MultiSelect
396
+ options={[1, 2, 3]}
397
+ on:keyup={(event) => console.log('key', event.target.value)}
398
+ />
399
+ ```
400
+
375
401
  ## TypeScript
376
402
 
377
403
  TypeScript users can import the types used for internal type safety:
@@ -461,12 +487,12 @@ For example, to change the background color of the options dropdown:
461
487
 
462
488
  The second method allows you to pass in custom classes to the important DOM elements of this component to target them with frameworks like [Tailwind CSS](https://tailwindcss.com).
463
489
 
464
- - `outerDivClass`
465
- - `ulSelectedClass`
466
- - `liSelectedClass`
467
- - `ulOptionsClass`
468
- - `liOptionClass`
469
- - `liActiveOptionClass`
490
+ - `outerDivClass`: wrapper `div` enclosing the whole component
491
+ - `ulSelectedClass`: list of selected options
492
+ - `liSelectedClass`: selected list items
493
+ - `ulOptionsClass`: available options listed in the dropdown when component is in `open` state
494
+ - `liOptionClass`: list items selectable from dropdown list
495
+ - `liActiveOptionClass`: the currently active dropdown list item (i.e. hovered or navigated to with arrow keys)
470
496
 
471
497
  This simplified version of the DOM structure of the component shows where these classes are inserted:
472
498
 
@@ -486,9 +512,9 @@ This simplified version of the DOM structure of the component shows where these
486
512
  </div>
487
513
  ```
488
514
 
489
- ### Granular control through global CSS
515
+ ### With global CSS
490
516
 
491
- You can alternatively style every part of this component with more fine-grained control by using the following `:global()` CSS selectors. `ul.selected` is the list of currently selected options rendered inside the component's input whereas `ul.options` is the list of available options that slides out when the component has focus.
517
+ Odd as it may seem, you get the most fine-grained control over the styling of every part of this component by using the following `:global()` CSS selectors. `ul.selected` is the list of currently selected options rendered inside the component's input whereas `ul.options` is the list of available options that slides out when the component is in its `open` state. See also [simplified DOM structure](#styling).
492
518
 
493
519
  ```css
494
520
  :global(div.multiselect) {