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.
- package/dist/CodeExample.svelte +2 -4
- package/dist/CopyButton.svelte +3 -2
- package/dist/CopyButton.svelte.d.ts +1 -1
- package/dist/GitHubCorner.svelte +2 -2
- package/dist/MultiSelect.svelte +326 -153
- package/dist/MultiSelect.svelte.d.ts +2 -2
- package/dist/Nav.svelte +57 -60
- package/dist/Nav.svelte.d.ts +5 -5
- package/dist/attachments.d.ts +17 -9
- package/dist/attachments.js +80 -22
- package/dist/types.d.ts +33 -4
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +5 -3
- package/package.json +21 -14
- package/readme.md +84 -6
|
@@ -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[]]
|
|
4
|
-
>import {
|
|
5
|
-
import
|
|
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
|
|
31
|
-
const first_link = dropdown?.querySelector(
|
|
32
|
-
|
|
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
|
|
56
|
-
const links = dropdown?.querySelectorAll(
|
|
57
|
-
|
|
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
|
|
74
|
-
?.querySelector(
|
|
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
|
|
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
|
|
123
|
-
<span
|
|
124
|
-
<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
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
href={
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
274
|
+
.dropdown {
|
|
281
275
|
position: relative;
|
|
282
276
|
}
|
|
283
|
-
.dropdown
|
|
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
|
|
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-
|
|
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-
|
|
296
|
+
.dropdown > div:first-child:hover {
|
|
302
297
|
background-color: var(--nav-link-bg-hover);
|
|
303
298
|
}
|
|
304
|
-
.dropdown-
|
|
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-
|
|
307
|
+
.dropdown > div:first-child > a[aria-current='page'] {
|
|
312
308
|
color: var(--nav-link-active-color);
|
|
313
309
|
}
|
|
314
|
-
.dropdown-
|
|
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
|
|
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
|
|
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
|
|
375
|
+
.burger[aria-expanded='true'] span:first-child {
|
|
380
376
|
transform: rotate(45deg);
|
|
381
377
|
}
|
|
382
|
-
.burger
|
|
378
|
+
.burger[aria-expanded='true'] span:nth-child(2) {
|
|
383
379
|
opacity: 0;
|
|
384
380
|
}
|
|
385
|
-
.burger
|
|
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
|
|
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
|
|
412
|
+
.dropdown {
|
|
417
413
|
padding: 2pt 8pt;
|
|
418
414
|
}
|
|
419
415
|
|
|
420
416
|
/* Mobile dropdown styles - show as expandable section */
|
|
421
|
-
.dropdown
|
|
417
|
+
.dropdown {
|
|
422
418
|
flex-direction: column;
|
|
423
419
|
align-items: stretch;
|
|
424
420
|
}
|
|
425
|
-
.dropdown-
|
|
421
|
+
.dropdown > div:first-child {
|
|
426
422
|
display: flex;
|
|
427
423
|
align-items: center;
|
|
428
424
|
justify-content: space-between;
|
|
429
425
|
}
|
|
430
|
-
.dropdown-
|
|
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-
|
|
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;
|
package/dist/Nav.svelte.d.ts
CHANGED
|
@@ -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[]]
|
|
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[]]
|
|
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[]]
|
|
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[]]
|
|
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[]]
|
|
41
|
+
type Nav<Route extends string | [string, string] | [string, string[]]> = InstanceType<typeof Nav<Route>>;
|
|
42
42
|
export default Nav;
|
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/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;
|