svelte-multiselect 11.2.4 โ 11.4.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.d.ts +3 -3
- package/dist/CmdPalette.svelte +8 -5
- package/dist/CmdPalette.svelte.d.ts +61 -16
- package/dist/CodeExample.svelte +12 -7
- package/dist/CodeExample.svelte.d.ts +6 -3
- package/dist/CopyButton.svelte +4 -3
- package/dist/CopyButton.svelte.d.ts +4 -4
- package/dist/FileDetails.svelte +3 -3
- package/dist/FileDetails.svelte.d.ts +6 -3
- package/dist/GitHubCorner.svelte +2 -2
- package/dist/GitHubCorner.svelte.d.ts +3 -3
- package/dist/Icon.svelte.d.ts +3 -3
- package/dist/MultiSelect.svelte +328 -153
- package/dist/MultiSelect.svelte.d.ts +9 -9
- package/dist/Nav.svelte +444 -0
- package/dist/Nav.svelte.d.ts +42 -0
- package/dist/PrevNext.svelte +3 -3
- package/dist/PrevNext.svelte.d.ts +51 -31
- package/dist/Toggle.svelte +2 -7
- package/dist/Toggle.svelte.d.ts +4 -9
- package/dist/Wiggle.svelte.d.ts +3 -3
- package/dist/attachments.d.ts +17 -9
- package/dist/attachments.js +80 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +33 -4
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +5 -3
- package/package.json +23 -16
- package/readme.md +84 -8
package/dist/Wiggle.svelte.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
|
|
2
|
+
type $$ComponentProps = {
|
|
3
3
|
wiggle?: boolean;
|
|
4
4
|
angle?: number;
|
|
5
5
|
scale?: number;
|
|
@@ -9,7 +9,7 @@ interface Props {
|
|
|
9
9
|
stiffness?: number;
|
|
10
10
|
damping?: number;
|
|
11
11
|
children?: Snippet;
|
|
12
|
-
}
|
|
13
|
-
declare const Wiggle: import("svelte").Component
|
|
12
|
+
};
|
|
13
|
+
declare const Wiggle: import("svelte").Component<$$ComponentProps, {}, "wiggle">;
|
|
14
14
|
type Wiggle = ReturnType<typeof Wiggle>;
|
|
15
15
|
export default Wiggle;
|
package/dist/attachments.d.ts
CHANGED
|
@@ -11,20 +11,21 @@ declare global {
|
|
|
11
11
|
}
|
|
12
12
|
export interface DraggableOptions {
|
|
13
13
|
handle_selector?: string;
|
|
14
|
+
disabled?: boolean;
|
|
14
15
|
on_drag_start?: (event: MouseEvent) => void;
|
|
15
16
|
on_drag?: (event: MouseEvent) => void;
|
|
16
17
|
on_drag_end?: (event: MouseEvent) => void;
|
|
17
18
|
}
|
|
18
19
|
export declare const draggable: (options?: DraggableOptions) => Attachment;
|
|
19
20
|
export declare function get_html_sort_value(element: HTMLElement): string;
|
|
20
|
-
export
|
|
21
|
-
header_selector?: string
|
|
22
|
-
asc_class?: string
|
|
23
|
-
desc_class?: string
|
|
24
|
-
sorted_style?:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
export interface SortableOptions {
|
|
22
|
+
header_selector?: string;
|
|
23
|
+
asc_class?: string;
|
|
24
|
+
desc_class?: string;
|
|
25
|
+
sorted_style?: Partial<CSSStyleDeclaration>;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare const sortable: (options?: SortableOptions) => (node: HTMLElement) => (() => void) | undefined;
|
|
28
29
|
export type HighlightOptions = {
|
|
29
30
|
query?: string;
|
|
30
31
|
disabled?: boolean;
|
|
@@ -33,6 +34,13 @@ export type HighlightOptions = {
|
|
|
33
34
|
css_class?: string;
|
|
34
35
|
};
|
|
35
36
|
export declare const highlight_matches: (ops: HighlightOptions) => (node: HTMLElement) => (() => boolean) | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Options for the tooltip attachment.
|
|
39
|
+
*
|
|
40
|
+
* @security Tooltip content is rendered as HTML. If you allow user-provided content
|
|
41
|
+
* to be set via `title`, `aria-label`, or `data-title` attributes, you MUST sanitize
|
|
42
|
+
* it first to prevent XSS attacks. This attachment does not perform any sanitization.
|
|
43
|
+
*/
|
|
36
44
|
export interface TooltipOptions {
|
|
37
45
|
content?: string;
|
|
38
46
|
placement?: `top` | `bottom` | `left` | `right`;
|
|
@@ -46,4 +54,4 @@ export type ClickOutsideConfig<T extends HTMLElement> = {
|
|
|
46
54
|
exclude?: string[];
|
|
47
55
|
callback?: (node: T, config: ClickOutsideConfig<T>) => void;
|
|
48
56
|
};
|
|
49
|
-
export declare const click_outside: <T extends HTMLElement>(config?: ClickOutsideConfig<T>) => (node: T) => () => void;
|
|
57
|
+
export declare const click_outside: <T extends HTMLElement>(config?: ClickOutsideConfig<T>) => (node: T) => (() => void) | undefined;
|
package/dist/attachments.js
CHANGED
|
@@ -3,6 +3,8 @@ import {} from 'svelte/attachments';
|
|
|
3
3
|
// @param options - Configuration options for dragging behavior
|
|
4
4
|
// @returns Attachment function that sets up dragging on an element
|
|
5
5
|
export const draggable = (options = {}) => (element) => {
|
|
6
|
+
if (options.disabled)
|
|
7
|
+
return;
|
|
6
8
|
const node = element;
|
|
7
9
|
// Use simple variables for maximum performance
|
|
8
10
|
let dragging = false;
|
|
@@ -92,23 +94,33 @@ export function get_html_sort_value(element) {
|
|
|
92
94
|
}
|
|
93
95
|
return element.textContent ?? ``;
|
|
94
96
|
}
|
|
95
|
-
export const sortable = (
|
|
96
|
-
|
|
97
|
+
export const sortable = (options = {}) => (node) => {
|
|
98
|
+
const { header_selector = `thead th`, asc_class = `table-sort-asc`, desc_class = `table-sort-desc`, sorted_style = { backgroundColor: `rgba(255, 255, 255, 0.1)` }, disabled = false, } = options;
|
|
99
|
+
if (disabled)
|
|
100
|
+
return;
|
|
101
|
+
// This action can be applied to standard HTML tables to make them sortable by
|
|
97
102
|
// clicking on column headers (and clicking again to toggle sorting direction)
|
|
98
103
|
const headers = Array.from(node.querySelectorAll(header_selector));
|
|
99
104
|
let sort_col_idx;
|
|
100
105
|
let sort_dir = 1; // 1 = asc, -1 = desc
|
|
101
|
-
// Store
|
|
102
|
-
const
|
|
106
|
+
// Store original state for cleanup
|
|
107
|
+
const header_state = [];
|
|
103
108
|
for (const [idx, header] of headers.entries()) {
|
|
109
|
+
const original_text = header.textContent ?? ``;
|
|
110
|
+
const original_style = header.getAttribute(`style`) ?? ``;
|
|
104
111
|
header.style.cursor = `pointer`; // add cursor pointer to headers
|
|
105
|
-
const init_styles = header.getAttribute(`style`) ?? ``;
|
|
106
112
|
const click_handler = () => {
|
|
107
|
-
// reset all headers to
|
|
108
|
-
for (const header of
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
// reset all headers to unsorted state
|
|
114
|
+
for (const { header: hdr, original_text, original_style } of header_state) {
|
|
115
|
+
hdr.textContent = original_text;
|
|
116
|
+
hdr.classList.remove(asc_class, desc_class);
|
|
117
|
+
if (original_style) {
|
|
118
|
+
hdr.setAttribute(`style`, original_style);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
hdr.removeAttribute(`style`);
|
|
122
|
+
}
|
|
123
|
+
hdr.style.cursor = `pointer`;
|
|
112
124
|
}
|
|
113
125
|
if (idx === sort_col_idx) {
|
|
114
126
|
sort_dir *= -1; // reverse sort direction
|
|
@@ -147,13 +159,20 @@ export const sortable = ({ header_selector = `thead th`, asc_class = `table-sort
|
|
|
147
159
|
table_body.appendChild(row);
|
|
148
160
|
};
|
|
149
161
|
header.addEventListener(`click`, click_handler);
|
|
150
|
-
|
|
162
|
+
header_state.push({ header, handler: click_handler, original_text, original_style });
|
|
151
163
|
}
|
|
152
|
-
// Return cleanup function
|
|
164
|
+
// Return cleanup function that fully restores original state
|
|
153
165
|
return () => {
|
|
154
|
-
for (const { header, handler } of
|
|
166
|
+
for (const { header, handler, original_text, original_style } of header_state) {
|
|
155
167
|
header.removeEventListener(`click`, handler);
|
|
156
|
-
header.
|
|
168
|
+
header.textContent = original_text;
|
|
169
|
+
header.classList.remove(asc_class, desc_class);
|
|
170
|
+
if (original_style) {
|
|
171
|
+
header.setAttribute(`style`, original_style);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
header.removeAttribute(`style`);
|
|
175
|
+
}
|
|
157
176
|
}
|
|
158
177
|
};
|
|
159
178
|
};
|
|
@@ -255,7 +274,8 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
255
274
|
function setup_tooltip(element) {
|
|
256
275
|
if (!element || safe_options.disabled)
|
|
257
276
|
return;
|
|
258
|
-
|
|
277
|
+
// Use let so content can be updated reactively
|
|
278
|
+
let content = safe_options.content || element.title ||
|
|
259
279
|
element.getAttribute(`aria-label`) || element.getAttribute(`data-title`);
|
|
260
280
|
if (!content)
|
|
261
281
|
return;
|
|
@@ -265,6 +285,36 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
265
285
|
element.setAttribute(`data-original-title`, element.title);
|
|
266
286
|
element.removeAttribute(`title`);
|
|
267
287
|
}
|
|
288
|
+
// Reactively update content when tooltip attributes change
|
|
289
|
+
const tooltip_attrs = [`title`, `aria-label`, `data-title`];
|
|
290
|
+
const observer = new MutationObserver((mutations) => {
|
|
291
|
+
if (safe_options.content)
|
|
292
|
+
return; // custom content takes precedence
|
|
293
|
+
for (const { type, attributeName } of mutations) {
|
|
294
|
+
if (type !== `attributes` || !attributeName)
|
|
295
|
+
continue;
|
|
296
|
+
const new_content = element.getAttribute(attributeName);
|
|
297
|
+
// null = attribute removed (by us), skip entirely
|
|
298
|
+
if (new_content === null)
|
|
299
|
+
continue;
|
|
300
|
+
// Always remove title to prevent browser's native tooltip (even if empty)
|
|
301
|
+
// Disconnect observer temporarily to avoid re-entrancy from our own removal
|
|
302
|
+
if (attributeName === `title`) {
|
|
303
|
+
observer.disconnect();
|
|
304
|
+
element.removeAttribute(`title`);
|
|
305
|
+
observer.observe(element, { attributes: true, attributeFilter: tooltip_attrs });
|
|
306
|
+
}
|
|
307
|
+
// Only update content if non-empty
|
|
308
|
+
if (!new_content)
|
|
309
|
+
continue;
|
|
310
|
+
content = new_content;
|
|
311
|
+
// Only update tooltip if this element owns it
|
|
312
|
+
if (current_tooltip?._owner === element) {
|
|
313
|
+
current_tooltip.innerHTML = content.replace(/\r/g, `<br/>`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
observer.observe(element, { attributes: true, attributeFilter: tooltip_attrs });
|
|
268
318
|
function show_tooltip() {
|
|
269
319
|
clear_tooltip();
|
|
270
320
|
show_timeout = setTimeout(() => {
|
|
@@ -277,7 +327,7 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
277
327
|
position: absolute; z-index: 9999; opacity: 0;
|
|
278
328
|
background: var(--tooltip-bg, #333); color: var(--text-color, white); border: var(--tooltip-border, none);
|
|
279
329
|
padding: var(--tooltip-padding, 6px 10px); border-radius: var(--tooltip-radius, 6px); font-size: var(--tooltip-font-size, 13px); line-height: 1.4;
|
|
280
|
-
max-width: var(--tooltip-max-width, 280px); word-wrap: break-word; pointer-events: none;
|
|
330
|
+
max-width: var(--tooltip-max-width, 280px); word-wrap: break-word; text-wrap: balance; pointer-events: none;
|
|
281
331
|
filter: var(--tooltip-shadow, drop-shadow(0 2px 8px rgba(0,0,0,0.25))); transition: opacity 0.15s ease-out;
|
|
282
332
|
`;
|
|
283
333
|
// Apply custom styles if provided (these will override base styles due to CSS specificity)
|
|
@@ -422,7 +472,7 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
422
472
|
tooltip.style.top = `${top + globalThis.scrollY}px`;
|
|
423
473
|
const custom_opacity = trigger_styles.getPropertyValue(`--tooltip-opacity`).trim();
|
|
424
474
|
tooltip.style.opacity = custom_opacity || `1`;
|
|
425
|
-
current_tooltip = tooltip;
|
|
475
|
+
current_tooltip = Object.assign(tooltip, { _owner: element });
|
|
426
476
|
}, safe_options.delay || 100);
|
|
427
477
|
}
|
|
428
478
|
function hide_tooltip() {
|
|
@@ -435,14 +485,22 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
435
485
|
}
|
|
436
486
|
}
|
|
437
487
|
}
|
|
488
|
+
function handle_scroll(event) {
|
|
489
|
+
// Hide if document or any ancestor scrolls (would move element). Skip internal element scrolls.
|
|
490
|
+
const target = event.target;
|
|
491
|
+
if (target instanceof Node && target !== element && target.contains(element)) {
|
|
492
|
+
hide_tooltip();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
438
495
|
const events = [`mouseenter`, `mouseleave`, `focus`, `blur`];
|
|
439
496
|
const handlers = [show_tooltip, hide_tooltip, show_tooltip, hide_tooltip];
|
|
440
497
|
events.forEach((event, idx) => element.addEventListener(event, handlers[idx]));
|
|
441
|
-
// Hide tooltip when user scrolls
|
|
442
|
-
globalThis.addEventListener(`scroll`,
|
|
498
|
+
// Hide tooltip when user scrolls the page (not element-level scrolls like input fields)
|
|
499
|
+
globalThis.addEventListener(`scroll`, handle_scroll, true);
|
|
443
500
|
return () => {
|
|
501
|
+
observer.disconnect();
|
|
444
502
|
events.forEach((event, idx) => element.removeEventListener(event, handlers[idx]));
|
|
445
|
-
globalThis.removeEventListener(`scroll`,
|
|
503
|
+
globalThis.removeEventListener(`scroll`, handle_scroll, true);
|
|
446
504
|
const original_title = element.getAttribute(`data-original-title`);
|
|
447
505
|
if (original_title) {
|
|
448
506
|
element.setAttribute(`title`, original_title);
|
|
@@ -468,9 +526,9 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
468
526
|
};
|
|
469
527
|
export const click_outside = (config = {}) => (node) => {
|
|
470
528
|
const { callback, enabled = true, exclude = [] } = config;
|
|
529
|
+
if (!enabled)
|
|
530
|
+
return; // Early return avoids registering unused listener
|
|
471
531
|
function handle_click(event) {
|
|
472
|
-
if (!enabled)
|
|
473
|
-
return;
|
|
474
532
|
const target = event.target;
|
|
475
533
|
const path = event.composedPath();
|
|
476
534
|
// Check if click target is the node or inside it
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { default as FileDetails } from './FileDetails.svelte';
|
|
|
7
7
|
export { default as GitHubCorner } from './GitHubCorner.svelte';
|
|
8
8
|
export { default as Icon } from './Icon.svelte';
|
|
9
9
|
export { default, default as MultiSelect } from './MultiSelect.svelte';
|
|
10
|
+
export { default as Nav } from './Nav.svelte';
|
|
10
11
|
export { default as PrevNext } from './PrevNext.svelte';
|
|
11
12
|
export { default as Toggle } from './Toggle.svelte';
|
|
12
13
|
export * from './types';
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { default as FileDetails } from './FileDetails.svelte';
|
|
|
7
7
|
export { default as GitHubCorner } from './GitHubCorner.svelte';
|
|
8
8
|
export { default as Icon } from './Icon.svelte';
|
|
9
9
|
export { default, default as MultiSelect } from './MultiSelect.svelte';
|
|
10
|
+
export { default as Nav } from './Nav.svelte';
|
|
10
11
|
export { default as PrevNext } from './PrevNext.svelte';
|
|
11
12
|
export { default as Toggle } from './Toggle.svelte';
|
|
12
13
|
export * from './types';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FlipParams } from 'svelte/animate';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
|
|
3
4
|
export type Option = string | number | ObjectOption;
|
|
@@ -16,6 +17,10 @@ export type ObjectOption = {
|
|
|
16
17
|
style?: OptionStyle;
|
|
17
18
|
[key: string]: unknown;
|
|
18
19
|
};
|
|
20
|
+
export type PlaceholderConfig = {
|
|
21
|
+
text: string;
|
|
22
|
+
persistent?: boolean;
|
|
23
|
+
};
|
|
19
24
|
export interface MultiSelectEvents<T extends Option = Option> {
|
|
20
25
|
onadd?: (data: {
|
|
21
26
|
option: T;
|
|
@@ -29,10 +34,13 @@ export interface MultiSelectEvents<T extends Option = Option> {
|
|
|
29
34
|
onremoveAll?: (data: {
|
|
30
35
|
options: T[];
|
|
31
36
|
}) => unknown;
|
|
37
|
+
onselectAll?: (data: {
|
|
38
|
+
options: T[];
|
|
39
|
+
}) => unknown;
|
|
32
40
|
onchange?: (data: {
|
|
33
41
|
option?: T;
|
|
34
42
|
options?: T[];
|
|
35
|
-
type: `add` | `remove` | `removeAll`;
|
|
43
|
+
type: `add` | `remove` | `removeAll` | `selectAll`;
|
|
36
44
|
}) => unknown;
|
|
37
45
|
onopen?: (data: {
|
|
38
46
|
event: Event;
|
|
@@ -41,6 +49,23 @@ export interface MultiSelectEvents<T extends Option = Option> {
|
|
|
41
49
|
event: Event;
|
|
42
50
|
}) => unknown;
|
|
43
51
|
}
|
|
52
|
+
export interface LoadOptionsParams {
|
|
53
|
+
search: string;
|
|
54
|
+
offset: number;
|
|
55
|
+
limit: number;
|
|
56
|
+
}
|
|
57
|
+
export interface LoadOptionsResult<T extends Option = Option> {
|
|
58
|
+
options: T[];
|
|
59
|
+
hasMore: boolean;
|
|
60
|
+
}
|
|
61
|
+
export type LoadOptionsFn<T extends Option = Option> = (params: LoadOptionsParams) => Promise<LoadOptionsResult<T>>;
|
|
62
|
+
export interface LoadOptionsConfig<T extends Option = Option> {
|
|
63
|
+
fetch: LoadOptionsFn<T>;
|
|
64
|
+
debounceMs?: number;
|
|
65
|
+
batchSize?: number;
|
|
66
|
+
onOpen?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export type LoadOptions<T extends Option = Option> = LoadOptionsFn<T> | LoadOptionsConfig<T>;
|
|
44
69
|
type AfterInputProps = Pick<MultiSelectProps, `selected` | `disabled` | `invalid` | `id` | `placeholder` | `open` | `required`>;
|
|
45
70
|
type UserMsgProps = {
|
|
46
71
|
searchText: string;
|
|
@@ -73,7 +98,7 @@ export interface PortalParams {
|
|
|
73
98
|
target_node?: HTMLElement | null;
|
|
74
99
|
active?: boolean;
|
|
75
100
|
}
|
|
76
|
-
export interface MultiSelectProps<T extends Option = Option> extends MultiSelectEvents<T>, MultiSelectSnippets<T>, Omit<HTMLAttributes<HTMLDivElement>, `children` | `onchange` | `onclose`> {
|
|
101
|
+
export interface MultiSelectProps<T extends Option = Option> extends MultiSelectEvents<T>, MultiSelectSnippets<T>, Omit<HTMLAttributes<HTMLDivElement>, `children` | `onchange` | `onclose` | `placeholder`> {
|
|
77
102
|
activeIndex?: number | null;
|
|
78
103
|
activeOption?: T | null;
|
|
79
104
|
createOptionMsg?: string | null;
|
|
@@ -116,12 +141,12 @@ export interface MultiSelectProps<T extends Option = Option> extends MultiSelect
|
|
|
116
141
|
name?: string | null;
|
|
117
142
|
noMatchingOptionsMsg?: string;
|
|
118
143
|
open?: boolean;
|
|
119
|
-
options
|
|
144
|
+
options?: T[];
|
|
120
145
|
outerDiv?: HTMLDivElement | null;
|
|
121
146
|
outerDivClass?: string;
|
|
122
147
|
parseLabelsAsHtml?: boolean;
|
|
123
148
|
pattern?: string | null;
|
|
124
|
-
placeholder?: string | null;
|
|
149
|
+
placeholder?: string | PlaceholderConfig | null;
|
|
125
150
|
removeAllTitle?: string;
|
|
126
151
|
removeBtnTitle?: string;
|
|
127
152
|
minSelect?: number | null;
|
|
@@ -138,5 +163,9 @@ export interface MultiSelectProps<T extends Option = Option> extends MultiSelect
|
|
|
138
163
|
ulOptionsStyle?: string | null;
|
|
139
164
|
value?: T | T[] | null;
|
|
140
165
|
portal?: PortalParams;
|
|
166
|
+
selectAllOption?: boolean | string;
|
|
167
|
+
liSelectAllClass?: string;
|
|
168
|
+
loadOptions?: LoadOptions<T>;
|
|
169
|
+
selectedFlipParams?: FlipParams;
|
|
141
170
|
}
|
|
142
171
|
export {};
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Option } from './types';
|
|
2
|
+
export declare const is_object: (val: unknown) => val is Record<string, unknown>;
|
|
2
3
|
export declare const get_label: (opt: Option) => string | number;
|
|
3
4
|
export declare function get_style(option: Option, key?: `selected` | `option` | null | undefined): string;
|
|
4
5
|
export declare function fuzzy_match(search_text: string, target_text: string): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
// Type guard for checking if a value is a non-null object
|
|
2
|
+
export const is_object = (val) => typeof val === `object` && val !== null;
|
|
1
3
|
// Get the label key from an option object or the option itself
|
|
2
4
|
// if it's a string or number
|
|
3
5
|
export const get_label = (opt) => {
|
|
4
|
-
if (opt
|
|
6
|
+
if (is_object(opt)) {
|
|
5
7
|
if (opt.label === undefined) {
|
|
6
8
|
const opt_str = JSON.stringify(opt);
|
|
7
|
-
console.error(`MultiSelect option
|
|
9
|
+
console.error(`MultiSelect: option is an object but has no label key`, opt_str);
|
|
8
10
|
}
|
|
9
11
|
return opt.label;
|
|
10
12
|
}
|
|
@@ -27,7 +29,7 @@ export function get_style(option, key = null) {
|
|
|
27
29
|
if (key && key in option.style)
|
|
28
30
|
return option.style[key] ?? ``;
|
|
29
31
|
else {
|
|
30
|
-
console.error(`
|
|
32
|
+
console.error(`MultiSelect: invalid style object for option`, option);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
}
|
package/package.json
CHANGED
|
@@ -5,39 +5,46 @@
|
|
|
5
5
|
"homepage": "https://janosh.github.io/svelte-multiselect",
|
|
6
6
|
"repository": "https://github.com/janosh/svelte-multiselect",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"version": "11.
|
|
8
|
+
"version": "11.4.0",
|
|
9
9
|
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "vite dev",
|
|
12
|
+
"build": "vite build",
|
|
13
|
+
"preview": "vite preview",
|
|
14
|
+
"test": "vitest --run && playwright test",
|
|
15
|
+
"check": "svelte-check"
|
|
16
|
+
},
|
|
10
17
|
"svelte": "./dist/index.js",
|
|
11
18
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
12
19
|
"peerDependencies": {
|
|
13
20
|
"svelte": "^5.35.6"
|
|
14
21
|
},
|
|
15
22
|
"devDependencies": {
|
|
16
|
-
"@playwright/test": "^1.
|
|
17
|
-
"@stylistic/eslint-plugin": "^5.
|
|
23
|
+
"@playwright/test": "^1.57.0",
|
|
24
|
+
"@stylistic/eslint-plugin": "^5.6.1",
|
|
18
25
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
19
|
-
"@sveltejs/kit": "^2.
|
|
20
|
-
"@sveltejs/package": "2.5.
|
|
26
|
+
"@sveltejs/kit": "^2.49.1",
|
|
27
|
+
"@sveltejs/package": "2.5.7",
|
|
21
28
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
22
|
-
"@types/node": "^24.
|
|
23
|
-
"@vitest/coverage-v8": "^
|
|
24
|
-
"eslint": "^9.
|
|
25
|
-
"eslint-plugin-svelte": "^3.
|
|
26
|
-
"happy-dom": "^
|
|
29
|
+
"@types/node": "^24.10.1",
|
|
30
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
31
|
+
"eslint": "^9.39.1",
|
|
32
|
+
"eslint-plugin-svelte": "^3.13.1",
|
|
33
|
+
"happy-dom": "^20.0.11",
|
|
27
34
|
"hastscript": "^9.0.1",
|
|
28
35
|
"mdsvex": "^0.12.6",
|
|
29
36
|
"mdsvexamples": "^0.5.0",
|
|
30
37
|
"rehype-autolink-headings": "^7.1.0",
|
|
31
38
|
"rehype-slug": "^6.0.0",
|
|
32
|
-
"svelte": "^5.
|
|
33
|
-
"svelte-check": "^4.3.
|
|
39
|
+
"svelte": "^5.45.6",
|
|
40
|
+
"svelte-check": "^4.3.4",
|
|
34
41
|
"svelte-preprocess": "^6.0.3",
|
|
35
42
|
"svelte-toc": "^0.6.2",
|
|
36
|
-
"svelte2tsx": "^0.7.
|
|
43
|
+
"svelte2tsx": "^0.7.45",
|
|
37
44
|
"typescript": "5.9.3",
|
|
38
|
-
"typescript-eslint": "^8.
|
|
39
|
-
"vite": "^7.
|
|
40
|
-
"vitest": "^
|
|
45
|
+
"typescript-eslint": "^8.48.1",
|
|
46
|
+
"vite": "^7.2.7",
|
|
47
|
+
"vitest": "^4.0.15"
|
|
41
48
|
},
|
|
42
49
|
"keywords": [
|
|
43
50
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://github.com/janosh/svelte-multiselect/actions/workflows/gh-pages.yml)
|
|
10
10
|
[](https://npmjs.com/package/svelte-multiselect)
|
|
11
11
|
[](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md)
|
|
12
|
-
[](https://svelte.dev/playground/a5a14b8f15d64cb083b567292480db05)
|
|
13
13
|
[](https://stackblitz.com/github/janosh/svelte-multiselect)
|
|
14
14
|
|
|
15
15
|
</h4>
|
|
@@ -32,8 +32,6 @@
|
|
|
32
32
|
- **Single / multiple select:** pass `maxSelect={1, 2, 3, ...}` prop to restrict the number of selectable options
|
|
33
33
|
- **Configurable:** see props
|
|
34
34
|
|
|
35
|
-
<slot name="nav" />
|
|
36
|
-
|
|
37
35
|
## ๐งช   Coverage
|
|
38
36
|
|
|
39
37
|
| Statements | Branches | Lines |
|
|
@@ -174,10 +172,18 @@ These are the core props you'll use in most cases:
|
|
|
174
172
|
```
|
|
175
173
|
|
|
176
174
|
1. ```ts
|
|
177
|
-
placeholder: string | null = null
|
|
175
|
+
placeholder: string | { text: string; persistent?: boolean } | null = null
|
|
178
176
|
```
|
|
179
177
|
|
|
180
|
-
Text shown when no options are selected.
|
|
178
|
+
Text shown when no options are selected. Can be a simple string or an object with extended options:
|
|
179
|
+
|
|
180
|
+
```svelte
|
|
181
|
+
<!-- Simple string -->
|
|
182
|
+
<MultiSelect placeholder="Choose..." />
|
|
183
|
+
|
|
184
|
+
<!-- Object with persistent option (stays visible even when options selected) -->
|
|
185
|
+
<MultiSelect placeholder={{ text: 'Add items...', persistent: true }} />
|
|
186
|
+
```
|
|
181
187
|
|
|
182
188
|
1. ```ts
|
|
183
189
|
disabled: boolean = false
|
|
@@ -231,6 +237,41 @@ These are the core props you'll use in most cases:
|
|
|
231
237
|
|
|
232
238
|
### Advanced Props
|
|
233
239
|
|
|
240
|
+
1. ```ts
|
|
241
|
+
loadOptions: LoadOptionsFn | LoadOptionsConfig = undefined
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Dynamic loading for large datasets.** Enables lazy loading / infinite scroll instead of passing static `options`. Pass either a function or an object with config:
|
|
245
|
+
|
|
246
|
+
```svelte
|
|
247
|
+
<!-- Simple: just a function -->
|
|
248
|
+
<MultiSelect loadOptions={myFetchFn} />
|
|
249
|
+
|
|
250
|
+
<!-- With config -->
|
|
251
|
+
<MultiSelect loadOptions={{ fetch: myFetchFn, debounceMs: 500, batchSize: 20 }} />
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The function receives `{ search, offset, limit }` and must return `{ options, hasMore }`:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
async function load_options({ search, offset, limit }) {
|
|
258
|
+
const response = await fetch(`/api/items?q=${search}&skip=${offset}&take=${limit}`)
|
|
259
|
+
const { items, total } = await response.json()
|
|
260
|
+
return { options: items, hasMore: offset + limit < total }
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Config options (when passing an object):
|
|
265
|
+
|
|
266
|
+
| Key | Type | Default | Description |
|
|
267
|
+
| ------------ | --------- | ------- | ------------------------------------------- |
|
|
268
|
+
| `fetch` | `fn` | โ | Async function to load options (required) |
|
|
269
|
+
| `debounceMs` | `number` | `300` | Debounce delay for search queries |
|
|
270
|
+
| `batchSize` | `number` | `50` | Number of options to load per batch |
|
|
271
|
+
| `onOpen` | `boolean` | `true` | Whether to load options when dropdown opens |
|
|
272
|
+
|
|
273
|
+
Features automatic state management, debounced search, infinite scroll pagination, and loading indicators. See the [infinite-scroll demo](https://janosh.github.io/svelte-multiselect/infinite-scroll) for live examples.
|
|
274
|
+
|
|
234
275
|
1. ```ts
|
|
235
276
|
activeIndex: number | null = null // bindable
|
|
236
277
|
```
|
|
@@ -270,10 +311,10 @@ These are the core props you'll use in most cases:
|
|
|
270
311
|
Function to determine option equality. Default compares by lowercased label.
|
|
271
312
|
|
|
272
313
|
1. ```ts
|
|
273
|
-
closeDropdownOnSelect: boolean | 'if-mobile' | 'retain-focus' =
|
|
314
|
+
closeDropdownOnSelect: boolean | 'if-mobile' | 'retain-focus' = false
|
|
274
315
|
```
|
|
275
316
|
|
|
276
|
-
Whether to close dropdown after selection. `'if-mobile'` closes
|
|
317
|
+
Whether to close dropdown after selection. `false` (default) keeps dropdown open for rapid multi-selection. `true` closes after each selection. `'if-mobile'` closes on mobile devices only (screen width below `breakpoint`). `'retain-focus'` closes dropdown but keeps input focused for rapid typing to create custom options from text input (see `allowUserOptions`).
|
|
277
318
|
|
|
278
319
|
1. ```ts
|
|
279
320
|
resetFilterOnAdd: boolean = true
|
|
@@ -351,12 +392,36 @@ These are the core props you'll use in most cases:
|
|
|
351
392
|
|
|
352
393
|
Screen width (px) that separates 'mobile' from 'desktop' behavior.
|
|
353
394
|
|
|
395
|
+
1. ```ts
|
|
396
|
+
fuzzy: boolean = true
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Whether to use fuzzy matching for filtering options. When `true` (default), matches non-consecutive characters (e.g., "ga" matches "Grapes" and "Green Apple"). When `false`, uses substring matching only.
|
|
400
|
+
|
|
354
401
|
1. ```ts
|
|
355
402
|
highlightMatches: boolean = true
|
|
356
403
|
```
|
|
357
404
|
|
|
358
405
|
Whether to highlight matching text in dropdown options.
|
|
359
406
|
|
|
407
|
+
1. ```ts
|
|
408
|
+
keepSelectedInDropdown: false | 'plain' | 'checkboxes' = false
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Controls whether selected options remain visible in dropdown. `false` (default) hides selected options. `'plain'` shows them with visual distinction. `'checkboxes'` prefixes each option with a checkbox.
|
|
412
|
+
|
|
413
|
+
1. ```ts
|
|
414
|
+
selectAllOption: boolean | string = false
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Adds a "Select All" option at the top of the dropdown. `true` shows default label, or pass a custom string label.
|
|
418
|
+
|
|
419
|
+
1. ```ts
|
|
420
|
+
liSelectAllClass: string = ''
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
CSS class applied to the "Select All" `<li>` element.
|
|
424
|
+
|
|
360
425
|
1. ```ts
|
|
361
426
|
parseLabelsAsHtml: boolean = false
|
|
362
427
|
```
|
|
@@ -369,6 +434,12 @@ These are the core props you'll use in most cases:
|
|
|
369
434
|
|
|
370
435
|
Whether selected options can be reordered by dragging.
|
|
371
436
|
|
|
437
|
+
1. ```ts
|
|
438
|
+
selectedFlipParams: FlipParams = { duration: 100 }
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Animation parameters for the [Svelte flip animation](https://svelte.dev/docs/svelte/svelte-animate) when reordering selected options via drag-and-drop. Set `{ duration: 0 }` to disable animation. Accepts `duration`, `delay`, and `easing` properties.
|
|
442
|
+
|
|
372
443
|
### Message Props
|
|
373
444
|
|
|
374
445
|
1. ```ts
|
|
@@ -684,7 +755,12 @@ You can also import [the types this component uses](https://github.com/janosh/sv
|
|
|
684
755
|
|
|
685
756
|
```ts
|
|
686
757
|
import {
|
|
687
|
-
|
|
758
|
+
LoadOptions, // Dynamic option loading callback
|
|
759
|
+
LoadOptionsConfig,
|
|
760
|
+
LoadOptionsFn,
|
|
761
|
+
LoadOptionsParams,
|
|
762
|
+
LoadOptionsResult,
|
|
763
|
+
MultiSelectEvents,
|
|
688
764
|
MultiSelectEvents,
|
|
689
765
|
ObjectOption,
|
|
690
766
|
Option,
|