svelte-multiselect 11.3.0 → 11.5.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.
@@ -1,74 +0,0 @@
1
- <script lang="ts">let { checked = $bindable(false), onkeydown, children, input_props, ...rest } = $props();
2
- // normally input type=checkbox toggles on space bar, this handler also responds to enter
3
- function handle_keydown(event) {
4
- onkeydown?.(event);
5
- if (event.key === `Enter`) {
6
- event.preventDefault();
7
- const target = event.target;
8
- target.click(); // simulate real user toggle so 'change' is dispatched
9
- }
10
- }
11
- export {};
12
- </script>
13
-
14
- <label {...rest}>
15
- {@render children?.()}
16
- <input
17
- type="checkbox"
18
- bind:checked
19
- {...input_props}
20
- onkeydown={handle_keydown}
21
- />
22
- <span></span>
23
- </label>
24
-
25
- <style>
26
- label {
27
- display: var(--toggle-label-display, inline-flex);
28
- align-items: var(--toggle-label-align-items, center);
29
- width: var(--toggle-label-width, max-content);
30
- vertical-align: var(--toggle-label-vertical-align, middle);
31
- }
32
- span {
33
- box-sizing: border-box;
34
- height: var(--toggle-knob-height, 1.5em);
35
- width: var(--toggle-knob-width, 3em);
36
- padding: var(--toggle-knob-padding, 0.1em);
37
- border: var(--toggle-knob-border, 1px solid lightgray);
38
- border-radius: var(--toggle-knob-border-radius, 0.75em);
39
- transition: var(--toggle-knob-transition, 0.3s);
40
- }
41
- input:checked + span {
42
- background: var(--toggle-background, black);
43
- }
44
- input {
45
- position: absolute;
46
- opacity: 0;
47
- width: var(--toggle-input-width, 1em);
48
- }
49
- input + span::after {
50
- content: '';
51
- display: var(--toggle-knob-after-display, block);
52
- height: var(--toggle-knob-after-height, 1.2em);
53
- width: var(--toggle-knob-after-width, 1.2em);
54
- border-radius: var(--toggle-knob-after-border-radius, 50%);
55
- background: var(--toggle-knob-after-background, gray);
56
- transition: var(--toggle-knob-after-transition, 0.3s);
57
- }
58
- input:checked + span::after {
59
- background: var(--toggle-knob-after-background, green);
60
- transform: var(
61
- --toggle-knob-after-transform,
62
- translate(
63
- calc(
64
- var(--toggle-knob-width, 3em) - var(--toggle-knob-height, 1.5em)
65
- + var(--toggle-knob-padding, 0.1em)
66
- - var(--toggle-knob-border, 2px)
67
- )
68
- )
69
- );
70
- }
71
- input:focus + span {
72
- border: var(--toggle-knob-focus-border, 1px solid cornflowerblue);
73
- }
74
- </style>
@@ -1,11 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { HTMLAttributes } from 'svelte/elements';
3
- type $$ComponentProps = HTMLAttributes<HTMLLabelElement> & {
4
- checked?: boolean;
5
- onkeydown?: (event: KeyboardEvent) => void;
6
- children?: Snippet<[]>;
7
- input_props?: HTMLAttributes<HTMLInputElement>;
8
- };
9
- declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "checked">;
10
- type Toggle = ReturnType<typeof Toggle>;
11
- export default Toggle;
@@ -1,15 +0,0 @@
1
- <script lang="ts">import { Spring } from 'svelte/motion';
2
- let { wiggle = $bindable(false), angle = 0, scale = 1, dx = 0, dy = 0, duration = 200, stiffness = 0.05, damping = 0.1, children, } = $props();
3
- const store = Spring.of(() => (wiggle ? { scale, angle, dx, dy } : { angle: 0, scale: 1, dx: 0, dy: 0 }), { stiffness, damping });
4
- $effect.pre(() => {
5
- if (wiggle)
6
- setTimeout(() => (wiggle = false), duration);
7
- });
8
- </script>
9
-
10
- <span
11
- style:transform="rotate({store.current.angle}deg) scale({store.current.scale})
12
- translate({store.current.dx}px, {store.current.dy}px)"
13
- >
14
- {@render children?.()}
15
- </span>
@@ -1,15 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- type $$ComponentProps = {
3
- wiggle?: boolean;
4
- angle?: number;
5
- scale?: number;
6
- dx?: number;
7
- dy?: number;
8
- duration?: number;
9
- stiffness?: number;
10
- damping?: number;
11
- children?: Snippet;
12
- };
13
- declare const Wiggle: import("svelte").Component<$$ComponentProps, {}, "wiggle">;
14
- type Wiggle = ReturnType<typeof Wiggle>;
15
- export default Wiggle;
@@ -1,49 +0,0 @@
1
- import { type Attachment } from 'svelte/attachments';
2
- declare global {
3
- interface CSS {
4
- highlights: HighlightRegistry;
5
- }
6
- interface HighlightRegistry extends Map<string, Highlight> {
7
- clear(): void;
8
- delete(key: string): boolean;
9
- set(key: string, value: Highlight): this;
10
- }
11
- }
12
- export interface DraggableOptions {
13
- handle_selector?: string;
14
- on_drag_start?: (event: MouseEvent) => void;
15
- on_drag?: (event: MouseEvent) => void;
16
- on_drag_end?: (event: MouseEvent) => void;
17
- }
18
- export declare const draggable: (options?: DraggableOptions) => Attachment;
19
- export declare function get_html_sort_value(element: HTMLElement): string;
20
- export declare const sortable: ({ header_selector, asc_class, desc_class, sorted_style, }?: {
21
- header_selector?: string | undefined;
22
- asc_class?: string | undefined;
23
- desc_class?: string | undefined;
24
- sorted_style?: {
25
- backgroundColor: string;
26
- } | undefined;
27
- }) => (node: HTMLElement) => () => void;
28
- export type HighlightOptions = {
29
- query?: string;
30
- disabled?: boolean;
31
- fuzzy?: boolean;
32
- node_filter?: (node: Node) => number;
33
- css_class?: string;
34
- };
35
- export declare const highlight_matches: (ops: HighlightOptions) => (node: HTMLElement) => (() => boolean) | undefined;
36
- export interface TooltipOptions {
37
- content?: string;
38
- placement?: `top` | `bottom` | `left` | `right`;
39
- delay?: number;
40
- disabled?: boolean;
41
- style?: string;
42
- }
43
- export declare const tooltip: (options?: TooltipOptions) => Attachment;
44
- export type ClickOutsideConfig<T extends HTMLElement> = {
45
- enabled?: boolean;
46
- exclude?: string[];
47
- callback?: (node: T, config: ClickOutsideConfig<T>) => void;
48
- };
49
- export declare const click_outside: <T extends HTMLElement>(config?: ClickOutsideConfig<T>) => (node: T) => () => void;
@@ -1,489 +0,0 @@
1
- import {} from 'svelte/attachments';
2
- // Svelte 5 attachment factory to make an element draggable
3
- // @param options - Configuration options for dragging behavior
4
- // @returns Attachment function that sets up dragging on an element
5
- export const draggable = (options = {}) => (element) => {
6
- const node = element;
7
- // Use simple variables for maximum performance
8
- let dragging = false;
9
- let start = { x: 0, y: 0 };
10
- const initial = { left: 0, top: 0 };
11
- const handle = options.handle_selector
12
- ? node.querySelector(options.handle_selector)
13
- : node;
14
- if (!handle) {
15
- console.warn(`Draggable: handle not found with selector "${options.handle_selector}"`);
16
- return;
17
- }
18
- function handle_mousedown(event) {
19
- // Only drag if mousedown is on the handle or its children
20
- if (!handle?.contains?.(event.target))
21
- return;
22
- dragging = true;
23
- // For position: fixed elements, use getBoundingClientRect for viewport-relative position
24
- const computed_style = getComputedStyle(node);
25
- if (computed_style.position === `fixed`) {
26
- const rect = node.getBoundingClientRect();
27
- initial.left = rect.left;
28
- initial.top = rect.top;
29
- }
30
- else {
31
- // For other positioning, use offset values
32
- initial.left = node.offsetLeft;
33
- initial.top = node.offsetTop;
34
- }
35
- node.style.left = `${initial.left}px`;
36
- node.style.top = `${initial.top}px`;
37
- node.style.right = `auto`; // Prevent conflict with left
38
- start = { x: event.clientX, y: event.clientY };
39
- document.body.style.userSelect = `none`; // Prevent text selection during drag
40
- if (handle)
41
- handle.style.cursor = `grabbing`;
42
- globalThis.addEventListener(`mousemove`, handle_mousemove);
43
- globalThis.addEventListener(`mouseup`, handle_mouseup);
44
- options.on_drag_start?.(event); // Call optional callback
45
- }
46
- function handle_mousemove(event) {
47
- if (!dragging)
48
- return;
49
- // Use the exact same calculation as the fast old implementation
50
- const dx = event.clientX - start.x;
51
- const dy = event.clientY - start.y;
52
- node.style.left = `${initial.left + dx}px`;
53
- node.style.top = `${initial.top + dy}px`;
54
- // Only call callback if it exists (minimize overhead)
55
- if (options.on_drag)
56
- options.on_drag(event);
57
- }
58
- function handle_mouseup(event) {
59
- if (!dragging)
60
- return;
61
- dragging = false;
62
- event.stopPropagation();
63
- document.body.style.userSelect = ``;
64
- if (handle)
65
- handle.style.cursor = `grab`;
66
- globalThis.removeEventListener(`mousemove`, handle_mousemove);
67
- globalThis.removeEventListener(`mouseup`, handle_mouseup);
68
- options.on_drag_end?.(event); // Call optional callback
69
- }
70
- if (handle) {
71
- handle.addEventListener(`mousedown`, handle_mousedown);
72
- handle.style.cursor = `grab`;
73
- }
74
- // Return cleanup function (this is the attachment pattern)
75
- return () => {
76
- globalThis.removeEventListener(`mousemove`, handle_mousemove);
77
- globalThis.removeEventListener(`mouseup`, handle_mouseup);
78
- if (handle) {
79
- handle.removeEventListener(`mousedown`, handle_mousedown);
80
- handle.style.cursor = ``; // Reset cursor
81
- }
82
- };
83
- };
84
- export function get_html_sort_value(element) {
85
- if (element.dataset.sortValue !== undefined) {
86
- return element.dataset.sortValue;
87
- }
88
- for (const child of Array.from(element.children)) {
89
- const child_val = get_html_sort_value(child);
90
- if (child_val !== ``)
91
- return child_val;
92
- }
93
- return element.textContent ?? ``;
94
- }
95
- export const sortable = ({ header_selector = `thead th`, asc_class = `table-sort-asc`, desc_class = `table-sort-desc`, sorted_style = { backgroundColor: `rgba(255, 255, 255, 0.1)` }, } = {}) => (node) => {
96
- // this action can be applied to bob-standard HTML tables to make them sortable by
97
- // clicking on column headers (and clicking again to toggle sorting direction)
98
- const headers = Array.from(node.querySelectorAll(header_selector));
99
- let sort_col_idx;
100
- let sort_dir = 1; // 1 = asc, -1 = desc
101
- // Store event listeners for cleanup
102
- const event_listeners = [];
103
- for (const [idx, header] of headers.entries()) {
104
- header.style.cursor = `pointer`; // add cursor pointer to headers
105
- const init_styles = header.getAttribute(`style`) ?? ``;
106
- const click_handler = () => {
107
- // reset all headers to initial state
108
- for (const header of headers) {
109
- header.textContent = header.textContent?.replace(/ ↑| ↓/, ``) ?? ``;
110
- header.classList.remove(asc_class, desc_class);
111
- header.setAttribute(`style`, init_styles);
112
- }
113
- if (idx === sort_col_idx) {
114
- sort_dir *= -1; // reverse sort direction
115
- }
116
- else {
117
- sort_col_idx = idx; // set new sort column index
118
- sort_dir = 1; // reset sort direction
119
- }
120
- header.classList.add(sort_dir > 0 ? asc_class : desc_class);
121
- Object.assign(header.style, sorted_style);
122
- header.textContent = `${header.textContent?.replace(/ ↑| ↓/, ``)} ${sort_dir > 0 ? `↑` : `↓`}`;
123
- const table_body = node.querySelector(`tbody`);
124
- if (!table_body)
125
- return;
126
- // re-sort table
127
- const rows = Array.from(table_body.querySelectorAll(`tr`));
128
- rows.sort((row_1, row_2) => {
129
- const cell_1 = row_1.cells[sort_col_idx];
130
- const cell_2 = row_2.cells[sort_col_idx];
131
- const val_1 = get_html_sort_value(cell_1);
132
- const val_2 = get_html_sort_value(cell_2);
133
- if (val_1 === val_2)
134
- return 0;
135
- if (val_1 === ``)
136
- return 1; // treat empty string as lower than any value
137
- if (val_2 === ``)
138
- return -1; // any value is considered higher than empty string
139
- const num_1 = Number(val_1);
140
- const num_2 = Number(val_2);
141
- if (isNaN(num_1) && isNaN(num_2)) {
142
- return sort_dir * val_1.localeCompare(val_2, undefined, { numeric: true });
143
- }
144
- return sort_dir * (num_1 - num_2);
145
- });
146
- for (const row of rows)
147
- table_body.appendChild(row);
148
- };
149
- header.addEventListener(`click`, click_handler);
150
- event_listeners.push({ header, handler: click_handler });
151
- }
152
- // Return cleanup function
153
- return () => {
154
- for (const { header, handler } of event_listeners) {
155
- header.removeEventListener(`click`, handler);
156
- header.style.cursor = ``; // Reset cursor
157
- }
158
- };
159
- };
160
- export const highlight_matches = (ops) => (node) => {
161
- const { query = ``, disabled = false, fuzzy = false, node_filter = () => NodeFilter.FILTER_ACCEPT, css_class = `highlight-match`, } = ops;
162
- // abort if CSS highlight API not supported
163
- if (typeof CSS === `undefined` || !CSS.highlights)
164
- return;
165
- // always clear our own highlight first
166
- CSS.highlights.delete(css_class);
167
- // if disabled or empty query, stop after cleanup
168
- if (!query || disabled)
169
- return;
170
- const tree_walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
171
- acceptNode: node_filter,
172
- });
173
- const text_nodes = [];
174
- let current_node = tree_walker.nextNode();
175
- while (current_node) {
176
- text_nodes.push(current_node);
177
- current_node = tree_walker.nextNode();
178
- }
179
- // iterate over all text nodes and find matches
180
- const ranges = text_nodes.map((el) => {
181
- const text = el.textContent?.toLowerCase();
182
- if (!text)
183
- return [];
184
- const search = query.toLowerCase();
185
- if (fuzzy) {
186
- // Fuzzy highlighting: highlight individual characters that match in order
187
- const matching_indices = [];
188
- let search_idx = 0;
189
- let target_idx = 0;
190
- // Find matching character indices
191
- while (search_idx < search.length && target_idx < text.length) {
192
- if (search[search_idx] === text[target_idx]) {
193
- matching_indices.push(target_idx);
194
- search_idx++;
195
- }
196
- target_idx++;
197
- }
198
- // Only create ranges if we found all characters in order
199
- if (search_idx === search.length) {
200
- return matching_indices.map((index) => {
201
- const range = new Range();
202
- range.setStart(el, index);
203
- range.setEnd(el, index + 1); // highlight single character
204
- return range;
205
- });
206
- }
207
- return [];
208
- }
209
- else {
210
- // Substring highlighting: highlight consecutive substrings
211
- const indices = [];
212
- let start_pos = 0;
213
- while (start_pos < text.length) {
214
- const index = text.indexOf(search, start_pos);
215
- if (index === -1)
216
- break;
217
- indices.push(index);
218
- start_pos = index + search.length;
219
- }
220
- // create range object for each substring found in the text node
221
- return indices.map((index) => {
222
- const range = new Range();
223
- range.setStart(el, index);
224
- range.setEnd(el, index + search.length);
225
- return range;
226
- });
227
- }
228
- });
229
- // create Highlight object from ranges and add to registry
230
- CSS.highlights.set(css_class, new Highlight(...ranges.flat()));
231
- // Return cleanup function
232
- return () => CSS.highlights.delete(css_class);
233
- };
234
- // Global tooltip state to ensure only one tooltip is shown at a time
235
- let current_tooltip = null;
236
- let show_timeout;
237
- let hide_timeout;
238
- function clear_tooltip() {
239
- if (show_timeout)
240
- clearTimeout(show_timeout);
241
- if (hide_timeout)
242
- clearTimeout(hide_timeout);
243
- if (current_tooltip) {
244
- current_tooltip.remove();
245
- current_tooltip = null;
246
- }
247
- }
248
- export const tooltip = (options = {}) => (node) => {
249
- // Handle null/undefined elements
250
- if (!node || !(node instanceof HTMLElement))
251
- return;
252
- // Handle null/undefined options
253
- const safe_options = options || {};
254
- const cleanup_functions = [];
255
- function setup_tooltip(element) {
256
- if (!element || safe_options.disabled)
257
- return;
258
- const content = safe_options.content || element.title ||
259
- element.getAttribute(`aria-label`) || element.getAttribute(`data-title`);
260
- if (!content)
261
- return;
262
- // Store original title and remove it to prevent default tooltip
263
- // Only store title if we're not using custom content
264
- if (element.title && !safe_options.content) {
265
- element.setAttribute(`data-original-title`, element.title);
266
- element.removeAttribute(`title`);
267
- }
268
- function show_tooltip() {
269
- clear_tooltip();
270
- show_timeout = setTimeout(() => {
271
- const tooltip = document.createElement(`div`);
272
- tooltip.className = `custom-tooltip`;
273
- const placement = safe_options.placement || `bottom`;
274
- tooltip.setAttribute(`data-placement`, placement);
275
- // Apply base styles
276
- tooltip.style.cssText = `
277
- position: absolute; z-index: 9999; opacity: 0;
278
- background: var(--tooltip-bg, #333); color: var(--text-color, white); border: var(--tooltip-border, none);
279
- 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;
281
- filter: var(--tooltip-shadow, drop-shadow(0 2px 8px rgba(0,0,0,0.25))); transition: opacity 0.15s ease-out;
282
- `;
283
- // Apply custom styles if provided (these will override base styles due to CSS specificity)
284
- if (safe_options.style) {
285
- // Parse and apply custom styles as individual properties for better control
286
- const custom_styles = safe_options.style.split(`;`).filter((style) => style.trim());
287
- custom_styles.forEach((style) => {
288
- const [property, value] = style.split(`:`).map((s) => s.trim());
289
- if (property && value)
290
- tooltip.style.setProperty(property, value);
291
- });
292
- }
293
- tooltip.innerHTML = content?.replace(/\r/g, `<br/>`) ?? ``;
294
- // Mirror CSS custom properties from the trigger node onto the tooltip element
295
- const trigger_styles = getComputedStyle(element);
296
- [
297
- `--tooltip-bg`,
298
- `--text-color`,
299
- `--tooltip-border`,
300
- `--tooltip-padding`,
301
- `--tooltip-radius`,
302
- `--tooltip-font-size`,
303
- `--tooltip-shadow`,
304
- `--tooltip-max-width`,
305
- `--tooltip-opacity`,
306
- `--tooltip-arrow-size`,
307
- ].forEach((name) => {
308
- const value = trigger_styles.getPropertyValue(name).trim();
309
- if (value)
310
- tooltip.style.setProperty(name, value);
311
- });
312
- // Append early so we can read computed border styles for arrow border
313
- document.body.appendChild(tooltip);
314
- // Arrow elements: optional border triangle behind fill triangle
315
- const tooltip_styles = getComputedStyle(tooltip);
316
- const arrow = document.createElement(`div`);
317
- arrow.className = `custom-tooltip-arrow`;
318
- arrow.style.cssText =
319
- `position: absolute; width: 0; height: 0; pointer-events: none;`;
320
- const arrow_size_raw = trigger_styles.getPropertyValue(`--tooltip-arrow-size`)
321
- .trim();
322
- const arrow_size_num = Number.parseInt(arrow_size_raw || ``, 10);
323
- const arrow_px = Number.isFinite(arrow_size_num) ? arrow_size_num : 6;
324
- const border_color = tooltip_styles.borderTopColor;
325
- const border_width_num = Number.parseFloat(tooltip_styles.borderTopWidth || `0`);
326
- const has_border = !!border_color && border_color !== `rgba(0, 0, 0, 0)` &&
327
- border_width_num > 0;
328
- const maybe_append_border_arrow = () => {
329
- if (!has_border)
330
- return;
331
- const border_arrow = document.createElement(`div`);
332
- border_arrow.className = `custom-tooltip-arrow-border`;
333
- border_arrow.style.cssText =
334
- `position: absolute; width: 0; height: 0; pointer-events: none;`;
335
- const border_size = arrow_px + (border_width_num * 1.4);
336
- if (placement === `top`) {
337
- border_arrow.style.left = `calc(50% - ${border_size}px)`;
338
- border_arrow.style.bottom = `-${border_size}px`;
339
- border_arrow.style.borderLeft = `${border_size}px solid transparent`;
340
- border_arrow.style.borderRight = `${border_size}px solid transparent`;
341
- border_arrow.style.borderTop = `${border_size}px solid ${border_color}`;
342
- }
343
- else if (placement === `left`) {
344
- border_arrow.style.top = `calc(50% - ${border_size}px)`;
345
- border_arrow.style.right = `-${border_size}px`;
346
- border_arrow.style.borderTop = `${border_size}px solid transparent`;
347
- border_arrow.style.borderBottom = `${border_size}px solid transparent`;
348
- border_arrow.style.borderLeft = `${border_size}px solid ${border_color}`;
349
- }
350
- else if (placement === `right`) {
351
- border_arrow.style.top = `calc(50% - ${border_size}px)`;
352
- border_arrow.style.left = `-${border_size}px`;
353
- border_arrow.style.borderTop = `${border_size}px solid transparent`;
354
- border_arrow.style.borderBottom = `${border_size}px solid transparent`;
355
- border_arrow.style.borderRight = `${border_size}px solid ${border_color}`;
356
- }
357
- else { // bottom
358
- border_arrow.style.left = `calc(50% - ${border_size}px)`;
359
- border_arrow.style.top = `-${border_size}px`;
360
- border_arrow.style.borderLeft = `${border_size}px solid transparent`;
361
- border_arrow.style.borderRight = `${border_size}px solid transparent`;
362
- border_arrow.style.borderBottom = `${border_size}px solid ${border_color}`;
363
- }
364
- tooltip.appendChild(border_arrow);
365
- };
366
- // Create the fill arrow on top
367
- if (placement === `top`) {
368
- arrow.style.left = `calc(50% - ${arrow_px}px)`;
369
- arrow.style.bottom = `-${arrow_px}px`;
370
- arrow.style.borderLeft = `${arrow_px}px solid transparent`;
371
- arrow.style.borderRight = `${arrow_px}px solid transparent`;
372
- arrow.style.borderTop = `${arrow_px}px solid var(--tooltip-bg, #333)`;
373
- }
374
- else if (placement === `left`) {
375
- arrow.style.top = `calc(50% - ${arrow_px}px)`;
376
- arrow.style.right = `-${arrow_px}px`;
377
- arrow.style.borderTop = `${arrow_px}px solid transparent`;
378
- arrow.style.borderBottom = `${arrow_px}px solid transparent`;
379
- arrow.style.borderLeft = `${arrow_px}px solid var(--tooltip-bg, #333)`;
380
- }
381
- else if (placement === `right`) {
382
- arrow.style.top = `calc(50% - ${arrow_px}px)`;
383
- arrow.style.left = `-${arrow_px}px`;
384
- arrow.style.borderTop = `${arrow_px}px solid transparent`;
385
- arrow.style.borderBottom = `${arrow_px}px solid transparent`;
386
- arrow.style.borderRight = `${arrow_px}px solid var(--tooltip-bg, #333)`;
387
- }
388
- else { // bottom
389
- arrow.style.left = `calc(50% - ${arrow_px}px)`;
390
- arrow.style.top = `-${arrow_px}px`;
391
- arrow.style.borderLeft = `${arrow_px}px solid transparent`;
392
- arrow.style.borderRight = `${arrow_px}px solid transparent`;
393
- arrow.style.borderBottom = `${arrow_px}px solid var(--tooltip-bg, #333)`;
394
- }
395
- maybe_append_border_arrow();
396
- tooltip.appendChild(arrow);
397
- // Position tooltip
398
- const rect = element.getBoundingClientRect();
399
- const tooltip_rect = tooltip.getBoundingClientRect();
400
- const margin = 12;
401
- let top = 0, left = 0;
402
- if (placement === `top`) {
403
- top = rect.top - tooltip_rect.height - margin;
404
- left = rect.left + rect.width / 2 - tooltip_rect.width / 2;
405
- }
406
- else if (placement === `left`) {
407
- top = rect.top + rect.height / 2 - tooltip_rect.height / 2;
408
- left = rect.left - tooltip_rect.width - margin;
409
- }
410
- else if (placement === `right`) {
411
- top = rect.top + rect.height / 2 - tooltip_rect.height / 2;
412
- left = rect.right + margin;
413
- }
414
- else { // bottom
415
- top = rect.bottom + margin;
416
- left = rect.left + rect.width / 2 - tooltip_rect.width / 2;
417
- }
418
- // Keep in viewport
419
- left = Math.max(8, Math.min(left, globalThis.innerWidth - tooltip_rect.width - 8));
420
- top = Math.max(8, Math.min(top, globalThis.innerHeight - tooltip_rect.height - 8));
421
- tooltip.style.left = `${left + globalThis.scrollX}px`;
422
- tooltip.style.top = `${top + globalThis.scrollY}px`;
423
- const custom_opacity = trigger_styles.getPropertyValue(`--tooltip-opacity`).trim();
424
- tooltip.style.opacity = custom_opacity || `1`;
425
- current_tooltip = tooltip;
426
- }, safe_options.delay || 100);
427
- }
428
- function hide_tooltip() {
429
- clear_tooltip();
430
- if (current_tooltip) {
431
- current_tooltip.style.opacity = `0`;
432
- if (current_tooltip) {
433
- current_tooltip.remove();
434
- current_tooltip = null;
435
- }
436
- }
437
- }
438
- const events = [`mouseenter`, `mouseleave`, `focus`, `blur`];
439
- const handlers = [show_tooltip, hide_tooltip, show_tooltip, hide_tooltip];
440
- events.forEach((event, idx) => element.addEventListener(event, handlers[idx]));
441
- // Hide tooltip when user scrolls
442
- globalThis.addEventListener(`scroll`, hide_tooltip, true);
443
- return () => {
444
- events.forEach((event, idx) => element.removeEventListener(event, handlers[idx]));
445
- globalThis.removeEventListener(`scroll`, hide_tooltip, true);
446
- const original_title = element.getAttribute(`data-original-title`);
447
- if (original_title) {
448
- element.setAttribute(`title`, original_title);
449
- element.removeAttribute(`data-original-title`);
450
- }
451
- };
452
- }
453
- // Setup tooltip for main node and children
454
- const main_cleanup = setup_tooltip(node);
455
- if (main_cleanup)
456
- cleanup_functions.push(main_cleanup);
457
- node.querySelectorAll(`[title], [aria-label], [data-title]`).forEach((element) => {
458
- const child_cleanup = setup_tooltip(element);
459
- if (child_cleanup)
460
- cleanup_functions.push(child_cleanup);
461
- });
462
- if (cleanup_functions.length === 0)
463
- return;
464
- return () => {
465
- cleanup_functions.forEach((cleanup) => cleanup());
466
- clear_tooltip();
467
- };
468
- };
469
- export const click_outside = (config = {}) => (node) => {
470
- const { callback, enabled = true, exclude = [] } = config;
471
- function handle_click(event) {
472
- if (!enabled)
473
- return;
474
- const target = event.target;
475
- const path = event.composedPath();
476
- // Check if click target is the node or inside it
477
- if (path.includes(node))
478
- return;
479
- // Check excluded selectors
480
- if (exclude.some((selector) => target.closest(selector)))
481
- return;
482
- // Execute callback if provided, passing node and full config
483
- callback?.(node, { callback, enabled, exclude });
484
- // Dispatch custom event if click was outside
485
- node.dispatchEvent(new CustomEvent(`outside-click`));
486
- }
487
- document.addEventListener(`click`, handle_click, true);
488
- return () => document.removeEventListener(`click`, handle_click, true);
489
- };