svelte-multiselect 11.4.0 → 11.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import type { MultiSelectProps } from './types';
2
2
  declare function $$render<Option extends import('./types').Option>(): {
3
3
  props: MultiSelectProps<Option>;
4
4
  exports: {};
5
- bindings: "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText";
5
+ bindings: "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText" | "collapsedGroups" | "collapseAllGroups" | "expandAllGroups";
6
6
  slots: {};
7
7
  events: {};
8
8
  };
@@ -10,7 +10,7 @@ declare class __sveltets_Render<Option extends import('./types').Option> {
10
10
  props(): ReturnType<typeof $$render<Option>>['props'];
11
11
  events(): ReturnType<typeof $$render<Option>>['events'];
12
12
  slots(): ReturnType<typeof $$render<Option>>['slots'];
13
- bindings(): "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText";
13
+ bindings(): "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText" | "collapsedGroups" | "collapseAllGroups" | "expandAllGroups";
14
14
  exports(): {};
15
15
  }
16
16
  interface $$IsomorphicComponent {
package/dist/Nav.svelte CHANGED
@@ -240,7 +240,7 @@ function parse_route(route) {
240
240
  position: relative;
241
241
  margin: -0.75em auto 1.25em;
242
242
  --nav-border-radius: 6pt;
243
- --nav-surface-bg: light-dark(#fff, #1a1a1a);
243
+ --nav-surface-bg: light-dark(#fafafa, #1a1a1a);
244
244
  --nav-surface-border: light-dark(rgba(128, 128, 128, 0.25), rgba(200, 200, 200, 0.2));
245
245
  --nav-surface-shadow: light-dark(
246
246
  0 2px 8px rgba(0, 0, 0, 0.15),
@@ -399,7 +399,7 @@ function parse_route(route) {
399
399
  z-index: var(--nav-mobile-z-index, 2);
400
400
  flex-direction: column;
401
401
  align-items: stretch;
402
- justify-content: flex-start;
402
+ justify-content: start;
403
403
  gap: 0.2em;
404
404
  max-width: 90vw;
405
405
  border-radius: 6px;
@@ -56,7 +56,9 @@ export {};
56
56
  {:else}
57
57
  <div>
58
58
  {#if titles.prev}<span>{@html titles.prev}</span>{/if}
59
- <a {...link_props} href={prev[0]}>{prev[0]}</a>
59
+ <a data-sveltekit-preload-data="hover" {...link_props} href={prev[0]}>{
60
+ prev[0]
61
+ }</a>
60
62
  </div>
61
63
  {/if}
62
64
  {/if}
@@ -69,7 +71,9 @@ export {};
69
71
  {:else}
70
72
  <div>
71
73
  {#if titles.next}<span>{@html titles.next}</span>{/if}
72
- <a {...link_props} href={next[0]}>{next[0]}</a>
74
+ <a data-sveltekit-preload-data="hover" {...link_props} href={next[0]}>{
75
+ next[0]
76
+ }</a>
73
77
  </div>
74
78
  {/if}
75
79
  {/if}
@@ -34,12 +34,12 @@ export {};
34
34
  height: var(--toggle-knob-height, 1.5em);
35
35
  width: var(--toggle-knob-width, 3em);
36
36
  padding: var(--toggle-knob-padding, 0.1em);
37
- border: var(--toggle-knob-border, 1px solid lightgray);
37
+ border: var(--toggle-knob-border, 1px solid light-dark(lightgray, #555));
38
38
  border-radius: var(--toggle-knob-border-radius, 0.75em);
39
39
  transition: var(--toggle-knob-transition, 0.3s);
40
40
  }
41
41
  input:checked + span {
42
- background: var(--toggle-background, black);
42
+ background: var(--toggle-background, light-dark(#222, #555));
43
43
  }
44
44
  input {
45
45
  position: absolute;
@@ -52,11 +52,11 @@ export {};
52
52
  height: var(--toggle-knob-after-height, 1.2em);
53
53
  width: var(--toggle-knob-after-width, 1.2em);
54
54
  border-radius: var(--toggle-knob-after-border-radius, 50%);
55
- background: var(--toggle-knob-after-background, gray);
55
+ background: var(--toggle-knob-after-background, light-dark(gray, #888));
56
56
  transition: var(--toggle-knob-after-transition, 0.3s);
57
57
  }
58
58
  input:checked + span::after {
59
- background: var(--toggle-knob-after-background, green);
59
+ background: var(--toggle-knob-after-background, light-dark(green, #4ade80));
60
60
  transform: var(
61
61
  --toggle-knob-after-transform,
62
62
  translate(
@@ -69,6 +69,9 @@ export {};
69
69
  );
70
70
  }
71
71
  input:focus + span {
72
- border: var(--toggle-knob-focus-border, 1px solid cornflowerblue);
72
+ border: var(
73
+ --toggle-knob-focus-border,
74
+ 1px solid light-dark(cornflowerblue, #6495ed)
75
+ );
73
76
  }
74
77
  </style>
@@ -16,7 +16,25 @@ export interface DraggableOptions {
16
16
  on_drag?: (event: MouseEvent) => void;
17
17
  on_drag_end?: (event: MouseEvent) => void;
18
18
  }
19
+ export type Dimensions = {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ export type ResizeCallback = (event: MouseEvent, dimensions: Dimensions) => void;
24
+ export interface ResizableOptions {
25
+ edges?: (`top` | `right` | `bottom` | `left`)[];
26
+ min_width?: number;
27
+ min_height?: number;
28
+ max_width?: number;
29
+ max_height?: number;
30
+ handle_size?: number;
31
+ disabled?: boolean;
32
+ on_resize_start?: ResizeCallback;
33
+ on_resize?: ResizeCallback;
34
+ on_resize_end?: ResizeCallback;
35
+ }
19
36
  export declare const draggable: (options?: DraggableOptions) => Attachment;
37
+ export declare const resizable: (options?: ResizableOptions) => Attachment;
20
38
  export declare function get_html_sort_value(element: HTMLElement): string;
21
39
  export interface SortableOptions {
22
40
  header_selector?: string;
@@ -34,13 +52,6 @@ export type HighlightOptions = {
34
52
  css_class?: string;
35
53
  };
36
54
  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
- */
44
55
  export interface TooltipOptions {
45
56
  content?: string;
46
57
  placement?: `top` | `bottom` | `left` | `right`;
@@ -83,6 +83,104 @@ export const draggable = (options = {}) => (element) => {
83
83
  }
84
84
  };
85
85
  };
86
+ // Automatically sets `position: relative` on elements with `position: static`
87
+ // to enable proper positioning during resize. This may affect existing layouts.
88
+ export const resizable = (options = {}) => (element) => {
89
+ if (options.disabled)
90
+ return;
91
+ const node = element;
92
+ const { edges = [`right`, `bottom`], min_width = 50, min_height = 50, max_width = Infinity, max_height = Infinity, handle_size = 8, on_resize_start, on_resize, on_resize_end, } = options;
93
+ if (min_width > max_width || min_height > max_height) {
94
+ console.warn(`resizable: min dimensions exceed max dimensions (min_width=${min_width}, max_width=${max_width}, min_height=${min_height}, max_height=${max_height})`);
95
+ return; // Invalid config would cause clamp() to produce inconsistent results
96
+ }
97
+ let active_edge = null;
98
+ let start = { x: 0, y: 0 };
99
+ let initial = { width: 0, height: 0, left: 0, top: 0 };
100
+ const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
101
+ if (getComputedStyle(node).position === `static`)
102
+ node.style.position = `relative`;
103
+ const get_edge = ({ clientX: cx, clientY: cy }) => {
104
+ const { left, right, top, bottom } = node.getBoundingClientRect();
105
+ if (edges.includes(`right`) && cx >= right - handle_size && cx <= right) {
106
+ return `right`;
107
+ }
108
+ if (edges.includes(`bottom`) && cy >= bottom - handle_size && cy <= bottom) {
109
+ return `bottom`;
110
+ }
111
+ if (edges.includes(`left`) && cx >= left && cx <= left + handle_size)
112
+ return `left`;
113
+ if (edges.includes(`top`) && cy >= top && cy <= top + handle_size)
114
+ return `top`;
115
+ return null;
116
+ };
117
+ function on_mousedown(event) {
118
+ active_edge = get_edge(event);
119
+ if (!active_edge)
120
+ return;
121
+ start = { x: event.clientX, y: event.clientY };
122
+ initial = {
123
+ width: node.offsetWidth,
124
+ height: node.offsetHeight,
125
+ left: node.offsetLeft,
126
+ top: node.offsetTop,
127
+ };
128
+ document.body.style.userSelect = `none`;
129
+ on_resize_start?.(event, { width: initial.width, height: initial.height });
130
+ globalThis.addEventListener(`mousemove`, on_mousemove);
131
+ globalThis.addEventListener(`mouseup`, on_mouseup);
132
+ }
133
+ function on_mousemove(event) {
134
+ if (!active_edge)
135
+ return;
136
+ const dx = event.clientX - start.x, dy = event.clientY - start.y;
137
+ let { width, height } = initial;
138
+ if (active_edge === `right`)
139
+ width = clamp(initial.width + dx, min_width, max_width);
140
+ else if (active_edge === `left`) {
141
+ const clamped = clamp(initial.width - dx, min_width, max_width);
142
+ node.style.left = `${initial.left - (clamped - initial.width)}px`;
143
+ width = clamped;
144
+ }
145
+ if (active_edge === `bottom`) {
146
+ height = clamp(initial.height + dy, min_height, max_height);
147
+ }
148
+ else if (active_edge === `top`) {
149
+ const clamped = clamp(initial.height - dy, min_height, max_height);
150
+ node.style.top = `${initial.top - (clamped - initial.height)}px`;
151
+ height = clamped;
152
+ }
153
+ node.style.width = `${width}px`;
154
+ node.style.height = `${height}px`;
155
+ on_resize?.(event, { width, height });
156
+ }
157
+ function on_mouseup(event) {
158
+ if (!active_edge)
159
+ return;
160
+ document.body.style.userSelect = ``;
161
+ on_resize_end?.(event, { width: node.offsetWidth, height: node.offsetHeight });
162
+ globalThis.removeEventListener(`mousemove`, on_mousemove);
163
+ globalThis.removeEventListener(`mouseup`, on_mouseup);
164
+ active_edge = null;
165
+ }
166
+ function on_hover(event) {
167
+ const edge = get_edge(event);
168
+ node.style.cursor = edge === `right` || edge === `left`
169
+ ? `ew-resize`
170
+ : edge === `top` || edge === `bottom`
171
+ ? `ns-resize`
172
+ : ``;
173
+ }
174
+ node.addEventListener(`mousedown`, on_mousedown);
175
+ node.addEventListener(`mousemove`, on_hover);
176
+ return () => {
177
+ node.removeEventListener(`mousedown`, on_mousedown);
178
+ node.removeEventListener(`mousemove`, on_hover);
179
+ globalThis.removeEventListener(`mousemove`, on_mousemove);
180
+ globalThis.removeEventListener(`mouseup`, on_mouseup);
181
+ node.style.cursor = ``;
182
+ };
183
+ };
86
184
  export function get_html_sort_value(element) {
87
185
  if (element.dataset.sortValue !== undefined) {
88
186
  return element.dataset.sortValue;
package/dist/icons.d.ts CHANGED
@@ -11,6 +11,14 @@ export declare const icon_data: {
11
11
  readonly viewBox: "0 0 16 16";
12
12
  readonly path: "M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z";
13
13
  };
14
+ readonly ChevronRight: {
15
+ readonly viewBox: "0 0 16 16";
16
+ readonly path: "M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8L4.646 2.354a.5.5 0 0 1 0-.708z";
17
+ };
18
+ readonly ChevronDown: {
19
+ readonly viewBox: "0 0 16 16";
20
+ readonly path: "M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z";
21
+ };
14
22
  readonly Collapse: {
15
23
  readonly viewBox: "0 0 24 24";
16
24
  readonly path: "M12 7.59L7.05 2.64L5.64 4.05L12 10.41l6.36-6.36l-1.41-1.41L12 7.59zM5.64 19.95l1.41 1.41L12 16.41l4.95 4.95l1.41-1.41L12 13.59l-6.36 6.36z";
package/dist/icons.js CHANGED
@@ -11,6 +11,14 @@ export const icon_data = {
11
11
  viewBox: `0 0 16 16`,
12
12
  path: `M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z`,
13
13
  },
14
+ ChevronRight: {
15
+ viewBox: `0 0 16 16`,
16
+ path: `M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8L4.646 2.354a.5.5 0 0 1 0-.708z`,
17
+ },
18
+ ChevronDown: {
19
+ viewBox: `0 0 16 16`,
20
+ path: `M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z`,
21
+ },
14
22
  Collapse: {
15
23
  viewBox: `0 0 24 24`,
16
24
  path: `M12 7.59L7.05 2.64L5.64 4.05L12 10.41l6.36-6.36l-1.41-1.41L12 7.59zM5.64 19.95l1.41 1.41L12 16.41l4.95 4.95l1.41-1.41L12 13.59l-6.36 6.36z`,
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { FlipParams } from 'svelte/animate';
2
1
  import type { Snippet } from 'svelte';
2
+ import type { FlipParams } from 'svelte/animate';
3
3
  import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
4
4
  export type Option = string | number | ObjectOption;
5
5
  export type OptionStyle = string | {
@@ -15,6 +15,7 @@ export type ObjectOption = {
15
15
  disabledTitle?: string;
16
16
  selectedTitle?: string;
17
17
  style?: OptionStyle;
18
+ group?: string;
18
19
  [key: string]: unknown;
19
20
  };
20
21
  export type PlaceholderConfig = {
@@ -37,10 +38,13 @@ export interface MultiSelectEvents<T extends Option = Option> {
37
38
  onselectAll?: (data: {
38
39
  options: T[];
39
40
  }) => unknown;
41
+ onreorder?: (data: {
42
+ options: T[];
43
+ }) => unknown;
40
44
  onchange?: (data: {
41
45
  option?: T;
42
46
  options?: T[];
43
- type: `add` | `remove` | `removeAll` | `selectAll`;
47
+ type: `add` | `remove` | `removeAll` | `selectAll` | `reorder`;
44
48
  }) => unknown;
45
49
  onopen?: (data: {
46
50
  event: Event;
@@ -48,6 +52,16 @@ export interface MultiSelectEvents<T extends Option = Option> {
48
52
  onclose?: (data: {
49
53
  event: Event;
50
54
  }) => unknown;
55
+ ongroupToggle?: (data: {
56
+ group: string;
57
+ collapsed: boolean;
58
+ }) => unknown;
59
+ oncollapseAll?: (data: {
60
+ groups: string[];
61
+ }) => unknown;
62
+ onexpandAll?: (data: {
63
+ groups: string[];
64
+ }) => unknown;
51
65
  }
52
66
  export interface LoadOptionsParams {
53
67
  search: string;
@@ -72,6 +86,16 @@ type UserMsgProps = {
72
86
  msgType: false | `dupe` | `create` | `no-match`;
73
87
  msg: null | string;
74
88
  };
89
+ export type GroupHeaderProps<T extends Option = Option> = {
90
+ group: string;
91
+ options: T[];
92
+ collapsed: boolean;
93
+ };
94
+ export type GroupedOptions<T extends Option = Option> = {
95
+ group: string | null;
96
+ options: T[];
97
+ collapsed: boolean;
98
+ };
75
99
  export interface MultiSelectSnippets<T extends Option = Option> {
76
100
  expandIcon?: Snippet<[{
77
101
  open: boolean;
@@ -93,6 +117,7 @@ export interface MultiSelectSnippets<T extends Option = Option> {
93
117
  idx: number;
94
118
  }]>;
95
119
  userMsg?: Snippet<[UserMsgProps]>;
120
+ groupHeader?: Snippet<[GroupHeaderProps<T>]>;
96
121
  }
97
122
  export interface PortalParams {
98
123
  target_node?: HTMLElement | null;
@@ -167,5 +192,25 @@ export interface MultiSelectProps<T extends Option = Option> extends MultiSelect
167
192
  liSelectAllClass?: string;
168
193
  loadOptions?: LoadOptions<T>;
169
194
  selectedFlipParams?: FlipParams;
195
+ collapsibleGroups?: boolean;
196
+ collapsedGroups?: Set<string>;
197
+ groupSelectAll?: boolean;
198
+ ungroupedPosition?: `first` | `last`;
199
+ groupSortOrder?: `none` | `asc` | `desc` | ((a: string, b: string) => number);
200
+ searchExpandsCollapsedGroups?: boolean;
201
+ searchMatchesGroups?: boolean;
202
+ keyboardExpandsCollapsedGroups?: boolean;
203
+ stickyGroupHeaders?: boolean;
204
+ liGroupHeaderClass?: string;
205
+ liGroupHeaderStyle?: string | null;
206
+ collapseAllGroups?: () => void;
207
+ expandAllGroups?: () => void;
208
+ shortcuts?: Partial<KeyboardShortcuts>;
209
+ }
210
+ export interface KeyboardShortcuts {
211
+ select_all?: string | null;
212
+ clear_all?: string | null;
213
+ open?: string | null;
214
+ close?: string | null;
170
215
  }
171
216
  export {};
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { Option } from './types';
2
2
  export declare const is_object: (val: unknown) => val is Record<string, unknown>;
3
+ export declare const has_group: <T extends Option>(opt: T) => opt is T & {
4
+ group: string;
5
+ };
3
6
  export declare const get_label: (opt: Option) => string | number;
4
7
  export declare function get_style(option: Option, key?: `selected` | `option` | null | undefined): string;
5
8
  export declare function fuzzy_match(search_text: string, target_text: string): boolean;
package/dist/utils.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // Type guard for checking if a value is a non-null object
2
2
  export const is_object = (val) => typeof val === `object` && val !== null;
3
+ // Type guard for checking if an option has a group key
4
+ export const has_group = (opt) => is_object(opt) && typeof opt.group === `string`;
3
5
  // Get the label key from an option object or the option itself
4
6
  // if it's a string or number
5
7
  export const get_label = (opt) => {
package/package.json CHANGED
@@ -5,14 +5,16 @@
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.4.0",
8
+ "version": "11.5.1",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "dev": "vite dev",
12
12
  "build": "vite build",
13
13
  "preview": "vite preview",
14
14
  "test": "vitest --run && playwright test",
15
- "check": "svelte-check"
15
+ "check": "svelte-check",
16
+ "package": "svelte-package",
17
+ "prepublishOnly": "svelte-package && test -d dist"
16
18
  },
17
19
  "svelte": "./dist/index.js",
18
20
  "bugs": "https://github.com/janosh/svelte-multiselect/issues",
@@ -23,12 +25,12 @@
23
25
  "@playwright/test": "^1.57.0",
24
26
  "@stylistic/eslint-plugin": "^5.6.1",
25
27
  "@sveltejs/adapter-static": "^3.0.10",
26
- "@sveltejs/kit": "^2.49.1",
28
+ "@sveltejs/kit": "^2.49.2",
27
29
  "@sveltejs/package": "2.5.7",
28
30
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
29
- "@types/node": "^24.10.1",
30
- "@vitest/coverage-v8": "^4.0.15",
31
- "eslint": "^9.39.1",
31
+ "@types/node": "^25.0.3",
32
+ "@vitest/coverage-v8": "^4.0.16",
33
+ "eslint": "^9.39.2",
32
34
  "eslint-plugin-svelte": "^3.13.1",
33
35
  "happy-dom": "^20.0.11",
34
36
  "hastscript": "^9.0.1",
@@ -36,15 +38,15 @@
36
38
  "mdsvexamples": "^0.5.0",
37
39
  "rehype-autolink-headings": "^7.1.0",
38
40
  "rehype-slug": "^6.0.0",
39
- "svelte": "^5.45.6",
40
- "svelte-check": "^4.3.4",
41
+ "svelte": "^5.46.1",
42
+ "svelte-check": "^4.3.5",
41
43
  "svelte-preprocess": "^6.0.3",
42
44
  "svelte-toc": "^0.6.2",
43
- "svelte2tsx": "^0.7.45",
45
+ "svelte2tsx": "^0.7.46",
44
46
  "typescript": "5.9.3",
45
- "typescript-eslint": "^8.48.1",
46
- "vite": "^7.2.7",
47
- "vitest": "^4.0.15"
47
+ "typescript-eslint": "^8.51.0",
48
+ "vite": "^7.3.0",
49
+ "vitest": "^4.0.16"
48
50
  },
49
51
  "keywords": [
50
52
  "svelte",