svelte-multiselect 2.0.0 → 3.1.1

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.
@@ -2,6 +2,7 @@
2
2
  import { fly } from 'svelte/transition';
3
3
  import { onClickOutside } from './actions';
4
4
  import { CrossIcon, ExpandIcon, ReadOnlyIcon } from './icons';
5
+ import Wiggle from './Wiggle.svelte';
5
6
  export let selected = [];
6
7
  export let selectedLabels = [];
7
8
  export let selectedValues = [];
@@ -16,12 +17,13 @@ export let id = undefined;
16
17
  export let noOptionsMsg = `No matching options`;
17
18
  export let activeOption = null;
18
19
  export let outerDivClass = ``;
19
- export let ulTokensClass = ``;
20
- export let liTokenClass = ``;
20
+ export let ulSelectedClass = ``;
21
+ export let liSelectedClass = ``;
21
22
  export let ulOptionsClass = ``;
22
23
  export let liOptionClass = ``;
23
24
  export let removeBtnTitle = `Remove`;
24
25
  export let removeAllTitle = `Remove all`;
26
+ // https://github.com/sveltejs/svelte/issues/6964
25
27
  export let defaultDisabledTitle = `This option is disabled`;
26
28
  if (maxSelect !== null && maxSelect < 0) {
27
29
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
@@ -30,12 +32,13 @@ if (!(options?.length > 0))
30
32
  console.error(`MultiSelect missing options`);
31
33
  if (!Array.isArray(selected))
32
34
  console.error(`selected prop must be an array`);
33
- function isObject(item) {
34
- return typeof item === `object` && !Array.isArray(item) && item !== null;
35
- }
36
35
  onMount(() => {
37
36
  selected = _options.filter((op) => op?.preselected);
38
37
  });
38
+ let wiggle = false;
39
+ function isObject(item) {
40
+ return typeof item === `object` && !Array.isArray(item) && item !== null;
41
+ }
39
42
  // process proto options to full ones with mandatory labels
40
43
  $: _options = options.map((rawOp) => {
41
44
  // convert to objects internally if user passed list of strings or numbers as options
@@ -78,35 +81,37 @@ $: if (
78
81
  // make the first filtered option active
79
82
  activeOption = matchingEnabledOptions[0];
80
83
  function add(label) {
84
+ if (selected.length - (maxSelect ?? 0) < 1)
85
+ wiggle = true;
81
86
  if (!readonly &&
82
87
  !selectedLabels.includes(label) &&
83
- // for maxselect = 1 we always replace current token with new selection
88
+ // for maxselect = 1 we always replace current option with new selection
84
89
  (maxSelect == null || maxSelect == 1 || selected.length < maxSelect)) {
85
90
  searchText = ``; // reset search string on selection
86
- const token = _options.find((op) => op.label === label);
87
- if (!token) {
91
+ const option = _options.find((op) => op.label === label);
92
+ if (!option) {
88
93
  console.error(`MultiSelect: option with label ${label} not found`);
89
94
  return;
90
95
  }
91
96
  if (maxSelect === 1) {
92
- selected = [token];
97
+ selected = [option];
93
98
  }
94
99
  else {
95
- selected = [token, ...selected];
100
+ selected = [option, ...selected];
96
101
  }
97
102
  if (selected.length === maxSelect)
98
103
  setOptionsVisible(false);
99
- dispatch(`add`, { token });
100
- dispatch(`change`, { token, type: `add` });
104
+ dispatch(`add`, { option });
105
+ dispatch(`change`, { option, type: `add` });
101
106
  }
102
107
  }
103
108
  function remove(label) {
104
109
  if (selected.length === 0 || readonly)
105
110
  return;
106
- selected = selected.filter((token) => label !== token.label);
107
- const token = _options.find((option) => option.label === label);
108
- dispatch(`remove`, { token });
109
- dispatch(`change`, { token, type: `remove` });
111
+ selected = selected.filter((option) => label !== option.label);
112
+ const option = _options.find((option) => option.label === label);
113
+ dispatch(`remove`, { option });
114
+ dispatch(`change`, { option, type: `remove` });
110
115
  }
111
116
  function setOptionsVisible(show) {
112
117
  // nothing to do if visibility is already as intended
@@ -130,9 +135,7 @@ function handleKeydown(event) {
130
135
  // on enter key: toggle active option and reset search text
131
136
  else if (event.key === `Enter`) {
132
137
  if (activeOption) {
133
- const { label, disabled } = activeOption;
134
- if (disabled)
135
- return;
138
+ const { label } = activeOption;
136
139
  selectedLabels.includes(label) ? remove(label) : add(label);
137
140
  searchText = ``;
138
141
  } // no active option means the options dropdown is closed in which case enter means open it
@@ -148,17 +151,30 @@ function handleKeydown(event) {
148
151
  }
149
152
  const increment = event.key === `ArrowUp` ? -1 : 1;
150
153
  const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
154
+ const ulOps = document.querySelector(`ul.options`);
151
155
  if (newActiveIdx < 0) {
152
156
  // wrap around top
153
157
  activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
154
- // wrap around bottom
158
+ if (ulOps)
159
+ ulOps.scrollTop = ulOps.scrollHeight;
155
160
  }
156
161
  else if (newActiveIdx === matchingEnabledOptions.length) {
162
+ // wrap around bottom
157
163
  activeOption = matchingEnabledOptions[0];
158
- // default case
164
+ if (ulOps)
165
+ ulOps.scrollTop = 0;
159
166
  }
160
- else
167
+ else {
168
+ // default case
161
169
  activeOption = matchingEnabledOptions[newActiveIdx];
170
+ const li = document.querySelector(`ul.options > li.active`);
171
+ // scrollIntoViewIfNeeded() scrolls top edge of element into view so when moving
172
+ // downwards, we scroll to next sibling to make element fully visible
173
+ if (increment === 1)
174
+ li?.nextSibling?.scrollIntoViewIfNeeded();
175
+ else
176
+ li?.scrollIntoViewIfNeeded();
177
+ }
162
178
  }
163
179
  else if (event.key === `Backspace`) {
164
180
  const label = selectedLabels.pop();
@@ -167,8 +183,8 @@ function handleKeydown(event) {
167
183
  }
168
184
  }
169
185
  const removeAll = () => {
170
- dispatch(`remove`, { token: selected });
171
- dispatch(`change`, { token: selected, type: `remove` });
186
+ dispatch(`removeAll`, { options: selected });
187
+ dispatch(`change`, { options: selected, type: `removeAll` });
172
188
  selected = [];
173
189
  searchText = ``;
174
190
  };
@@ -181,7 +197,7 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
181
197
  };
182
198
  </script>
183
199
 
184
- <!-- z-index: 2 when showOptions is true ensures the ul.tokens of one <MultiSelect />
200
+ <!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
185
201
  display above those of another following shortly after it -->
186
202
  <div
187
203
  {id}
@@ -194,31 +210,24 @@ display above those of another following shortly after it -->
194
210
  use:onClickOutside={() => dispatch(`blur`)}
195
211
  >
196
212
  <ExpandIcon height="14pt" style="padding: 0 3pt 0 1pt;" />
197
- <ul class="tokens {ulTokensClass}">
198
- {#if maxSelect == 1 && selected[0]?.label}
199
- <span on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}>
200
- {selected[0].label}
201
- </span>
202
- {:else}
203
- {#each selected as { label }}
204
- <li
205
- class={liTokenClass}
206
- on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
207
- >
208
- {label}
209
- {#if !readonly}
210
- <button
211
- on:mouseup|stopPropagation={() => remove(label)}
212
- on:keydown={handleEnterAndSpaceKeys(() => remove(label))}
213
- type="button"
214
- title="{removeBtnTitle} {label}"
215
- >
216
- <CrossIcon height="12pt" />
217
- </button>
218
- {/if}
219
- </li>
220
- {/each}
221
- {/if}
213
+ <ul class="selected {ulSelectedClass}">
214
+ {#each selected as option, idx}
215
+ <li class={liSelectedClass}>
216
+ <slot name="renderSelected" {option} {idx}>
217
+ {option.label}
218
+ </slot>
219
+ {#if !readonly}
220
+ <button
221
+ on:mouseup|stopPropagation={() => remove(option.label)}
222
+ on:keydown={handleEnterAndSpaceKeys(() => remove(option.label))}
223
+ type="button"
224
+ title="{removeBtnTitle} {option.label}"
225
+ >
226
+ <CrossIcon height="12pt" />
227
+ </button>
228
+ {/if}
229
+ </li>
230
+ {/each}
222
231
  <input
223
232
  bind:this={input}
224
233
  autocomplete="off"
@@ -234,7 +243,9 @@ display above those of another following shortly after it -->
234
243
  <ReadOnlyIcon height="14pt" />
235
244
  {:else if selected.length > 0}
236
245
  {#if maxSelect !== null && maxSelect > 1}
237
- <span style="padding: 0 3pt;">{maxSelectMsg(selected.length, maxSelect)}</span>
246
+ <Wiggle bind:wiggle angle={20}>
247
+ <span style="padding: 0 3pt;">{maxSelectMsg(selected.length, maxSelect)}</span>
248
+ </Wiggle>
238
249
  {/if}
239
250
  <button
240
251
  type="button"
@@ -253,7 +264,9 @@ display above those of another following shortly after it -->
253
264
  class:hidden={!showOptions}
254
265
  transition:fly|local={{ duration: 300, y: 40 }}
255
266
  >
256
- {#each matchingOptions as { label, disabled, title = '', selectedTitle, disabledTitle = defaultDisabledTitle }}
267
+ {#each matchingOptions as option, idx}
268
+ {@const { label, disabled, title = null, selectedTitle } = option}
269
+ {@const { disabledTitle = defaultDisabledTitle } = option}
257
270
  <li
258
271
  on:mouseup|preventDefault|stopPropagation
259
272
  on:mousedown|preventDefault|stopPropagation={() => {
@@ -266,7 +279,9 @@ display above those of another following shortly after it -->
266
279
  title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
267
280
  class={liOptionClass}
268
281
  >
269
- {label}
282
+ <slot name="renderOptions" {option} {idx}>
283
+ {option.label}
284
+ </slot>
270
285
  </li>
271
286
  {:else}
272
287
  {noOptionsMsg}
@@ -276,26 +291,28 @@ display above those of another following shortly after it -->
276
291
  </div>
277
292
 
278
293
  <style>
279
- :where(.multiselect) {
294
+ :where(div.multiselect) {
280
295
  position: relative;
281
296
  margin: 1em 0;
282
297
  border: var(--sms-border, 1pt solid lightgray);
283
298
  border-radius: var(--sms-border-radius, 5pt);
299
+ background: var(--sms-input-bg);
300
+ height: var(--sms-input-height, 2em);
284
301
  align-items: center;
285
302
  min-height: 18pt;
286
303
  display: flex;
287
304
  cursor: text;
288
305
  padding: 0 3pt;
289
306
  }
290
- :where(.multiselect:focus-within) {
307
+ :where(div.multiselect:focus-within) {
291
308
  border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
292
309
  }
293
- :where(.multiselect.readonly) {
310
+ :where(div.multiselect.readonly) {
294
311
  background: var(--sms-readonly-bg, lightgray);
295
312
  }
296
313
 
297
- :where(ul.tokens > li) {
298
- background: var(--sms-token-bg, var(--sms-active-color, cornflowerblue));
314
+ :where(ul.selected > li) {
315
+ background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
299
316
  align-items: center;
300
317
  border-radius: 4pt;
301
318
  display: flex;
@@ -305,7 +322,7 @@ display above those of another following shortly after it -->
305
322
  white-space: nowrap;
306
323
  height: 16pt;
307
324
  }
308
- :where(ul.tokens > li button, button.remove-all) {
325
+ :where(ul.selected > li button, button.remove-all) {
309
326
  align-items: center;
310
327
  border-radius: 50%;
311
328
  display: flex;
@@ -320,29 +337,32 @@ display above those of another following shortly after it -->
320
337
  outline: none;
321
338
  padding: 0 2pt;
322
339
  }
323
- :where(ul.tokens > li button:hover, button.remove-all:hover) {
340
+ :where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
324
341
  color: var(--sms-remove-x-hover-focus-color, lightskyblue);
325
342
  }
326
343
  :where(button:focus) {
327
- color: var(--sms-remove-x-hover-focus-color, lightskyblue);
328
344
  transform: scale(1.04);
329
345
  }
330
346
 
331
- :where(.multiselect input) {
347
+ :where(div.multiselect input) {
332
348
  border: none;
333
349
  outline: none;
334
350
  background: none;
335
351
  color: var(--sms-text-color, inherit);
336
352
  flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
337
353
  min-width: 2em;
354
+ /* minimum font-size > 16px ensures iOS doesn't zoom in when focusing input */
355
+ /* https://stackoverflow.com/a/6394497 */
356
+ font-size: calc(16px + 0.1vw);
338
357
  }
339
358
 
340
- :where(ul.tokens) {
359
+ :where(ul.selected) {
341
360
  display: flex;
342
361
  padding: 0;
343
362
  margin: 0;
344
363
  flex-wrap: wrap;
345
364
  flex: 1;
365
+ overscroll-behavior: none;
346
366
  }
347
367
 
348
368
  :where(ul.options) {
@@ -16,8 +16,8 @@ declare const __propDef: {
16
16
  noOptionsMsg?: string | undefined;
17
17
  activeOption?: Option | null | undefined;
18
18
  outerDivClass?: string | undefined;
19
- ulTokensClass?: string | undefined;
20
- liTokenClass?: string | undefined;
19
+ ulSelectedClass?: string | undefined;
20
+ liSelectedClass?: string | undefined;
21
21
  ulOptionsClass?: string | undefined;
22
22
  liOptionClass?: string | undefined;
23
23
  removeBtnTitle?: string | undefined;
@@ -29,7 +29,16 @@ declare const __propDef: {
29
29
  } & {
30
30
  [evt: string]: CustomEvent<any>;
31
31
  };
32
- slots: {};
32
+ slots: {
33
+ renderSelected: {
34
+ option: Option;
35
+ idx: any;
36
+ };
37
+ renderOptions: {
38
+ option: Option;
39
+ idx: any;
40
+ };
41
+ };
33
42
  };
34
43
  export declare type MultiSelectProps = typeof __propDef.props;
35
44
  export declare type MultiSelectEvents = typeof __propDef.events;
package/Wiggle.svelte ADDED
@@ -0,0 +1,24 @@
1
+ <script >import { spring } from 'svelte/motion';
2
+ // bind to this state and set it to true from parent
3
+ export let wiggle = false;
4
+ // intended use case: set max value during wiggle for one of angle, scale, dx, dy through props
5
+ export let angle = 0; // try 20
6
+ export let scale = 1; // try 1.2
7
+ export let dx = 0; // try 10
8
+ export let dy = 0; // try 10
9
+ export let duration = 200;
10
+ export let stiffness = 0.05;
11
+ export let damping = 0.1;
12
+ let restState = { angle: 0, scale: 1, dx: 0, dy: 0 };
13
+ let store = spring(restState, { stiffness, damping });
14
+ $: store.set(wiggle ? { scale, angle, dx, dy } : restState);
15
+ $: if (wiggle)
16
+ setTimeout(() => (wiggle = false), duration);
17
+ </script>
18
+
19
+ <span
20
+ style:transform="rotate({$store.angle}deg) scale({$store.scale}) translate({$store.dx}px,
21
+ {$store.dy}px)"
22
+ >
23
+ <slot />
24
+ </span>
@@ -0,0 +1,25 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ wiggle?: boolean | undefined;
5
+ angle?: number | undefined;
6
+ scale?: number | undefined;
7
+ dx?: number | undefined;
8
+ dy?: number | undefined;
9
+ duration?: number | undefined;
10
+ stiffness?: number | undefined;
11
+ damping?: number | undefined;
12
+ };
13
+ events: {
14
+ [evt: string]: CustomEvent<any>;
15
+ };
16
+ slots: {
17
+ default: {};
18
+ };
19
+ };
20
+ export declare type WiggleProps = typeof __propDef.props;
21
+ export declare type WiggleEvents = typeof __propDef.events;
22
+ export declare type WiggleSlots = typeof __propDef.slots;
23
+ export default class Wiggle extends SvelteComponentTyped<WiggleProps, WiggleEvents, WiggleSlots> {
24
+ }
25
+ export {};
package/package.json CHANGED
@@ -5,33 +5,32 @@
5
5
  "homepage": "https://svelte-multiselect.netlify.app",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "2.0.0",
8
+ "version": "3.1.1",
9
9
  "type": "module",
10
- "svelte": "MultiSelect.svelte",
11
- "bugs": {
12
- "url": "https://github.com/janosh/svelte-multiselect/issues"
13
- },
10
+ "svelte": "index.js",
11
+ "bugs": "https://github.com/janosh/svelte-multiselect/issues",
14
12
  "devDependencies": {
15
- "@sveltejs/adapter-static": "^1.0.0-next.22",
16
- "@sveltejs/kit": "^1.0.0-next.202",
17
- "@typescript-eslint/eslint-plugin": "^5.7.0",
18
- "@typescript-eslint/parser": "^5.7.0",
19
- "eslint": "^8.5.0",
20
- "eslint-plugin-svelte3": "^3.2.1",
13
+ "@sveltejs/adapter-static": "^1.0.0-next.26",
14
+ "@sveltejs/kit": "^1.0.0-next.239",
15
+ "@typescript-eslint/eslint-plugin": "^5.10.0",
16
+ "@typescript-eslint/parser": "^5.10.0",
17
+ "eslint": "^8.7.0",
18
+ "eslint-plugin-svelte3": "^3.4.0",
21
19
  "hastscript": "^7.0.2",
22
20
  "mdsvex": "^0.9.8",
23
21
  "prettier": "^2.5.1",
24
- "prettier-plugin-svelte": "^2.5.1",
25
- "rehype-autolink-headings": "^6.1.0",
26
- "rehype-slug": "^5.0.0",
27
- "svelte": "^3.44.3",
28
- "svelte-check": "^2.2.11",
29
- "svelte-preprocess": "^4.10.1",
30
- "svelte-toc": "^0.1.10",
31
- "svelte2tsx": "^0.4.12",
22
+ "prettier-plugin-svelte": "^2.6.0",
23
+ "rehype-autolink-headings": "^6.1.1",
24
+ "rehype-slug": "^5.0.1",
25
+ "svelte": "^3.46.2",
26
+ "svelte-check": "^2.3.0",
27
+ "svelte-github-corner": "^0.1.0",
28
+ "svelte-preprocess": "^4.10.2",
29
+ "svelte-toc": "^0.2.2",
30
+ "svelte2tsx": "^0.4.14",
32
31
  "tslib": "^2.3.1",
33
- "typescript": "^4.5.4",
34
- "vite": "^2.7.3"
32
+ "typescript": "^4.5.5",
33
+ "vite": "^2.7.13"
35
34
  },
36
35
  "keywords": [
37
36
  "svelte",
@@ -46,7 +45,6 @@
46
45
  "exports": {
47
46
  "./package.json": "./package.json",
48
47
  "./MultiSelect.svelte": "./MultiSelect.svelte",
49
- "./actions": "./actions.js",
50
48
  ".": "./index.js"
51
49
  }
52
50
  }
package/readme.md CHANGED
@@ -1,10 +1,7 @@
1
- <div class="maybe-hide">
2
-
3
- <p align="center">
4
- <img src="static/favicon.svg" alt="Svelte MultiSelect" height=80>
5
- </p>
6
-
7
- <h1 align="center">Svelte MultiSelect</h1>
1
+ <h1 align="center">
2
+ <img src="https://raw.githubusercontent.com/janosh/svelte-toc/main/static/favicon.svg" alt="Svelte MultiSelect" height=60>
3
+ <br>&ensp;Svelte MultiSelect
4
+ </h1>
8
5
 
9
6
  <h4 align="center">
10
7
 
@@ -14,25 +11,39 @@
14
11
 
15
12
  </h4>
16
13
 
14
+ <div class="hide-in-docs">
15
+
17
16
  **[Live demo](https://svelte-multiselect.netlify.app)**.
18
17
 
19
18
  </div>
20
19
 
21
- <!-- remove above in docs -->
22
-
23
20
  **Keyboard-friendly, zero-dependency multi-select Svelte component.**
24
21
 
22
+ <slot />
23
+
25
24
  ## Key Features
26
25
 
27
26
  - **Single / multiple select:** pass `maxSelect={1}` prop to only allow one selection
28
27
  - **Dropdowns:** scrollable lists for large numbers of options
29
28
  - **Searchable:** start typing to filter options
30
- - **Tagging:** selected options are recorded as tags within the text input
29
+ - **Tagging:** selected options are recorded as tags in the input
31
30
  - **Server-side rendering:** no reliance on browser objects like `window` or `document`
32
31
  - **Configurable:** see [props](#props)
33
32
  - **No dependencies:** needs only Svelte as dev dependency
34
33
  - **Keyboard friendly** for mouse-less form completion
35
34
 
35
+ ## Recent breaking changes
36
+
37
+ - v2.0.0 added the ability to pass options as objects. As a result, `bind:selected` no longer returns simple strings but objects, even if you still pass in `options` as strings. To get the same stuff you would have gotten from `bind:selected` before, there's now `bind:selectedLabels` (and `bind:selectedValues`).
38
+ - v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
39
+
40
+ ```js
41
+ on:add={(e) => console.log(e.detail.token.label)} // v2.0.0
42
+ on:add={(e) => console.log(e.detail.option.label)} // v3.0.0
43
+ ```
44
+
45
+ It also added a separate event type `removeAll` for when the user removes all currently selected options at once which previously fired a normal `remove`. The props `ulTokensClass` and `liTokenClass` were renamed to `ulSelectedClass` and `liSelectedClass`. Similarly, the CSS variable `--sms-token-bg` changed to `--sms-selected-bg`.
46
+
36
47
  ## Installation
37
48
 
38
49
  ```sh
@@ -93,53 +104,118 @@ Full list of props/bindable variables for this component:
93
104
 
94
105
  </div>
95
106
 
107
+ ## Slots
108
+
109
+ `MultiSelect.svelte` accepts two named slots
110
+
111
+ - `slot="renderOptions"`
112
+ - `slot="renderSelected"`
113
+
114
+ to customize rendering individual options in the dropdown and the list of selected tags, respectively. Each renderer receives the full `option` object along with the zero-indexed position (`idx`) in its list, both available via the `let:` directive:
115
+
116
+ ```svelte
117
+ <MultiSelect options={[`Banana`, `Apple`, `Mango`]}>
118
+ <span let:idx let:option slot="renderOptions">
119
+ {idx + 1}. {option.label}
120
+ {option.label === `Mango` ? `🎉` : ``}
121
+ </span>
122
+
123
+ <span let:idx let:option slot="renderSelected">
124
+ #{idx + 1}
125
+ {option.label}
126
+ </span>
127
+ </MultiSelect>
128
+ ```
129
+
96
130
  ## Events
97
131
 
98
132
  `MultiSelect.svelte` dispatches the following events:
99
133
 
100
- | name | details | description |
101
- | -------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
102
- | `add` | `token: string` | Triggers when a new token is selected. |
103
- | `remove` | `token: string` | Triggers when one or all selected tokens are removed. `event.detail.token` will be a single or multiple tokens, respectively. |
104
- | `change` | `token: string`, `type: string` | Triggers when a token is either added or removed, or all tokens are removed at once. `event.detail.type` will be either `'add'` or `'remove'`. |
105
- | `blur` | none | Triggers when the input field looses focus. |
134
+ | name | detail | description |
135
+ | ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
136
+ | `add` | `{ option: Option }` | Triggers when a new option is selected. |
137
+ | `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed. |
138
+ | `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected. |
139
+ | `change` | `{ option?: Option, options?: Option[] }`, `type: 'add' \| 'remove' \| 'removeAll'` | Triggers when a option is either added or removed, or all options are removed at once. |
140
+ | `blur` | none | Triggers when the input field looses focus. |
106
141
 
107
142
  ### Examples
108
143
 
109
144
  <!-- prettier-ignore -->
110
- - `on:add={(event) => console.log(event.detail.token.label)}`
111
- - `on:remove={(event) => console.log(event.detail.token.label)}`.
112
- - ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.token.label}'`)}``
145
+ - `on:add={(event) => console.log(event.detail.option.label)}`
146
+ - `on:remove={(event) => console.log(event.detail.option.label)}`.
147
+ - ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option.label}'`)}``
113
148
  - `on:blur={yourFunctionHere}`
114
149
 
115
150
  ```svelte
116
151
  <MultiSelect
117
- on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.token.label}'`)}
152
+ on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.option.label}'`)}
118
153
  />
119
154
  ```
120
155
 
156
+ ## TypeScript
157
+
158
+ TypeScript users can import the types used for internal type safety:
159
+
160
+ ```svelte
161
+ <script lang="ts">
162
+ import MultiSelect, {
163
+ Option,
164
+ Primitive,
165
+ ProtoOption,
166
+ } from 'svelte-multiselect'
167
+
168
+ const myOptions: Option[] = [
169
+ { label: 'foo', value: 42 },
170
+ { label: 'bar', value: 69 },
171
+ ]
172
+ </script>
173
+ ```
174
+
121
175
  ## Styling
122
176
 
123
- There are 3 ways to style this component.
177
+ There are 3 ways to style this component. To understand which options do what, it helps to keep in mind this simplified DOM structure of the component:
178
+
179
+ ```svelte
180
+ <div class="multiselect">
181
+ <ul class="selected">
182
+ <li>Selected 1</li>
183
+ <li>Selected 2</li>
184
+ </ul>
185
+ <ul class="options">
186
+ <li>Option 1</li>
187
+ <li>Option 2</li>
188
+ </ul>
189
+ </div>
190
+ ```
124
191
 
125
192
  ### With CSS variables
126
193
 
127
- The first, if you only want to make small adjustments, allows you to pass the following CSS variables directly to the component as props.
128
-
129
- - `border: var(--sms-border, 1pt solid lightgray)`: Border around top-level `div.multiselect`. Change this to e.g. to `1px solid red` to indicate this form field is in an invalid state.
130
- - `border-radius: var(--sms-border-radius, 5pt)`: `div.multiselect` border radius.
131
- - `color: var(--sms-text-color, inherit)`: Input text color.
132
- - `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: `div.multiselect` border when focused. Falls back to `--sms-active-color` if not set which in turn falls back on `cornflowerblue`.
133
- - `background: var(--sms-readonly-bg, lightgray)`: Background when in readonly state.
134
- - `background: var(--sms-token-bg, var(--sms-active-color, cornflowerblue))`: Background of selected tokens.
135
- - `color: var(--sms-remove-x-hover+focus-color, lightgray)`: Hover color of cross icon to remove selected tokens.
136
- - `color: var(--sms-remove-x-hover-focus-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
137
- - `background: var(--sms-options-bg, white)`: Background of options list.
138
- - `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
139
- - `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
140
- - `background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue))`: Background of active (currently with arrow keys highlighted) list item.
141
- - `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
142
- - `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
194
+ If you only want to make small adjustments, you can pass the following CSS variables directly to the component as props or define them in a `:global()` CSS context.
195
+
196
+ - `div.multiselect`:
197
+ - `border: var(--sms-border, 1pt solid lightgray)`: Change this to e.g. to `1px solid red` to indicate this form field is in an invalid state.
198
+ - `border-radius: var(--sms-border-radius, 5pt)`: Input border radius.
199
+ - `background: var(--sms-input-bg)`: Input background.
200
+ - `height: var(--sms-input-height, 2em)`: Input height.
201
+ - `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: Border when focused. Falls back to `--sms-active-color` if not set which in turn falls back on `cornflowerblue`.
202
+ - `background: var(--sms-readonly-bg, lightgray)`: Background when in readonly state.
203
+ - `div.multiselect input`
204
+ - `color: var(--sms-text-color, inherit)`: Input text color.
205
+ - `ul.selected > li`:
206
+ - `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
207
+ - `ul.selected > li button:hover, button.remove-all:hover`
208
+ - `color: var(--sms-remove-x-hover-focus-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
209
+ - `ul.options`
210
+ - `background: var(--sms-options-bg, white)`: Background of options list.
211
+ - `ul.options > li.selected`
212
+ - `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
213
+ - `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
214
+ - `ul.options > li.active`
215
+ - `background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue))`: Background of active (currently with arrow keys highlighted) list item.
216
+ - `ul.options > li.disabled`
217
+ - `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
218
+ - `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
143
219
 
144
220
  For example, to change the background color of the options dropdown:
145
221
 
@@ -152,38 +228,38 @@ For example, to change the background color of the options dropdown:
152
228
  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).
153
229
 
154
230
  - `outerDivClass`
155
- - `ulTokensClass`
156
- - `liTokenClass`
231
+ - `ulSelectedClass`
232
+ - `liSelectedClass`
157
233
  - `ulOptionsClass`
158
234
  - `liOptionClass`
159
235
 
160
236
  This simplified version of the DOM structure of this component shows where these classes are inserted:
161
237
 
162
238
  ```svelte
163
- <div class={outerDivClass}>
164
- <ul class={ulTokensClass}>
165
- <li class={liTokenClass}>First selected tag</li>
166
- <li class={liTokenClass}>Second selected tag</li>
239
+ <div class="multiselect {outerDivClass}">
240
+ <ul class="selected {ulSelectedClass}">
241
+ <li class={liSelectedClass}>Selected 1</li>
242
+ <li class={liSelectedClass}>Selected 2</li>
167
243
  </ul>
168
- <ul class={ulOptionsClass}>
169
- <li class={liOptionClass}>First available option</li>
170
- <li class={liOptionClass}>Second available option</li>
244
+ <ul class="options {ulOptionsClass}">
245
+ <li class={liOptionClass}>Option 1</li>
246
+ <li class={liOptionClass}>Option 2</li>
171
247
  </ul>
172
248
  </div>
173
249
  ```
174
250
 
175
251
  ### Granular control through global CSS
176
252
 
177
- You can alternatively style every part of this component with more fine-grained control by using the following `:global()` CSS selectors. `ul.tokens` 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.
253
+ 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.
178
254
 
179
255
  ```css
180
256
  :global(.multiselect) {
181
257
  /* top-level wrapper div */
182
258
  }
183
- :global(.multiselect ul.tokens > li) {
259
+ :global(.multiselect ul.selected > li) {
184
260
  /* selected options */
185
261
  }
186
- :global(.multiselect ul.tokens > li button),
262
+ :global(.multiselect ul.selected > li button),
187
263
  :global(.multiselect button.remove-all) {
188
264
  /* buttons to remove a single or all selected options at once */
189
265
  }