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.
- package/MultiSelect.svelte +46 -33
- package/MultiSelect.svelte.d.ts +2 -2
- package/actions.js +23 -0
- package/package.json +3 -1
- package/readme.md +33 -20
package/MultiSelect.svelte
CHANGED
|
@@ -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
|
|
20
|
-
export let
|
|
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
|
|
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
|
|
87
|
-
if (!
|
|
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 = [
|
|
93
|
+
selected = [option];
|
|
93
94
|
}
|
|
94
95
|
else {
|
|
95
|
-
selected = [
|
|
96
|
+
selected = [option, ...selected];
|
|
96
97
|
}
|
|
97
98
|
if (selected.length === maxSelect)
|
|
98
99
|
setOptionsVisible(false);
|
|
99
|
-
dispatch(`add`, {
|
|
100
|
-
dispatch(`change`, {
|
|
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((
|
|
107
|
-
const
|
|
108
|
-
dispatch(`remove`, {
|
|
109
|
-
dispatch(`change`, {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
171
|
-
dispatch(`change`, {
|
|
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.
|
|
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="
|
|
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={
|
|
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 =
|
|
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.
|
|
298
|
-
background: var(--sms-
|
|
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.
|
|
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.
|
|
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.
|
|
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) {
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ declare const __propDef: {
|
|
|
16
16
|
noOptionsMsg?: string | undefined;
|
|
17
17
|
activeOption?: Option | null | undefined;
|
|
18
18
|
outerDivClass?: string | undefined;
|
|
19
|
-
|
|
20
|
-
|
|
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": "
|
|
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
|
|
101
|
-
|
|
|
102
|
-
| `add`
|
|
103
|
-
| `remove`
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
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.
|
|
111
|
-
- `on:remove={(event) => console.log(event.detail.
|
|
112
|
-
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.
|
|
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.
|
|
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-
|
|
135
|
-
- `color: var(--sms-remove-x-hover+focus-color, lightgray)`: Hover color of cross icon to remove selected
|
|
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
|
-
- `
|
|
156
|
-
- `
|
|
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={
|
|
165
|
-
<li class={
|
|
166
|
-
<li class={
|
|
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.
|
|
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.
|
|
196
|
+
:global(.multiselect ul.selected > li) {
|
|
184
197
|
/* selected options */
|
|
185
198
|
}
|
|
186
|
-
:global(.multiselect ul.
|
|
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
|
}
|