svelte-multiselect 6.0.3 → 6.1.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.
@@ -48,6 +48,8 @@ export let selectedValues = [];
48
48
  export let sortSelected = false;
49
49
  export let ulOptionsClass = ``;
50
50
  export let ulSelectedClass = ``;
51
+ export let inputmode = ``;
52
+ export let pattern = ``;
51
53
  if (!(options?.length > 0)) {
52
54
  if (allowUserOptions) {
53
55
  options = []; // initializing as array avoids errors when component mounts
@@ -85,9 +87,9 @@ if (activeIndex !== null && !matchingOptions[activeIndex]) {
85
87
  throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
86
88
  }
87
89
  // update activeOption when activeIndex changes
88
- $: activeOption = activeIndex ? matchingOptions[activeIndex] : null;
90
+ $: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
89
91
  // add an option to selected list
90
- function add(label) {
92
+ function add(label, event) {
91
93
  if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
92
94
  wiggle = true;
93
95
  // to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
@@ -139,7 +141,7 @@ function add(label) {
139
141
  }
140
142
  }
141
143
  if (selected.length === maxSelect)
142
- close_dropdown();
144
+ close_dropdown(event);
143
145
  else if (focusInputOnSelect === true ||
144
146
  (focusInputOnSelect === `desktop` && window_width > breakpoint)) {
145
147
  input?.focus();
@@ -165,24 +167,27 @@ function remove(label) {
165
167
  dispatch(`remove`, { option });
166
168
  dispatch(`change`, { option, type: `remove` });
167
169
  }
168
- function open_dropdown() {
170
+ function open_dropdown(event) {
169
171
  if (disabled)
170
172
  return;
171
173
  open = true;
172
- input?.focus();
173
- dispatch(`focus`);
174
+ if (!(event instanceof FocusEvent)) {
175
+ // avoid double-focussing input when event that opened dropdown was already input FocusEvent
176
+ input?.focus();
177
+ }
178
+ dispatch(`open`, { event });
174
179
  }
175
- function close_dropdown() {
180
+ function close_dropdown(event) {
176
181
  open = false;
177
182
  input?.blur();
178
183
  activeOption = null;
179
- dispatch(`blur`);
184
+ dispatch(`close`, { event });
180
185
  }
181
186
  // handle all keyboard events this component receives
182
187
  async function handle_keydown(event) {
183
188
  // on escape or tab out of input: dismiss options dropdown and reset search text
184
189
  if (event.key === `Escape` || event.key === `Tab`) {
185
- close_dropdown();
190
+ close_dropdown(event);
186
191
  searchText = ``;
187
192
  }
188
193
  // on enter key: toggle active option and reset search text
@@ -190,17 +195,17 @@ async function handle_keydown(event) {
190
195
  event.preventDefault(); // prevent enter key from triggering form submission
191
196
  if (activeOption) {
192
197
  const label = get_label(activeOption);
193
- selectedLabels.includes(label) ? remove(label) : add(label);
198
+ selectedLabels.includes(label) ? remove(label) : add(label, event);
194
199
  searchText = ``;
195
200
  }
196
201
  else if (allowUserOptions && searchText.length > 0) {
197
202
  // user entered text but no options match, so if allowUserOptions is truthy, we create new option
198
- add(searchText);
203
+ add(searchText, event);
199
204
  }
200
205
  // no active option and no search text means the options dropdown is closed
201
206
  // in which case enter means open it
202
207
  else
203
- open_dropdown();
208
+ open_dropdown(event);
204
209
  }
205
210
  // on up/down arrow keys: update active option
206
211
  else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
@@ -257,7 +262,7 @@ const if_enter_or_space = (handler) => (event) => {
257
262
  };
258
263
  function on_click_outside(event) {
259
264
  if (outerDiv && !outerDiv.contains(event.target)) {
260
- close_dropdown();
265
+ close_dropdown(event);
261
266
  }
262
267
  }
263
268
  </script>
@@ -321,13 +326,29 @@ function on_click_outside(event) {
321
326
  bind:value={searchText}
322
327
  on:mouseup|self|stopPropagation={open_dropdown}
323
328
  on:keydown={handle_keydown}
329
+ on:focus
324
330
  on:focus={open_dropdown}
325
331
  {id}
326
332
  {name}
327
333
  {disabled}
334
+ {inputmode}
335
+ {pattern}
328
336
  placeholder={selectedLabels.length ? `` : placeholder}
329
337
  aria-invalid={invalid ? `true` : null}
338
+ on:blur
339
+ on:change
340
+ on:click
341
+ on:keydown
342
+ on:keyup
343
+ on:mousedown
344
+ on:mouseenter
345
+ on:mouseleave
346
+ on:touchcancel
347
+ on:touchend
348
+ on:touchmove
349
+ on:touchstart
330
350
  />
351
+ <!-- the above on:* lines forward potentially useful DOM events -->
331
352
  </li>
332
353
  </ul>
333
354
  {#if loading}
@@ -375,8 +396,8 @@ function on_click_outside(event) {
375
396
  {@const active = activeIndex === idx}
376
397
  <li
377
398
  on:mousedown|stopPropagation
378
- on:mouseup|stopPropagation={() => {
379
- if (!disabled) is_selected(label) ? remove(label) : add(label)
399
+ on:mouseup|stopPropagation={(event) => {
400
+ if (!disabled) is_selected(label) ? remove(label) : add(label, event)
380
401
  }}
381
402
  title={disabled
382
403
  ? disabledTitle
@@ -407,7 +428,7 @@ function on_click_outside(event) {
407
428
  {#if allowUserOptions && searchText}
408
429
  <li
409
430
  on:mousedown|stopPropagation
410
- on:mouseup|stopPropagation={() => add(searchText)}
431
+ on:mouseup|stopPropagation={(event) => add(searchText, event)}
411
432
  title={addOptionMsg}
412
433
  class:active={add_option_msg_is_active}
413
434
  on:mouseover={() => (add_option_msg_is_active = true)}
@@ -43,6 +43,8 @@ declare const __propDef: {
43
43
  sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
44
44
  ulOptionsClass?: string | undefined;
45
45
  ulSelectedClass?: string | undefined;
46
+ inputmode?: string | undefined;
47
+ pattern?: string | undefined;
46
48
  };
47
49
  slots: {
48
50
  selected: {
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": "6.1.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,7 @@
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).
43
44
 
44
45
  ## Installation
45
46
 
@@ -97,7 +98,8 @@ import type { Option } from 'svelte-multiselect'
97
98
  allowUserOptions: boolean | 'append' = false
98
99
  ```
99
100
 
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.
101
+ 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.
102
+ 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
103
 
102
104
  1. ```ts
103
105
  autocomplete: string = `off`
@@ -287,11 +289,16 @@ import type { Option } from 'svelte-multiselect'
287
289
  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
290
 
289
291
  1. ```ts
290
- userInputAs: 'string' | 'number' | 'object' =
291
- options.length > 0 ? (typeof options[0] as 'object' | 'string' | 'number') : 'string'
292
+ inputmode: string = ``
292
293
  ```
293
294
 
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
+ 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.
296
+
297
+ 1. ```ts
298
+ pattern: string = ``
299
+ ```
300
+
301
+ 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.
295
302
 
296
303
  ## Slots
297
304
 
@@ -353,10 +360,16 @@ Example:
353
360
  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
361
 
355
362
  1. ```ts
356
- on:blur={() => console.log('Multiselect input lost focus')}
363
+ on:open={(event) => console.log(`Multiselect dropdown was opened by ${event}`)}
364
+ ```
365
+
366
+ Triggers when the dropdown list of options appears. Event is the DOM's `FocusEvent`,`KeyboardEvent` or `ClickEvent` that initiated this Svelte `dispatch` event.
367
+
368
+ 1. ```ts
369
+ on:close={(event) => console.log(`Multiselect dropdown was closed by ${event}`)}
357
370
  ```
358
371
 
359
- Triggers when the input field looses focus.
372
+ Triggers when the dropdown list of options disappears. Event is the DOM's `FocusEvent`, `KeyboardEvent` or `ClickEvent` that initiated this Svelte `dispatch` event.
360
373
 
361
374
  For example, here's how you might annoy your users with an alert every time one or more options are added or removed:
362
375
 
@@ -372,6 +385,15 @@ For example, here's how you might annoy your users with an alert every time one
372
385
 
373
386
  > Note: Depending on the data passed to the component the `options(s)` payload will either be objects or simple strings/numbers.
374
387
 
388
+ 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):
389
+
390
+ ```svelte
391
+ <MultiSelect
392
+ options={[1, 2, 3]}
393
+ on:keyup={(event) => console.log('key', event.target.value)}
394
+ />
395
+ ```
396
+
375
397
  ## TypeScript
376
398
 
377
399
  TypeScript users can import the types used for internal type safety:
@@ -486,9 +508,9 @@ This simplified version of the DOM structure of the component shows where these
486
508
  </div>
487
509
  ```
488
510
 
489
- ### Granular control through global CSS
511
+ ### With global CSS
490
512
 
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.
513
+ 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
514
 
493
515
  ```css
494
516
  :global(div.multiselect) {