@sveltia/ui 0.78.4 → 0.79.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/components/alert/alert-content.svelte +51 -0
- package/dist/components/alert/alert-content.svelte.d.ts +39 -0
- package/dist/components/alert/alert.d.ts +3 -0
- package/dist/components/alert/alert.js +21 -0
- package/dist/components/alert/alert.svelte +6 -30
- package/dist/components/alert/infobar.svelte +7 -21
- package/dist/components/alert/infobar.svelte.d.ts +2 -2
- package/dist/components/button/button.svelte +11 -11
- package/dist/components/button/button.svelte.d.ts +2 -2
- package/dist/components/button/select-button.svelte +1 -1
- package/dist/components/button/select-button.svelte.d.ts +2 -2
- package/dist/components/button/split-button.svelte +1 -1
- package/dist/components/button/split-button.svelte.d.ts +3 -3
- package/dist/components/calendar/calendar.d.ts +2 -0
- package/dist/components/calendar/calendar.js +34 -0
- package/dist/components/calendar/calendar.svelte +49 -5
- package/dist/components/checkbox/checkbox.svelte +11 -4
- package/dist/components/checkbox/checkbox.svelte.d.ts +2 -2
- package/dist/components/dialog/alert-dialog.svelte +1 -1
- package/dist/components/dialog/alert-dialog.svelte.d.ts +2 -2
- package/dist/components/dialog/confirmation-dialog.svelte +1 -1
- package/dist/components/dialog/confirmation-dialog.svelte.d.ts +2 -2
- package/dist/components/dialog/dialog.svelte +8 -18
- package/dist/components/dialog/dialog.svelte.d.ts +2 -2
- package/dist/components/dialog/modal-close-button.svelte +50 -0
- package/dist/components/dialog/modal-close-button.svelte.d.ts +43 -0
- package/dist/components/dialog/prompt-dialog.svelte +1 -1
- package/dist/components/dialog/prompt-dialog.svelte.d.ts +4 -4
- package/dist/components/disclosure/disclosure.svelte +1 -1
- package/dist/components/drawer/drawer.svelte +12 -34
- package/dist/components/drawer/drawer.svelte.d.ts +1 -1
- package/dist/components/emoji/caret.d.ts +1 -1
- package/dist/components/emoji/caret.js +1 -1
- package/dist/components/emoji/emoji-suggestions.svelte +1 -1
- package/dist/components/emoji/emoji-suggestions.svelte.d.ts +3 -3
- package/dist/components/emoji/emoji.d.ts +2 -2
- package/dist/components/emoji/emoji.js +1 -1
- package/dist/components/emoji/position.d.ts +2 -2
- package/dist/components/emoji/position.js +1 -1
- package/dist/components/grid/grid-body.svelte +6 -2
- package/dist/components/grid/grid-cell.svelte +3 -9
- package/dist/components/grid/grid-col-header.svelte +3 -9
- package/dist/components/grid/grid-foot.svelte +4 -11
- package/dist/components/grid/grid-head.svelte +4 -11
- package/dist/components/grid/grid-row-header.svelte +3 -10
- package/dist/components/grid/grid-row.svelte +3 -16
- package/dist/components/listbox/listbox.svelte +1 -1
- package/dist/components/listbox/listbox.svelte.d.ts +1 -1
- package/dist/components/listbox/option.svelte +1 -1
- package/dist/components/listbox/option.svelte.d.ts +2 -2
- package/dist/components/menu/menu-button.svelte +2 -2
- package/dist/components/menu/menu-button.svelte.d.ts +4 -4
- package/dist/components/menu/menu-item-checkable.svelte +71 -0
- package/dist/components/menu/menu-item-checkable.svelte.d.ts +25 -0
- package/dist/components/menu/menu-item-checkbox.svelte +3 -36
- package/dist/components/menu/menu-item-checkbox.svelte.d.ts +3 -3
- package/dist/components/menu/menu-item-group.svelte +8 -5
- package/dist/components/menu/menu-item-group.svelte.d.ts +9 -1
- package/dist/components/menu/menu-item-radio.svelte +3 -36
- package/dist/components/menu/menu-item-radio.svelte.d.ts +3 -3
- package/dist/components/menu/menu-item.svelte +22 -6
- package/dist/components/menu/menu-item.svelte.d.ts +4 -4
- package/dist/components/radio/radio.svelte +12 -1
- package/dist/components/resizable-pane/resizable-handle.svelte +14 -18
- package/dist/components/resizable-pane/resizable-pane-group.svelte +9 -6
- package/dist/components/resizable-pane/resizable-pane.svelte +4 -4
- package/dist/components/resizable-pane/resizable-pane.svelte.d.ts +7 -6
- package/dist/components/resizable-pane/sizing.d.ts +5 -4
- package/dist/components/resizable-pane/sizing.js +84 -25
- package/dist/components/scroll/infinite-scroll.svelte +16 -21
- package/dist/components/select/combobox.svelte +43 -52
- package/dist/components/select/combobox.svelte.d.ts +2 -2
- package/dist/components/select/select-tags.svelte +7 -1
- package/dist/components/select/select.svelte +2 -22
- package/dist/components/select/select.svelte.d.ts +1 -1
- package/dist/components/slider/slider.d.ts +6 -0
- package/dist/components/slider/slider.js +55 -2
- package/dist/components/slider/slider.svelte +63 -32
- package/dist/components/table/table-cell.svelte +3 -9
- package/dist/components/table/table-col-header.svelte +3 -9
- package/dist/components/table/table-foot.svelte +4 -11
- package/dist/components/table/table-head.svelte +4 -11
- package/dist/components/table/table-part.svelte +69 -0
- package/dist/components/table/table-part.svelte.d.ts +48 -0
- package/dist/components/table/table-row-header.svelte +3 -10
- package/dist/components/table/table-row.svelte +3 -10
- package/dist/components/tabs/tab-list.svelte +12 -0
- package/dist/components/tabs/tab.svelte +1 -1
- package/dist/components/tabs/tab.svelte.d.ts +2 -2
- package/dist/components/text-editor/constants.d.ts +5 -5
- package/dist/components/text-editor/constants.js +1 -1
- package/dist/components/text-editor/core.d.ts +4 -5
- package/dist/components/text-editor/core.js +372 -285
- package/dist/components/text-editor/emoji-autocomplete.svelte +1 -1
- package/dist/components/text-editor/lexical-root.svelte +1 -1
- package/dist/components/text-editor/raw-markdown.d.ts +6 -33
- package/dist/components/text-editor/raw-markdown.js +63 -8
- package/dist/components/text-editor/shiki/facade.d.ts +1 -1
- package/dist/components/text-editor/shiki/facade.js +63 -48
- package/dist/components/text-editor/shiki/generated.d.ts +1 -1
- package/dist/components/text-editor/shiki/generated.js +1 -1
- package/dist/components/text-editor/shiki/highlighter.d.ts +6 -1
- package/dist/components/text-editor/shiki/highlighter.js +3 -4
- package/dist/components/text-editor/shiki/loader.d.ts +1 -1
- package/dist/components/text-editor/shiki/loader.js +1 -1
- package/dist/components/text-editor/shiki/version.d.ts +1 -1
- package/dist/components/text-editor/shiki/version.js +1 -1
- package/dist/components/text-editor/store.svelte.d.ts +1 -1
- package/dist/components/text-editor/store.svelte.js +5 -2
- package/dist/components/text-editor/text-editor.svelte +2 -2
- package/dist/components/text-editor/text-editor.svelte.d.ts +3 -3
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +24 -16
- package/dist/components/text-editor/toolbar/format-text-button.svelte +7 -31
- package/dist/components/text-editor/toolbar/format-text-button.svelte.d.ts +1 -1
- package/dist/components/text-editor/toolbar/insert-component.d.ts +3 -0
- package/dist/components/text-editor/toolbar/insert-component.js +34 -0
- package/dist/components/text-editor/toolbar/insert-item-button.svelte +4 -24
- package/dist/components/text-editor/toolbar/insert-item-button.svelte.d.ts +1 -1
- package/dist/components/text-editor/toolbar/insert-link-button.svelte +9 -25
- package/dist/components/text-editor/toolbar/insert-menu-button.svelte +4 -24
- package/dist/components/text-editor/toolbar/insert-menu-button.svelte.d.ts +1 -1
- package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +1 -1
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +31 -35
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte.d.ts +1 -1
- package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte +0 -10
- package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte.d.ts +1 -11
- package/dist/components/text-editor/transformers/table.d.ts +0 -1
- package/dist/components/text-editor/transformers/table.js +1 -2
- package/dist/components/text-field/emoji-autocomplete.svelte +1 -1
- package/dist/components/text-field/masked-input.svelte +179 -0
- package/dist/components/text-field/masked-input.svelte.d.ts +42 -0
- package/dist/components/text-field/number-input.js +7 -1
- package/dist/components/text-field/number-input.svelte +13 -5
- package/dist/components/text-field/number-input.svelte.d.ts +3 -3
- package/dist/components/text-field/password-input.svelte +3 -116
- package/dist/components/text-field/password-input.svelte.d.ts +3 -3
- package/dist/components/text-field/search-bar.svelte +1 -1
- package/dist/components/text-field/search-bar.svelte.d.ts +3 -3
- package/dist/components/text-field/secret-input.svelte +3 -119
- package/dist/components/text-field/secret-input.svelte.d.ts +3 -3
- package/dist/components/text-field/text-area.svelte +1 -1
- package/dist/components/text-field/text-area.svelte.d.ts +3 -3
- package/dist/components/text-field/text-input.svelte +1 -1
- package/dist/components/text-field/text-input.svelte.d.ts +3 -3
- package/dist/components/toast/toast.svelte +11 -20
- package/dist/components/toast/toast.svelte.d.ts +1 -1
- package/dist/components/tree/tree-item.svelte +1 -1
- package/dist/components/tree/tree-item.svelte.d.ts +1 -1
- package/dist/components/tree/tree.svelte +1 -1
- package/dist/components/tree/tree.svelte.d.ts +1 -1
- package/dist/components/util/font-links.svelte +1 -0
- package/dist/components/util/modal.svelte +37 -1
- package/dist/components/util/modal.svelte.d.ts +1 -1
- package/dist/components/util/popup.svelte +2 -5
- package/dist/components/util/popup.svelte.d.ts +2 -2
- package/dist/components/util/visibility-observer.svelte +1 -0
- package/dist/locales/ar.yaml +1 -1
- package/dist/locales/sk.yaml +136 -0
- package/dist/services/group.svelte.d.ts +10 -1
- package/dist/services/group.svelte.js +361 -251
- package/dist/services/navigation.d.ts +19 -0
- package/dist/services/navigation.js +84 -0
- package/dist/services/popup-position.d.ts +90 -0
- package/dist/services/popup-position.js +275 -0
- package/dist/services/popup.svelte.d.ts +16 -17
- package/dist/services/popup.svelte.js +25 -169
- package/dist/services/scroll.d.ts +1 -0
- package/dist/services/scroll.js +17 -0
- package/dist/services/select.svelte.d.ts +2 -2
- package/dist/services/select.svelte.js +1 -1
- package/dist/services/text.d.ts +2 -0
- package/dist/services/text.js +28 -0
- package/dist/services/tree.svelte.d.ts +3 -11
- package/dist/services/tree.svelte.js +35 -77
- package/dist/services/type-ahead.d.ts +0 -4
- package/dist/services/type-ahead.js +3 -1
- package/dist/services/widget.d.ts +8 -0
- package/dist/services/widget.js +39 -0
- package/dist/shiki-engine.js +15 -15
- package/dist/styles/mixins.scss +126 -0
- package/package.json +24 -20
|
@@ -2,8 +2,11 @@ import { isRTL } from '@sveltia/i18n';
|
|
|
2
2
|
import { generateElementId } from '@sveltia/utils/element';
|
|
3
3
|
import { sleep } from '@sveltia/utils/misc';
|
|
4
4
|
import { on } from 'svelte/events';
|
|
5
|
+
import { calculatePopupStyle, isShallowEqual, mirrorPosition } from './popup-position.js';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
|
-
* @import { PopupPosition } from '../typedefs';
|
|
8
|
+
* @import { PopupPosition } from '../typedefs.js';
|
|
9
|
+
* @import { PopupStyle } from './popup-position.js';
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
/**
|
|
@@ -51,10 +54,7 @@ class Popup {
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
style = $state(
|
|
54
|
-
/**
|
|
55
|
-
* @type {{ inset: string | undefined, zIndex: number | undefined, minWidth: string | undefined,
|
|
56
|
-
* maxWidth: string | undefined, height: string | undefined }}
|
|
57
|
-
*/
|
|
57
|
+
/** @type {PopupStyle} */
|
|
58
58
|
({
|
|
59
59
|
inset: undefined,
|
|
60
60
|
zIndex: undefined,
|
|
@@ -93,11 +93,17 @@ class Popup {
|
|
|
93
93
|
*/
|
|
94
94
|
#removeAnchorListeners = [];
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* ID of the animation frame requested to recalculate the position after a resize.
|
|
98
|
+
* @type {number}
|
|
99
|
+
*/
|
|
100
|
+
_rafId = 0;
|
|
101
|
+
|
|
96
102
|
/**
|
|
97
103
|
* Initialize a new `Popup` instance. Note that the `popupElement` is optional, because the
|
|
98
104
|
* element is typically mounted only while the popup is open. Use {@link attachPopupElement} to
|
|
99
105
|
* provide it later.
|
|
100
|
-
* @param {
|
|
106
|
+
* @param {HTMLElement} anchorElement Element that triggers the popup, typically a `<button>`.
|
|
101
107
|
* @param {HTMLDialogElement | undefined} popupElement `<dialog>` element to be used for the
|
|
102
108
|
* popup, if it’s already in the DOM tree.
|
|
103
109
|
* @param {PopupPosition} position Where to show the popup content.
|
|
@@ -260,9 +266,8 @@ class Popup {
|
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
/**
|
|
263
|
-
* Detach the `<dialog>` element, typically because it’s being unmounted. The
|
|
264
|
-
* attribute
|
|
265
|
-
* animation is complete.
|
|
269
|
+
* Detach the `<dialog>` element, typically because it’s being unmounted. The anchor’s
|
|
270
|
+
* `aria-controls` attribute is removed as well, since the content is leaving the DOM tree.
|
|
266
271
|
*/
|
|
267
272
|
detachPopupElement() {
|
|
268
273
|
this.#removeEventListeners?.();
|
|
@@ -325,23 +330,6 @@ class Popup {
|
|
|
325
330
|
return;
|
|
326
331
|
}
|
|
327
332
|
|
|
328
|
-
const anchorRect = this.positionBaseElement.getBoundingClientRect();
|
|
329
|
-
const rootBounds = { width: window.innerWidth, height: window.innerHeight };
|
|
330
|
-
|
|
331
|
-
// Clip the anchor’s rect to the viewport, mirroring what `IntersectionObserver` used to report
|
|
332
|
-
// as `intersectionRect`
|
|
333
|
-
const intersectionRect = {
|
|
334
|
-
top: Math.max(anchorRect.top, 0),
|
|
335
|
-
left: Math.max(anchorRect.left, 0),
|
|
336
|
-
right: Math.min(anchorRect.right, rootBounds.width),
|
|
337
|
-
bottom: Math.min(anchorRect.bottom, rootBounds.height),
|
|
338
|
-
width: 0,
|
|
339
|
-
height: 0,
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
intersectionRect.width = Math.max(0, intersectionRect.right - intersectionRect.left);
|
|
343
|
-
intersectionRect.height = Math.max(0, intersectionRect.bottom - intersectionRect.top);
|
|
344
|
-
|
|
345
333
|
// Measure the content at its natural size, with any limit previously applied here taken off
|
|
346
334
|
// first. A popup that delegates its scrolling to a child — the combobox hands it to the option
|
|
347
335
|
// list, so the filter stays put — reports a `scrollHeight` no larger than the `max-height` it’s
|
|
@@ -349,6 +337,7 @@ class Popup {
|
|
|
349
337
|
// clipped values would make the cap stick: once the popup had shrunk to fit a small viewport,
|
|
350
338
|
// it could never grow back when the viewport did. The properties are put back synchronously,
|
|
351
339
|
// before the browser can paint, so nothing flickers.
|
|
340
|
+
const anchorRect = this.positionBaseElement.getBoundingClientRect();
|
|
352
341
|
const { maxHeight: appliedMaxHeight, maxWidth: appliedMaxWidth } = content.style;
|
|
353
342
|
|
|
354
343
|
content.style.maxHeight = '';
|
|
@@ -359,148 +348,16 @@ class Popup {
|
|
|
359
348
|
content.style.maxHeight = appliedMaxHeight;
|
|
360
349
|
content.style.maxWidth = appliedMaxWidth;
|
|
361
350
|
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const upwardMargin = intersectionRect.bottom - 8;
|
|
371
|
-
let { position } = this;
|
|
372
|
-
let height;
|
|
373
|
-
|
|
374
|
-
// Normalize RTL-friendly positions to LTR for LTR documents
|
|
375
|
-
// @todo Rename `PopupPosition` enums to be direction-agnostic
|
|
376
|
-
if (isRTL()) {
|
|
377
|
-
if (position.endsWith('-left')) {
|
|
378
|
-
position = /** @type {PopupPosition} */ (position.replace('-left', '-right'));
|
|
379
|
-
} else if (position.endsWith('-right')) {
|
|
380
|
-
position = /** @type {PopupPosition} */ (position.replace('-right', '-left'));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (position.startsWith('left-')) {
|
|
384
|
-
position = /** @type {PopupPosition} */ (position.replace('left-', 'right-'));
|
|
385
|
-
} else if (position.startsWith('right-')) {
|
|
386
|
-
position = /** @type {PopupPosition} */ (position.replace('right-', 'left-'));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Alter the position if the space is limited
|
|
391
|
-
if (position.startsWith('bottom-')) {
|
|
392
|
-
if (contentHeight > bottomMargin) {
|
|
393
|
-
if (topMargin > bottomMargin) {
|
|
394
|
-
position = /** @type {PopupPosition} */ (position.replace('bottom-', 'top-'));
|
|
395
|
-
height = topMargin;
|
|
396
|
-
} else {
|
|
397
|
-
height = bottomMargin;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Only a popup that opens beside its anchor carries a `-top`/`-bottom` suffix, and it grows
|
|
403
|
-
// away from the edge it’s aligned with: down from the anchor’s top, or up from its bottom. It
|
|
404
|
-
// gets the same treatment as the dropdown above — align with the other edge when the content
|
|
405
|
-
// doesn’t fit and that edge has more room, and cap it either way, so a long submenu scrolls
|
|
406
|
-
// instead of running past the viewport
|
|
407
|
-
if (position.endsWith('-top')) {
|
|
408
|
-
if (contentHeight > downwardMargin) {
|
|
409
|
-
if (upwardMargin > downwardMargin) {
|
|
410
|
-
position = /** @type {PopupPosition} */ (position.replace('-top', '-bottom'));
|
|
411
|
-
height = upwardMargin;
|
|
412
|
-
} else {
|
|
413
|
-
height = downwardMargin;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
} else if (position.endsWith('-bottom')) {
|
|
417
|
-
if (contentHeight > upwardMargin) {
|
|
418
|
-
if (downwardMargin > upwardMargin) {
|
|
419
|
-
position = /** @type {PopupPosition} */ (position.replace('-bottom', '-top'));
|
|
420
|
-
height = downwardMargin;
|
|
421
|
-
} else {
|
|
422
|
-
height = upwardMargin;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// If the popup overflows the viewport, change the position
|
|
428
|
-
if (position.endsWith('-left')) {
|
|
429
|
-
if (intersectionRect.left + contentWidth > rootBounds.width - 8) {
|
|
430
|
-
position = /** @type {PopupPosition} */ (position.replace('-left', '-right'));
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (position.endsWith('-right')) {
|
|
435
|
-
if (intersectionRect.right - contentWidth < 8) {
|
|
436
|
-
position = /** @type {PopupPosition} */ (position.replace('-right', '-left'));
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// The two checks above align a dropdown that opens below or above its anchor, so neither
|
|
441
|
-
// covers a popup that opens beside one — a submenu — running off the edge it opens towards.
|
|
442
|
-
// That gets the same treatment as the vertical flip above: switch to the other side, but only
|
|
443
|
-
// when it has more room, so a submenu with nowhere to go stays where the caller put it
|
|
444
|
-
if (position.startsWith('right-')) {
|
|
445
|
-
if (contentWidth > rightMargin && leftMargin > rightMargin) {
|
|
446
|
-
position = /** @type {PopupPosition} */ (position.replace('right-', 'left-'));
|
|
447
|
-
}
|
|
448
|
-
} else if (position.startsWith('left-')) {
|
|
449
|
-
if (contentWidth > leftMargin && rightMargin > leftMargin) {
|
|
450
|
-
position = /** @type {PopupPosition} */ (position.replace('left-', 'right-'));
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const top = position.startsWith('bottom-')
|
|
455
|
-
? `${Math.round(intersectionRect.bottom)}px`
|
|
456
|
-
: position.endsWith('-top')
|
|
457
|
-
? `${Math.round(intersectionRect.top)}px`
|
|
458
|
-
: 'auto';
|
|
459
|
-
|
|
460
|
-
const right = position.startsWith('left-')
|
|
461
|
-
? `${Math.round(rootBounds.width - intersectionRect.left)}px`
|
|
462
|
-
: position.endsWith('-right')
|
|
463
|
-
? `${Math.round(rootBounds.width - intersectionRect.right)}px`
|
|
464
|
-
: 'auto';
|
|
465
|
-
|
|
466
|
-
const bottom = position.startsWith('top-')
|
|
467
|
-
? `${Math.round(rootBounds.height - intersectionRect.top)}px`
|
|
468
|
-
: position.endsWith('-bottom')
|
|
469
|
-
? `${Math.round(rootBounds.height - intersectionRect.bottom)}px`
|
|
470
|
-
: 'auto';
|
|
471
|
-
|
|
472
|
-
const left = position.startsWith('right-')
|
|
473
|
-
? `${Math.round(intersectionRect.right)}px`
|
|
474
|
-
: position.endsWith('-left')
|
|
475
|
-
? `${Math.round(intersectionRect.left)}px`
|
|
476
|
-
: 'auto';
|
|
477
|
-
|
|
478
|
-
const style = {
|
|
479
|
-
inset: [top, right, bottom, left].join(' '),
|
|
480
|
-
zIndex: 1000,
|
|
481
|
-
minWidth: `${Math.round(intersectionRect.width)}px`,
|
|
482
|
-
// A popup opening beside the anchor is capped by the room on that side, not by the width the
|
|
483
|
-
// anchor’s own edges leave, which is what the two dropdown cases below measure
|
|
484
|
-
maxWidth: position.startsWith('right-')
|
|
485
|
-
? `${Math.round(rightMargin)}px`
|
|
486
|
-
: position.startsWith('left-')
|
|
487
|
-
? `${Math.round(leftMargin)}px`
|
|
488
|
-
: position.endsWith('-left')
|
|
489
|
-
? `${Math.round(rootBounds.width - intersectionRect.left - 8)}px`
|
|
490
|
-
: `${Math.round(intersectionRect.right - 8)}px`,
|
|
491
|
-
// `undefined` removes the `max-height` the popup is given, letting it take its natural size
|
|
492
|
-
// again. `auto` would look equivalent but isn’t a valid `max-height`, so the browser would
|
|
493
|
-
// drop it and leave whatever limit was applied last in place.
|
|
494
|
-
height: height ? `${Math.round(height)}px` : undefined,
|
|
495
|
-
};
|
|
351
|
+
const style = calculatePopupStyle({
|
|
352
|
+
// Normalize RTL-friendly positions to LTR for LTR documents
|
|
353
|
+
position: isRTL() ? mirrorPosition(this.position) : this.position,
|
|
354
|
+
anchorRect,
|
|
355
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
356
|
+
contentWidth,
|
|
357
|
+
contentHeight,
|
|
358
|
+
});
|
|
496
359
|
|
|
497
|
-
if (
|
|
498
|
-
style.inset !== this.style.inset ||
|
|
499
|
-
style.zIndex !== this.style.zIndex ||
|
|
500
|
-
style.minWidth !== this.style.minWidth ||
|
|
501
|
-
style.maxWidth !== this.style.maxWidth ||
|
|
502
|
-
style.height !== this.style.height
|
|
503
|
-
) {
|
|
360
|
+
if (!isShallowEqual(style, this.style)) {
|
|
504
361
|
this.style = style;
|
|
505
362
|
}
|
|
506
363
|
}
|
|
@@ -541,8 +398,7 @@ class Popup {
|
|
|
541
398
|
|
|
542
399
|
/**
|
|
543
400
|
* Activate a new popup.
|
|
544
|
-
* @param {
|
|
401
|
+
* @param {ConstructorParameters<typeof Popup>} args Arguments passed to the `Popup` constructor.
|
|
545
402
|
* @returns {Popup} New popup.
|
|
546
403
|
*/
|
|
547
|
-
// @ts-ignore
|
|
548
404
|
export const activatePopup = (...args) => new Popup(...args);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function scrollIntoViewIfNeeded(element: HTMLElement): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scrolling helpers shared by the composite widgets.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scroll the given element into view if needed, by as little as possible. Falls back to the legacy
|
|
7
|
+
* boolean argument on a browser that rejects the options object.
|
|
8
|
+
* @internal
|
|
9
|
+
* @param {HTMLElement} element Element to be scrolled into view.
|
|
10
|
+
*/
|
|
11
|
+
export const scrollIntoViewIfNeeded = (element) => {
|
|
12
|
+
try {
|
|
13
|
+
element.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'auto' });
|
|
14
|
+
} catch {
|
|
15
|
+
element.scrollIntoView(true);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -52,5 +52,5 @@ export class OptionRegistry {
|
|
|
52
52
|
export function createOptionRegistry(): OptionRegistry;
|
|
53
53
|
export function getOptionRegistry(): OptionRegistry | undefined;
|
|
54
54
|
export function getSelectedItemDetail(target: HTMLElement): SelectedItemDetail;
|
|
55
|
-
import type { OptionEntry } from '../typedefs';
|
|
56
|
-
import type { SelectedItemDetail } from '../typedefs';
|
|
55
|
+
import type { OptionEntry } from '../typedefs.js';
|
|
56
|
+
import type { SelectedItemDetail } from '../typedefs.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text normalization shared by search filtering and type-ahead.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Diacritic characters regex for normalization. We use a regex instead of `Intl` APIs for better
|
|
7
|
+
* performance, since `transliterate` is slow and we only need basic normalization.
|
|
8
|
+
*/
|
|
9
|
+
const DIACRITIC_RE = /\p{Diacritic}/gu;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fold the given string for comparison: strip diacritics and convert to lower case, keeping
|
|
13
|
+
* whitespace as is.
|
|
14
|
+
* @internal
|
|
15
|
+
* @param {string} value Original value.
|
|
16
|
+
* @returns {string} Folded value.
|
|
17
|
+
*/
|
|
18
|
+
export const fold = (value) => value.normalize('NFD').replace(DIACRITIC_RE, '').toLocaleLowerCase();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize the given string for search value comparison. Since `transliterate` is slow, we only
|
|
22
|
+
* apply basic normalization.
|
|
23
|
+
* @internal
|
|
24
|
+
* @param {string} value Original value.
|
|
25
|
+
* @returns {string} Normalized value.
|
|
26
|
+
* @todo Move this to `@sveltia/utils`.
|
|
27
|
+
*/
|
|
28
|
+
export const normalize = (value) => fold(value.trim());
|
|
@@ -35,16 +35,6 @@ export class Tree {
|
|
|
35
35
|
* @type {HTMLElement | undefined}
|
|
36
36
|
*/
|
|
37
37
|
anchor: HTMLElement | undefined;
|
|
38
|
-
/**
|
|
39
|
-
* Currently accumulated type-ahead search terms.
|
|
40
|
-
* @type {string}
|
|
41
|
-
*/
|
|
42
|
-
typeAheadTerms: string;
|
|
43
|
-
/**
|
|
44
|
-
* Timer used to reset the type-ahead search terms.
|
|
45
|
-
* @type {ReturnType<typeof globalThis.setTimeout> | undefined}
|
|
46
|
-
*/
|
|
47
|
-
typeAheadTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
|
|
48
38
|
/**
|
|
49
39
|
* Whether {@link activate} has run.
|
|
50
40
|
* @type {boolean}
|
|
@@ -217,7 +207,8 @@ export class Tree {
|
|
|
217
207
|
*/
|
|
218
208
|
expandSiblings(item: HTMLElement): void;
|
|
219
209
|
/**
|
|
220
|
-
* Move focus to the next item that matches the accumulated type-ahead search terms.
|
|
210
|
+
* Move focus to the next item that matches the accumulated type-ahead search terms. Typing the
|
|
211
|
+
* same character repeatedly cycles through the items starting with it.
|
|
221
212
|
* @param {string} char Typed character.
|
|
222
213
|
* @param {HTMLElement} currentItem Currently focused item.
|
|
223
214
|
*/
|
|
@@ -242,6 +233,7 @@ export class Tree {
|
|
|
242
233
|
* Clean up event listeners.
|
|
243
234
|
*/
|
|
244
235
|
destroy(): void;
|
|
236
|
+
#private;
|
|
245
237
|
}
|
|
246
238
|
export function activateTree(params?: object): Attachment;
|
|
247
239
|
import type { Attachment } from 'svelte/attachments';
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { isRTL } from '@sveltia/i18n';
|
|
2
2
|
import { generateElementId } from '@sveltia/utils/element';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { getArrowKeys } from './navigation.js';
|
|
4
|
+
import { scrollIntoViewIfNeeded } from './scroll.js';
|
|
5
|
+
import { normalize } from './text.js';
|
|
5
6
|
import { getSelectedItemDetail } from './select.svelte.js';
|
|
7
|
+
import { findTypeAheadMatch, TypeAhead } from './type-ahead.js';
|
|
8
|
+
import { activateBefore, activateLater } from './widget.js';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* @import { Attachment } from 'svelte/attachments';
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
|
-
/**
|
|
12
|
-
* How long to wait, in milliseconds, before the type-ahead search terms are reset.
|
|
13
|
-
*/
|
|
14
|
-
const TYPE_AHEAD_TIMEOUT = 500;
|
|
15
14
|
/**
|
|
16
15
|
* CSS selector to retrieve the tree items.
|
|
17
16
|
*/
|
|
@@ -25,6 +24,12 @@ const ITEM_SELECTOR = '[role="treeitem"]';
|
|
|
25
24
|
* @see https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
|
|
26
25
|
*/
|
|
27
26
|
export class Tree {
|
|
27
|
+
/**
|
|
28
|
+
* Keystroke buffer for type-ahead.
|
|
29
|
+
* @type {TypeAhead}
|
|
30
|
+
*/
|
|
31
|
+
#typeAhead = new TypeAhead();
|
|
32
|
+
|
|
28
33
|
/**
|
|
29
34
|
* Initialize a new `Tree` instance.
|
|
30
35
|
* @param {HTMLElement} parent Parent element.
|
|
@@ -55,18 +60,6 @@ export class Tree {
|
|
|
55
60
|
*/
|
|
56
61
|
this.anchor = undefined;
|
|
57
62
|
|
|
58
|
-
/**
|
|
59
|
-
* Currently accumulated type-ahead search terms.
|
|
60
|
-
* @type {string}
|
|
61
|
-
*/
|
|
62
|
-
this.typeAheadTerms = '';
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Timer used to reset the type-ahead search terms.
|
|
66
|
-
* @type {ReturnType<typeof globalThis.setTimeout> | undefined}
|
|
67
|
-
*/
|
|
68
|
-
this.typeAheadTimer = undefined;
|
|
69
|
-
|
|
70
63
|
/**
|
|
71
64
|
* Whether {@link activate} has run.
|
|
72
65
|
* @type {boolean}
|
|
@@ -79,26 +72,12 @@ export class Tree {
|
|
|
79
72
|
*/
|
|
80
73
|
this.destroyed = false;
|
|
81
74
|
|
|
82
|
-
// eslint-disable-next-line jsdoc/require-description
|
|
83
75
|
/** @type {(event: MouseEvent) => void} */
|
|
84
|
-
this._onClick = (event) =>
|
|
85
|
-
this.activate();
|
|
86
|
-
this.onClick(event);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// eslint-disable-next-line jsdoc/require-description
|
|
76
|
+
this._onClick = activateBefore(this, (event) => this.onClick(event));
|
|
90
77
|
/** @type {(event: KeyboardEvent) => void} */
|
|
91
|
-
this._onKeyDown = (event) =>
|
|
92
|
-
this.activate();
|
|
93
|
-
this.onKeyDown(event);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// eslint-disable-next-line jsdoc/require-description
|
|
78
|
+
this._onKeyDown = activateBefore(this, (event) => this.onKeyDown(event));
|
|
97
79
|
/** @type {(event: FocusEvent) => void} */
|
|
98
|
-
this._onFocusIn = (event) =>
|
|
99
|
-
this.activate();
|
|
100
|
-
this.onFocusIn(event);
|
|
101
|
-
};
|
|
80
|
+
this._onFocusIn = activateBefore(this, (event) => this.onFocusIn(event));
|
|
102
81
|
|
|
103
82
|
// The items can be added or removed at any time, e.g. when a subtree is lazily rendered
|
|
104
83
|
this.observer = new globalThis.MutationObserver(() => {
|
|
@@ -114,16 +93,9 @@ export class Tree {
|
|
|
114
93
|
parent.addEventListener('keydown', this._onKeyDown);
|
|
115
94
|
parent.addEventListener('focusin', this._onFocusIn);
|
|
116
95
|
|
|
117
|
-
// Wait a bit before the child components are mounted
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// The widget may have been unmounted in the meantime, and the observer set up by
|
|
122
|
-
// `activate()` would then never be disconnected
|
|
123
|
-
if (!this.destroyed) {
|
|
124
|
-
this.activate();
|
|
125
|
-
}
|
|
126
|
-
})();
|
|
96
|
+
// Wait a bit before the child components are mounted. If the widget is unmounted in the
|
|
97
|
+
// meantime, the observer set up by `activate()` would never be disconnected.
|
|
98
|
+
activateLater(this, () => this.destroyed);
|
|
127
99
|
}
|
|
128
100
|
|
|
129
101
|
/**
|
|
@@ -394,11 +366,7 @@ export class Tree {
|
|
|
394
366
|
* @param {HTMLElement} element Element to be scrolled into view.
|
|
395
367
|
*/
|
|
396
368
|
scrollIntoView(element) {
|
|
397
|
-
|
|
398
|
-
element.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'auto' });
|
|
399
|
-
} catch {
|
|
400
|
-
element.scrollIntoView(true);
|
|
401
|
-
}
|
|
369
|
+
scrollIntoViewIfNeeded(element);
|
|
402
370
|
}
|
|
403
371
|
|
|
404
372
|
/**
|
|
@@ -473,10 +441,13 @@ export class Tree {
|
|
|
473
441
|
}
|
|
474
442
|
|
|
475
443
|
const { multi, anchor } = this;
|
|
444
|
+
const items = multi && range && anchor && anchor !== item ? this.visibleItems : [];
|
|
445
|
+
const anchorIndex = items.indexOf(/** @type {HTMLElement} */ (anchor));
|
|
476
446
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
447
|
+
// An anchor that has been hidden in a collapsed parent, or removed, can’t start a range, so
|
|
448
|
+
// the item is selected on its own instead, becoming the new anchor
|
|
449
|
+
if (anchorIndex !== -1) {
|
|
450
|
+
const indexes = [anchorIndex, items.indexOf(item)].sort((a, b) => a - b);
|
|
480
451
|
|
|
481
452
|
items.forEach((element, index) => {
|
|
482
453
|
this.setSelected(
|
|
@@ -560,34 +531,22 @@ export class Tree {
|
|
|
560
531
|
}
|
|
561
532
|
|
|
562
533
|
/**
|
|
563
|
-
* Move focus to the next item that matches the accumulated type-ahead search terms.
|
|
534
|
+
* Move focus to the next item that matches the accumulated type-ahead search terms. Typing the
|
|
535
|
+
* same character repeatedly cycles through the items starting with it.
|
|
564
536
|
* @param {string} char Typed character.
|
|
565
537
|
* @param {HTMLElement} currentItem Currently focused item.
|
|
566
538
|
*/
|
|
567
539
|
typeAhead(char, currentItem) {
|
|
568
|
-
globalThis.clearTimeout(this.typeAheadTimer);
|
|
569
|
-
|
|
570
|
-
this.typeAheadTerms += char;
|
|
571
|
-
this.typeAheadTimer = globalThis.setTimeout(() => {
|
|
572
|
-
this.typeAheadTerms = '';
|
|
573
|
-
}, TYPE_AHEAD_TIMEOUT);
|
|
574
|
-
|
|
575
|
-
const terms = normalize(this.typeAheadTerms);
|
|
576
540
|
const items = this.activeItems;
|
|
577
|
-
const index = items.indexOf(currentItem);
|
|
578
541
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
...items.slice(0, index + 1),
|
|
585
|
-
];
|
|
586
|
-
|
|
587
|
-
const match = orderedItems.find((item) => normalize(this.getLabel(item)).startsWith(terms));
|
|
542
|
+
const index = findTypeAheadMatch(
|
|
543
|
+
items.map((item) => normalize(this.getLabel(item))),
|
|
544
|
+
this.#typeAhead.push(char),
|
|
545
|
+
items.indexOf(currentItem),
|
|
546
|
+
);
|
|
588
547
|
|
|
589
|
-
if (
|
|
590
|
-
this.focusItem(
|
|
548
|
+
if (index !== -1 && items[index] !== currentItem) {
|
|
549
|
+
this.focusItem(items[index]);
|
|
591
550
|
}
|
|
592
551
|
}
|
|
593
552
|
|
|
@@ -668,8 +627,7 @@ export class Tree {
|
|
|
668
627
|
const currentItem = target ?? this.currentItem ?? activeItems[0];
|
|
669
628
|
const index = activeItems.indexOf(currentItem);
|
|
670
629
|
// In RTL, the Left and Right arrow keys are swapped
|
|
671
|
-
const forwardKey =
|
|
672
|
-
const backwardKey = isRTL() ? 'ArrowRight' : 'ArrowLeft';
|
|
630
|
+
const { prevKey: backwardKey, nextKey: forwardKey } = getArrowKeys('horizontal', isRTL());
|
|
673
631
|
|
|
674
632
|
if (key === 'a' && (ctrlKey || metaKey)) {
|
|
675
633
|
if (multi) {
|
|
@@ -787,7 +745,7 @@ export class Tree {
|
|
|
787
745
|
*/
|
|
788
746
|
destroy() {
|
|
789
747
|
this.destroyed = true;
|
|
790
|
-
|
|
748
|
+
this.#typeAhead.reset();
|
|
791
749
|
this.observer.disconnect();
|
|
792
750
|
this.parent.removeEventListener('click', this._onClick);
|
|
793
751
|
this.parent.removeEventListener('keydown', this._onKeyDown);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* listbox and menu groups.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { fold } from './text.js';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* How long, in milliseconds, successive keystrokes keep adding up to one type-ahead prefix.
|
|
8
10
|
*/
|
|
@@ -26,7 +28,7 @@ export class TypeAhead {
|
|
|
26
28
|
*/
|
|
27
29
|
push(key) {
|
|
28
30
|
globalThis.clearTimeout(this.#timer);
|
|
29
|
-
this.#prefix += key
|
|
31
|
+
this.#prefix += fold(key);
|
|
30
32
|
this.#timer = /** @type {number} */ (
|
|
31
33
|
/** @type {unknown} */ (
|
|
32
34
|
globalThis.setTimeout(() => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function activateBefore<E extends Event>(widget: ActivatableWidget, handler: (event: E) => void): (event: E) => void;
|
|
2
|
+
export function activateLater(widget: ActivatableWidget, isDestroyed: () => boolean): Promise<void>;
|
|
3
|
+
export type ActivatableWidget = {
|
|
4
|
+
/**
|
|
5
|
+
* Set up the members, unless they already are.
|
|
6
|
+
*/
|
|
7
|
+
activate: () => void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation scaffolding shared by the composite widgets, `Group` and `Tree`, which set up their
|
|
3
|
+
* members only once the child components have had a chance to mount.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sleep } from '@sveltia/utils/misc';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} ActivatableWidget
|
|
10
|
+
* @property {() => void} activate Set up the members, unless they already are.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create an event listener that activates the widget before handing the event on, so an event
|
|
15
|
+
* that comes before the delayed activation is not lost.
|
|
16
|
+
* @template {Event} E
|
|
17
|
+
* @param {ActivatableWidget} widget Widget.
|
|
18
|
+
* @param {(event: E) => void} handler Function that handles the event.
|
|
19
|
+
* @returns {(event: E) => void} Event listener.
|
|
20
|
+
*/
|
|
21
|
+
export const activateBefore = (widget, handler) => (event) => {
|
|
22
|
+
widget.activate();
|
|
23
|
+
handler(event);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Activate the widget after a short delay, giving the relevant components time to mount, unless
|
|
28
|
+
* the widget has been destroyed in the meantime.
|
|
29
|
+
* @param {ActivatableWidget} widget Widget.
|
|
30
|
+
* @param {() => boolean} isDestroyed Function that tells whether the widget has been destroyed.
|
|
31
|
+
*/
|
|
32
|
+
export const activateLater = async (widget, isDestroyed) => {
|
|
33
|
+
await sleep(100);
|
|
34
|
+
|
|
35
|
+
// The widget may have been unmounted in the meantime
|
|
36
|
+
if (!isDestroyed()) {
|
|
37
|
+
widget.activate();
|
|
38
|
+
}
|
|
39
|
+
};
|