svelte-multiselect 2.0.0 → 3.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.
@@ -16,12 +16,13 @@ export let id = undefined;
16
16
  export let noOptionsMsg = `No matching options`;
17
17
  export let activeOption = null;
18
18
  export let outerDivClass = ``;
19
- export let ulTokensClass = ``;
20
- export let liTokenClass = ``;
19
+ export let ulSelectedClass = ``;
20
+ export let liSelectedClass = ``;
21
21
  export let ulOptionsClass = ``;
22
22
  export let liOptionClass = ``;
23
23
  export let removeBtnTitle = `Remove`;
24
24
  export let removeAllTitle = `Remove all`;
25
+ // https://github.com/sveltejs/svelte/issues/6964
25
26
  export let defaultDisabledTitle = `This option is disabled`;
26
27
  if (maxSelect !== null && maxSelect < 0) {
27
28
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
@@ -30,12 +31,12 @@ if (!(options?.length > 0))
30
31
  console.error(`MultiSelect missing options`);
31
32
  if (!Array.isArray(selected))
32
33
  console.error(`selected prop must be an array`);
33
- function isObject(item) {
34
- return typeof item === `object` && !Array.isArray(item) && item !== null;
35
- }
36
34
  onMount(() => {
37
35
  selected = _options.filter((op) => op?.preselected);
38
36
  });
37
+ function isObject(item) {
38
+ return typeof item === `object` && !Array.isArray(item) && item !== null;
39
+ }
39
40
  // process proto options to full ones with mandatory labels
40
41
  $: _options = options.map((rawOp) => {
41
42
  // convert to objects internally if user passed list of strings or numbers as options
@@ -80,33 +81,33 @@ $: if (
80
81
  function add(label) {
81
82
  if (!readonly &&
82
83
  !selectedLabels.includes(label) &&
83
- // for maxselect = 1 we always replace current token with new selection
84
+ // for maxselect = 1 we always replace current option with new selection
84
85
  (maxSelect == null || maxSelect == 1 || selected.length < maxSelect)) {
85
86
  searchText = ``; // reset search string on selection
86
- const token = _options.find((op) => op.label === label);
87
- if (!token) {
87
+ const option = _options.find((op) => op.label === label);
88
+ if (!option) {
88
89
  console.error(`MultiSelect: option with label ${label} not found`);
89
90
  return;
90
91
  }
91
92
  if (maxSelect === 1) {
92
- selected = [token];
93
+ selected = [option];
93
94
  }
94
95
  else {
95
- selected = [token, ...selected];
96
+ selected = [option, ...selected];
96
97
  }
97
98
  if (selected.length === maxSelect)
98
99
  setOptionsVisible(false);
99
- dispatch(`add`, { token });
100
- dispatch(`change`, { token, type: `add` });
100
+ dispatch(`add`, { option });
101
+ dispatch(`change`, { option, type: `add` });
101
102
  }
102
103
  }
103
104
  function remove(label) {
104
105
  if (selected.length === 0 || readonly)
105
106
  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` });
107
+ selected = selected.filter((option) => label !== option.label);
108
+ const option = _options.find((option) => option.label === label);
109
+ dispatch(`remove`, { option });
110
+ dispatch(`change`, { option, type: `remove` });
110
111
  }
111
112
  function setOptionsVisible(show) {
112
113
  // nothing to do if visibility is already as intended
@@ -130,9 +131,7 @@ function handleKeydown(event) {
130
131
  // on enter key: toggle active option and reset search text
131
132
  else if (event.key === `Enter`) {
132
133
  if (activeOption) {
133
- const { label, disabled } = activeOption;
134
- if (disabled)
135
- return;
134
+ const { label } = activeOption;
136
135
  selectedLabels.includes(label) ? remove(label) : add(label);
137
136
  searchText = ``;
138
137
  } // no active option means the options dropdown is closed in which case enter means open it
@@ -148,17 +147,30 @@ function handleKeydown(event) {
148
147
  }
149
148
  const increment = event.key === `ArrowUp` ? -1 : 1;
150
149
  const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
150
+ const ulOps = document.querySelector(`ul.options`);
151
151
  if (newActiveIdx < 0) {
152
152
  // wrap around top
153
153
  activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
154
- // wrap around bottom
154
+ if (ulOps)
155
+ ulOps.scrollTop = ulOps.scrollHeight;
155
156
  }
156
157
  else if (newActiveIdx === matchingEnabledOptions.length) {
158
+ // wrap around bottom
157
159
  activeOption = matchingEnabledOptions[0];
158
- // default case
160
+ if (ulOps)
161
+ ulOps.scrollTop = 0;
159
162
  }
160
- else
163
+ else {
164
+ // default case
161
165
  activeOption = matchingEnabledOptions[newActiveIdx];
166
+ const li = document.querySelector(`ul.options > li.active`);
167
+ // scrollIntoViewIfNeeded() scrolls top edge of element into view so when moving
168
+ // downwards, we scroll to next sibling to make element fully visible
169
+ if (increment === 1)
170
+ li?.nextSibling?.scrollIntoViewIfNeeded();
171
+ else
172
+ li?.scrollIntoViewIfNeeded();
173
+ }
162
174
  }
163
175
  else if (event.key === `Backspace`) {
164
176
  const label = selectedLabels.pop();
@@ -167,8 +179,8 @@ function handleKeydown(event) {
167
179
  }
168
180
  }
169
181
  const removeAll = () => {
170
- dispatch(`remove`, { token: selected });
171
- dispatch(`change`, { token: selected, type: `remove` });
182
+ dispatch(`removeAll`, { options: selected });
183
+ dispatch(`change`, { options: selected, type: `removeAll` });
172
184
  selected = [];
173
185
  searchText = ``;
174
186
  };
@@ -181,7 +193,7 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
181
193
  };
182
194
  </script>
183
195
 
184
- <!-- z-index: 2 when showOptions is true ensures the ul.tokens of one <MultiSelect />
196
+ <!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
185
197
  display above those of another following shortly after it -->
186
198
  <div
187
199
  {id}
@@ -194,7 +206,7 @@ display above those of another following shortly after it -->
194
206
  use:onClickOutside={() => dispatch(`blur`)}
195
207
  >
196
208
  <ExpandIcon height="14pt" style="padding: 0 3pt 0 1pt;" />
197
- <ul class="tokens {ulTokensClass}">
209
+ <ul class="selected {ulSelectedClass}">
198
210
  {#if maxSelect == 1 && selected[0]?.label}
199
211
  <span on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}>
200
212
  {selected[0].label}
@@ -202,7 +214,7 @@ display above those of another following shortly after it -->
202
214
  {:else}
203
215
  {#each selected as { label }}
204
216
  <li
205
- class={liTokenClass}
217
+ class={liSelectedClass}
206
218
  on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
207
219
  >
208
220
  {label}
@@ -253,7 +265,7 @@ display above those of another following shortly after it -->
253
265
  class:hidden={!showOptions}
254
266
  transition:fly|local={{ duration: 300, y: 40 }}
255
267
  >
256
- {#each matchingOptions as { label, disabled, title = '', selectedTitle, disabledTitle = defaultDisabledTitle }}
268
+ {#each matchingOptions as { label, disabled, title = null, selectedTitle, disabledTitle = defaultDisabledTitle }}
257
269
  <li
258
270
  on:mouseup|preventDefault|stopPropagation
259
271
  on:mousedown|preventDefault|stopPropagation={() => {
@@ -294,8 +306,8 @@ display above those of another following shortly after it -->
294
306
  background: var(--sms-readonly-bg, lightgray);
295
307
  }
296
308
 
297
- :where(ul.tokens > li) {
298
- background: var(--sms-token-bg, var(--sms-active-color, cornflowerblue));
309
+ :where(ul.selected > li) {
310
+ background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
299
311
  align-items: center;
300
312
  border-radius: 4pt;
301
313
  display: flex;
@@ -305,7 +317,7 @@ display above those of another following shortly after it -->
305
317
  white-space: nowrap;
306
318
  height: 16pt;
307
319
  }
308
- :where(ul.tokens > li button, button.remove-all) {
320
+ :where(ul.selected > li button, button.remove-all) {
309
321
  align-items: center;
310
322
  border-radius: 50%;
311
323
  display: flex;
@@ -320,7 +332,7 @@ display above those of another following shortly after it -->
320
332
  outline: none;
321
333
  padding: 0 2pt;
322
334
  }
323
- :where(ul.tokens > li button:hover, button.remove-all:hover) {
335
+ :where(ul.selected > li button:hover, button.remove-all:hover) {
324
336
  color: var(--sms-remove-x-hover-focus-color, lightskyblue);
325
337
  }
326
338
  :where(button:focus) {
@@ -337,12 +349,13 @@ display above those of another following shortly after it -->
337
349
  min-width: 2em;
338
350
  }
339
351
 
340
- :where(ul.tokens) {
352
+ :where(ul.selected) {
341
353
  display: flex;
342
354
  padding: 0;
343
355
  margin: 0;
344
356
  flex-wrap: wrap;
345
357
  flex: 1;
358
+ overscroll-behavior: none;
346
359
  }
347
360
 
348
361
  :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;
package/actions.js CHANGED
@@ -14,3 +14,26 @@ export function onClickOutside(node, cb) {
14
14
  },
15
15
  };
16
16
  }
17
+ // import { spring } from 'svelte/motion'
18
+ // export default function boop(node: HTMLElement, params = {}) {
19
+ // const { setter } = params
20
+ // const springyRotation = spring(
21
+ // { x: 0, y: 0, rotation: 0, scale: 1 },
22
+ // { stiffness: 0.1, damping: 0.15 }
23
+ // )
24
+ // node.style.display = `inline-block`
25
+ // const unsubscribe = springyRotation.subscribe(({ x, y, rotation, scale }) => {
26
+ // node.style.transform = `translate(${x}px, ${y}px) rotate(${rotation}deg) scale(${scale})`
27
+ // })
28
+ // return {
29
+ // update({ isBooped: x = 0, y = 0, rotation = 0, scale = 1, timing }) {
30
+ // springyRotation.set(
31
+ // isBooped
32
+ // ? { x, y, rotation, scale }
33
+ // : { x: 0, y: 0, rotation: 0, scale: 1 }
34
+ // )
35
+ // if (isBooped) window.setTimeout(() => setter(false), timing)
36
+ // },
37
+ // destroy: unsubscribe,
38
+ // }
39
+ // }
package/package.json CHANGED
@@ -5,7 +5,7 @@
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.0.0",
9
9
  "type": "module",
10
10
  "svelte": "MultiSelect.svelte",
11
11
  "bugs": {
@@ -14,8 +14,10 @@
14
14
  "devDependencies": {
15
15
  "@sveltejs/adapter-static": "^1.0.0-next.22",
16
16
  "@sveltejs/kit": "^1.0.0-next.202",
17
+ "@testing-library/svelte": "^3.0.3",
17
18
  "@typescript-eslint/eslint-plugin": "^5.7.0",
18
19
  "@typescript-eslint/parser": "^5.7.0",
20
+ "ava": "^3.15.0",
19
21
  "eslint": "^8.5.0",
20
22
  "eslint-plugin-svelte3": "^3.2.1",
21
23
  "hastscript": "^7.0.2",
package/readme.md CHANGED
@@ -33,6 +33,18 @@
33
33
  - **No dependencies:** needs only Svelte as dev dependency
34
34
  - **Keyboard friendly** for mouse-less form completion
35
35
 
36
+ > ## Recent breaking changes
37
+ >
38
+ > - v2.0.0 added the ability to pass options as objects. As a result, `bind:selected` no longer returns simple strings but objects as well, even if you still pass in `options` as strings.
39
+ > - v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
40
+ >
41
+ > ```js
42
+ > on:add={(e) => console.log(e.detail.token.label)} // v2.0.0
43
+ > on:add={(e) => console.log(e.detail.option.label)} // v3.0.0
44
+ > ```
45
+ >
46
+ > 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`.
47
+
36
48
  ## Installation
37
49
 
38
50
  ```sh
@@ -97,24 +109,25 @@ Full list of props/bindable variables for this component:
97
109
 
98
110
  `MultiSelect.svelte` dispatches the following events:
99
111
 
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. |
112
+ | name | detail | description |
113
+ | ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
114
+ | `add` | `{ option: Option }` | Triggers when a new option is selected. |
115
+ | `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed. |
116
+ | `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected. |
117
+ | `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. |
118
+ | `blur` | none | Triggers when the input field looses focus. |
106
119
 
107
120
  ### Examples
108
121
 
109
122
  <!-- 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}'`)}``
123
+ - `on:add={(event) => console.log(event.detail.option.label)}`
124
+ - `on:remove={(event) => console.log(event.detail.option.label)}`.
125
+ - ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option.label}'`)}``
113
126
  - `on:blur={yourFunctionHere}`
114
127
 
115
128
  ```svelte
116
129
  <MultiSelect
117
- on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.token.label}'`)}
130
+ on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.option.label}'`)}
118
131
  />
119
132
  ```
120
133
 
@@ -131,8 +144,8 @@ The first, if you only want to make small adjustments, allows you to pass the fo
131
144
  - `color: var(--sms-text-color, inherit)`: Input text color.
132
145
  - `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
146
  - `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.
147
+ - `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
148
+ - `color: var(--sms-remove-x-hover+focus-color, lightgray)`: Hover color of cross icon to remove selected options.
136
149
  - `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
150
  - `background: var(--sms-options-bg, white)`: Background of options list.
138
151
  - `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
@@ -152,8 +165,8 @@ For example, to change the background color of the options dropdown:
152
165
  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
166
 
154
167
  - `outerDivClass`
155
- - `ulTokensClass`
156
- - `liTokenClass`
168
+ - `ulSelectedClass`
169
+ - `liSelectedClass`
157
170
  - `ulOptionsClass`
158
171
  - `liOptionClass`
159
172
 
@@ -161,9 +174,9 @@ This simplified version of the DOM structure of this component shows where these
161
174
 
162
175
  ```svelte
163
176
  <div class={outerDivClass}>
164
- <ul class={ulTokensClass}>
165
- <li class={liTokenClass}>First selected tag</li>
166
- <li class={liTokenClass}>Second selected tag</li>
177
+ <ul class={ulSelectedClass}>
178
+ <li class={liSelectedClass}>First selected tag</li>
179
+ <li class={liSelectedClass}>Second selected tag</li>
167
180
  </ul>
168
181
  <ul class={ulOptionsClass}>
169
182
  <li class={liOptionClass}>First available option</li>
@@ -174,16 +187,16 @@ This simplified version of the DOM structure of this component shows where these
174
187
 
175
188
  ### Granular control through global CSS
176
189
 
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.
190
+ 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
191
 
179
192
  ```css
180
193
  :global(.multiselect) {
181
194
  /* top-level wrapper div */
182
195
  }
183
- :global(.multiselect ul.tokens > li) {
196
+ :global(.multiselect ul.selected > li) {
184
197
  /* selected options */
185
198
  }
186
- :global(.multiselect ul.tokens > li button),
199
+ :global(.multiselect ul.selected > li button),
187
200
  :global(.multiselect button.remove-all) {
188
201
  /* buttons to remove a single or all selected options at once */
189
202
  }