svelte-multiselect 5.0.6 → 6.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.
@@ -4,7 +4,7 @@ import CircleSpinner from './CircleSpinner.svelte';
4
4
  import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
5
5
  import Wiggle from './Wiggle.svelte';
6
6
  export let searchText = ``;
7
- export let showOptions = false;
7
+ export let open = false;
8
8
  export let maxSelect = null; // null means any number of options are selectable
9
9
  export let maxSelectMsg = null;
10
10
  export let disabled = false;
@@ -21,11 +21,14 @@ export let id = undefined;
21
21
  export let name = id;
22
22
  export let noOptionsMsg = `No matching options`;
23
23
  export let activeOption = null;
24
+ export let activeIndex = null;
24
25
  export let filterFunc = (op, searchText) => {
25
26
  if (!searchText)
26
27
  return true;
27
28
  return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase());
28
29
  };
30
+ export let focusInputOnSelect = `desktop`;
31
+ export let breakpoint = 800; // any screen with more horizontal pixels is considered desktop, below is mobile
29
32
  export let outerDivClass = ``;
30
33
  export let ulSelectedClass = ``;
31
34
  export let liSelectedClass = ``;
@@ -65,6 +68,7 @@ if (!Array.isArray(selected)) {
65
68
  }
66
69
  const dispatch = createEventDispatcher();
67
70
  let activeMsg = false; // controls active state of <li>{addOptionMsg}</li>
71
+ let window_width;
68
72
  let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
69
73
  $: selectedLabels = selected.map(get_label);
70
74
  $: selectedValues = selected.map(get_value);
@@ -78,9 +82,12 @@ $: matchingOptions = options.filter((op) => filterFunc(op, searchText) &&
78
82
  !(op instanceof Object && op.disabled) &&
79
83
  !selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
80
84
  );
81
- // reset activeOption if it's no longer in the matchingOptions list
82
- $: if (activeOption && !matchingOptions.includes(activeOption))
83
- activeOption = null;
85
+ // raise if matchingOptions[activeIndex] does not yield a value
86
+ if (activeIndex !== null && !matchingOptions[activeIndex]) {
87
+ throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
88
+ }
89
+ // update activeOption when activeIndex changes
90
+ $: activeOption = activeIndex ? matchingOptions[activeIndex] : null;
84
91
  // add an option to selected list
85
92
  function add(label) {
86
93
  if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
@@ -134,9 +141,11 @@ function add(label) {
134
141
  }
135
142
  }
136
143
  if (selected.length === maxSelect)
137
- setOptionsVisible(false);
138
- else
144
+ close_dropdown();
145
+ else if (focusInputOnSelect === true ||
146
+ (focusInputOnSelect === `desktop` && window_width > breakpoint)) {
139
147
  input?.focus();
148
+ }
140
149
  dispatch(`add`, { option });
141
150
  dispatch(`change`, { option, type: `add` });
142
151
  }
@@ -158,25 +167,24 @@ function remove(label) {
158
167
  dispatch(`remove`, { option });
159
168
  dispatch(`change`, { option, type: `remove` });
160
169
  }
