svelte-multiselect 10.3.0 → 11.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/dist/CircleSpinner.svelte +3 -4
- package/dist/CircleSpinner.svelte.d.ts +7 -17
- package/dist/CmdPalette.svelte +19 -24
- package/dist/CmdPalette.svelte.d.ts +22 -30
- package/dist/MultiSelect.svelte +239 -265
- package/dist/MultiSelect.svelte.d.ts +16 -105
- package/dist/Wiggle.svelte +10 -19
- package/dist/Wiggle.svelte.d.ts +14 -24
- package/dist/icons/ChevronExpand.svelte +5 -1
- package/dist/icons/ChevronExpand.svelte.d.ts +4 -23
- package/dist/icons/Cross.svelte +6 -2
- package/dist/icons/Cross.svelte.d.ts +4 -23
- package/dist/icons/Disabled.svelte +5 -1
- package/dist/icons/Disabled.svelte.d.ts +4 -23
- package/dist/icons/Octocat.svelte +5 -1
- package/dist/icons/Octocat.svelte.d.ts +4 -23
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -7
- package/dist/props.d.ts +138 -0
- package/dist/props.js +1 -0
- package/dist/types.d.ts +0 -40
- package/dist/utils.d.ts +22 -3
- package/dist/utils.js +60 -3
- package/package.json +35 -32
- package/readme.md +44 -65
package/dist/MultiSelect.svelte
CHANGED
|
@@ -1,94 +1,29 @@
|
|
|
1
|
-
<script>import {
|
|
1
|
+
<script lang="ts">import { tick } from 'svelte';
|
|
2
2
|
import { flip } from 'svelte/animate';
|
|
3
3
|
import CircleSpinner from './CircleSpinner.svelte';
|
|
4
4
|
import Wiggle from './Wiggle.svelte';
|
|
5
5
|
import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
|
|
6
|
-
import { get_label, get_style } from './utils';
|
|
7
|
-
|
|
8
|
-
export let activeOption = null;
|
|
9
|
-
export let createOptionMsg = `Create this option...`;
|
|
10
|
-
export let allowUserOptions = false;
|
|
11
|
-
export let allowEmpty = false; // added for https://github.com/janosh/svelte-multiselect/issues/192
|
|
12
|
-
export let autocomplete = `off`;
|
|
13
|
-
export let autoScroll = true;
|
|
14
|
-
export let breakpoint = 800; // any screen with more horizontal pixels is considered desktop, below is mobile
|
|
15
|
-
export let defaultDisabledTitle = `This option is disabled`;
|
|
16
|
-
export let disabled = false;
|
|
17
|
-
export let disabledInputTitle = `This input is disabled`;
|
|
18
|
-
// prettier-ignore
|
|
19
|
-
export let duplicateOptionMsg = `This option is already selected`;
|
|
20
|
-
export let duplicates = false; // whether to allow duplicate options
|
|
21
|
-
// takes two options and returns true if they are equal
|
|
22
|
-
// case-insensitive equality comparison after string coercion and looks only at the `label` key of object options by default
|
|
23
|
-
export let key = (opt) => `${get_label(opt)}`.toLowerCase();
|
|
24
|
-
export let filterFunc = (opt, searchText) => {
|
|
6
|
+
import { get_label, get_style, highlight_matching_nodes } from './utils';
|
|
7
|
+
let { activeIndex = $bindable(null), activeOption = $bindable(null), createOptionMsg = `Create this option...`, allowUserOptions = false, allowEmpty = false, autocomplete = `off`, autoScroll = true, breakpoint = 800, defaultDisabledTitle = `This option is disabled`, disabled = false, disabledInputTitle = `This input is disabled`, duplicateOptionMsg = `This option is already selected`, duplicates = false, key = (opt) => `${get_label(opt)}`.toLowerCase(), filterFunc = (opt, searchText) => {
|
|
25
8
|
if (!searchText)
|
|
26
9
|
return true;
|
|
27
10
|
return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
28
|
-
}
|
|
29
|
-
export let closeDropdownOnSelect = `desktop`;
|
|
30
|
-
export let form_input = null;
|
|
31
|
-
export let highlightMatches = true;
|
|
32
|
-
export let id = null;
|
|
33
|
-
export let input = null;
|
|
34
|
-
export let inputClass = ``;
|
|
35
|
-
export let inputStyle = null;
|
|
36
|
-
export let inputmode = null;
|
|
37
|
-
export let invalid = false;
|
|
38
|
-
export let liActiveOptionClass = ``;
|
|
39
|
-
export let liActiveUserMsgClass = ``;
|
|
40
|
-
export let liOptionClass = ``;
|
|
41
|
-
export let liOptionStyle = null;
|
|
42
|
-
export let liSelectedClass = ``;
|
|
43
|
-
export let liSelectedStyle = null;
|
|
44
|
-
export let liUserMsgClass = ``;
|
|
45
|
-
export let loading = false;
|
|
46
|
-
export let matchingOptions = [];
|
|
47
|
-
export let maxOptions = undefined;
|
|
48
|
-
export let maxSelect = null; // null means there is no upper limit for selected.length
|
|
49
|
-
export let maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``);
|
|
50
|
-
export let maxSelectMsgClass = ``;
|
|
51
|
-
export let name = null;
|
|
52
|
-
export let noMatchingOptionsMsg = `No matching options`;
|
|
53
|
-
export let open = false;
|
|
54
|
-
export let options;
|
|
55
|
-
export let outerDiv = null;
|
|
56
|
-
export let outerDivClass = ``;
|
|
57
|
-
export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
|
|
58
|
-
export let pattern = null;
|
|
59
|
-
export let placeholder = null;
|
|
60
|
-
export let removeAllTitle = `Remove all`;
|
|
61
|
-
export let removeBtnTitle = `Remove`;
|
|
62
|
-
export let minSelect = null; // null means there is no lower limit for selected.length
|
|
63
|
-
export let required = false;
|
|
64
|
-
export let resetFilterOnAdd = true;
|
|
65
|
-
export let searchText = ``;
|
|
66
|
-
export let selected = options
|
|
11
|
+
}, closeDropdownOnSelect = `desktop`, form_input = $bindable(null), highlightMatches = true, id = null, input = $bindable(null), inputClass = ``, inputStyle = null, inputmode = null, invalid = $bindable(false), liActiveOptionClass = ``, liActiveUserMsgClass = ``, liOptionClass = ``, liOptionStyle = null, liSelectedClass = ``, liSelectedStyle = null, liUserMsgClass = ``, loading = false, matchingOptions = $bindable([]), maxOptions = undefined, maxSelect = null, maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``), maxSelectMsgClass = ``, name = null, noMatchingOptionsMsg = `No matching options`, open = $bindable(false), options = $bindable(), outerDiv = $bindable(null), outerDivClass = ``, parseLabelsAsHtml = false, pattern = null, placeholder = null, removeAllTitle = `Remove all`, removeBtnTitle = `Remove`, minSelect = null, required = false, resetFilterOnAdd = true, searchText = $bindable(``), selected = $bindable(options
|
|
67
12
|
?.filter((opt) => opt instanceof Object && opt?.preselected)
|
|
68
|
-
.slice(0, maxSelect ?? undefined) ?? []
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
export let value = null;
|
|
77
|
-
const selected_to_value = (selected) => {
|
|
78
|
-
value = maxSelect === 1 ? selected[0] ?? null : selected;
|
|
79
|
-
};
|
|
80
|
-
const value_to_selected = (value) => {
|
|
13
|
+
.slice(0, maxSelect ?? undefined) ?? []), sortSelected = false, selectedOptionsDraggable = !sortSelected, style = null, ulOptionsClass = ``, ulSelectedClass = ``, ulSelectedStyle = null, ulOptionsStyle = null, value = $bindable(null), expandIcon, selectedItem, children, removeIcon, afterInput, spinner, disabledIcon, option, userMsg, onblur, onclick, onfocus, onkeydown, onkeyup, onmousedown, onmouseenter, onmouseleave, ontouchcancel, ontouchend, ontouchmove, ontouchstart, onadd, oncreate, onremove, onremoveAll, onchange, onopen, onclose, ...rest } = $props();
|
|
14
|
+
$effect.pre(() => {
|
|
15
|
+
// if maxSelect=1, value is the single item in selected (or null if selected is empty)
|
|
16
|
+
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
|
17
|
+
// https://github.com/janosh/svelte-multiselect/issues/136
|
|
18
|
+
value = maxSelect === 1 ? (selected[0] ?? null) : selected;
|
|
19
|
+
}); // sync selected updates to value
|
|
20
|
+
$effect.pre(() => {
|
|
81
21
|
if (maxSelect === 1)
|
|
82
22
|
selected = value ? [value] : [];
|
|
83
23
|
else
|
|
84
24
|
selected = value ?? [];
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
|
88
|
-
// https://github.com/janosh/svelte-multiselect/issues/136
|
|
89
|
-
$: selected_to_value(selected);
|
|
90
|
-
$: value_to_selected(value);
|
|
91
|
-
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
25
|
+
}); // sync value updates to selected
|
|
26
|
+
let wiggle = $state(false); // controls wiggle animation when user tries to exceed maxSelect
|
|
92
27
|
if (!(options?.length > 0)) {
|
|
93
28
|
if (allowUserOptions || loading || disabled || allowEmpty) {
|
|
94
29
|
options = []; // initializing as array avoids errors when component mounts
|
|
@@ -123,21 +58,25 @@ if (maxOptions &&
|
|
|
123
58
|
(typeof maxOptions != `number` || maxOptions < 0 || maxOptions % 1 != 0)) {
|
|
124
59
|
console.error(`MultiSelect's maxOptions must be undefined or a positive integer, got ${maxOptions}`);
|
|
125
60
|
}
|
|
126
|
-
|
|
127
|
-
let
|
|
128
|
-
let window_width;
|
|
61
|
+
let option_msg_is_active = $state(false); // controls active state of <li>{createOptionMsg}</li>
|
|
62
|
+
let window_width = $state(0);
|
|
129
63
|
// options matching the current search text
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
64
|
+
$effect.pre(() => {
|
|
65
|
+
matchingOptions = options.filter((opt) => filterFunc(opt, searchText) &&
|
|
66
|
+
// remove already selected options from dropdown list unless duplicate selections are allowed
|
|
67
|
+
(!selected.map(key).includes(key(opt)) || duplicates));
|
|
68
|
+
});
|
|
133
69
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
134
70
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
135
71
|
throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
|
|
136
72
|
}
|
|
137
73
|
// update activeOption when activeIndex changes
|
|
138
|
-
|
|
74
|
+
$effect(() => {
|
|
75
|
+
activeOption = matchingOptions[activeIndex ?? -1] ?? null;
|
|
76
|
+
});
|
|
139
77
|
// add an option to selected list
|
|
140
78
|
function add(option, event) {
|
|
79
|
+
event.stopPropagation();
|
|
141
80
|
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
142
81
|
wiggle = true;
|
|
143
82
|
if (!isNaN(Number(option)) && typeof selected.map(get_label)[0] === `number`) {
|
|
@@ -166,7 +105,7 @@ function add(option, event) {
|
|
|
166
105
|
else {
|
|
167
106
|
option = searchText; // else create custom option as string
|
|
168
107
|
}
|
|
169
|
-
|
|
108
|
+
oncreate?.({ option });
|
|
170
109
|
}
|
|
171
110
|
if (allowUserOptions === `append`)
|
|
172
111
|
options = [...options, option];
|
|
@@ -196,21 +135,22 @@ function add(option, event) {
|
|
|
196
135
|
}
|
|
197
136
|
const reached_max_select = selected.length === maxSelect;
|
|
198
137
|
const dropdown_should_close = closeDropdownOnSelect === true ||
|
|
199
|
-
(closeDropdownOnSelect === `desktop` && window_width < breakpoint);
|
|
138
|
+
(closeDropdownOnSelect === `desktop` && window_width && window_width < breakpoint);
|
|
200
139
|
if (reached_max_select || dropdown_should_close) {
|
|
201
140
|
close_dropdown(event);
|
|
202
141
|
}
|
|
203
142
|
else if (!dropdown_should_close) {
|
|
204
143
|
input?.focus();
|
|
205
144
|
}
|
|
206
|
-
|
|
207
|
-
|
|
145
|
+
onadd?.({ option });
|
|
146
|
+
onchange?.({ option, type: `add` });
|
|
208
147
|
invalid = false; // reset error status whenever new items are selected
|
|
209
148
|
form_input?.setCustomValidity(``);
|
|
210
149
|
}
|
|
211
150
|
}
|
|
212
151
|
// remove an option from selected list
|
|
213
|
-
function remove(to_remove) {
|
|
152
|
+
function remove(to_remove, event) {
|
|
153
|
+
event.stopPropagation();
|
|
214
154
|
if (selected.length === 0)
|
|
215
155
|
return;
|
|
216
156
|
const idx = selected.findIndex((opt) => key(opt) === key(to_remove));
|
|
@@ -228,10 +168,11 @@ function remove(to_remove) {
|
|
|
228
168
|
selected = [...selected]; // trigger Svelte rerender
|
|
229
169
|
invalid = false; // reset error status whenever items are removed
|
|
230
170
|
form_input?.setCustomValidity(``);
|
|
231
|
-
|
|
232
|
-
|
|
171
|
+
onremove?.({ option });
|
|
172
|
+
onchange?.({ option, type: `remove` });
|
|
233
173
|
}
|
|
234
174
|
function open_dropdown(event) {
|
|
175
|
+
event.stopPropagation();
|
|
235
176
|
if (disabled)
|
|
236
177
|
return;
|
|
237
178
|
open = true;
|
|
@@ -239,45 +180,54 @@ function open_dropdown(event) {
|
|
|
239
180
|
// avoid double-focussing input when event that opened dropdown was already input FocusEvent
|
|
240
181
|
input?.focus();
|
|
241
182
|
}
|
|
242
|
-
|
|
183
|
+
onopen?.({ event });
|
|
243
184
|
}
|
|
244
185
|
function close_dropdown(event) {
|
|
245
186
|
open = false;
|
|
246
187
|
input?.blur();
|
|
247
188
|
activeIndex = null;
|
|
248
|
-
|
|
189
|
+
onclose?.({ event });
|
|
249
190
|
}
|
|
250
191
|
// handle all keyboard events this component receives
|
|
251
192
|
async function handle_keydown(event) {
|
|
252
193
|
// on escape or tab out of input: close options dropdown and reset search text
|
|
253
194
|
if (event.key === `Escape` || event.key === `Tab`) {
|
|
195
|
+
event.stopPropagation();
|
|
254
196
|
close_dropdown(event);
|
|
255
197
|
searchText = ``;
|
|
256
198
|
}
|
|
257
199
|
// on enter key: toggle active option and reset search text
|
|
258
200
|
else if (event.key === `Enter`) {
|
|
201
|
+
event.stopPropagation();
|
|
259
202
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
260
203
|
if (activeOption) {
|
|
261
|
-
selected.includes(activeOption)
|
|
204
|
+
if (selected.includes(activeOption))
|
|
205
|
+
remove(activeOption, event);
|
|
206
|
+
else
|
|
207
|
+
add(activeOption, event);
|
|
262
208
|
searchText = ``;
|
|
263
209
|
}
|
|
264
210
|
else if (allowUserOptions && searchText.length > 0) {
|
|
265
211
|
// user entered text but no options match, so if allowUserOptions is truthy, we create new option
|
|
266
212
|
add(searchText, event);
|
|
267
213
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
214
|
+
else {
|
|
215
|
+
// no active option and no search text means the options dropdown is closed
|
|
216
|
+
// in which case enter means open it
|
|
271
217
|
open_dropdown(event);
|
|
218
|
+
}
|
|
272
219
|
}
|
|
273
220
|
// on up/down arrow keys: update active option
|
|
274
221
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
222
|
+
event.stopPropagation();
|
|
275
223
|
// if no option is active yet, but there are matching options, make first one active
|
|
276
224
|
if (activeIndex === null && matchingOptions.length > 0) {
|
|
225
|
+
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
277
226
|
activeIndex = 0;
|
|
278
227
|
return;
|
|
279
228
|
}
|
|
280
229
|
else if (allowUserOptions && !matchingOptions.length && searchText.length > 0) {
|
|
230
|
+
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
281
231
|
// if allowUserOptions is truthy and user entered text but no options match, we make
|
|
282
232
|
// <li>{addUserMsg}</li> active on keydown (or toggle it if already active)
|
|
283
233
|
option_msg_is_active = !option_msg_is_active;
|
|
@@ -287,7 +237,7 @@ async function handle_keydown(event) {
|
|
|
287
237
|
// if no option is active and no options are matching, do nothing
|
|
288
238
|
return;
|
|
289
239
|
}
|
|
290
|
-
event.preventDefault();
|
|
240
|
+
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
291
241
|
// if none of the above special cases apply, we make next/prev option
|
|
292
242
|
// active with wrap around at both ends
|
|
293
243
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
@@ -305,24 +255,28 @@ async function handle_keydown(event) {
|
|
|
305
255
|
}
|
|
306
256
|
// on backspace key: remove last selected option
|
|
307
257
|
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
308
|
-
|
|
258
|
+
event.stopPropagation();
|
|
259
|
+
// Don't prevent default, allow normal backspace behavior if not removing
|
|
260
|
+
remove(selected.at(-1), event);
|
|
309
261
|
}
|
|
310
262
|
// make first matching option active on any keypress (if none of the above special cases match)
|
|
311
|
-
else if (matchingOptions.length > 0) {
|
|
263
|
+
else if (matchingOptions.length > 0 && activeIndex === null) {
|
|
264
|
+
// Don't stop propagation or prevent default here, allow normal character input
|
|
312
265
|
activeIndex = 0;
|
|
313
266
|
}
|
|
314
267
|
}
|
|
315
|
-
function remove_all() {
|
|
316
|
-
|
|
317
|
-
|
|
268
|
+
function remove_all(event) {
|
|
269
|
+
event.stopPropagation();
|
|
270
|
+
onremoveAll?.({ options: selected });
|
|
271
|
+
onchange?.({ options: selected, type: `removeAll` });
|
|
318
272
|
selected = [];
|
|
319
273
|
searchText = ``;
|
|
320
274
|
}
|
|
321
|
-
|
|
275
|
+
let is_selected = $derived((label) => selected.map(get_label).includes(label));
|
|
322
276
|
const if_enter_or_space = (handler) => (event) => {
|
|
323
277
|
if ([`Enter`, `Space`].includes(event.code)) {
|
|
324
278
|
event.preventDefault();
|
|
325
|
-
handler();
|
|
279
|
+
handler(event);
|
|
326
280
|
}
|
|
327
281
|
};
|
|
328
282
|
function on_click_outside(event) {
|
|
@@ -330,9 +284,10 @@ function on_click_outside(event) {
|
|
|
330
284
|
close_dropdown(event);
|
|
331
285
|
}
|
|
332
286
|
}
|
|
333
|
-
let drag_idx = null;
|
|
287
|
+
let drag_idx = $state(null);
|
|
334
288
|
// event handlers enable dragging to reorder selected options
|
|
335
289
|
const drop = (target_idx) => (event) => {
|
|
290
|
+
event.preventDefault();
|
|
336
291
|
if (!event.dataTransfer)
|
|
337
292
|
return;
|
|
338
293
|
event.dataTransfer.dropEffect = `move`;
|
|
@@ -357,62 +312,38 @@ const dragstart = (idx) => (event) => {
|
|
|
357
312
|
event.dataTransfer.dropEffect = `move`;
|
|
358
313
|
event.dataTransfer.setData(`text/plain`, `${idx}`);
|
|
359
314
|
};
|
|
360
|
-
let ul_options;
|
|
315
|
+
let ul_options = $state();
|
|
361
316
|
// highlight text matching user-entered search text in available options
|
|
362
317
|
function highlight_matching_options(event) {
|
|
363
|
-
if (!highlightMatches ||
|
|
364
|
-
return;
|
|
365
|
-
// clear previous ranges from HighlightRegistry
|
|
366
|
-
CSS.highlights.clear();
|
|
318
|
+
if (!highlightMatches || !ul_options)
|
|
319
|
+
return;
|
|
367
320
|
// get input's search query
|
|
368
321
|
const query = event?.target?.value.trim().toLowerCase();
|
|
369
322
|
if (!query)
|
|
370
323
|
return;
|
|
371
|
-
|
|
372
|
-
acceptNode: (node) => {
|
|
373
|
-
// don't highlight text in the "no matching options" message
|
|
374
|
-
if (node?.textContent === noMatchingOptionsMsg)
|
|
375
|
-
return NodeFilter.FILTER_REJECT;
|
|
376
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
const text_nodes = [];
|
|
380
|
-
let current_node = tree_walker.nextNode();
|
|
381
|
-
while (current_node) {
|
|
382
|
-
text_nodes.push(current_node);
|
|
383
|
-
current_node = tree_walker.nextNode();
|
|
384
|
-
}
|
|
385
|
-
// iterate over all text nodes and find matches
|
|
386
|
-
const ranges = text_nodes.map((el) => {
|
|
387
|
-
const text = el.textContent?.toLowerCase();
|
|
388
|
-
const indices = [];
|
|
389
|
-
let start_pos = 0;
|
|
390
|
-
while (text && start_pos < text.length) {
|
|
391
|
-
const index = text.indexOf(query, start_pos);
|
|
392
|
-
if (index === -1)
|
|
393
|
-
break;
|
|
394
|
-
indices.push(index);
|
|
395
|
-
start_pos = index + query.length;
|
|
396
|
-
}
|
|
397
|
-
// create range object for each str found in the text node
|
|
398
|
-
return indices.map((index) => {
|
|
399
|
-
const range = new Range();
|
|
400
|
-
range.setStart(el, index);
|
|
401
|
-
range.setEnd(el, index + query.length);
|
|
402
|
-
return range;
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
// create Highlight object from ranges and add to registry
|
|
406
|
-
CSS.highlights.set(`sms-search-matches`, new Highlight(...ranges.flat()));
|
|
324
|
+
highlight_matching_nodes(ul_options, query, noMatchingOptionsMsg);
|
|
407
325
|
}
|
|
326
|
+
const handle_input_keydown = (event) => {
|
|
327
|
+
handle_keydown(event); // Restore internal logic
|
|
328
|
+
// Call original forwarded handler
|
|
329
|
+
onkeydown?.(event);
|
|
330
|
+
};
|
|
331
|
+
const handle_input_focus = (event) => {
|
|
332
|
+
open_dropdown(event); // Internal logic
|
|
333
|
+
// Call original forwarded handler
|
|
334
|
+
onfocus?.(event);
|
|
335
|
+
};
|
|
408
336
|
// reset form validation when required prop changes
|
|
409
337
|
// https://github.com/janosh/svelte-multiselect/issues/285
|
|
410
|
-
|
|
338
|
+
$effect.pre(() => {
|
|
339
|
+
required = required; // trigger effect when required changes
|
|
340
|
+
form_input?.setCustomValidity(``);
|
|
341
|
+
});
|
|
411
342
|
</script>
|
|
412
343
|
|
|
413
344
|
<svelte:window
|
|
414
|
-
|
|
415
|
-
|
|
345
|
+
onclick={on_click_outside}
|
|
346
|
+
ontouchstart={on_click_outside}
|
|
416
347
|
bind:innerWidth={window_width}
|
|
417
348
|
/>
|
|
418
349
|
|
|
@@ -423,7 +354,7 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
423
354
|
class:open
|
|
424
355
|
class:invalid
|
|
425
356
|
class="multiselect {outerDivClass}"
|
|
426
|
-
|
|
357
|
+
onmouseup={open_dropdown}
|
|
427
358
|
title={disabled ? disabledInputTitle : null}
|
|
428
359
|
data-id={id}
|
|
429
360
|
role="searchbox"
|
|
@@ -434,14 +365,14 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
434
365
|
<!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
|
|
435
366
|
<input
|
|
436
367
|
{name}
|
|
437
|
-
{required}
|
|
368
|
+
required={Boolean(required)}
|
|
438
369
|
value={selected.length >= Number(required) ? JSON.stringify(selected) : null}
|
|
439
370
|
tabindex="-1"
|
|
440
371
|
aria-hidden="true"
|
|
441
372
|
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
|
442
373
|
class="form-control"
|
|
443
374
|
bind:this={form_input}
|
|
444
|
-
|
|
375
|
+
oninvalid={() => {
|
|
445
376
|
invalid = true
|
|
446
377
|
let msg
|
|
447
378
|
if (maxSelect && maxSelect > 1 && Number(required) > 1) {
|
|
@@ -454,49 +385,60 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
454
385
|
form_input?.setCustomValidity(msg)
|
|
455
386
|
}}
|
|
456
387
|
/>
|
|
457
|
-
|
|
388
|
+
{#if expandIcon}
|
|
389
|
+
{@render expandIcon({ open })}
|
|
390
|
+
{:else}
|
|
458
391
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt; cursor: pointer;" />
|
|
459
|
-
|
|
392
|
+
{/if}
|
|
460
393
|
<ul
|
|
461
394
|
class="selected {ulSelectedClass}"
|
|
462
395
|
aria-label="selected options"
|
|
463
396
|
style={ulSelectedStyle}
|
|
464
397
|
>
|
|
465
398
|
{#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
|
|
399
|
+
{@const selectedOptionStyle =
|
|
400
|
+
[get_style(option, `selected`), liSelectedStyle].filter(Boolean).join(` `) ||
|
|
401
|
+
null}
|
|
466
402
|
<li
|
|
467
403
|
class={liSelectedClass}
|
|
468
404
|
role="option"
|
|
469
405
|
aria-selected="true"
|
|
470
406
|
animate:flip={{ duration: 100 }}
|
|
471
407
|
draggable={selectedOptionsDraggable && !disabled && selected.length > 1}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
on:dragover|preventDefault
|
|
408
|
+
ondragstart={dragstart(idx)}
|
|
409
|
+
ondrop={drop(idx)}
|
|
410
|
+
ondragenter={() => (drag_idx = idx)}
|
|
476
411
|
class:active={drag_idx === idx}
|
|
477
|
-
style=
|
|
412
|
+
style={selectedOptionStyle}
|
|
478
413
|
>
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
414
|
+
{#if selectedItem}
|
|
415
|
+
{@render selectedItem({
|
|
416
|
+
option,
|
|
417
|
+
idx,
|
|
418
|
+
})}
|
|
419
|
+
{:else if children}
|
|
420
|
+
{@render children({
|
|
421
|
+
option,
|
|
422
|
+
idx,
|
|
423
|
+
})}
|
|
424
|
+
{:else if parseLabelsAsHtml}
|
|
425
|
+
{@html get_label(option)}
|
|
426
|
+
{:else}
|
|
427
|
+
{get_label(option)}
|
|
428
|
+
{/if}
|
|
489
429
|
{#if !disabled && (minSelect === null || selected.length > minSelect)}
|
|
490
430
|
<button
|
|
491
|
-
|
|
492
|
-
|
|
431
|
+
onclick={(event) => remove(option, event)}
|
|
432
|
+
onkeydown={if_enter_or_space((event) => remove(option, event))}
|
|
493
433
|
type="button"
|
|
494
434
|
title="{removeBtnTitle} {get_label(option)}"
|
|
495
435
|
class="remove"
|
|
496
436
|
>
|
|
497
|
-
|
|
437
|
+
{#if removeIcon}
|
|
438
|
+
{@render removeIcon()}
|
|
439
|
+
{:else}
|
|
498
440
|
<CrossIcon width="15px" />
|
|
499
|
-
|
|
441
|
+
{/if}
|
|
500
442
|
</button>
|
|
501
443
|
{/if}
|
|
502
444
|
</li>
|
|
@@ -506,11 +448,6 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
506
448
|
style={inputStyle}
|
|
507
449
|
bind:this={input}
|
|
508
450
|
bind:value={searchText}
|
|
509
|
-
on:mouseup|self|stopPropagation={open_dropdown}
|
|
510
|
-
on:keydown|stopPropagation={handle_keydown}
|
|
511
|
-
on:focus
|
|
512
|
-
on:focus={open_dropdown}
|
|
513
|
-
on:input={highlight_matching_options}
|
|
514
451
|
{id}
|
|
515
452
|
{disabled}
|
|
516
453
|
{autocomplete}
|
|
@@ -518,41 +455,47 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
518
455
|
{pattern}
|
|
519
456
|
placeholder={selected.length == 0 ? placeholder : null}
|
|
520
457
|
aria-invalid={invalid ? `true` : null}
|
|
521
|
-
ondrop=
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
458
|
+
ondrop={() => false}
|
|
459
|
+
onmouseup={open_dropdown}
|
|
460
|
+
onkeydown={handle_input_keydown}
|
|
461
|
+
onfocus={handle_input_focus}
|
|
462
|
+
oninput={highlight_matching_options}
|
|
463
|
+
{onblur}
|
|
464
|
+
{onclick}
|
|
465
|
+
{onkeyup}
|
|
466
|
+
{onmousedown}
|
|
467
|
+
{onmouseenter}
|
|
468
|
+
{onmouseleave}
|
|
469
|
+
{ontouchcancel}
|
|
470
|
+
{ontouchend}
|
|
471
|
+
{ontouchmove}
|
|
472
|
+
{ontouchstart}
|
|
473
|
+
{...rest}
|
|
534
474
|
/>
|
|
535
475
|
<!-- the above on:* lines forward potentially useful DOM events -->
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
/>
|
|
476
|
+
{@render afterInput?.({
|
|
477
|
+
selected,
|
|
478
|
+
disabled,
|
|
479
|
+
invalid,
|
|
480
|
+
id,
|
|
481
|
+
placeholder,
|
|
482
|
+
open,
|
|
483
|
+
required,
|
|
484
|
+
})}
|
|
546
485
|
</ul>
|
|
547
486
|
{#if loading}
|
|
548
|
-
|
|
487
|
+
{#if spinner}
|
|
488
|
+
{@render spinner()}
|
|
489
|
+
{:else}
|
|
549
490
|
<CircleSpinner />
|
|
550
|
-
|
|
491
|
+
{/if}
|
|
551
492
|
{/if}
|
|
552
493
|
{#if disabled}
|
|
553
|
-
|
|
494
|
+
{#if disabledIcon}
|
|
495
|
+
{@render disabledIcon()}
|
|
496
|
+
{:else}
|
|
554
497
|
<DisabledIcon width="14pt" style="margin: 0 2pt;" data-name="disabled-icon" />
|
|
555
|
-
|
|
498
|
+
{/if}
|
|
556
499
|
{:else if selected.length > 0}
|
|
557
500
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
558
501
|
<Wiggle bind:wiggle angle={20}>
|
|
@@ -566,12 +509,14 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
566
509
|
type="button"
|
|
567
510
|
class="remove remove-all"
|
|
568
511
|
title={removeAllTitle}
|
|
569
|
-
|
|
570
|
-
|
|
512
|
+
onclick={remove_all}
|
|
513
|
+
onkeydown={if_enter_or_space(remove_all)}
|
|
571
514
|
>
|
|
572
|
-
|
|
515
|
+
{#if removeIcon}
|
|
516
|
+
{@render removeIcon()}
|
|
517
|
+
{:else}
|
|
573
518
|
<CrossIcon width="15px" />
|
|
574
|
-
|
|
519
|
+
{/if}
|
|
575
520
|
</button>
|
|
576
521
|
{/if}
|
|
577
522
|
{/if}
|
|
@@ -588,19 +533,21 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
588
533
|
bind:this={ul_options}
|
|
589
534
|
style={ulOptionsStyle}
|
|
590
535
|
>
|
|
591
|
-
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as
|
|
536
|
+
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as optionItem, idx (duplicates ? [key(optionItem), idx] : key(optionItem))}
|
|
592
537
|
{@const {
|
|
593
538
|
label,
|
|
594
539
|
disabled = null,
|
|
595
540
|
title = null,
|
|
596
541
|
selectedTitle = null,
|
|
597
542
|
disabledTitle = defaultDisabledTitle,
|
|
598
|
-
} =
|
|
543
|
+
} = optionItem instanceof Object ? optionItem : { label: optionItem }}
|
|
599
544
|
{@const active = activeIndex === idx}
|
|
545
|
+
{@const optionStyle =
|
|
546
|
+
[get_style(optionItem, `option`), liOptionStyle].filter(Boolean).join(` `) ||
|
|
547
|
+
null}
|
|
600
548
|
<li
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (!disabled) add(option, event)
|
|
549
|
+
onclick={(event) => {
|
|
550
|
+
if (!disabled) add(optionItem, event)
|
|
604
551
|
}}
|
|
605
552
|
title={disabled
|
|
606
553
|
? disabledTitle
|
|
@@ -609,27 +556,37 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
609
556
|
class:active
|
|
610
557
|
class:disabled
|
|
611
558
|
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
612
|
-
|
|
559
|
+
onmouseover={() => {
|
|
613
560
|
if (!disabled) activeIndex = idx
|
|
614
561
|
}}
|
|
615
|
-
|
|
562
|
+
onfocus={() => {
|
|
616
563
|
if (!disabled) activeIndex = idx
|
|
617
564
|
}}
|
|
618
|
-
on:mouseout={() => (activeIndex = null)}
|
|
619
|
-
on:blur={() => (activeIndex = null)}
|
|
620
565
|
role="option"
|
|
621
566
|
aria-selected="false"
|
|
622
|
-
style=
|
|
567
|
+
style={optionStyle}
|
|
568
|
+
onkeydown={(event) => {
|
|
569
|
+
if (!disabled && (event.key === `Enter` || event.code === `Space`)) {
|
|
570
|
+
event.preventDefault()
|
|
571
|
+
add(optionItem, event)
|
|
572
|
+
}
|
|
573
|
+
}}
|
|
623
574
|
>
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
575
|
+
{#if option}
|
|
576
|
+
{@render option({
|
|
577
|
+
option: optionItem,
|
|
578
|
+
idx,
|
|
579
|
+
})}
|
|
580
|
+
{:else if children}
|
|
581
|
+
{@render children({
|
|
582
|
+
option: optionItem,
|
|
583
|
+
idx,
|
|
584
|
+
})}
|
|
585
|
+
{:else if parseLabelsAsHtml}
|
|
586
|
+
{@html get_label(optionItem)}
|
|
587
|
+
{:else}
|
|
588
|
+
{get_label(optionItem)}
|
|
589
|
+
{/if}
|
|
633
590
|
</li>
|
|
634
591
|
{/each}
|
|
635
592
|
{#if searchText}
|
|
@@ -646,16 +603,31 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
646
603
|
'no-match': noMatchingOptionsMsg,
|
|
647
604
|
}[msgType]}
|
|
648
605
|
<li
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
606
|
+
onclick={(event) => {
|
|
607
|
+
if (msgType === `create` && allowUserOptions) {
|
|
608
|
+
add(searchText as Option, event)
|
|
609
|
+
}
|
|
610
|
+
}}
|
|
611
|
+
onkeydown={(event) => {
|
|
612
|
+
if (
|
|
613
|
+
msgType === `create` &&
|
|
614
|
+
allowUserOptions &&
|
|
615
|
+
(event.key === `Enter` || event.code === `Space`)
|
|
616
|
+
) {
|
|
617
|
+
event.preventDefault()
|
|
618
|
+
add(searchText as Option, event)
|
|
619
|
+
}
|
|
652
620
|
}}
|
|
653
|
-
title={
|
|
621
|
+
title={msgType === `create`
|
|
622
|
+
? createOptionMsg
|
|
623
|
+
: msgType === `dupe`
|
|
624
|
+
? duplicateOptionMsg
|
|
625
|
+
: ``}
|
|
654
626
|
class:active={option_msg_is_active}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
627
|
+
onmouseover={() => (option_msg_is_active = true)}
|
|
628
|
+
onfocus={() => (option_msg_is_active = true)}
|
|
629
|
+
onmouseout={() => (option_msg_is_active = false)}
|
|
630
|
+
onblur={() => (option_msg_is_active = false)}
|
|
659
631
|
role="option"
|
|
660
632
|
aria-selected="false"
|
|
661
633
|
class="user-msg {liUserMsgClass} {option_msg_is_active
|
|
@@ -667,9 +639,11 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
667
639
|
'no-match': `default`,
|
|
668
640
|
}[msgType]}
|
|
669
641
|
>
|
|
670
|
-
|
|
642
|
+
{#if userMsg}
|
|
643
|
+
{@render userMsg({ searchText, msgType, msg })}
|
|
644
|
+
{:else}
|
|
671
645
|
{msg}
|
|
672
|
-
|
|
646
|
+
{/if}
|
|
673
647
|
</li>
|
|
674
648
|
{/if}
|
|
675
649
|
{/if}
|
|
@@ -678,7 +652,7 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
678
652
|
</div>
|
|
679
653
|
|
|
680
654
|
<style>
|
|
681
|
-
:
|
|
655
|
+
:is(div.multiselect) {
|
|
682
656
|
position: relative;
|
|
683
657
|
align-items: center;
|
|
684
658
|
display: flex;
|
|
@@ -695,27 +669,27 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
695
669
|
min-height: var(--sms-min-height, 22pt);
|
|
696
670
|
margin: var(--sms-margin);
|
|
697
671
|
}
|
|
698
|
-
:
|
|
672
|
+
:is(div.multiselect.open) {
|
|
699
673
|
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
700
674
|
displays above that of another slightly below it on the page */
|
|
701
675
|
z-index: var(--sms-open-z-index, 4);
|
|
702
676
|
}
|
|
703
|
-
:
|
|
677
|
+
:is(div.multiselect:focus-within) {
|
|
704
678
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
705
679
|
}
|
|
706
|
-
:
|
|
680
|
+
:is(div.multiselect.disabled) {
|
|
707
681
|
background: var(--sms-disabled-bg, lightgray);
|
|
708
682
|
cursor: not-allowed;
|
|
709
683
|
}
|
|
710
684
|
|
|
711
|
-
:
|
|
685
|
+
:is(div.multiselect > ul.selected) {
|
|
712
686
|
display: flex;
|
|
713
687
|
flex: 1;
|
|
714
688
|
padding: 0;
|
|
715
689
|
margin: 0;
|
|
716
690
|
flex-wrap: wrap;
|
|
717
691
|
}
|
|
718
|
-
:
|
|
692
|
+
:is(div.multiselect > ul.selected > li) {
|
|
719
693
|
align-items: center;
|
|
720
694
|
border-radius: 3pt;
|
|
721
695
|
display: flex;
|
|
@@ -727,13 +701,13 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
727
701
|
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
|
728
702
|
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
729
703
|
}
|
|
730
|
-
:
|
|
704
|
+
:is(div.multiselect > ul.selected > li[draggable='true']) {
|
|
731
705
|
cursor: grab;
|
|
732
706
|
}
|
|
733
|
-
:
|
|
707
|
+
:is(div.multiselect > ul.selected > li.active) {
|
|
734
708
|
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
735
709
|
}
|
|
736
|
-
:
|
|
710
|
+
:is(div.multiselect button) {
|
|
737
711
|
border-radius: 50%;
|
|
738
712
|
display: flex;
|
|
739
713
|
transition: 0.2s;
|
|
@@ -742,22 +716,22 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
742
716
|
border: none;
|
|
743
717
|
cursor: pointer;
|
|
744
718
|
outline: none;
|
|
745
|
-
padding:
|
|
719
|
+
padding: 1pt;
|
|
746
720
|
margin: 0 0 0 3pt; /* CSS reset */
|
|
747
721
|
}
|
|
748
|
-
:
|
|
722
|
+
:is(div.multiselect button.remove-all) {
|
|
749
723
|
margin: 0 3pt;
|
|
750
724
|
}
|
|
751
|
-
:
|
|
725
|
+
:is(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
752
726
|
color: var(--sms-remove-btn-hover-color, lightskyblue);
|
|
753
727
|
background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
|
|
754
728
|
}
|
|
755
729
|
|
|
756
|
-
:
|
|
730
|
+
:is(div.multiselect input) {
|
|
757
731
|
margin: auto 0; /* CSS reset */
|
|
758
732
|
padding: 0; /* CSS reset */
|
|
759
733
|
}
|
|
760
|
-
:
|
|
734
|
+
:is(div.multiselect > ul.selected > input) {
|
|
761
735
|
border: none;
|
|
762
736
|
outline: none;
|
|
763
737
|
background: none;
|
|
@@ -769,13 +743,13 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
769
743
|
cursor: inherit; /* needed for disabled state */
|
|
770
744
|
border-radius: 0; /* reset ul.selected > li */
|
|
771
745
|
}
|
|
772
|
-
/* don't wrap ::placeholder rules in :
|
|
746
|
+
/* don't wrap ::placeholder rules in :is() as it seems to be overpowered by browser defaults i.t.o. specificity */
|
|
773
747
|
div.multiselect > ul.selected > input::placeholder {
|
|
774
748
|
padding-left: 5pt;
|
|
775
749
|
color: var(--sms-placeholder-color);
|
|
776
750
|
opacity: var(--sms-placeholder-opacity);
|
|
777
751
|
}
|
|
778
|
-
:
|
|
752
|
+
:is(div.multiselect > input.form-control) {
|
|
779
753
|
width: 2em;
|
|
780
754
|
position: absolute;
|
|
781
755
|
background: transparent;
|
|
@@ -786,7 +760,7 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
786
760
|
pointer-events: none;
|
|
787
761
|
}
|
|
788
762
|
|
|
789
|
-
:
|
|
763
|
+
:is(div.multiselect > ul.options) {
|
|
790
764
|
list-style: none;
|
|
791
765
|
top: 100%;
|
|
792
766
|
left: 0;
|
|
@@ -805,35 +779,35 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
805
779
|
padding: var(--sms-options-padding);
|
|
806
780
|
margin: var(--sms-options-margin, inherit);
|
|
807
781
|
}
|
|
808
|
-
:
|
|
782
|
+
:is(div.multiselect > ul.options.hidden) {
|
|
809
783
|
visibility: hidden;
|
|
810
784
|
opacity: 0;
|
|
811
785
|
transform: translateY(50px);
|
|
812
786
|
}
|
|
813
|
-
:
|
|
787
|
+
:is(div.multiselect > ul.options > li) {
|
|
814
788
|
padding: 3pt 2ex;
|
|
815
789
|
cursor: pointer;
|
|
816
790
|
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
817
791
|
}
|
|
818
|
-
:
|
|
792
|
+
:is(div.multiselect > ul.options .user-msg) {
|
|
819
793
|
/* block needed so vertical padding applies to span */
|
|
820
794
|
display: block;
|
|
821
795
|
padding: 3pt 2ex;
|
|
822
796
|
}
|
|
823
|
-
:
|
|
797
|
+
:is(div.multiselect > ul.options > li.selected) {
|
|
824
798
|
background: var(--sms-li-selected-bg);
|
|
825
799
|
color: var(--sms-li-selected-color);
|
|
826
800
|
}
|
|
827
|
-
:
|
|
801
|
+
:is(div.multiselect > ul.options > li.active) {
|
|
828
802
|
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
829
803
|
}
|
|
830
|
-
:
|
|
804
|
+
:is(div.multiselect > ul.options > li.disabled) {
|
|
831
805
|
cursor: not-allowed;
|
|
832
806
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
833
807
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
834
808
|
}
|
|
835
809
|
|
|
836
|
-
:
|
|
810
|
+
:is(span.max-select-msg) {
|
|
837
811
|
padding: 0 3pt;
|
|
838
812
|
}
|
|
839
813
|
::highlight(sms-search-matches) {
|