svelte-multiselect 11.5.1 → 11.5.2
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 +1 -1
- package/dist/MultiSelect.svelte +117 -67
- package/dist/Nav.svelte +317 -134
- package/dist/Nav.svelte.d.ts +38 -37
- package/dist/Wiggle.svelte +9 -2
- package/dist/Wiggle.svelte.d.ts +7 -4
- package/dist/attachments.d.ts +5 -1
- package/dist/attachments.js +161 -108
- package/dist/heading-anchors.d.ts +14 -0
- package/dist/heading-anchors.js +120 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +30 -0
- package/package.json +18 -17
- package/readme.md +25 -1
package/dist/Nav.svelte.d.ts
CHANGED
|
@@ -1,42 +1,43 @@
|
|
|
1
1
|
import type { Page } from '@sveltejs/kit';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
href: string;
|
|
14
|
-
label: string;
|
|
15
|
-
}]>;
|
|
16
|
-
menu_props?: HTMLAttributes<HTMLDivElement>;
|
|
17
|
-
link_props?: HTMLAttributes<HTMLAnchorElement>;
|
|
18
|
-
page?: Page;
|
|
19
|
-
labels?: Record<string, string>;
|
|
20
|
-
} & Omit<HTMLAttributes<HTMLElement>, "children">;
|
|
21
|
-
exports: {};
|
|
22
|
-
bindings: "";
|
|
23
|
-
slots: {};
|
|
24
|
-
events: {};
|
|
25
|
-
};
|
|
26
|
-
declare class __sveltets_Render<Route extends string | [string, string] | [string, string[]]> {
|
|
27
|
-
props(): ReturnType<typeof $$render<Route>>['props'];
|
|
28
|
-
events(): ReturnType<typeof $$render<Route>>['events'];
|
|
29
|
-
slots(): ReturnType<typeof $$render<Route>>['slots'];
|
|
30
|
-
bindings(): "";
|
|
31
|
-
exports(): {};
|
|
4
|
+
import { type TooltipOptions } from './attachments';
|
|
5
|
+
import type { NavRoute, NavRouteObject } from './types';
|
|
6
|
+
interface ItemSnippetParams {
|
|
7
|
+
route: NavRouteObject;
|
|
8
|
+
href: string;
|
|
9
|
+
label: string;
|
|
10
|
+
is_active: boolean;
|
|
11
|
+
is_dropdown: boolean;
|
|
12
|
+
render_default: Snippet;
|
|
32
13
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
14
|
+
type $$ComponentProps = {
|
|
15
|
+
routes: NavRoute[];
|
|
16
|
+
children?: Snippet<[{
|
|
17
|
+
is_open: boolean;
|
|
18
|
+
panel_id: string;
|
|
19
|
+
routes: NavRoute[];
|
|
20
|
+
}]>;
|
|
21
|
+
item?: Snippet<[ItemSnippetParams]>;
|
|
22
|
+
link?: Snippet<[{
|
|
23
|
+
href: string;
|
|
24
|
+
label: string;
|
|
25
|
+
}]>;
|
|
26
|
+
menu_props?: HTMLAttributes<HTMLDivElement>;
|
|
27
|
+
link_props?: HTMLAttributes<HTMLAnchorElement>;
|
|
28
|
+
page?: Page;
|
|
29
|
+
labels?: Record<string, string>;
|
|
30
|
+
tooltips?: Record<string, string | Omit<TooltipOptions, `disabled`>>;
|
|
31
|
+
tooltip_options?: Omit<TooltipOptions, `content`>;
|
|
32
|
+
breakpoint?: number;
|
|
33
|
+
onnavigate?: (data: {
|
|
34
|
+
href: string;
|
|
35
|
+
event: MouseEvent;
|
|
36
|
+
route: NavRouteObject;
|
|
37
|
+
}) => void | false;
|
|
38
|
+
onopen?: () => void;
|
|
39
|
+
onclose?: () => void;
|
|
40
|
+
} & Omit<HTMLAttributes<HTMLElementTagNameMap[`nav`]>, `children`>;
|
|
41
|
+
declare const Nav: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
42
|
+
type Nav = ReturnType<typeof Nav>;
|
|
42
43
|
export default Nav;
|
package/dist/Wiggle.svelte
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<script lang="ts">import { Spring } from 'svelte/motion';
|
|
2
|
-
let { wiggle = $bindable(false), angle = 0, scale = 1, dx = 0, dy = 0, duration = 200,
|
|
3
|
-
const store = Spring.of(() => (wiggle ? { scale, angle, dx, dy } : { angle: 0, scale: 1, dx: 0, dy: 0 }),
|
|
2
|
+
let { wiggle = $bindable(false), angle = 0, scale = 1, dx = 0, dy = 0, duration = 200, spring_options = $bindable({ stiffness: 0.05, damping: 0.1 }), children, ...rest } = $props();
|
|
3
|
+
const store = Spring.of(() => (wiggle ? { scale, angle, dx, dy } : { angle: 0, scale: 1, dx: 0, dy: 0 }), spring_options);
|
|
4
|
+
// update spring physics when options change
|
|
5
|
+
$effect(() => {
|
|
6
|
+
store.stiffness = spring_options.stiffness;
|
|
7
|
+
store.damping = spring_options.damping;
|
|
8
|
+
});
|
|
4
9
|
$effect.pre(() => {
|
|
5
10
|
if (wiggle)
|
|
6
11
|
setTimeout(() => (wiggle = false), duration);
|
|
@@ -8,6 +13,8 @@ $effect.pre(() => {
|
|
|
8
13
|
</script>
|
|
9
14
|
|
|
10
15
|
<span
|
|
16
|
+
{...rest}
|
|
17
|
+
style:display="inline-block"
|
|
11
18
|
style:transform="rotate({store.current.angle}deg) scale({store.current.scale})
|
|
12
19
|
translate({store.current.dx}px, {store.current.dy}px)"
|
|
13
20
|
>
|
package/dist/Wiggle.svelte.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
type
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type $$ComponentProps = HTMLAttributes<HTMLSpanElement> & {
|
|
3
4
|
wiggle?: boolean;
|
|
4
5
|
angle?: number;
|
|
5
6
|
scale?: number;
|
|
6
7
|
dx?: number;
|
|
7
8
|
dy?: number;
|
|
8
9
|
duration?: number;
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
spring_options?: {
|
|
11
|
+
stiffness: number;
|
|
12
|
+
damping: number;
|
|
13
|
+
};
|
|
11
14
|
children?: Snippet;
|
|
12
15
|
};
|
|
13
|
-
declare const Wiggle: import("svelte").Component<$$ComponentProps, {}, "wiggle">;
|
|
16
|
+
declare const Wiggle: import("svelte").Component<$$ComponentProps, {}, "wiggle" | "spring_options">;
|
|
14
17
|
type Wiggle = ReturnType<typeof Wiggle>;
|
|
15
18
|
export default Wiggle;
|
package/dist/attachments.d.ts
CHANGED
|
@@ -56,8 +56,12 @@ export interface TooltipOptions {
|
|
|
56
56
|
content?: string;
|
|
57
57
|
placement?: `top` | `bottom` | `left` | `right`;
|
|
58
58
|
delay?: number;
|
|
59
|
-
|
|
59
|
+
hide_delay?: number;
|
|
60
|
+
disabled?: boolean | `touch-devices`;
|
|
60
61
|
style?: string;
|
|
62
|
+
show_arrow?: boolean;
|
|
63
|
+
offset?: number;
|
|
64
|
+
allow_html?: boolean;
|
|
61
65
|
}
|
|
62
66
|
export declare const tooltip: (options?: TooltipOptions) => Attachment;
|
|
63
67
|
export type ClickOutsideConfig<T extends HTMLElement> = {
|
package/dist/attachments.js
CHANGED
|
@@ -358,35 +358,46 @@ function clear_tooltip() {
|
|
|
358
358
|
if (hide_timeout)
|
|
359
359
|
clearTimeout(hide_timeout);
|
|
360
360
|
if (current_tooltip) {
|
|
361
|
+
// Remove aria-describedby from the trigger element
|
|
362
|
+
current_tooltip._owner?.removeAttribute(`aria-describedby`);
|
|
361
363
|
current_tooltip.remove();
|
|
362
364
|
current_tooltip = null;
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
367
|
export const tooltip = (options = {}) => (node) => {
|
|
366
|
-
//
|
|
367
|
-
if (
|
|
368
|
+
// SSR guard + element validation
|
|
369
|
+
if (typeof document === `undefined` || !(node instanceof HTMLElement))
|
|
368
370
|
return;
|
|
369
|
-
// Handle null/undefined options
|
|
370
|
-
const safe_options = options || {};
|
|
371
371
|
const cleanup_functions = [];
|
|
372
|
+
// Handle disabled option
|
|
373
|
+
if (options.disabled === true)
|
|
374
|
+
return;
|
|
375
|
+
// Track current input method for 'touch-devices' option (runtime detection, not capability sniffing)
|
|
376
|
+
// This allows tooltips on hybrid devices (Surface, iPad with mouse) when using mouse/stylus
|
|
377
|
+
let last_pointer_type = `mouse`;
|
|
378
|
+
const track_pointer = (event) => {
|
|
379
|
+
last_pointer_type = event.pointerType;
|
|
380
|
+
};
|
|
381
|
+
if (options.disabled === `touch-devices`) {
|
|
382
|
+
document.addEventListener(`pointerdown`, track_pointer, true);
|
|
383
|
+
cleanup_functions.push(() => document.removeEventListener(`pointerdown`, track_pointer, true));
|
|
384
|
+
}
|
|
372
385
|
function setup_tooltip(element) {
|
|
373
|
-
if (!element || safe_options.disabled)
|
|
374
|
-
return;
|
|
375
386
|
// Use let so content can be updated reactively
|
|
376
|
-
let content =
|
|
387
|
+
let content = options.content || element.title ||
|
|
377
388
|
element.getAttribute(`aria-label`) || element.getAttribute(`data-title`);
|
|
378
389
|
if (!content)
|
|
379
390
|
return;
|
|
380
391
|
// Store original title and remove it to prevent default tooltip
|
|
381
392
|
// Only store title if we're not using custom content
|
|
382
|
-
if (element.title && !
|
|
393
|
+
if (element.title && !options.content) {
|
|
383
394
|
element.setAttribute(`data-original-title`, element.title);
|
|
384
395
|
element.removeAttribute(`title`);
|
|
385
396
|
}
|
|
386
397
|
// Reactively update content when tooltip attributes change
|
|
387
398
|
const tooltip_attrs = [`title`, `aria-label`, `data-title`];
|
|
388
399
|
const observer = new MutationObserver((mutations) => {
|
|
389
|
-
if (
|
|
400
|
+
if (options.content)
|
|
390
401
|
return; // custom content takes precedence
|
|
391
402
|
for (const { type, attributeName } of mutations) {
|
|
392
403
|
if (type !== `attributes` || !attributeName)
|
|
@@ -408,20 +419,33 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
408
419
|
content = new_content;
|
|
409
420
|
// Only update tooltip if this element owns it
|
|
410
421
|
if (current_tooltip?._owner === element) {
|
|
411
|
-
|
|
422
|
+
if (options.allow_html !== false) {
|
|
423
|
+
current_tooltip.innerHTML = content.replace(/\r/g, `<br/>`);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
current_tooltip.textContent = content;
|
|
427
|
+
}
|
|
412
428
|
}
|
|
413
429
|
}
|
|
414
430
|
});
|
|
415
431
|
observer.observe(element, { attributes: true, attributeFilter: tooltip_attrs });
|
|
416
432
|
function show_tooltip() {
|
|
433
|
+
// Skip tooltip on touch input when 'touch-devices' option is set
|
|
434
|
+
if (options.disabled === `touch-devices` && last_pointer_type === `touch`)
|
|
435
|
+
return;
|
|
417
436
|
clear_tooltip();
|
|
418
437
|
show_timeout = setTimeout(() => {
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
const placement =
|
|
422
|
-
|
|
438
|
+
const tooltip_el = document.createElement(`div`);
|
|
439
|
+
tooltip_el.className = `custom-tooltip`;
|
|
440
|
+
const placement = options.placement || `bottom`;
|
|
441
|
+
tooltip_el.setAttribute(`data-placement`, placement);
|
|
442
|
+
// Accessibility: link tooltip to trigger element
|
|
443
|
+
const tooltip_id = `tooltip-${crypto.randomUUID()}`;
|
|
444
|
+
tooltip_el.id = tooltip_id;
|
|
445
|
+
tooltip_el.setAttribute(`role`, `tooltip`);
|
|
446
|
+
element.setAttribute(`aria-describedby`, tooltip_id);
|
|
423
447
|
// Apply base styles
|
|
424
|
-
|
|
448
|
+
tooltip_el.style.cssText = `
|
|
425
449
|
position: absolute; z-index: 9999; opacity: 0;
|
|
426
450
|
background: var(--tooltip-bg, #333); color: var(--text-color, white); border: var(--tooltip-border, none);
|
|
427
451
|
padding: var(--tooltip-padding, 6px 10px); border-radius: var(--tooltip-radius, 6px); font-size: var(--tooltip-font-size, 13px); line-height: 1.4;
|
|
@@ -429,16 +453,22 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
429
453
|
filter: var(--tooltip-shadow, drop-shadow(0 2px 8px rgba(0,0,0,0.25))); transition: opacity 0.15s ease-out;
|
|
430
454
|
`;
|
|
431
455
|
// Apply custom styles if provided (these will override base styles due to CSS specificity)
|
|
432
|
-
if (
|
|
456
|
+
if (options.style) {
|
|
433
457
|
// Parse and apply custom styles as individual properties for better control
|
|
434
|
-
const custom_styles =
|
|
458
|
+
const custom_styles = options.style.split(`;`).filter((style) => style.trim());
|
|
435
459
|
custom_styles.forEach((style) => {
|
|
436
460
|
const [property, value] = style.split(`:`).map((s) => s.trim());
|
|
437
461
|
if (property && value)
|
|
438
|
-
|
|
462
|
+
tooltip_el.style.setProperty(property, value);
|
|
439
463
|
});
|
|
440
464
|
}
|
|
441
|
-
|
|
465
|
+
// Security: allow_html defaults to true; set to false for plain text rendering
|
|
466
|
+
if (options.allow_html !== false) {
|
|
467
|
+
tooltip_el.innerHTML = content?.replace(/\r/g, `<br/>`) ?? ``;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
tooltip_el.textContent = content ?? ``;
|
|
471
|
+
}
|
|
442
472
|
// Mirror CSS custom properties from the trigger node onto the tooltip element
|
|
443
473
|
const trigger_styles = getComputedStyle(element);
|
|
444
474
|
[
|
|
@@ -455,97 +485,100 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
455
485
|
].forEach((name) => {
|
|
456
486
|
const value = trigger_styles.getPropertyValue(name).trim();
|
|
457
487
|
if (value)
|
|
458
|
-
|
|
488
|
+
tooltip_el.style.setProperty(name, value);
|
|
459
489
|
});
|
|
460
490
|
// Append early so we can read computed border styles for arrow border
|
|
461
|
-
document.body.appendChild(
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const arrow_size_raw = trigger_styles.getPropertyValue(`--tooltip-arrow-size`)
|
|
469
|
-
.trim();
|
|
470
|
-
const arrow_size_num = Number.parseInt(arrow_size_raw || ``, 10);
|
|
471
|
-
const arrow_px = Number.isFinite(arrow_size_num) ? arrow_size_num : 6;
|
|
472
|
-
const border_color = tooltip_styles.borderTopColor;
|
|
473
|
-
const border_width_num = Number.parseFloat(tooltip_styles.borderTopWidth || `0`);
|
|
474
|
-
const has_border = !!border_color && border_color !== `rgba(0, 0, 0, 0)` &&
|
|
475
|
-
border_width_num > 0;
|
|
476
|
-
const maybe_append_border_arrow = () => {
|
|
477
|
-
if (!has_border)
|
|
478
|
-
return;
|
|
479
|
-
const border_arrow = document.createElement(`div`);
|
|
480
|
-
border_arrow.className = `custom-tooltip-arrow-border`;
|
|
481
|
-
border_arrow.style.cssText =
|
|
491
|
+
document.body.appendChild(tooltip_el);
|
|
492
|
+
// Create arrow elements only if show_arrow is not false
|
|
493
|
+
if (options.show_arrow !== false) {
|
|
494
|
+
const tooltip_styles = getComputedStyle(tooltip_el);
|
|
495
|
+
const arrow = document.createElement(`div`);
|
|
496
|
+
arrow.className = `custom-tooltip-arrow`;
|
|
497
|
+
arrow.style.cssText =
|
|
482
498
|
`position: absolute; width: 0; height: 0; pointer-events: none;`;
|
|
483
|
-
const
|
|
499
|
+
const arrow_size_raw = trigger_styles.getPropertyValue(`--tooltip-arrow-size`)
|
|
500
|
+
.trim();
|
|
501
|
+
const arrow_size_num = Number.parseInt(arrow_size_raw || ``, 10);
|
|
502
|
+
const arrow_px = Number.isFinite(arrow_size_num) ? arrow_size_num : 6;
|
|
503
|
+
const border_color = tooltip_styles.borderTopColor;
|
|
504
|
+
const border_width_num = Number.parseFloat(tooltip_styles.borderTopWidth || `0`);
|
|
505
|
+
const has_border = !!border_color && border_color !== `rgba(0, 0, 0, 0)` &&
|
|
506
|
+
border_width_num > 0;
|
|
507
|
+
// Helper to create border arrow behind fill arrow
|
|
508
|
+
const append_border_arrow = () => {
|
|
509
|
+
if (!has_border)
|
|
510
|
+
return;
|
|
511
|
+
const border_arrow = document.createElement(`div`);
|
|
512
|
+
border_arrow.className = `custom-tooltip-arrow-border`;
|
|
513
|
+
border_arrow.style.cssText =
|
|
514
|
+
`position: absolute; width: 0; height: 0; pointer-events: none;`;
|
|
515
|
+
const border_size = arrow_px + (border_width_num * 1.4);
|
|
516
|
+
if (placement === `top`) {
|
|
517
|
+
border_arrow.style.left = `calc(50% - ${border_size}px)`;
|
|
518
|
+
border_arrow.style.bottom = `-${border_size}px`;
|
|
519
|
+
border_arrow.style.borderLeft = `${border_size}px solid transparent`;
|
|
520
|
+
border_arrow.style.borderRight = `${border_size}px solid transparent`;
|
|
521
|
+
border_arrow.style.borderTop = `${border_size}px solid ${border_color}`;
|
|
522
|
+
}
|
|
523
|
+
else if (placement === `left`) {
|
|
524
|
+
border_arrow.style.top = `calc(50% - ${border_size}px)`;
|
|
525
|
+
border_arrow.style.right = `-${border_size}px`;
|
|
526
|
+
border_arrow.style.borderTop = `${border_size}px solid transparent`;
|
|
527
|
+
border_arrow.style.borderBottom = `${border_size}px solid transparent`;
|
|
528
|
+
border_arrow.style.borderLeft = `${border_size}px solid ${border_color}`;
|
|
529
|
+
}
|
|
530
|
+
else if (placement === `right`) {
|
|
531
|
+
border_arrow.style.top = `calc(50% - ${border_size}px)`;
|
|
532
|
+
border_arrow.style.left = `-${border_size}px`;
|
|
533
|
+
border_arrow.style.borderTop = `${border_size}px solid transparent`;
|
|
534
|
+
border_arrow.style.borderBottom = `${border_size}px solid transparent`;
|
|
535
|
+
border_arrow.style.borderRight = `${border_size}px solid ${border_color}`;
|
|
536
|
+
}
|
|
537
|
+
else { // bottom
|
|
538
|
+
border_arrow.style.left = `calc(50% - ${border_size}px)`;
|
|
539
|
+
border_arrow.style.top = `-${border_size}px`;
|
|
540
|
+
border_arrow.style.borderLeft = `${border_size}px solid transparent`;
|
|
541
|
+
border_arrow.style.borderRight = `${border_size}px solid transparent`;
|
|
542
|
+
border_arrow.style.borderBottom = `${border_size}px solid ${border_color}`;
|
|
543
|
+
}
|
|
544
|
+
tooltip_el.appendChild(border_arrow);
|
|
545
|
+
};
|
|
546
|
+
// Style and position fill arrow
|
|
484
547
|
if (placement === `top`) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
548
|
+
arrow.style.left = `calc(50% - ${arrow_px}px)`;
|
|
549
|
+
arrow.style.bottom = `-${arrow_px}px`;
|
|
550
|
+
arrow.style.borderLeft = `${arrow_px}px solid transparent`;
|
|
551
|
+
arrow.style.borderRight = `${arrow_px}px solid transparent`;
|
|
552
|
+
arrow.style.borderTop = `${arrow_px}px solid var(--tooltip-bg, #333)`;
|
|
490
553
|
}
|
|
491
554
|
else if (placement === `left`) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
555
|
+
arrow.style.top = `calc(50% - ${arrow_px}px)`;
|
|
556
|
+
arrow.style.right = `-${arrow_px}px`;
|
|
557
|
+
arrow.style.borderTop = `${arrow_px}px solid transparent`;
|
|
558
|
+
arrow.style.borderBottom = `${arrow_px}px solid transparent`;
|
|
559
|
+
arrow.style.borderLeft = `${arrow_px}px solid var(--tooltip-bg, #333)`;
|
|
497
560
|
}
|
|
498
561
|
else if (placement === `right`) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
562
|
+
arrow.style.top = `calc(50% - ${arrow_px}px)`;
|
|
563
|
+
arrow.style.left = `-${arrow_px}px`;
|
|
564
|
+
arrow.style.borderTop = `${arrow_px}px solid transparent`;
|
|
565
|
+
arrow.style.borderBottom = `${arrow_px}px solid transparent`;
|
|
566
|
+
arrow.style.borderRight = `${arrow_px}px solid var(--tooltip-bg, #333)`;
|
|
504
567
|
}
|
|
505
568
|
else { // bottom
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
569
|
+
arrow.style.left = `calc(50% - ${arrow_px}px)`;
|
|
570
|
+
arrow.style.top = `-${arrow_px}px`;
|
|
571
|
+
arrow.style.borderLeft = `${arrow_px}px solid transparent`;
|
|
572
|
+
arrow.style.borderRight = `${arrow_px}px solid transparent`;
|
|
573
|
+
arrow.style.borderBottom = `${arrow_px}px solid var(--tooltip-bg, #333)`;
|
|
511
574
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
// Create the fill arrow on top
|
|
515
|
-
if (placement === `top`) {
|
|
516
|
-
arrow.style.left = `calc(50% - ${arrow_px}px)`;
|
|
517
|
-
arrow.style.bottom = `-${arrow_px}px`;
|
|
518
|
-
arrow.style.borderLeft = `${arrow_px}px solid transparent`;
|
|
519
|
-
arrow.style.borderRight = `${arrow_px}px solid transparent`;
|
|
520
|
-
arrow.style.borderTop = `${arrow_px}px solid var(--tooltip-bg, #333)`;
|
|
575
|
+
append_border_arrow();
|
|
576
|
+
tooltip_el.appendChild(arrow);
|
|
521
577
|
}
|
|
522
|
-
else if (placement === `left`) {
|
|
523
|
-
arrow.style.top = `calc(50% - ${arrow_px}px)`;
|
|
524
|
-
arrow.style.right = `-${arrow_px}px`;
|
|
525
|
-
arrow.style.borderTop = `${arrow_px}px solid transparent`;
|
|
526
|
-
arrow.style.borderBottom = `${arrow_px}px solid transparent`;
|
|
527
|
-
arrow.style.borderLeft = `${arrow_px}px solid var(--tooltip-bg, #333)`;
|
|
528
|
-
}
|
|
529
|
-
else if (placement === `right`) {
|
|
530
|
-
arrow.style.top = `calc(50% - ${arrow_px}px)`;
|
|
531
|
-
arrow.style.left = `-${arrow_px}px`;
|
|
532
|
-
arrow.style.borderTop = `${arrow_px}px solid transparent`;
|
|
533
|
-
arrow.style.borderBottom = `${arrow_px}px solid transparent`;
|
|
534
|
-
arrow.style.borderRight = `${arrow_px}px solid var(--tooltip-bg, #333)`;
|
|
535
|
-
}
|
|
536
|
-
else { // bottom
|
|
537
|
-
arrow.style.left = `calc(50% - ${arrow_px}px)`;
|
|
538
|
-
arrow.style.top = `-${arrow_px}px`;
|
|
539
|
-
arrow.style.borderLeft = `${arrow_px}px solid transparent`;
|
|
540
|
-
arrow.style.borderRight = `${arrow_px}px solid transparent`;
|
|
541
|
-
arrow.style.borderBottom = `${arrow_px}px solid var(--tooltip-bg, #333)`;
|
|
542
|
-
}
|
|
543
|
-
maybe_append_border_arrow();
|
|
544
|
-
tooltip.appendChild(arrow);
|
|
545
578
|
// Position tooltip
|
|
546
579
|
const rect = element.getBoundingClientRect();
|
|
547
|
-
const tooltip_rect =
|
|
548
|
-
const margin = 12;
|
|
580
|
+
const tooltip_rect = tooltip_el.getBoundingClientRect();
|
|
581
|
+
const margin = options.offset ?? 12;
|
|
549
582
|
let top = 0, left = 0;
|
|
550
583
|
if (placement === `top`) {
|
|
551
584
|
top = rect.top - tooltip_rect.height - margin;
|
|
@@ -566,22 +599,26 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
566
599
|
// Keep in viewport
|
|
567
600
|
left = Math.max(8, Math.min(left, globalThis.innerWidth - tooltip_rect.width - 8));
|
|
568
601
|
top = Math.max(8, Math.min(top, globalThis.innerHeight - tooltip_rect.height - 8));
|
|
569
|
-
|
|
570
|
-
|
|
602
|
+
tooltip_el.style.left = `${left + globalThis.scrollX}px`;
|
|
603
|
+
tooltip_el.style.top = `${top + globalThis.scrollY}px`;
|
|
571
604
|
const custom_opacity = trigger_styles.getPropertyValue(`--tooltip-opacity`).trim();
|
|
572
|
-
|
|
573
|
-
current_tooltip = Object.assign(
|
|
574
|
-
},
|
|
605
|
+
tooltip_el.style.opacity = custom_opacity || `1`;
|
|
606
|
+
current_tooltip = Object.assign(tooltip_el, { _owner: element });
|
|
607
|
+
}, options.delay || 100);
|
|
608
|
+
}
|
|
609
|
+
function handle_keydown(event) {
|
|
610
|
+
if (event.key === `Escape` && current_tooltip?._owner === element)
|
|
611
|
+
clear_tooltip();
|
|
575
612
|
}
|
|
576
613
|
function hide_tooltip() {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
current_tooltip = null;
|
|
583
|
-
}
|
|
614
|
+
if (show_timeout)
|
|
615
|
+
clearTimeout(show_timeout);
|
|
616
|
+
const delay = options.hide_delay ?? 0;
|
|
617
|
+
if (delay > 0) {
|
|
618
|
+
hide_timeout = setTimeout(() => clear_tooltip(), delay);
|
|
584
619
|
}
|
|
620
|
+
else
|
|
621
|
+
clear_tooltip();
|
|
585
622
|
}
|
|
586
623
|
function handle_scroll(event) {
|
|
587
624
|
// Hide if document or any ancestor scrolls (would move element). Skip internal element scrolls.
|
|
@@ -595,10 +632,26 @@ export const tooltip = (options = {}) => (node) => {
|
|
|
595
632
|
events.forEach((event, idx) => element.addEventListener(event, handlers[idx]));
|
|
596
633
|
// Hide tooltip when user scrolls the page (not element-level scrolls like input fields)
|
|
597
634
|
globalThis.addEventListener(`scroll`, handle_scroll, true);
|
|
635
|
+
// Add Escape key listener to dismiss tooltip
|
|
636
|
+
document.addEventListener(`keydown`, handle_keydown);
|
|
637
|
+
// Watch for element removal from DOM to prevent orphaned tooltips
|
|
638
|
+
const removal_observer = new MutationObserver((mutations) => {
|
|
639
|
+
const was_removed = mutations.some((mut) => Array.from(mut.removedNodes).some((node) => node === element || (node instanceof Element && node.contains(element))));
|
|
640
|
+
if (was_removed && current_tooltip?._owner === element)
|
|
641
|
+
clear_tooltip();
|
|
642
|
+
});
|
|
643
|
+
if (element.parentElement) {
|
|
644
|
+
removal_observer.observe(element.parentElement, { childList: true, subtree: true });
|
|
645
|
+
}
|
|
598
646
|
return () => {
|
|
599
647
|
observer.disconnect();
|
|
648
|
+
removal_observer.disconnect();
|
|
600
649
|
events.forEach((event, idx) => element.removeEventListener(event, handlers[idx]));
|
|
601
650
|
globalThis.removeEventListener(`scroll`, handle_scroll, true);
|
|
651
|
+
document.removeEventListener(`keydown`, handle_keydown);
|
|
652
|
+
// Clear tooltip if this element owns it (also removes aria-describedby)
|
|
653
|
+
if (current_tooltip?._owner === element)
|
|
654
|
+
clear_tooltip();
|
|
602
655
|
const original_title = element.getAttribute(`data-original-title`);
|
|
603
656
|
if (original_title) {
|
|
604
657
|
element.setAttribute(`title`, original_title);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** @type {() => import('svelte/compiler').PreprocessorGroup} */
|
|
2
|
+
export declare function heading_ids(): {
|
|
3
|
+
name: string;
|
|
4
|
+
markup({ content }: {
|
|
5
|
+
content: string;
|
|
6
|
+
}): {
|
|
7
|
+
code: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export interface HeadingAnchorsOptions {
|
|
11
|
+
selector?: string;
|
|
12
|
+
icon_svg?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const heading_anchors: (options?: HeadingAnchorsOptions) => (node: Element) => (() => void) | undefined;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Svelte preprocessor that adds IDs to headings at build time for SSR support
|
|
2
|
+
// This ensures fragment navigation (#heading-id) works on initial page load
|
|
3
|
+
// Match headings in two contexts:
|
|
4
|
+
// 1. Start of line (for .svelte files with formatted HTML)
|
|
5
|
+
// 2. After > (for mdsvex output where HTML is on single line, e.g., "</p> <h2>")
|
|
6
|
+
// Avoid matching inside src={...} attributes by requiring these specific contexts
|
|
7
|
+
// Note: [^>]* for attributes won't match if an attribute value contains > (e.g., data-foo="a>b")
|
|
8
|
+
// This edge case is rare in practice and would require significantly more complex parsing
|
|
9
|
+
const heading_regex_line_start = /^(\s*)<(h[2-6])([^>]*)>([\s\S]*?)<\/\2>/gim;
|
|
10
|
+
const heading_regex_after_tag = /(>)(\s*)<(h[2-6])([^>]*)>([\s\S]*?)<\/\3>/gi;
|
|
11
|
+
// Remove Svelte expressions handling nested braces (e.g., {fn({a: 1})})
|
|
12
|
+
// Treats unmatched } as literal text to avoid dropping content
|
|
13
|
+
function strip_svelte_expressions(str) {
|
|
14
|
+
let result = ``;
|
|
15
|
+
let depth = 0;
|
|
16
|
+
for (const char of str) {
|
|
17
|
+
if (char === `{`)
|
|
18
|
+
depth++;
|
|
19
|
+
else if (char === `}` && depth > 0)
|
|
20
|
+
depth--;
|
|
21
|
+
else if (depth === 0)
|
|
22
|
+
result += char;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
// Generate URL-friendly slug from text
|
|
27
|
+
const slugify = (text) => text
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/\s+/g, `-`)
|
|
30
|
+
.replace(/[^\w-]/g, ``)
|
|
31
|
+
.replace(/-+/g, `-`) // collapse multiple dashes
|
|
32
|
+
.replace(/^-|-$/g, ``); // trim leading/trailing dashes
|
|
33
|
+
/** @type {() => import('svelte/compiler').PreprocessorGroup} */
|
|
34
|
+
export function heading_ids() {
|
|
35
|
+
return {
|
|
36
|
+
name: `heading-ids`,
|
|
37
|
+
markup({ content }) {
|
|
38
|
+
const seen_ids = new Map();
|
|
39
|
+
let result = content;
|
|
40
|
+
const process_heading = (attrs, inner) => {
|
|
41
|
+
// Skip if already has an id (use ^|\s to avoid matching data-id, aria-id, etc.)
|
|
42
|
+
if (/(^|\s)id\s*=/.test(attrs))
|
|
43
|
+
return null;
|
|
44
|
+
const text = strip_svelte_expressions(inner.replace(/<[^>]+>/g, ``)).trim();
|
|
45
|
+
if (!text)
|
|
46
|
+
return null;
|
|
47
|
+
const base_id = slugify(text);
|
|
48
|
+
if (!base_id)
|
|
49
|
+
return null;
|
|
50
|
+
// Handle duplicates within same file
|
|
51
|
+
const count = seen_ids.get(base_id) ?? 0;
|
|
52
|
+
seen_ids.set(base_id, count + 1);
|
|
53
|
+
return count ? `${base_id}-${count}` : base_id;
|
|
54
|
+
};
|
|
55
|
+
// Pass 1: Match headings at start of line (for .svelte files)
|
|
56
|
+
result = result.replace(heading_regex_line_start, (match, indent, tag, attrs, inner) => {
|
|
57
|
+
const id = process_heading(attrs, inner);
|
|
58
|
+
return id ? `${indent}<${tag} id="${id}"${attrs}>${inner}</${tag}>` : match;
|
|
59
|
+
});
|
|
60
|
+
// Pass 2: Match headings after closing tag (for mdsvex single-line output)
|
|
61
|
+
result = result.replace(heading_regex_after_tag, (match, gt, space, tag, attrs, inner) => {
|
|
62
|
+
const id = process_heading(attrs, inner);
|
|
63
|
+
return id ? `${gt}${space}<${tag} id="${id}"${attrs}>${inner}</${tag}>` : match;
|
|
64
|
+
});
|
|
65
|
+
return { code: result };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// SVG link icon for heading anchors
|
|
70
|
+
const link_svg = `<svg width="16" height="16" viewBox="0 0 16 16" aria-label="Link to heading" role="img"><path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0z" fill="currentColor"/></svg>`;
|
|
71
|
+
// Add anchor link to a single heading element
|
|
72
|
+
function add_anchor_to_heading(heading, icon_svg = link_svg) {
|
|
73
|
+
if (heading.querySelector(`a[aria-hidden="true"]`))
|
|
74
|
+
return;
|
|
75
|
+
if (!heading.id) {
|
|
76
|
+
// Generate ID from text content (fallback for dynamic headings)
|
|
77
|
+
const base_id = slugify((heading.textContent ?? ``).trim());
|
|
78
|
+
if (!base_id)
|
|
79
|
+
return;
|
|
80
|
+
// Ensure unique ID in document
|
|
81
|
+
let counter = 0;
|
|
82
|
+
while (document.getElementById(counter ? `${base_id}-${counter}` : base_id))
|
|
83
|
+
counter++;
|
|
84
|
+
heading.id = counter ? `${base_id}-${counter}` : base_id;
|
|
85
|
+
}
|
|
86
|
+
const anchor = document.createElement(`a`);
|
|
87
|
+
anchor.href = `#${heading.id}`;
|
|
88
|
+
anchor.setAttribute(`aria-hidden`, `true`);
|
|
89
|
+
anchor.innerHTML = icon_svg;
|
|
90
|
+
heading.appendChild(anchor);
|
|
91
|
+
}
|
|
92
|
+
// Svelte 5 attachment that adds anchor links to headings within a container
|
|
93
|
+
// Uses MutationObserver to handle dynamically added headings
|
|
94
|
+
export const heading_anchors = (options = {}) => (node) => {
|
|
95
|
+
if (typeof document === `undefined`)
|
|
96
|
+
return;
|
|
97
|
+
const selector = options.selector ?? `h2, h3, h4, h5, h6`;
|
|
98
|
+
const icon_svg = options.icon_svg ?? link_svg;
|
|
99
|
+
// Process existing headings
|
|
100
|
+
for (const heading of Array.from(node.querySelectorAll(selector))) {
|
|
101
|
+
add_anchor_to_heading(heading, icon_svg);
|
|
102
|
+
}
|
|
103
|
+
// Watch for new headings
|
|
104
|
+
const observer = new MutationObserver((mutations) => {
|
|
105
|
+
for (const { addedNodes } of mutations) {
|
|
106
|
+
for (const added of Array.from(addedNodes)) {
|
|
107
|
+
if (added.nodeType !== Node.ELEMENT_NODE)
|
|
108
|
+
continue;
|
|
109
|
+
const el = added;
|
|
110
|
+
if (el.matches?.(selector))
|
|
111
|
+
add_anchor_to_heading(el, icon_svg);
|
|
112
|
+
for (const hdn of Array.from(el.querySelectorAll(selector))) {
|
|
113
|
+
add_anchor_to_heading(hdn, icon_svg);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
observer.observe(node, { childList: true, subtree: true });
|
|
119
|
+
return () => observer.disconnect();
|
|
120
|
+
};
|