svelte-multiselect 11.6.0 → 11.6.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.
- package/dist/MultiSelect.svelte +26 -10
- package/dist/Nav.svelte +34 -24
- package/dist/Nav.svelte.d.ts +1 -0
- package/dist/attachments.d.ts +1 -0
- package/dist/attachments.js +141 -36
- package/dist/live-examples/highlighter.js +6 -33
- package/dist/live-examples/index.d.ts +1 -1
- package/dist/live-examples/index.js +1 -1
- package/dist/live-examples/mdsvex-transform.js +6 -5
- package/dist/types.d.ts +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +4 -0
- package/package.json +1 -1
package/dist/MultiSelect.svelte
CHANGED
|
@@ -7,7 +7,7 @@ import CircleSpinner from './CircleSpinner.svelte';
|
|
|
7
7
|
import Icon from './Icon.svelte';
|
|
8
8
|
import * as utils from './utils';
|
|
9
9
|
import Wiggle from './Wiggle.svelte';
|
|
10
|
-
let { activeIndex = $bindable(null), activeOption = $bindable(null), createOptionMsg = `Create this option...`, allowUserOptions = false, allowEmpty = false, autocomplete = `off`, autoScroll = true, breakpoint = 800, defaultDisabledTitle = `This option is disabled`, disabled = false, disabledInputTitle = `This input is disabled`, duplicateOptionMsg = `This option is already selected`, duplicates = false, keepSelectedInDropdown = false, key = (opt) =>
|
|
10
|
+
let { activeIndex = $bindable(null), activeOption = $bindable(null), createOptionMsg = `Create this option...`, allowUserOptions = false, allowEmpty = false, autocomplete = `off`, autoScroll = true, breakpoint = 800, defaultDisabledTitle = `This option is disabled`, disabled = false, disabledInputTitle = `This input is disabled`, duplicateOptionMsg = `This option is already selected`, duplicates = false, keepSelectedInDropdown = false, key = (opt) => utils.get_option_key(opt), filterFunc = (opt, searchText) => {
|
|
11
11
|
if (!searchText)
|
|
12
12
|
return true;
|
|
13
13
|
const label = `${utils.get_label(opt)}`;
|
|
@@ -243,7 +243,16 @@ let selected_keys = $derived(selected.map(key));
|
|
|
243
243
|
let selected_labels = $derived(selected.map(utils.get_label));
|
|
244
244
|
// Sets for O(1) lookups (used in template, has_user_msg, group_header_state, batch operations)
|
|
245
245
|
let selected_keys_set = $derived(new Set(selected_keys));
|
|
246
|
-
|
|
246
|
+
// String-normalized for consistent comparison (numeric labels like 123 match "123")
|
|
247
|
+
let selected_labels_set = $derived(new Set(selected_labels.map((lbl) => `${lbl}`)));
|
|
248
|
+
// Lowercase labels set for case-insensitive duplicate detection
|
|
249
|
+
let selected_labels_lower_set = $derived(duplicates === `case-insensitive`
|
|
250
|
+
? new Set(selected_labels.map((lbl) => `${lbl}`.toLowerCase()))
|
|
251
|
+
: null);
|
|
252
|
+
// Helper to check if a label is already selected (respects case-insensitive mode)
|
|
253
|
+
const is_label_selected = (label) => selected_labels_lower_set
|
|
254
|
+
? selected_labels_lower_set.has(label.toLowerCase())
|
|
255
|
+
: selected_labels_set.has(label);
|
|
247
256
|
// Memoized Set of disabled option keys for O(1) lookups in large option sets
|
|
248
257
|
let disabled_option_keys = $derived(new Set(effective_options
|
|
249
258
|
.filter((opt) => utils.is_object(opt) && opt.disabled)
|
|
@@ -482,18 +491,26 @@ function add(option_to_add, event) {
|
|
|
482
491
|
typeof selected_labels[0] === `number`) {
|
|
483
492
|
option_to_add = Number(option_to_add); // convert to number if possible
|
|
484
493
|
}
|
|
485
|
-
|
|
494
|
+
// Check for duplicates by key, plus label check for user-created options
|
|
495
|
+
// For duplicates=false (default), label check only applies to user-typed text
|
|
496
|
+
// For duplicates='case-insensitive', label check applies to all options
|
|
497
|
+
// Use key comparison instead of reference equality (more robust with Svelte proxies)
|
|
498
|
+
const option_key = key(option_to_add);
|
|
499
|
+
const is_from_options = effective_options.some((opt) => key(opt) === option_key);
|
|
500
|
+
const check_label = duplicates === `case-insensitive` || !is_from_options;
|
|
501
|
+
const is_duplicate = selected_keys_set.has(key(option_to_add)) ||
|
|
502
|
+
(check_label && is_label_selected(`${utils.get_label(option_to_add)}`));
|
|
486
503
|
const max_reached = maxSelect !== null && maxSelect !== 1 &&
|
|
487
504
|
selected.length >= maxSelect;
|
|
488
505
|
// Fire events for blocked add attempts
|
|
489
506
|
if (max_reached) {
|
|
490
507
|
onmaxreached?.({ selected, maxSelect, attemptedOption: option_to_add });
|
|
491
508
|
}
|
|
492
|
-
if (is_duplicate &&
|
|
509
|
+
if (is_duplicate && duplicates !== true)
|
|
493
510
|
onduplicate?.({ option: option_to_add });
|
|
494
511
|
if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
|
495
|
-
(duplicates || !is_duplicate)) {
|
|
496
|
-
if (!
|
|
512
|
+
(duplicates === true || !is_duplicate)) {
|
|
513
|
+
if (!is_from_options && // first check if we find option in the options list
|
|
497
514
|
// this has the side-effect of not allowing to user to add the same
|
|
498
515
|
// custom option twice in append mode
|
|
499
516
|
[true, `append`].includes(allowUserOptions) &&
|
|
@@ -606,7 +623,7 @@ function handle_dropdown_after_select(event) {
|
|
|
606
623
|
// Check if a user message (create option, duplicate warning, no match) is visible
|
|
607
624
|
const has_user_msg = $derived(searchText.length > 0 &&
|
|
608
625
|
Boolean((allowUserOptions && createOptionMsg) ||
|
|
609
|
-
(
|
|
626
|
+
(duplicates !== true && is_label_selected(searchText)) ||
|
|
610
627
|
(navigable_options.length === 0 && noMatchingOptionsMsg)));
|
|
611
628
|
// Handle arrow key navigation through options (uses navigable_options to skip collapsed groups)
|
|
612
629
|
async function handle_arrow_navigation(direction) {
|
|
@@ -827,7 +844,7 @@ function toggle_group_selection(group_opts, group_collapsed, all_selected, event
|
|
|
827
844
|
}
|
|
828
845
|
}
|
|
829
846
|
// O(1) lookup using pre-computed Set instead of O(n) array.includes()
|
|
830
|
-
const is_selected = (label) => selected_labels_set.has(label);
|
|
847
|
+
const is_selected = (label) => selected_labels_set.has(`${label}`);
|
|
831
848
|
const if_enter_or_space = (handler) => (event) => {
|
|
832
849
|
if (event.key === `Enter` || event.code === `Space`) {
|
|
833
850
|
event.preventDefault();
|
|
@@ -1414,8 +1431,7 @@ function handle_options_scroll(event) {
|
|
|
1414
1431
|
{/if}
|
|
1415
1432
|
{/each}
|
|
1416
1433
|
{#if searchText}
|
|
1417
|
-
{@const
|
|
1418
|
-
{@const is_dupe = !duplicates && text_input_is_duplicate && `dupe`}
|
|
1434
|
+
{@const is_dupe = duplicates !== true && is_label_selected(searchText) && `dupe`}
|
|
1419
1435
|
{@const can_create = Boolean(allowUserOptions && createOptionMsg) && `create`}
|
|
1420
1436
|
{@const no_match =
|
|
1421
1437
|
Boolean(navigable_options?.length === 0 && noMatchingOptionsMsg) &&
|
package/dist/Nav.svelte
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<script lang="ts">import { click_outside, tooltip } from './attachments';
|
|
2
2
|
import Icon from './Icon.svelte';
|
|
3
3
|
import { get_uuid } from './utils';
|
|
4
|
-
let { routes = [], children, item, link, menu_props, link_props, page, labels, tooltips, tooltip_options, breakpoint = 767, onnavigate, onopen, onclose, ...rest } = $props();
|
|
4
|
+
let { routes = [], children, item, link, menu_props, link_props, page, labels, tooltips, tooltip_options, breakpoint = 767, dropdown_cooldown = 150, onnavigate, onopen, onclose, ...rest } = $props();
|
|
5
5
|
let is_open = $state(false);
|
|
6
6
|
let hovered_dropdown = $state(null);
|
|
7
7
|
let pinned_dropdown = $state(null);
|
|
8
8
|
let focused_item_index = $state(-1);
|
|
9
9
|
let is_touch_device = $state(false);
|
|
10
10
|
let is_mobile = $state(false);
|
|
11
|
+
let hide_timeout = null;
|
|
11
12
|
const panel_id = `nav-menu-${get_uuid()}`;
|
|
12
13
|
// Track previous is_open state for callbacks
|
|
13
14
|
let prev_is_open = $state(false);
|
|
@@ -34,7 +35,13 @@ $effect(() => {
|
|
|
34
35
|
}
|
|
35
36
|
prev_is_open = is_open;
|
|
36
37
|
});
|
|
38
|
+
$effect(() => () => {
|
|
39
|
+
if (hide_timeout)
|
|
40
|
+
clearTimeout(hide_timeout);
|
|
41
|
+
}); // cleanup on destroy
|
|
37
42
|
function close_menus() {
|
|
43
|
+
if (hide_timeout)
|
|
44
|
+
clearTimeout(hide_timeout);
|
|
38
45
|
is_open = false;
|
|
39
46
|
hovered_dropdown = null;
|
|
40
47
|
pinned_dropdown = null;
|
|
@@ -45,7 +52,6 @@ function toggle_dropdown(href, focus_first = false) {
|
|
|
45
52
|
pinned_dropdown = is_opening ? href : null;
|
|
46
53
|
hovered_dropdown = is_opening ? href : null;
|
|
47
54
|
focused_item_index = is_opening && focus_first ? 0 : -1;
|
|
48
|
-
// Focus management for keyboard users
|
|
49
55
|
if (is_opening && focus_first) {
|
|
50
56
|
setTimeout(() => {
|
|
51
57
|
document
|
|
@@ -54,19 +60,23 @@ function toggle_dropdown(href, focus_first = false) {
|
|
|
54
60
|
}, 0);
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
|
-
function
|
|
58
|
-
if (is_touch_device)
|
|
63
|
+
function open_dropdown(href, from_mouse = false) {
|
|
64
|
+
if (from_mouse && is_touch_device)
|
|
59
65
|
return;
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
if (hide_timeout)
|
|
67
|
+
clearTimeout(hide_timeout);
|
|
68
|
+
if (pinned_dropdown && pinned_dropdown !== href)
|
|
62
69
|
pinned_dropdown = null;
|
|
63
70
|
hovered_dropdown = href;
|
|
64
71
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
function schedule_hide(href, is_pinned) {
|
|
73
|
+
if (is_touch_device || is_pinned)
|
|
74
|
+
return;
|
|
75
|
+
clearTimeout(hide_timeout);
|
|
76
|
+
hide_timeout = setTimeout(() => {
|
|
77
|
+
if (hovered_dropdown === href)
|
|
78
|
+
hovered_dropdown = null;
|
|
79
|
+
}, dropdown_cooldown);
|
|
70
80
|
}
|
|
71
81
|
function onkeydown(event) {
|
|
72
82
|
if (event.key === `Escape`)
|
|
@@ -277,11 +287,9 @@ function get_external_attrs(route) {
|
|
|
277
287
|
data-href={parsed_route.href}
|
|
278
288
|
role="group"
|
|
279
289
|
aria-current={child_is_active ? `true` : undefined}
|
|
280
|
-
onmouseenter={() =>
|
|
281
|
-
onmouseleave={() =>
|
|
282
|
-
|
|
283
|
-
}}
|
|
284
|
-
onfocusin={() => handle_dropdown_focusin(parsed_route.href)}
|
|
290
|
+
onmouseenter={() => open_dropdown(parsed_route.href, true)}
|
|
291
|
+
onmouseleave={() => schedule_hide(parsed_route.href, is_pinned)}
|
|
292
|
+
onfocusin={() => open_dropdown(parsed_route.href)}
|
|
285
293
|
onfocusout={(event) => {
|
|
286
294
|
const next = event.relatedTarget as Node | null
|
|
287
295
|
if (!next || !(event.currentTarget as HTMLElement).contains(next)) {
|
|
@@ -339,9 +347,8 @@ function get_external_attrs(route) {
|
|
|
339
347
|
class:visible={dropdown_open}
|
|
340
348
|
role="menu"
|
|
341
349
|
tabindex="-1"
|
|
342
|
-
onmouseenter={() =>
|
|
343
|
-
|
|
344
|
-
}}
|
|
350
|
+
onmouseenter={() => open_dropdown(parsed_route.href, true)}
|
|
351
|
+
onmouseleave={() => schedule_hide(parsed_route.href, is_pinned)}
|
|
345
352
|
>
|
|
346
353
|
{#each filtered_sub_routes as child_href (child_href)}
|
|
347
354
|
{@const child_formatted = format_label(child_href, true)}
|
|
@@ -406,7 +413,7 @@ function get_external_attrs(route) {
|
|
|
406
413
|
nav {
|
|
407
414
|
position: relative;
|
|
408
415
|
margin: -0.75em auto 1.25em;
|
|
409
|
-
--nav-border-radius:
|
|
416
|
+
--nav-border-radius: 3pt;
|
|
410
417
|
--nav-surface-bg: light-dark(#fafafa, #1a1a1a);
|
|
411
418
|
--nav-surface-border: light-dark(
|
|
412
419
|
rgba(128, 128, 128, 0.25),
|
|
@@ -437,7 +444,7 @@ function get_external_attrs(route) {
|
|
|
437
444
|
}
|
|
438
445
|
.menu > span > a {
|
|
439
446
|
line-height: 1.3;
|
|
440
|
-
padding: var(--nav-item-padding);
|
|
447
|
+
padding: var(--nav-item-padding, 1pt 4pt);
|
|
441
448
|
text-decoration: none;
|
|
442
449
|
color: inherit;
|
|
443
450
|
}
|
|
@@ -497,7 +504,7 @@ function get_external_attrs(route) {
|
|
|
497
504
|
}
|
|
498
505
|
.dropdown > div:first-child > a, .dropdown > div:first-child > span {
|
|
499
506
|
line-height: 1.3;
|
|
500
|
-
padding: var(--nav-item-padding);
|
|
507
|
+
padding: var(--nav-item-padding, 1pt 4pt);
|
|
501
508
|
text-decoration: none;
|
|
502
509
|
color: inherit;
|
|
503
510
|
border-radius: var(--nav-border-radius) 0 0 var(--nav-border-radius);
|
|
@@ -534,9 +541,12 @@ function get_external_attrs(route) {
|
|
|
534
541
|
.dropdown > div:last-child {
|
|
535
542
|
position: absolute;
|
|
536
543
|
top: 100%;
|
|
537
|
-
left: 0;
|
|
544
|
+
left: var(--nav-dropdown-left, 0);
|
|
545
|
+
right: var(--nav-dropdown-right, auto);
|
|
538
546
|
margin: var(--nav-dropdown-margin, 2pt) 0 0 0;
|
|
539
|
-
min-width:
|
|
547
|
+
min-width: var(--nav-dropdown-min-width, 100%); /* at least as wide as parent */
|
|
548
|
+
max-width: var(--nav-dropdown-max-width, none);
|
|
549
|
+
width: var(--nav-dropdown-width, max-content); /* grow wider if content needs it */
|
|
540
550
|
background-color: var(--nav-dropdown-bg, var(--nav-surface-bg));
|
|
541
551
|
border: 1px solid var(--nav-dropdown-border-color, var(--nav-surface-border));
|
|
542
552
|
border-radius: var(--nav-border-radius, 6pt);
|
package/dist/Nav.svelte.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ type $$ComponentProps = {
|
|
|
30
30
|
tooltips?: Record<string, string | Omit<TooltipOptions, `disabled`>>;
|
|
31
31
|
tooltip_options?: Omit<TooltipOptions, `content`>;
|
|
32
32
|
breakpoint?: number;
|
|
33
|
+
dropdown_cooldown?: number;
|
|
33
34
|
onnavigate?: (data: {
|
|
34
35
|
href: string;
|
|
35
36
|
event: MouseEvent;
|
package/dist/attachments.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface TooltipOptions {
|
|
|
64
64
|
show_arrow?: boolean;
|
|
65
65
|
offset?: number;
|
|
66
66
|
allow_html?: boolean;
|
|
67
|
+
sanitize_html?: (html: string) => string;
|
|
67
68
|
}
|
|
68
69
|
export declare const tooltip: (options?: TooltipOptions) => Attachment;
|
|
69
70
|
export type ClickOutsideConfig<T extends HTMLElement> = {
|
package/dist/attachments.js
CHANGED
|
@@ -422,16 +422,141 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
422
422
|
content = new_content;
|
|
423
423
|
// Only update tooltip if this element owns it
|
|
424
424
|
if (current_tooltip?._owner === element) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
425
|
+
const content_el = current_tooltip.querySelector(`.tooltip-content`);
|
|
426
|
+
if (content_el) {
|
|
427
|
+
if (options.allow_html !== false) {
|
|
428
|
+
let html = content.replace(/\r/g, `<br/>`);
|
|
429
|
+
if (options.sanitize_html)
|
|
430
|
+
html = options.sanitize_html(html);
|
|
431
|
+
content_el.innerHTML = html;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
content_el.textContent = content;
|
|
435
|
+
}
|
|
436
|
+
// Re-run sizing/positioning after content change
|
|
437
|
+
resize_and_position_tooltip(current_tooltip, element);
|
|
430
438
|
}
|
|
431
439
|
}
|
|
432
440
|
}
|
|
433
441
|
});
|
|
434
442
|
observer.observe(element, { attributes: true, attributeFilter: tooltip_attrs });
|
|
443
|
+
// Shrink tooltip to fit content, then position for correct centering
|
|
444
|
+
function resize_and_position_tooltip(tooltip_el, trigger) {
|
|
445
|
+
// Reset width to allow natural sizing before measuring
|
|
446
|
+
tooltip_el.style.width = ``;
|
|
447
|
+
requestAnimationFrame(() => {
|
|
448
|
+
if (!document.body.contains(tooltip_el))
|
|
449
|
+
return;
|
|
450
|
+
const computed = getComputedStyle(tooltip_el);
|
|
451
|
+
const padding_h = parseFloat(computed.paddingLeft) +
|
|
452
|
+
parseFloat(computed.paddingRight);
|
|
453
|
+
const border_h = parseFloat(computed.borderLeftWidth) +
|
|
454
|
+
parseFloat(computed.borderRightWidth);
|
|
455
|
+
// Handle max-width: none as unbounded, fallback to 280 only for invalid values
|
|
456
|
+
const max_width_raw = computed.maxWidth;
|
|
457
|
+
const max_width_parsed = parseFloat(max_width_raw);
|
|
458
|
+
const max_width = max_width_raw === `none`
|
|
459
|
+
? Infinity
|
|
460
|
+
: Number.isFinite(max_width_parsed)
|
|
461
|
+
? max_width_parsed
|
|
462
|
+
: 280;
|
|
463
|
+
// With border-box, width includes padding+border; with content-box, subtract them
|
|
464
|
+
const box_adjust = computed.boxSizing === `border-box` ? 0 : padding_h + border_h;
|
|
465
|
+
const style = tooltip_el.style;
|
|
466
|
+
const placement = tooltip_el.getAttribute(`data-placement`) || `bottom`;
|
|
467
|
+
// Save styles, measure single-line width with wrapping disabled
|
|
468
|
+
const saved = {
|
|
469
|
+
maxWidth: style.maxWidth,
|
|
470
|
+
wordWrap: style.wordWrap,
|
|
471
|
+
textWrap: style.textWrap,
|
|
472
|
+
whiteSpace: style.whiteSpace,
|
|
473
|
+
};
|
|
474
|
+
Object.assign(style, {
|
|
475
|
+
maxWidth: `none`,
|
|
476
|
+
wordWrap: `normal`,
|
|
477
|
+
textWrap: `nowrap`,
|
|
478
|
+
whiteSpace: `nowrap`,
|
|
479
|
+
width: `auto`,
|
|
480
|
+
});
|
|
481
|
+
const single_line_width = tooltip_el.offsetWidth;
|
|
482
|
+
Object.assign(style, saved);
|
|
483
|
+
// Convert offsetWidth to the value needed for style.width
|
|
484
|
+
const content_width = single_line_width - box_adjust;
|
|
485
|
+
if (content_width <= max_width) {
|
|
486
|
+
// Single-line: set exact width and prevent wrapping
|
|
487
|
+
style.width = `${Math.max(0, content_width)}px`;
|
|
488
|
+
style.textWrap = `nowrap`;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Multi-line: binary search for minimum width that doesn't add line breaks
|
|
492
|
+
style.width = ``;
|
|
493
|
+
const baseline_height = tooltip_el.offsetHeight;
|
|
494
|
+
const initial_width = tooltip_el.offsetWidth;
|
|
495
|
+
// Get min-content (longest word) as lower bound
|
|
496
|
+
Object.assign(style, {
|
|
497
|
+
maxWidth: `none`,
|
|
498
|
+
wordWrap: `normal`,
|
|
499
|
+
width: `min-content`,
|
|
500
|
+
});
|
|
501
|
+
const min_width = tooltip_el.offsetWidth;
|
|
502
|
+
Object.assign(style, {
|
|
503
|
+
maxWidth: saved.maxWidth,
|
|
504
|
+
wordWrap: saved.wordWrap,
|
|
505
|
+
width: `${initial_width - box_adjust}px`,
|
|
506
|
+
});
|
|
507
|
+
// If longest word exceeds wrapped width, use min_width (can't shrink further)
|
|
508
|
+
if (min_width >= initial_width) {
|
|
509
|
+
style.width = `${min_width - box_adjust}px`;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
// Binary search for minimum width that maintains baseline height
|
|
513
|
+
// Work in offsetWidth units, convert to style.width only when setting
|
|
514
|
+
let low = min_width, high = initial_width, best = initial_width;
|
|
515
|
+
while (high - low > 1) {
|
|
516
|
+
const mid = Math.floor((low + high) / 2);
|
|
517
|
+
style.width = `${mid - box_adjust}px`;
|
|
518
|
+
if (tooltip_el.offsetHeight > baseline_height)
|
|
519
|
+
low = mid;
|
|
520
|
+
else {
|
|
521
|
+
best = mid;
|
|
522
|
+
high = mid;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
style.width = `${best - box_adjust}px`;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Position tooltip after width adjustment so centering uses final dimensions
|
|
529
|
+
const rect = trigger.getBoundingClientRect();
|
|
530
|
+
const tooltip_rect = tooltip_el.getBoundingClientRect();
|
|
531
|
+
const margin = options.offset ?? 12;
|
|
532
|
+
let top = 0, left = 0;
|
|
533
|
+
if (placement === `top`) {
|
|
534
|
+
top = rect.top - tooltip_rect.height - margin;
|
|
535
|
+
left = rect.left + rect.width / 2 - tooltip_rect.width / 2;
|
|
536
|
+
}
|
|
537
|
+
else if (placement === `left`) {
|
|
538
|
+
top = rect.top + rect.height / 2 - tooltip_rect.height / 2;
|
|
539
|
+
left = rect.left - tooltip_rect.width - margin;
|
|
540
|
+
}
|
|
541
|
+
else if (placement === `right`) {
|
|
542
|
+
top = rect.top + rect.height / 2 - tooltip_rect.height / 2;
|
|
543
|
+
left = rect.right + margin;
|
|
544
|
+
}
|
|
545
|
+
else { // bottom
|
|
546
|
+
top = rect.bottom + margin;
|
|
547
|
+
left = rect.left + rect.width / 2 - tooltip_rect.width / 2;
|
|
548
|
+
}
|
|
549
|
+
// Keep in viewport
|
|
550
|
+
const viewport_width = globalThis.innerWidth;
|
|
551
|
+
const viewport_height = globalThis.innerHeight;
|
|
552
|
+
left = Math.max(8, Math.min(left, viewport_width - tooltip_rect.width - 8));
|
|
553
|
+
top = Math.max(8, Math.min(top, viewport_height - tooltip_rect.height - 8));
|
|
554
|
+
style.left = `${left + globalThis.scrollX}px`;
|
|
555
|
+
style.top = `${top + globalThis.scrollY}px`;
|
|
556
|
+
style.opacity =
|
|
557
|
+
getComputedStyle(trigger).getPropertyValue(`--tooltip-opacity`).trim() || `1`;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
435
560
|
function show_tooltip() {
|
|
436
561
|
// Skip tooltip on touch input when 'touch-devices' option is set
|
|
437
562
|
if (options.disabled === `touch-devices` && last_pointer_type === `touch`)
|
|
@@ -449,7 +574,7 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
449
574
|
element.setAttribute(`aria-describedby`, tooltip_id);
|
|
450
575
|
// Apply base styles
|
|
451
576
|
tooltip_el.style.cssText = `
|
|
452
|
-
position: absolute; z-index: 9999; opacity: 0;
|
|
577
|
+
position: absolute; z-index: 9999; opacity: 0; display: inline-block;
|
|
453
578
|
background: var(--tooltip-bg, #333); color: var(--text-color, white); border: var(--tooltip-border, none);
|
|
454
579
|
padding: var(--tooltip-padding, 6px 10px); border-radius: var(--tooltip-radius, 6px); font-size: var(--tooltip-font-size, 13px); line-height: 1.4;
|
|
455
580
|
max-width: var(--tooltip-max-width, 280px); word-wrap: break-word; text-wrap: balance; pointer-events: none;
|
|
@@ -465,13 +590,20 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
465
590
|
tooltip_el.style.setProperty(property, value);
|
|
466
591
|
});
|
|
467
592
|
}
|
|
593
|
+
// Wrap content in a span for reactive content updates
|
|
594
|
+
const content_span = document.createElement(`span`);
|
|
595
|
+
content_span.className = `tooltip-content`;
|
|
468
596
|
// Security: allow_html defaults to true; set to false for plain text rendering
|
|
469
597
|
if (options.allow_html !== false) {
|
|
470
|
-
|
|
598
|
+
let html = content?.replace(/\r/g, `<br/>`) ?? ``;
|
|
599
|
+
if (options.sanitize_html)
|
|
600
|
+
html = options.sanitize_html(html);
|
|
601
|
+
content_span.innerHTML = html;
|
|
471
602
|
}
|
|
472
603
|
else {
|
|
473
|
-
|
|
604
|
+
content_span.textContent = content ?? ``;
|
|
474
605
|
}
|
|
606
|
+
tooltip_el.appendChild(content_span);
|
|
475
607
|
// Mirror CSS custom properties from the trigger node onto the tooltip element
|
|
476
608
|
const trigger_styles = getComputedStyle(element);
|
|
477
609
|
[
|
|
@@ -578,34 +710,7 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
578
710
|
append_border_arrow();
|
|
579
711
|
tooltip_el.appendChild(arrow);
|
|
580
712
|
}
|
|
581
|
-
|
|
582
|
-
const rect = element.getBoundingClientRect();
|
|
583
|
-
const tooltip_rect = tooltip_el.getBoundingClientRect();
|
|
584
|
-
const margin = options.offset ?? 12;
|
|
585
|
-
let top = 0, left = 0;
|
|
586
|
-
if (placement === `top`) {
|
|
587
|
-
top = rect.top - tooltip_rect.height - margin;
|
|
588
|
-
left = rect.left + rect.width / 2 - tooltip_rect.width / 2;
|
|
589
|
-
}
|
|
590
|
-
else if (placement === `left`) {
|
|
591
|
-
top = rect.top + rect.height / 2 - tooltip_rect.height / 2;
|
|
592
|
-
left = rect.left - tooltip_rect.width - margin;
|
|
593
|
-
}
|
|
594
|
-
else if (placement === `right`) {
|
|
595
|
-
top = rect.top + rect.height / 2 - tooltip_rect.height / 2;
|
|
596
|
-
left = rect.right + margin;
|
|
597
|
-
}
|
|
598
|
-
else { // bottom
|
|
599
|
-
top = rect.bottom + margin;
|
|
600
|
-
left = rect.left + rect.width / 2 - tooltip_rect.width / 2;
|
|
601
|
-
}
|
|
602
|
-
// Keep in viewport
|
|
603
|
-
left = Math.max(8, Math.min(left, globalThis.innerWidth - tooltip_rect.width - 8));
|
|
604
|
-
top = Math.max(8, Math.min(top, globalThis.innerHeight - tooltip_rect.height - 8));
|
|
605
|
-
tooltip_el.style.left = `${left + globalThis.scrollX}px`;
|
|
606
|
-
tooltip_el.style.top = `${top + globalThis.scrollY}px`;
|
|
607
|
-
const custom_opacity = trigger_styles.getPropertyValue(`--tooltip-opacity`).trim();
|
|
608
|
-
tooltip_el.style.opacity = custom_opacity || `1`;
|
|
713
|
+
resize_and_position_tooltip(tooltip_el, element);
|
|
609
714
|
current_tooltip = Object.assign(tooltip_el, { _owner: element });
|
|
610
715
|
}, options.delay || 100);
|
|
611
716
|
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
// Starry-night highlighter for mdsvex
|
|
2
|
-
import { createStarryNight } from '@wooorm/starry-night';
|
|
3
|
-
import source_css from '@wooorm/starry-night/source.css';
|
|
4
|
-
import source_js from '@wooorm/starry-night/source.js';
|
|
5
|
-
import source_json from '@wooorm/starry-night/source.json';
|
|
6
|
-
import source_shell from '@wooorm/starry-night/source.shell';
|
|
2
|
+
import { common, createStarryNight } from '@wooorm/starry-night';
|
|
7
3
|
import source_svelte from '@wooorm/starry-night/source.svelte';
|
|
8
|
-
import source_ts from '@wooorm/starry-night/source.ts';
|
|
9
|
-
import text_html_basic from '@wooorm/starry-night/text.html.basic';
|
|
10
4
|
// Escape HTML special characters in text content (not for attribute values)
|
|
11
5
|
const escape_html_text = (str) => str.replace(/&/g, `&`).replace(/</g, `<`).replace(/>/g, `>`);
|
|
12
6
|
// Convert HAST to HTML string (simplified - only handles what starry-night outputs)
|
|
@@ -14,43 +8,22 @@ export const hast_to_html = (node) => {
|
|
|
14
8
|
if (node.type === `text`)
|
|
15
9
|
return escape_html_text(node.value);
|
|
16
10
|
if (node.type === `root`)
|
|
17
|
-
return node.children
|
|
11
|
+
return node.children.map(hast_to_html).join(``);
|
|
18
12
|
const { tagName, properties, children } = node;
|
|
19
13
|
const cls = properties?.className?.join(` `);
|
|
20
14
|
const attrs = cls ? ` class="${cls}"` : ``;
|
|
21
15
|
const inner = children?.map(hast_to_html).join(``) ?? ``;
|
|
22
16
|
return `<${tagName}${attrs}>${inner}</${tagName}>`;
|
|
23
17
|
};
|
|
24
|
-
// Shared starry-night instance (grammars loaded once at build time)
|
|
25
|
-
export const starry_night = await createStarryNight([
|
|
26
|
-
source_svelte,
|
|
27
|
-
source_js,
|
|
28
|
-
source_ts,
|
|
29
|
-
source_css,
|
|
30
|
-
source_json,
|
|
31
|
-
source_shell,
|
|
32
|
-
text_html_basic,
|
|
33
|
-
]);
|
|
34
|
-
// Map code fence language to starry-night grammar scope
|
|
35
|
-
export const LANG_TO_SCOPE = {
|
|
36
|
-
svelte: `source.svelte`,
|
|
37
|
-
html: `text.html.basic`,
|
|
38
|
-
ts: `source.ts`,
|
|
39
|
-
typescript: `source.ts`,
|
|
40
|
-
js: `source.js`,
|
|
41
|
-
javascript: `source.js`,
|
|
42
|
-
css: `source.css`,
|
|
43
|
-
json: `source.json`,
|
|
44
|
-
shell: `source.shell`,
|
|
45
|
-
bash: `source.shell`,
|
|
46
|
-
sh: `source.shell`,
|
|
47
|
-
};
|
|
48
18
|
// Escape characters that would be interpreted as Svelte template syntax
|
|
49
19
|
const escape_svelte = (html) => html.replace(/\{/g, `{`).replace(/\}/g, `}`);
|
|
20
|
+
// Shared starry-night instance (grammars loaded once at build time)
|
|
21
|
+
// Uses common bundle (34 grammars) + Svelte
|
|
22
|
+
export const starry_night = await createStarryNight([...common, source_svelte]);
|
|
50
23
|
// mdsvex highlighter function
|
|
51
24
|
export function starry_night_highlighter(code, lang) {
|
|
52
25
|
const lang_key = lang?.toLowerCase();
|
|
53
|
-
const scope = lang_key ?
|
|
26
|
+
const scope = lang_key ? starry_night.flagToScope(lang_key) : undefined;
|
|
54
27
|
if (!scope) {
|
|
55
28
|
// Return escaped code if language not supported
|
|
56
29
|
const escaped = escape_svelte(escape_html_text(code));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { starry_night_highlighter } from './highlighter.js';
|
|
1
|
+
export { hast_to_html, starry_night, starry_night_highlighter } from './highlighter.js';
|
|
2
2
|
export { default as mdsvex_transform, EXAMPLE_COMPONENT_PREFIX, EXAMPLE_MODULE_PREFIX, } from './mdsvex-transform.js';
|
|
3
3
|
export { default as vite_plugin } from './vite-plugin.js';
|
|
4
4
|
import { sveltePreprocess as _sveltePreprocess } from 'svelte-preprocess';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Live examples - transforms ```svelte example code blocks into rendered components
|
|
2
2
|
// with syntax highlighting and live preview
|
|
3
|
-
export { starry_night_highlighter } from './highlighter.js';
|
|
3
|
+
export { hast_to_html, starry_night, starry_night_highlighter } from './highlighter.js';
|
|
4
4
|
export { default as mdsvex_transform, EXAMPLE_COMPONENT_PREFIX, EXAMPLE_MODULE_PREFIX, } from './mdsvex-transform.js';
|
|
5
5
|
export { default as vite_plugin } from './vite-plugin.js';
|
|
6
6
|
import { sveltePreprocess as _sveltePreprocess } from 'svelte-preprocess';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Remark plugin - transforms ```svelte example code blocks into rendered components
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { hast_to_html,
|
|
4
|
+
import { hast_to_html, starry_night } from './highlighter.js';
|
|
5
5
|
// Base64 encode to prevent preprocessors from modifying the content
|
|
6
6
|
const to_base64 = (src) => Buffer.from(src, `utf-8`).toString(`base64`);
|
|
7
7
|
// Escape backticks and template literal syntax for embedding in template literals
|
|
@@ -19,8 +19,6 @@ export const EXAMPLE_MODULE_PREFIX = `___live_example___`;
|
|
|
19
19
|
export const EXAMPLE_COMPONENT_PREFIX = `LiveExample___`;
|
|
20
20
|
// Languages that render as live Svelte components (O(1) lookup)
|
|
21
21
|
const LIVE_LANGUAGES = new Set([`svelte`, `html`]);
|
|
22
|
-
// All languages that support the `example` meta (O(1) lookup)
|
|
23
|
-
const EXAMPLE_LANGUAGES = new Set(Object.keys(LANG_TO_SCOPE));
|
|
24
22
|
// Simple tree traversal - finds all nodes of a given type
|
|
25
23
|
const visit = (tree, type, callback) => {
|
|
26
24
|
const walk = (nodes) => {
|
|
@@ -64,7 +62,7 @@ function remark(options = {}) {
|
|
|
64
62
|
};
|
|
65
63
|
const { csr, example, Wrapper } = meta;
|
|
66
64
|
// find code blocks with `example` meta in supported languages
|
|
67
|
-
if (example && node.lang &&
|
|
65
|
+
if (example && node.lang && starry_night.flagToScope(node.lang)) {
|
|
68
66
|
const is_live = LIVE_LANGUAGES.has(node.lang);
|
|
69
67
|
const wrapper_alias = is_live ? get_wrapper_alias(Wrapper ?? DEFAULT_WRAPPER) : ``;
|
|
70
68
|
const value = create_example_component(node.value || ``, meta, is_live ? examples.length : -1, // -1 for code-only (no component import needed)
|
|
@@ -144,7 +142,10 @@ function format_code(code, meta) {
|
|
|
144
142
|
}
|
|
145
143
|
function create_example_component(value, meta, index, lang, is_live, wrapper_alias) {
|
|
146
144
|
const code = format_code(value, meta);
|
|
147
|
-
const
|
|
145
|
+
const scope = starry_night.flagToScope(lang);
|
|
146
|
+
if (!scope)
|
|
147
|
+
throw new Error(`Unsupported language: ${lang}`);
|
|
148
|
+
const tree = starry_night.highlight(code, scope);
|
|
148
149
|
// Convert newlines to to prevent bundlers from stripping whitespace
|
|
149
150
|
const highlighted = hast_to_html(tree).replace(/\n/g, ` `);
|
|
150
151
|
// Code-only examples (ts, js, css, etc.) - just render highlighted code block
|
package/dist/types.d.ts
CHANGED
|
@@ -160,7 +160,7 @@ export interface MultiSelectProps<T extends Option = Option> extends MultiSelect
|
|
|
160
160
|
disabled?: boolean;
|
|
161
161
|
disabledInputTitle?: string;
|
|
162
162
|
duplicateOptionMsg?: string;
|
|
163
|
-
duplicates?: boolean
|
|
163
|
+
duplicates?: boolean | `case-insensitive`;
|
|
164
164
|
keepSelectedInDropdown?: false | `plain` | `checkboxes`;
|
|
165
165
|
key?: (opt: T) => unknown;
|
|
166
166
|
filterFunc?: (opt: T, searchText: string) => boolean;
|
package/dist/utils.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ export declare const has_group: <T extends Option>(opt: T) => opt is T & {
|
|
|
5
5
|
group: string;
|
|
6
6
|
};
|
|
7
7
|
export declare const get_label: (opt: Option) => string | number;
|
|
8
|
+
export declare const get_option_key: (opt: Option) => unknown;
|
|
8
9
|
export declare function get_style(option: Option, key?: `selected` | `option` | null | undefined): string;
|
|
9
10
|
export declare function fuzzy_match(search_text: string, target_text: string): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -25,6 +25,10 @@ export const get_label = (opt) => {
|
|
|
25
25
|
}
|
|
26
26
|
return `${opt}`;
|
|
27
27
|
};
|
|
28
|
+
// Generate a unique key for an option, preserving value identity
|
|
29
|
+
// For object options: uses value if defined, otherwise label (no case normalization)
|
|
30
|
+
// For primitives: the primitive itself
|
|
31
|
+
export const get_option_key = (opt) => is_object(opt) ? opt.value ?? get_label(opt) : opt;
|
|
28
32
|
// This function is used extract CSS strings from a {selected, option} style
|
|
29
33
|
// object to be used in the style attribute of the option.
|
|
30
34
|
// If the style is a string, it will be returned as is
|
package/package.json
CHANGED