161
- function setOptionsVisible(show) {
170
+ function open_dropdown() {
162
171
  if (disabled)
163
172
  return;
164
- showOptions = show;
165
- if (show) {
166
- input?.focus();
167
- dispatch(`focus`);
168
- }
169
- else {
170
- input?.blur();
171
- activeOption = null;
172
- dispatch(`blur`);
173
- }
173
+ open = true;
174
+ input?.focus();
175
+ dispatch(`focus`);
176
+ }
177
+ function close_dropdown() {
178
+ open = false;
179
+ input?.blur();
180
+ activeOption = null;
181
+ dispatch(`blur`);
174
182
  }
175
183
  // handle all keyboard events this component receives
176
- async function handleKeydown(event) {
184
+ async function handle_keydown(event) {
177
185
  // on escape or tab out of input: dismiss options dropdown and reset search text
178
186
  if (event.key === `Escape` || event.key === `Tab`) {
179
- setOptionsVisible(false);
187
+ close_dropdown();
180
188
  searchText = ``;
181
189
  }
182
190
  // on enter key: toggle active option and reset search text
@@ -194,7 +202,7 @@ async function handleKeydown(event) {
194
202
  // no active option and no search text means the options dropdown is closed
195
203
  // in which case enter means open it
196
204
  else
197
- setOptionsVisible(true);
205
+ open_dropdown();
198
206
  }
199
207
  // on up/down arrow keys: update active option
200
208
  else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
@@ -257,26 +265,29 @@ const if_enter_or_space = (handler) => (event) => {
257
265
  handler();
258
266
  }
259
267
  };
268
+ function on_click_outside(event) {
269
+ if (outerDiv && !outerDiv.contains(event.target)) {
270
+ close_dropdown();
271
+ }
272
+ }
260
273
  </script>
261
274
 
262
275
  <svelte:window
263
- on:click={(event) => {
264
- if (outerDiv && !outerDiv.contains(event.target)) {
265
- setOptionsVisible(false)
266
- }
267
- }}
276
+ on:click={on_click_outside}
277
+ on:touchstart={on_click_outside}
278
+ bind:innerWidth={window_width}
268
279
  />
269
280
 
270
281
  <div
271
282
  bind:this={outerDiv}
272
283
  class:disabled
273
284
  class:single={maxSelect === 1}
274
- class:open={showOptions}
275
- aria-expanded={showOptions}
285
+ class:open
286
+ aria-expanded={open}
276
287
  aria-multiselectable={maxSelect === null || maxSelect > 1}
277
288
  class:invalid
278
289
  class="multiselect {outerDivClass}"
279
- on:mouseup|stopPropagation={() => setOptionsVisible(true)}
290
+ on:mouseup|stopPropagation={open_dropdown}
280
291
  title={disabled ? disabledTitle : null}
281
292
  aria-disabled={disabled ? `true` : null}
282
293
  >
@@ -318,9 +329,9 @@ const if_enter_or_space = (handler) => (event) => {
318
329
  bind:this={input}
319
330
  {autocomplete}
320
331
  bind:value={searchText}
321
- on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
322
- on:keydown={handleKeydown}
323
- on:focus={() => setOptionsVisible(true)}
332
+ on:mouseup|self|stopPropagation={open_dropdown}
333
+ on:keydown={handle_keydown}
334
+ on:focus={open_dropdown}
324
335
  {id}
325
336
  {name}
326
337
  {disabled}
@@ -362,7 +373,7 @@ const if_enter_or_space = (handler) => (event) => {
362
373
 
363
374
  <!-- only render options dropdown if options or searchText is not empty needed to avoid briefly flashing empty dropdown -->
364
375
  {#if searchText || options?.length > 0}
365
- <ul class:hidden={!showOptions} class="options {ulOptionsClass}">
376
+ <ul class:hidden={!open} class="options {ulOptionsClass}">
366
377
  {#each matchingOptions as option, idx}
367
378
  {@const {
368
379
  label,
@@ -371,7 +382,7 @@ const if_enter_or_space = (handler) => (event) => {
371
382
  selectedTitle = null,
372
383
  disabledTitle = defaultDisabledTitle,
373
384
  } = option instanceof Object ? option : { label: option }}
374
- {@const active = activeOption && get_label(activeOption) === label}
385
+ {@const active = activeIndex === idx}
375
386
  <li
376
387
  on:mousedown|stopPropagation
377
388
  on:mouseup|stopPropagation={() => {
@@ -385,13 +396,13 @@ const if_enter_or_space = (handler) => (event) => {
385
396
  class:disabled
386
397
  class="{liOptionClass} {active ? liActiveOptionClass : ``}"
387
398
  on:mouseover={() => {
388
- if (!disabled) activeOption = option
399
+ if (!disabled) activeIndex = idx
389
400
  }}
390
401
  on:focus={() => {
391
- if (!disabled) activeOption = option
402
+ if (!disabled) activeIndex = idx
392
403
  }}
393
- on:mouseout={() => (activeOption = null)}
394
- on:blur={() => (activeOption = null)}
404
+ on:mouseout={() => (activeIndex = null)}
405
+ on:blur={() => (activeIndex = null)}
395
406
  aria-selected="false"
396
407
  >
397
408
  <slot name="option" {option} {idx}>
@@ -3,7 +3,7 @@ import type { MultiSelectEvents, Option } from './';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  searchText?: string | undefined;
6
- showOptions?: boolean | undefined;
6
+ open?: boolean | undefined;
7
7
  maxSelect?: number | null | undefined;
8
8
  maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
9
9
  disabled?: boolean | undefined;
@@ -20,7 +20,10 @@ declare const __propDef: {
20
20
  name?: string | undefined;
21
21
  noOptionsMsg?: string | undefined;
22
22
  activeOption?: Option | null | undefined;
23
+ activeIndex?: number | null | undefined;
23
24
  filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
25
+ focusInputOnSelect?: boolean | "desktop" | undefined;
26
+ breakpoint?: number | undefined;
24
27
  outerDivClass?: string | undefined;
25
28
  ulSelectedClass?: string | undefined;
26
29
  liSelectedClass?: string | undefined;
package/index.d.ts CHANGED
@@ -32,4 +32,4 @@ export declare type MultiSelectEvents = {
32
32
  [key in keyof DispatchEvents]: CustomEvent<DispatchEvents[key]>;
33
33
  };
34
34
  export declare const get_label: (op: Option) => string | number;
35
- export declare const get_value: (op: Option) => unknown;
35
+ export declare const get_value: (op: Option) => {};
package/package.json CHANGED
@@ -5,19 +5,21 @@
5
5
  "homepage": "https://svelte-multiselect.netlify.app",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "5.0.6",
8
+ "version": "6.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.24.1",
15
- "@sveltejs/adapter-static": "^1.0.0-next.38",
16
- "@sveltejs/kit": "^1.0.0-next.396",
17
- "@sveltejs/vite-plugin-svelte": "^1.0.1",
18
- "@typescript-eslint/eslint-plugin": "^5.31.0",
19
- "@typescript-eslint/parser": "^5.31.0",
20
- "eslint": "^8.20.0",
14
+ "@playwright/test": "^1.25.1",
15
+ "@sveltejs/adapter-netlify": "^1.0.0-next.76",
16
+ "@sveltejs/adapter-static": "^1.0.0-next.41",
17
+ "@sveltejs/kit": "^1.0.0-next.465",
18
+ "@sveltejs/package": "^1.0.0-next.3",
19
+ "@sveltejs/vite-plugin-svelte": "^1.0.4",
20
+ "@typescript-eslint/eslint-plugin": "^5.36.1",
21
+ "@typescript-eslint/parser": "^5.36.1",
22
+ "eslint": "^8.23.0",
21
23
  "eslint-plugin-svelte3": "^4.0.0",
22
24
  "hastscript": "^7.0.2",
23
25
  "jsdom": "^20.0.0",
@@ -26,16 +28,16 @@
26
28
  "prettier-plugin-svelte": "^2.7.0",
27
29
  "rehype-autolink-headings": "^6.1.1",
28
30
  "rehype-slug": "^5.0.1",
29
- "svelte": "^3.49.0",
30
- "svelte-check": "^2.8.0",
31
+ "svelte": "^3.50.0",
32
+ "svelte-check": "^2.9.0",
31
33
  "svelte-github-corner": "^0.1.0",
32
34
  "svelte-preprocess": "^4.10.6",
33
35
  "svelte-toc": "^0.2.10",
34
- "svelte2tsx": "^0.5.13",
36
+ "svelte2tsx": "^0.5.16",
35
37
  "tslib": "^2.4.0",
36
- "typescript": "^4.7.4",
37
- "vite": "^3.0.4",
38
- "vitest": "^0.19.1"
38
+ "typescript": "^4.8.2",
39
+ "vite": "^3.1.0-beta.2",
40
+ "vitest": "^0.22.1"
39
41
  },
40
42
  "keywords": [
41
43
  "svelte",
package/readme.md CHANGED
@@ -5,18 +5,19 @@
5
5
 
6
6
  <h4 align="center">
7
7
 
8
- [![REPL](https://img.shields.io/badge/Svelte-REPL-blue)](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
9
8
  [![Tests](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml/badge.svg)](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
10
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)
11
10
  [![NPM version](https://img.shields.io/npm/v/svelte-multiselect?logo=NPM&color=purple)](https://npmjs.com/package/svelte-multiselect)
12
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)
13
+ [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-darkblue?logo=pytorchlightning)](https://stackblitz.com/github/janosh/svelte-multiselect)
13
14
 
14
15
  </h4>
15
16
 
16
17
  **Keyboard-friendly, accessible and highly customizable multi-select component.**
17
- <strong class="hide-in-docs">
18
- <a href="https://svelte-multiselect.netlify.app">Docs</a>
19
- </strong>
18
+ <span class="hide-in-docs">
19
+ <a href="https://svelte-multiselect.netlify.app">View the docs</a>
20
+ </span>
20
21
 
21
22
  <slot name="examples" />
22
23
 
@@ -35,11 +36,8 @@
35
36
 
36
37
  ## Recent breaking changes
37
38
 
38
- - v4.0.1 renamed the `readonly` prop to `disabled` which now prevents all form of user interaction with this component including opening the dropdown list which was still possible before. See [#45](https://github.com/janosh/svelte-multiselect/issues/45) for details. The associated CSS class applied to the outer `div` was likewise renamed `div.multiselect.{readonly=>disabled}`.
39
-
40
- - v4.0.3 CSS variables starting with `--sms-input-<attr>` were renamed to just `--sms-<attr>`. E.g. `--sms-input-min-height` is now `--sms-min-height`.
41
-
42
- - v5.0.0 Support both simple and object options. Previously strings and numbers were converted to `{ value, label }` objects internally and returned by `bind:selected`. Now, if you pass in `string[]`, that's exactly what you'll get from `bind:selected`.
39
+ - v5.0.0 Supports both simple and object options. Previously strings and numbers were converted to `{ value, label }` objects internally and returned by `bind:selected`. Now, if you pass in `string[]`, that's exactly what you'll get from `bind:selected`.
40
+ - v6.0.0 The prop `showOptions` which controls whether the list of dropdown options is currently open or closed was renamed to just `open`.
43
41
 
44
42
  ## Installation
45
43
 
@@ -80,6 +78,7 @@ Full list of props/bindable variables for this component:
80
78
  | `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
81
79
  | `searchText` | `` | 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. |
82
80
  | `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
81
+ | `activeIndex` | `null` | Zero-based index of currently active option in the array of currently matching options, i.e. if the user typed a search string into the input and only a subset of options match, this index refers to the array position of the matching subset of options |
83
82
  | `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
84
83
  | `selected` | `[]` | 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. |
85
84
  | `selectedLabels` | `[]` | 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`. |
@@ -105,6 +104,8 @@ Full list of props/bindable variables for this component:
105
104
  | `defaultDisabledTitle` | `'This option is disabled'` | Title text to display when user hovers over a disabled option. Each option can override this through its `disabledTitle` attribute. |
106
105
  | `autocomplete` | `'off'` | Applied to the `<input>`. Specifies if browser is permitted to auto-fill this form field. See [MDN docs](https://developer.mozilla.org/docs/Web/HTML/Attributes/autocomplete) for other admissible values. |
107
106
  | `invalid` | `false` | If `required=true` and user tries to submit but `selected = []` is empty, `invalid` is automatically set to `true` and CSS class `invalid` applied to the top-level `div.multiselect`. `invalid` class is removed again as soon as the user selects an option. `invalid` can also be controlled externally by binding to it `<MultiSelect bind:invalid />` and setting it to `true` based on outside events or custom validation. |
107
+ | `focusInputOnSelect` | `'desktop'` | One of `true`, `false` or `'desktop'`. Whether to set the cursor back to the input element after selecting an element. 'desktop' means only do so if current window width is larger than the current value of `breakpoint` prop (default 800). |
108
+ | `breakpoint` | `800` | Screens wider than `breakpoint` in pixels will be considered `'desktop'`, everything narrower as `'mobile'`. |
108
109
 
109
110
  </div>
110
111
 
@@ -192,16 +193,14 @@ TypeScript users can import the types used for internal type safety:
192
193
 
193
194
  ```svelte
194
195
  <script lang="ts">
195
- import MultiSelect, {
196
- Option,
197
- Primitive,
198
- ProtoOption,
199
- } from 'svelte-multiselect'
196
+ import MultiSelect, { Option, ObjectOption } from 'svelte-multiselect'
200
197
 
201
- const myOptions: Option[] = [
198
+ const myOptions: ObjectOption[] = [
202
199
  { label: 'foo', value: 42 },
203
200
  { label: 'bar', value: 69 },
204
201
  ]
202
+ // an Option can be string | number | ObjectOption
203
+ const myNumbers: Option[] = [42, 69]
205
204
  </script>
206
205
  ```
207
206
 
@@ -391,6 +390,6 @@ To submit a PR, clone the repo, install dependencies and start the dev server to
391
390
  ```sh
392
391
  git clone https://github.com/janosh/svelte-multiselect
393
392
  cd svelte-multiselect
394
- yarn
395
- yarn dev
393
+ npm install
394
+ npm run dev
396
395
  ```