svelte-multiselect 11.3.0 → 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.
@@ -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" | "options" | "outerDiv" | "searchText";
5
+ bindings: "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText";
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" | "options" | "outerDiv" | "searchText";
13
+ bindings(): "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText";
14
14
  exports(): {};
15
15
  }
16
16
  interface $$IsomorphicComponent {
package/dist/Nav.svelte CHANGED
@@ -1,8 +1,8 @@
1
1
  <script
2
2
  lang="ts"
3
- generics="Route extends string | [string, string] | [string, string[]] = string | [string, string] | [string, string[]]"
4
- >import { Icon } from './';
5
- import { click_outside } from './';
3
+ generics="Route extends string | [string, string] | [string, string[]]"
4
+ >import { click_outside } from './attachments';
5
+ import Icon from './Icon.svelte';
6
6
  let { routes = [], children, link, menu_props, link_props, page, labels, ...rest } = $props();
7
7
  let is_open = $state(false);
8
8
  let hovered_dropdown = $state(null);
@@ -27,11 +27,9 @@ function toggle_dropdown(href, focus_first = false) {
27
27
  // Focus management for keyboard users
28
28
  if (is_opening && focus_first) {
29
29
  setTimeout(() => {
30
- const dropdown = document.querySelector(`.dropdown-wrapper[data-href="${href}"]`);
31
- const first_link = dropdown?.querySelector(`.dropdown a`);
32
- if (first_link instanceof HTMLElement) {
33
- first_link.focus();
34
- }
30
+ const dropdown = document.querySelector(`.dropdown[data-href="${href}"]`);
31
+ const first_link = dropdown?.querySelector(`[role="menuitem"]`);
32
+ first_link?.focus();
35
33
  }, 0);
36
34
  }
37
35
  }
@@ -52,11 +50,9 @@ function handle_dropdown_keydown(event, href, sub_routes) {
52
50
  const direction = key === `ArrowDown` ? 1 : -1;
53
51
  const new_index = Math.max(0, Math.min(sub_routes.length - 1, focused_item_index + direction));
54
52
  focused_item_index = new_index;
55
- const dropdown = document.querySelector(`.dropdown-wrapper[data-href="${href}"]`);
56
- const links = dropdown?.querySelectorAll(`.dropdown a`);
57
- if (links?.[new_index] instanceof HTMLElement) {
58
- links[new_index].focus();
59
- }
53
+ const dropdown = document.querySelector(`.dropdown[data-href="${href}"]`);
54
+ const links = dropdown?.querySelectorAll(`[role="menuitem"]`);
55
+ links?.[new_index]?.focus();
60
56
  }
61
57
  // Open dropdown with ArrowDown when closed
62
58
  if (hovered_dropdown !== href && key === `ArrowDown`) {
@@ -70,8 +66,8 @@ function handle_dropdown_item_keydown(event, href) {
70
66
  close_menus();
71
67
  // Return focus to dropdown toggle button
72
68
  document
73
- .querySelector(`.dropdown-wrapper[data-href="${href}"]`)
74
- ?.querySelector(`.dropdown-toggle`)
69
+ .querySelector(`.dropdown[data-href="${href}"]`)
70
+ ?.querySelector(`[data-dropdown-toggle]`)
75
71
  ?.focus();
76
72
  }
77
73
  }
@@ -110,18 +106,18 @@ function parse_route(route) {
110
106
  <nav
111
107
  {...rest}
112
108
  {@attach click_outside({ callback: close_menus })}
113
- class="bleed-1400 {rest.class ?? ``}"
114
109
  >
115
110
  <button
116
- class="burger-button"
111
+ class="burger"
112
+ type="button"
117
113
  onclick={() => is_open = !is_open}
118
114
  aria-label="Toggle navigation menu"
119
115
  aria-expanded={is_open}
120
116
  aria-controls={panel_id}
121
117
  >
122
- <span class="burger-line"></span>
123
- <span class="burger-line"></span>
124
- <span class="burger-line"></span>
118
+ <span aria-hidden="true"></span>
119
+ <span aria-hidden="true"></span>
120
+ <span aria-hidden="true"></span>
125
121
  </button>
126
122
 
127
123
  <div
@@ -143,7 +139,7 @@ function parse_route(route) {
143
139
  {@const parent_page_exists = sub_routes.includes(href)}
144
140
  {@const filtered_sub_routes = sub_routes.filter((route) => route !== href)}
145
141
  <div
146
- class="dropdown-wrapper"
142
+ class="dropdown"
147
143
  class:active={child_is_active}
148
144
  data-href={href}
149
145
  role="group"
@@ -158,20 +154,18 @@ function parse_route(route) {
158
154
  }
159
155
  }}
160
156
  >
161
- <div class="dropdown-trigger-wrapper">
162
- <svelte:element
163
- this={parent_page_exists ? `a` : `span`}
164
- href={parent_page_exists ? href : undefined}
165
- class="dropdown-trigger"
166
- aria-current={is_current(href)}
167
- onclick={close_menus}
168
- role={parent_page_exists ? undefined : `button`}
169
- style={parent.style}
170
- >
171
- {@html parent.label}
172
- </svelte:element>
157
+ <div>
158
+ {#if parent_page_exists}
159
+ {@const { label, style } = parent}
160
+ <a {href} aria-current={is_current(href)} onclick={close_menus} {style}>
161
+ {@html label}
162
+ </a>
163
+ {:else}
164
+ <span style={parent.style}>{@html parent.label}</span>
165
+ {/if}
173
166
  <button
174
- class="dropdown-toggle"
167
+ type="button"
168
+ data-dropdown-toggle
175
169
  aria-label="Toggle {parent.label} submenu"
176
170
  aria-expanded={hovered_dropdown === href}
177
171
  aria-haspopup="true"
@@ -185,7 +179,6 @@ function parse_route(route) {
185
179
  </button>
186
180
  </div>
187
181
  <div
188
- class="dropdown"
189
182
  class:visible={hovered_dropdown === href}
190
183
  role="menu"
191
184
  tabindex="-1"
@@ -258,6 +251,7 @@ function parse_route(route) {
258
251
  display: flex;
259
252
  gap: 1em;
260
253
  place-content: center;
254
+ place-items: center;
261
255
  flex-wrap: wrap;
262
256
  padding: 0.5em;
263
257
  }
@@ -277,13 +271,14 @@ function parse_route(route) {
277
271
  }
278
272
 
279
273
  /* Dropdown styles */
280
- .dropdown-wrapper {
274
+ .dropdown {
281
275
  position: relative;
282
276
  }
283
- .dropdown-wrapper.active .dropdown-trigger {
277
+ .dropdown.active > div:first-child a,
278
+ .dropdown.active > div:first-child span {
284
279
  color: var(--nav-link-active-color);
285
280
  }
286
- .dropdown-wrapper::after {
281
+ .dropdown::after {
287
282
  content: '';
288
283
  position: absolute;
289
284
  top: 100%;
@@ -291,27 +286,28 @@ function parse_route(route) {
291
286
  right: 0;
292
287
  height: var(--nav-dropdown-margin, 3pt);
293
288
  }
294
- .dropdown-trigger-wrapper {
289
+ .dropdown > div:first-child {
295
290
  display: flex;
296
291
  align-items: center;
297
292
  gap: 0;
298
293
  border-radius: var(--nav-border-radius);
299
294
  transition: background-color 0.2s;
300
295
  }
301
- .dropdown-trigger-wrapper:hover {
296
+ .dropdown > div:first-child:hover {
302
297
  background-color: var(--nav-link-bg-hover);
303
298
  }
304
- .dropdown-trigger {
299
+ .dropdown > div:first-child > a,
300
+ .dropdown > div:first-child > span {
305
301
  line-height: 1.3;
306
302
  padding: 1pt 5pt;
307
303
  text-decoration: none;
308
304
  color: inherit;
309
305
  border-radius: var(--nav-border-radius) 0 0 var(--nav-border-radius);
310
306
  }
311
- .dropdown-trigger[aria-current='page'] {
307
+ .dropdown > div:first-child > a[aria-current='page'] {
312
308
  color: var(--nav-link-active-color);
313
309
  }
314
- .dropdown-toggle {
310
+ .dropdown > div:first-child > button {
315
311
  padding: 1pt 3pt;
316
312
  border: none;
317
313
  background: transparent;
@@ -322,7 +318,7 @@ function parse_route(route) {
322
318
  justify-content: center;
323
319
  border-radius: 0 var(--nav-border-radius) var(--nav-border-radius) 0;
324
320
  }
325
- .dropdown {
321
+ .dropdown > div:last-child {
326
322
  position: absolute;
327
323
  top: 100%;
328
324
  left: 0;
@@ -338,10 +334,10 @@ function parse_route(route) {
338
334
  gap: var(--nav-dropdown-gap, 5pt);
339
335
  z-index: var(--nav-dropdown-z-index, 100);
340
336
  }
341
- .dropdown.visible {
337
+ .dropdown > div:last-child.visible {
342
338
  display: flex;
343
339
  }
344
- .dropdown a {
340
+ .dropdown > div:last-child a {
345
341
  padding: var(--nav-dropdown-link-padding, 1pt 4pt);
346
342
  border-radius: var(--nav-border-radius);
347
343
  text-decoration: none;
@@ -349,14 +345,14 @@ function parse_route(route) {
349
345
  white-space: nowrap;
350
346
  transition: background-color 0.2s;
351
347
  }
352
- .dropdown a:hover {
348
+ .dropdown > div:last-child a:hover {
353
349
  background-color: var(--nav-link-bg-hover);
354
350
  }
355
- .dropdown a[aria-current='page'] {
351
+ .dropdown > div:last-child a[aria-current='page'] {
356
352
  color: var(--nav-link-active-color);
357
353
  }
358
354
  /* Mobile burger button */
359
- .burger-button {
355
+ .burger {
360
356
  display: none;
361
357
  position: fixed;
362
358
  top: 1rem;
@@ -369,25 +365,25 @@ function parse_route(route) {
369
365
  padding: 0;
370
366
  z-index: var(--nav-toggle-btn-z-index, 10);
371
367
  }
372
- .burger-line {
368
+ .burger span {
373
369
  height: 0.18rem;
374
370
  background-color: var(--text-color);
375
371
  border-radius: 8px;
376
372
  transition: all 0.2s linear;
377
373
  transform-origin: 1px;
378
374
  }
379
- .burger-button[aria-expanded='true'] .burger-line:first-child {
375
+ .burger[aria-expanded='true'] span:first-child {
380
376
  transform: rotate(45deg);
381
377
  }
382
- .burger-button[aria-expanded='true'] .burger-line:nth-child(2) {
378
+ .burger[aria-expanded='true'] span:nth-child(2) {
383
379
  opacity: 0;
384
380
  }
385
- .burger-button[aria-expanded='true'] .burger-line:nth-child(3) {
381
+ .burger[aria-expanded='true'] span:nth-child(3) {
386
382
  transform: rotate(-45deg);
387
383
  }
388
384
  /* Mobile styles */
389
385
  @media (max-width: 767px) {
390
- .burger-button {
386
+ .burger {
391
387
  display: flex;
392
388
  }
393
389
  .menu {
@@ -413,29 +409,30 @@ function parse_route(route) {
413
409
  visibility: visible;
414
410
  }
415
411
  .menu > a,
416
- .dropdown-wrapper {
412
+ .dropdown {
417
413
  padding: 2pt 8pt;
418
414
  }
419
415
 
420
416
  /* Mobile dropdown styles - show as expandable section */
421
- .dropdown-wrapper {
417
+ .dropdown {
422
418
  flex-direction: column;
423
419
  align-items: stretch;
424
420
  }
425
- .dropdown-trigger-wrapper {
421
+ .dropdown > div:first-child {
426
422
  display: flex;
427
423
  align-items: center;
428
424
  justify-content: space-between;
429
425
  }
430
- .dropdown-trigger {
426
+ .dropdown > div:first-child > a,
427
+ .dropdown > div:first-child > span {
431
428
  flex: 1;
432
429
  border-radius: var(--nav-border-radius);
433
430
  }
434
- .dropdown-toggle {
431
+ .dropdown > div:first-child > button {
435
432
  padding: 4pt 8pt;
436
433
  border-radius: var(--nav-border-radius);
437
434
  }
438
- .dropdown {
435
+ .dropdown > div:last-child {
439
436
  position: static;
440
437
  border: none;
441
438
  box-shadow: none;
@@ -1,7 +1,7 @@
1
1
  import type { Page } from '@sveltejs/kit';
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
- declare function $$render<Route extends string | [string, string] | [string, string[]] = string | [string, string] | [string, string[]]>(): {
4
+ declare function $$render<Route extends string | [string, string] | [string, string[]]>(): {
5
5
  props: {
6
6
  routes: Route[];
7
7
  children?: Snippet<[{
@@ -23,7 +23,7 @@ declare function $$render<Route extends string | [string, string] | [string, str
23
23
  slots: {};
24
24
  events: {};
25
25
  };
26
- declare class __sveltets_Render<Route extends string | [string, string] | [string, string[]] = string | [string, string] | [string, string[]]> {
26
+ declare class __sveltets_Render<Route extends string | [string, string] | [string, string[]]> {
27
27
  props(): ReturnType<typeof $$render<Route>>['props'];
28
28
  events(): ReturnType<typeof $$render<Route>>['events'];
29
29
  slots(): ReturnType<typeof $$render<Route>>['slots'];
@@ -31,12 +31,12 @@ declare class __sveltets_Render<Route extends string | [string, string] | [strin
31
31
  exports(): {};
32
32
  }
33
33
  interface $$IsomorphicComponent {
34
- new <Route extends string | [string, string] | [string, string[]] = string | [string, string] | [string, string[]]>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Route>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Route>['props']>, ReturnType<__sveltets_Render<Route>['events']>, ReturnType<__sveltets_Render<Route>['slots']>> & {
34
+ new <Route extends string | [string, string] | [string, string[]]>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Route>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Route>['props']>, ReturnType<__sveltets_Render<Route>['events']>, ReturnType<__sveltets_Render<Route>['slots']>> & {
35
35
  $$bindings?: ReturnType<__sveltets_Render<Route>['bindings']>;
36
36
  } & ReturnType<__sveltets_Render<Route>['exports']>;
37
- <Route extends string | [string, string] | [string, string[]] = string | [string, string] | [string, string[]]>(internal: unknown, props: ReturnType<__sveltets_Render<Route>['props']> & {}): ReturnType<__sveltets_Render<Route>['exports']>;
37
+ <Route extends string | [string, string] | [string, string[]]>(internal: unknown, props: ReturnType<__sveltets_Render<Route>['props']> & {}): ReturnType<__sveltets_Render<Route>['exports']>;
38
38
  z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
39
39
  }
40
40
  declare const Nav: $$IsomorphicComponent;
41
- type Nav<Route extends string | [string, string] | [string, string[]] = string | [string, string] | [string, string[]]> = InstanceType<typeof Nav<Route>>;
41
+ type Nav<Route extends string | [string, string] | [string, string[]]> = InstanceType<typeof Nav<Route>>;
42
42
  export default Nav;
@@ -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 declare const sortable: ({ header_selector, asc_class, desc_class, sorted_style, }?: {
21
- header_selector?: string | undefined;
22
- asc_class?: string | undefined;
23
- desc_class?: string | undefined;
24
- sorted_style?: {
25
- backgroundColor: string;
26
- } | undefined;
27
- }) => (node: HTMLElement) => () => void;
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;
@@ -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 = ({ header_selector = `thead th`, asc_class = `table-sort-asc`, desc_class = `table-sort-desc`, sorted_style = { backgroundColor: `rgba(255, 255, 255, 0.1)` }, } = {}) => (node) => {
96
- // this action can be applied to bob-standard HTML tables to make them sortable by
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 event listeners for cleanup
102
- const event_listeners = [];
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 initial state
108
- for (const header of headers) {
109
- header.textContent = header.textContent?.replace(/ ↑| ↓/, ``) ?? ``;
110
- header.classList.remove(asc_class, desc_class);
111
- header.setAttribute(`style`, init_styles);
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
- event_listeners.push({ header, handler: click_handler });
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 event_listeners) {
166
+ for (const { header, handler, original_text, original_style } of header_state) {
155
167
  header.removeEventListener(`click`, handler);
156
- header.style.cursor = ``; // Reset cursor
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
- const content = safe_options.content || element.title ||
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`, hide_tooltip, true);
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`, hide_tooltip, true);
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/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: T[];
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;