flexdesk 0.17.18 → 0.18.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/CHANGELOG.md +53 -0
- package/README.md +1 -0
- package/css/flexdesk.css +7544 -5916
- package/css/reset.css +1 -1
- package/css/tokens.css +23 -1
- package/dist/charts.js +10 -6
- package/dist/charts.js.map +1 -1
- package/dist/chunk-5W4EEKSK.js +778 -0
- package/dist/chunk-5W4EEKSK.js.map +7 -0
- package/dist/chunk-DNJ3N4T4.js +82 -0
- package/dist/chunk-DNJ3N4T4.js.map +7 -0
- package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
- package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
- package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
- package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
- package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
- package/dist/chunk-PAI2ZUWV.js +260 -0
- package/dist/chunk-PAI2ZUWV.js.map +7 -0
- package/dist/chunk-Q7BALEPX.js +527 -0
- package/dist/chunk-Q7BALEPX.js.map +7 -0
- package/dist/chunk-QXAOTGCC.js +40 -0
- package/dist/chunk-QXAOTGCC.js.map +7 -0
- package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
- package/dist/chunk-RKVYIXDC.js.map +7 -0
- package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
- package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
- package/dist/chunk-UKP22RUG.js +219 -0
- package/dist/chunk-UKP22RUG.js.map +7 -0
- package/dist/chunk-VLOAI3HS.js +43 -0
- package/dist/chunk-VLOAI3HS.js.map +7 -0
- package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
- package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
- package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
- package/dist/chunk-ZL3VKINP.js.map +7 -0
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/flexdesk.css +7544 -5916
- package/dist/flow.js +29 -97
- package/dist/flow.js.map +3 -3
- package/dist/git.js +1356 -0
- package/dist/git.js.map +7 -0
- package/dist/reset.css +1 -1
- package/dist/tiles.js +20 -15
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +23 -1
- package/dist/widgets.js +3967 -1188
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +120 -32
- package/dist/wm.js.map +2 -2
- package/docs/README.md +9 -4
- package/docs/best-practices.md +2 -2
- package/docs/components/chat-shell.md +198 -0
- package/docs/components/conversation.md +10 -0
- package/docs/components/filter-builder.md +246 -0
- package/docs/components/forms-and-dialogs.md +22 -0
- package/docs/components/git.md +355 -0
- package/docs/components/input-fields.md +167 -2
- package/docs/components/keymap.md +8 -0
- package/docs/components/menus.md +82 -2
- package/docs/components/panels-and-desktops.md +16 -5
- package/docs/components/shell.md +104 -1
- package/docs/components/workspace-picker.md +149 -0
- package/docs/concepts/building-and-packaging.md +2 -2
- package/docs/concepts/packages-and-entry-points.md +14 -13
- package/docs/concepts/state-guard.md +3 -3
- package/docs/quickstart.md +1 -1
- package/git.js +13 -0
- package/package.json +6 -1
- package/src/flow/kit/dom.js +28 -119
- package/src/flow/kit/step_picker.js +1 -1
- package/src/flow/kit/value_picker.js +1 -1
- package/src/flow/outline/menu.js +1 -1
- package/src/git/diff_viewer.js +212 -0
- package/src/git/diff_window.js +51 -0
- package/src/git/git_graph.js +401 -0
- package/src/git/git_ops.js +61 -0
- package/src/git/git_panel.js +755 -0
- package/src/git/index.js +9 -0
- package/src/git/line_diff.js +202 -0
- package/src/tiles/tile_base.js +5 -4
- package/src/tiling/desktops.js +10 -8
- package/src/tiling/keymap.js +6 -5
- package/src/tiling/kind_taxonomy.js +15 -2
- package/src/tiling/shell.js +124 -17
- package/src/tiling/tile_renderer.js +2 -3
- package/src/ui/components/action_dropdown.js +4 -78
- package/src/ui/components/anchored_layer.js +255 -0
- package/src/ui/components/chat_shell.js +920 -0
- package/src/ui/components/chat_state_machine.js +223 -0
- package/src/ui/components/choice_controls.js +45 -2
- package/src/ui/components/color_field.js +3 -2
- package/src/ui/components/color_list_field.js +3 -2
- package/src/ui/components/combobox.js +422 -0
- package/src/ui/components/conversation_view.js +17 -2
- package/src/ui/components/data_table.js +2 -2
- package/src/ui/components/filter_builder.js +766 -0
- package/src/ui/components/filter_model.js +614 -0
- package/src/ui/components/managed_window.js +24 -47
- package/src/ui/components/modal.js +79 -109
- package/src/ui/components/option_list.js +160 -0
- package/src/ui/components/popover.js +94 -0
- package/src/ui/components/recent_items.js +169 -0
- package/src/ui/components/select.js +322 -0
- package/src/ui/components/tag_input.js +77 -8
- package/src/ui/components/workspace_picker.js +1155 -0
- package/src/ui/utils/fire_event.js +17 -0
- package/src/ui/utils/focusable.js +25 -0
- package/src/ui/utils/modal_host.js +27 -0
- package/widgets.js +12 -2
- package/css/base.css +0 -10547
- package/css/overrides.css +0 -2596
- package/dist/chunk-7JI7HHSY.js +0 -107
- package/dist/chunk-7JI7HHSY.js.map +0 -7
- package/dist/chunk-MIW7D6WC.js +0 -526
- package/dist/chunk-MIW7D6WC.js.map +0 -7
- package/dist/chunk-PGZ4EHSE.js.map +0 -7
- package/dist/chunk-XJNADRGR.js.map +0 -7
- /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
|
@@ -9,10 +9,7 @@
|
|
|
9
9
|
* Adds scrolling when the menu would exceed available vertical space.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const EDGE_MARGIN = 8;
|
|
14
|
-
/** Gap between trigger and dropdown in px. */
|
|
15
|
-
const GAP = 8;
|
|
12
|
+
import { positionAnchored } from './anchored_layer.js';
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
15
|
* ActionDropdown - A reusable dropdown component with smart auto-positioning.
|
|
@@ -371,11 +368,8 @@ export class ActionDropdown {
|
|
|
371
368
|
// =========================================================================
|
|
372
369
|
|
|
373
370
|
/**
|
|
374
|
-
* Position a dropdown menu relative to a trigger element
|
|
375
|
-
*
|
|
376
|
-
* Picks above/below based on available space, aligns left/right edge,
|
|
377
|
-
* and applies `max-height` + `overflow-y: auto` when the menu is taller
|
|
378
|
-
* than the available space.
|
|
371
|
+
* Position a dropdown menu relative to a trigger element: `positionAnchored`
|
|
372
|
+
* (`anchored_layer.js`), the one placement every floating panel uses.
|
|
379
373
|
*
|
|
380
374
|
* @param {HTMLElement} trigger - The element the dropdown opens from
|
|
381
375
|
* @param {HTMLElement} menu - The dropdown menu element to position
|
|
@@ -386,75 +380,7 @@ export class ActionDropdown {
|
|
|
386
380
|
*/
|
|
387
381
|
static position(trigger, menu, { align = 'start' } = {}) {
|
|
388
382
|
if (!trigger || !menu) return;
|
|
389
|
-
|
|
390
|
-
// Reset any prior inline position so natural size can be measured.
|
|
391
|
-
Object.assign(menu.style, {
|
|
392
|
-
position: 'fixed',
|
|
393
|
-
top: 'auto',
|
|
394
|
-
bottom: 'auto',
|
|
395
|
-
left: 'auto',
|
|
396
|
-
right: 'auto',
|
|
397
|
-
maxHeight: '',
|
|
398
|
-
overflowY: ''
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
const triggerRect = trigger.getBoundingClientRect();
|
|
402
|
-
const menuRect = menu.getBoundingClientRect();
|
|
403
|
-
const vw = window.innerWidth;
|
|
404
|
-
const vh = window.innerHeight;
|
|
405
|
-
|
|
406
|
-
// ── Vertical: prefer below, flip above if insufficient room ───────
|
|
407
|
-
const spaceBelow = vh - triggerRect.bottom - GAP - EDGE_MARGIN;
|
|
408
|
-
const spaceAbove = triggerRect.top - GAP - EDGE_MARGIN;
|
|
409
|
-
|
|
410
|
-
let top;
|
|
411
|
-
let maxHeight;
|
|
412
|
-
|
|
413
|
-
if (menuRect.height <= spaceBelow) {
|
|
414
|
-
// Fits below — use natural height.
|
|
415
|
-
top = triggerRect.bottom + GAP;
|
|
416
|
-
maxHeight = spaceBelow;
|
|
417
|
-
} else if (menuRect.height <= spaceAbove) {
|
|
418
|
-
// Fits above — use natural height.
|
|
419
|
-
top = triggerRect.top - GAP - menuRect.height;
|
|
420
|
-
maxHeight = spaceAbove;
|
|
421
|
-
} else if (spaceBelow >= spaceAbove) {
|
|
422
|
-
// More space below — scroll.
|
|
423
|
-
top = triggerRect.bottom + GAP;
|
|
424
|
-
maxHeight = spaceBelow;
|
|
425
|
-
} else {
|
|
426
|
-
// More space above — scroll.
|
|
427
|
-
maxHeight = spaceAbove;
|
|
428
|
-
top = EDGE_MARGIN;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// ── Horizontal: align with the trigger's start (left) or end (right)
|
|
432
|
-
// edge, flip to the other edge if off-screen ──
|
|
433
|
-
let left;
|
|
434
|
-
if (align === 'end') {
|
|
435
|
-
left = triggerRect.right - menuRect.width;
|
|
436
|
-
if (left < EDGE_MARGIN) {
|
|
437
|
-
// Align left edges instead.
|
|
438
|
-
left = triggerRect.left;
|
|
439
|
-
}
|
|
440
|
-
} else {
|
|
441
|
-
left = triggerRect.left;
|
|
442
|
-
if (left + menuRect.width > vw - EDGE_MARGIN) {
|
|
443
|
-
// Align right edges instead.
|
|
444
|
-
left = triggerRect.right - menuRect.width;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Still off-screen? Clamp to edges.
|
|
449
|
-
left = Math.max(EDGE_MARGIN, Math.min(left, vw - menuRect.width - EDGE_MARGIN));
|
|
450
|
-
|
|
451
|
-
Object.assign(menu.style, {
|
|
452
|
-
position: 'fixed',
|
|
453
|
-
top: `${Math.round(top)}px`,
|
|
454
|
-
left: `${Math.round(left)}px`,
|
|
455
|
-
maxHeight: `${Math.round(maxHeight)}px`,
|
|
456
|
-
overflowY: 'auto'
|
|
457
|
-
});
|
|
383
|
+
positionAnchored(trigger, menu, { align });
|
|
458
384
|
}
|
|
459
385
|
}
|
|
460
386
|
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE WAY TO HANG A PANEL OFF AN ELEMENT. (0.18.0)
|
|
3
|
+
*
|
|
4
|
+
* `positionAnchored` is the placement: `ActionDropdown.position`, `CategoryPicker`,
|
|
5
|
+
* a tile's menu, `openPopover`, `createSelect` and `createCombobox` all place
|
|
6
|
+
* through it, so none of them has a second idea of what "below, else above,
|
|
7
|
+
* inside the window" means.
|
|
8
|
+
*
|
|
9
|
+
* `openAnchoredLayer` is the behaviour around the placement:
|
|
10
|
+
*
|
|
11
|
+
* - the panel is mounted on FlexDesk's modal host (`modalHost()`) or the anchor's
|
|
12
|
+
* `document.body`, never inside the anchor's parent, so no `overflow: hidden`
|
|
13
|
+
* ancestor clips it. It is taken off again on close: a closed layer leaves
|
|
14
|
+
* nothing behind to forget;
|
|
15
|
+
* - it is placed again when the window resizes, when anything outside it
|
|
16
|
+
* scrolls and when its own size changes; an anchor that has left the document
|
|
17
|
+
* closes it;
|
|
18
|
+
* - it closes on a press outside it and on Escape.
|
|
19
|
+
*
|
|
20
|
+
* LAYERS STACK, because a popover's own controls hang panels too. A select inside
|
|
21
|
+
* a popover is a layer whose anchor is inside the popover; that gives every rule
|
|
22
|
+
* below one answer:
|
|
23
|
+
*
|
|
24
|
+
* - OPENING a layer closes every open layer that does not own its anchor (so
|
|
25
|
+
* there is one popover at a time, and a popover's select does not close it);
|
|
26
|
+
* - A PRESS on what a layer owns is not outside it, and neither is one on its anchor;
|
|
27
|
+
* - ESCAPE goes to the top layer alone and to nothing else, or the `ManagedWindow`
|
|
28
|
+
* around the field would close too and take a half-filled form with it. From
|
|
29
|
+
* inside the panel it is heard as it bubbles out, so that the panel's content
|
|
30
|
+
* may answer it first (a picker climbing a level); from anywhere else, on the
|
|
31
|
+
* way in;
|
|
32
|
+
* - CLOSING a layer closes the layers above it first.
|
|
33
|
+
*
|
|
34
|
+
* WHAT A LAYER OWNS is one question with one answer, for a press and for an anchor
|
|
35
|
+
* alike: the panel; every layer above it; and what floats without being a layer, which
|
|
36
|
+
* is named here: an `ActionDropdown` menu, a `CategoryPicker`, a context menu, a modal
|
|
37
|
+
* window shown after the layer opened (a dialog its content opened), and whatever the
|
|
38
|
+
* caller's `inside` selector names. A modal already showing when the layer opened is
|
|
39
|
+
* UNDER it, however far down the chain of layers it was opened from: it owns none of
|
|
40
|
+
* that and takes no key from it. An open menu or picker, and a modal over the layer,
|
|
41
|
+
* get Escape first.
|
|
42
|
+
*
|
|
43
|
+
* An anchor that has left the document has nothing to hang a panel from: opening on
|
|
44
|
+
* one opens nothing and closes nothing, and answers `null`.
|
|
45
|
+
*/
|
|
46
|
+
import { modalHost } from '../utils/modal_host.js';
|
|
47
|
+
|
|
48
|
+
/** Viewport margin in px: a placed panel stays this far from the edges. */
|
|
49
|
+
const EDGE_MARGIN = 8;
|
|
50
|
+
/** Gap between an anchor and its panel in px, unless the caller gives one. */
|
|
51
|
+
const GAP = 8;
|
|
52
|
+
|
|
53
|
+
const NO_BOX = { left: 0, right: 0, top: 0, bottom: 0, width: 0, height: 0 };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Place `panel` against `anchor`: `position: fixed`, so no ancestor clips it;
|
|
57
|
+
* under the anchor when it fits there, else above it when it fits there, else on
|
|
58
|
+
* the side with more room, scrolling inside (`max-height` + `overflow-y: auto`);
|
|
59
|
+
* kept `margin` inside the window. The client size is the window: a page
|
|
60
|
+
* scrollbar is not room to draw in.
|
|
61
|
+
*
|
|
62
|
+
* @param {HTMLElement|null} anchor Null places it at the window's top-left corner.
|
|
63
|
+
* @param {HTMLElement} panel
|
|
64
|
+
* @param {object} [opts]
|
|
65
|
+
* @param {'start'|'end'} [opts.align='start'] Which anchor edge the panel lines up
|
|
66
|
+
* with first: `'start'` its left, `'end'` its right. Either flips to the other
|
|
67
|
+
* edge when the panel would leave the window.
|
|
68
|
+
* @param {number} [opts.gap=8] Px between the anchor and the panel.
|
|
69
|
+
* @param {number} [opts.margin=8] Px kept clear of the window's edges.
|
|
70
|
+
* @param {boolean} [opts.matchWidth] The panel is at least as wide as the anchor.
|
|
71
|
+
*/
|
|
72
|
+
export function positionAnchored(anchor, panel, { align = 'start', gap = GAP, margin = EDGE_MARGIN, matchWidth = false } = {}) {
|
|
73
|
+
if (!panel) return;
|
|
74
|
+
|
|
75
|
+
// Reset any prior inline position so natural size can be measured.
|
|
76
|
+
Object.assign(panel.style, {
|
|
77
|
+
position: 'fixed',
|
|
78
|
+
top: 'auto',
|
|
79
|
+
bottom: 'auto',
|
|
80
|
+
left: 'auto',
|
|
81
|
+
right: 'auto',
|
|
82
|
+
maxHeight: '',
|
|
83
|
+
overflowY: '',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const box = anchor?.getBoundingClientRect?.() || NO_BOX;
|
|
87
|
+
if (matchWidth) panel.style.minWidth = `${Math.round(box.width)}px`;
|
|
88
|
+
const size = panel.getBoundingClientRect();
|
|
89
|
+
const doc = anchor?.ownerDocument || document;
|
|
90
|
+
const view = doc.defaultView || window;
|
|
91
|
+
const vw = doc.documentElement.clientWidth || view.innerWidth;
|
|
92
|
+
const vh = doc.documentElement.clientHeight || view.innerHeight;
|
|
93
|
+
|
|
94
|
+
// ── Vertical: prefer below, flip above if insufficient room ───────
|
|
95
|
+
const spaceBelow = vh - box.bottom - gap - margin;
|
|
96
|
+
const spaceAbove = box.top - gap - margin;
|
|
97
|
+
|
|
98
|
+
let top;
|
|
99
|
+
let maxHeight;
|
|
100
|
+
if (size.height <= spaceBelow) {
|
|
101
|
+
top = box.bottom + gap;
|
|
102
|
+
maxHeight = spaceBelow;
|
|
103
|
+
} else if (size.height <= spaceAbove) {
|
|
104
|
+
top = box.top - gap - size.height;
|
|
105
|
+
maxHeight = spaceAbove;
|
|
106
|
+
} else if (spaceBelow >= spaceAbove) {
|
|
107
|
+
// More space below: scroll.
|
|
108
|
+
top = box.bottom + gap;
|
|
109
|
+
maxHeight = spaceBelow;
|
|
110
|
+
} else {
|
|
111
|
+
// More space above: scroll.
|
|
112
|
+
maxHeight = spaceAbove;
|
|
113
|
+
top = margin;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Horizontal: the chosen edge, the other if off-screen, then clamped ──
|
|
117
|
+
let left;
|
|
118
|
+
if (align === 'end') {
|
|
119
|
+
left = box.right - size.width;
|
|
120
|
+
if (left < margin) left = box.left;
|
|
121
|
+
} else {
|
|
122
|
+
left = box.left;
|
|
123
|
+
if (left + size.width > vw - margin) left = box.right - size.width;
|
|
124
|
+
}
|
|
125
|
+
left = Math.max(margin, Math.min(left, vw - size.width - margin));
|
|
126
|
+
|
|
127
|
+
Object.assign(panel.style, {
|
|
128
|
+
position: 'fixed',
|
|
129
|
+
top: `${Math.round(top)}px`,
|
|
130
|
+
left: `${Math.round(left)}px`,
|
|
131
|
+
maxHeight: `${Math.round(maxHeight)}px`,
|
|
132
|
+
overflowY: 'auto',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** What floats without being a layer. See the header. */
|
|
137
|
+
const FOREIGN_FLOATS = '.twm-action-dropdown-menu, .twm-category-picker, .twm-context-menu';
|
|
138
|
+
/** Which of those answer Escape themselves while open. */
|
|
139
|
+
const FOREIGN_ESCAPE = '.twm-action-dropdown-menu:not([hidden]), .twm-category-picker:not([hidden])';
|
|
140
|
+
const MODAL_WINDOW = '.twm-managed-window--modal';
|
|
141
|
+
|
|
142
|
+
/** The open layers, bottom first. */
|
|
143
|
+
const LAYERS = [];
|
|
144
|
+
|
|
145
|
+
const holds = (node, target) => !!node && typeof node.contains === 'function' && node.contains(target);
|
|
146
|
+
|
|
147
|
+
const shownModals = (doc) => [...doc.querySelectorAll(MODAL_WINDOW)].filter((win) => win.style.display !== 'none');
|
|
148
|
+
|
|
149
|
+
/** Is `node` in an open layer? A modal's focus trap leaves such focus alone: the panel is outside the modal's frame. */
|
|
150
|
+
export const isInAnchoredLayer = (node) => LAYERS.some((layer) => holds(layer.el, node));
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Open `el` as a layer against `anchor`.
|
|
154
|
+
*
|
|
155
|
+
* @param {object} o
|
|
156
|
+
* @param {HTMLElement|null} o.anchor
|
|
157
|
+
* @param {HTMLElement} o.el The panel. Mounted here, removed on close; reusable.
|
|
158
|
+
* @param {'start'|'end'} [o.align]
|
|
159
|
+
* @param {number} [o.gap]
|
|
160
|
+
* @param {boolean} [o.matchWidth]
|
|
161
|
+
* @param {string} [o.inside] A selector for floating things of the caller's own:
|
|
162
|
+
* a press in one is not outside this layer.
|
|
163
|
+
* @param {boolean} [o.returnFocus=true] Closed with focus inside the panel, or on
|
|
164
|
+
* nothing, focus goes back to the anchor, unless the reason is `'blur'`: focus
|
|
165
|
+
* that left is going where it was sent.
|
|
166
|
+
* @param {(reason: string) => void} [o.onClose] After it is gone. The reasons are
|
|
167
|
+
* `'outside'`, `'escape'`, `'replaced'`, `'parent'`, `'detached'`, and whatever
|
|
168
|
+
* the caller passes to `close`.
|
|
169
|
+
* @returns {{el: HTMLElement, anchor: HTMLElement|null, place: () => void, owns: (node: Node) => boolean,
|
|
170
|
+
* close: (reason?: string) => void}|null}
|
|
171
|
+
* `null` when `anchor` has left the document: nothing opened, `onClose` will not be called.
|
|
172
|
+
*/
|
|
173
|
+
export function openAnchoredLayer({ anchor = null, el, align = 'start', gap = GAP, matchWidth = false, inside = null,
|
|
174
|
+
returnFocus = true, onClose = null }) {
|
|
175
|
+
if (anchor && anchor.isConnected === false) return null;
|
|
176
|
+
|
|
177
|
+
while (LAYERS.length && !LAYERS[LAYERS.length - 1].owns(anchor)) LAYERS[LAYERS.length - 1].close('replaced');
|
|
178
|
+
|
|
179
|
+
const doc = anchor?.ownerDocument || document;
|
|
180
|
+
const view = doc.defaultView || window;
|
|
181
|
+
(modalHost() || doc.body).appendChild(el);
|
|
182
|
+
const modalsUnder = new Set(shownModals(doc));
|
|
183
|
+
|
|
184
|
+
let closed = false;
|
|
185
|
+
const place = () => {
|
|
186
|
+
if (closed) return;
|
|
187
|
+
if (anchor && anchor.isConnected === false) { layer.close('detached'); return; }
|
|
188
|
+
positionAnchored(anchor, el, { align, gap, matchWidth });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const owns = (target) => {
|
|
192
|
+
if (holds(el, target)) return true;
|
|
193
|
+
for (let i = LAYERS.indexOf(layer) + 1; i < LAYERS.length; i += 1) if (holds(LAYERS[i].el, target)) return true;
|
|
194
|
+
if (typeof target?.closest !== 'function') return false;
|
|
195
|
+
if (target.closest(FOREIGN_FLOATS)) return true;
|
|
196
|
+
const modal = target.closest(MODAL_WINDOW);
|
|
197
|
+
if (modal && !modalsUnder.has(modal)) return true;
|
|
198
|
+
return !!inside && !!target.closest(inside);
|
|
199
|
+
};
|
|
200
|
+
const onPress = (ev) => { if (!holds(anchor, ev.target) && !owns(ev.target)) layer.close('outside'); };
|
|
201
|
+
const onDocKey = (ev) => { if (!holds(el, ev.target)) onKey(ev); };
|
|
202
|
+
const onKey = (ev) => {
|
|
203
|
+
if (ev.key !== 'Escape' || ev.isComposing || ev.defaultPrevented) return;
|
|
204
|
+
if (LAYERS[LAYERS.length - 1] !== layer) return;
|
|
205
|
+
if (doc.querySelector(FOREIGN_ESCAPE)) return;
|
|
206
|
+
if (shownModals(doc).some((win) => !modalsUnder.has(win))) return;
|
|
207
|
+
ev.preventDefault();
|
|
208
|
+
ev.stopPropagation();
|
|
209
|
+
layer.close('escape');
|
|
210
|
+
};
|
|
211
|
+
const onScroll = (ev) => { if (!holds(el, ev.target)) place(); };
|
|
212
|
+
// Placed before a caller fills it, and it changes with a filter: placed again
|
|
213
|
+
// whenever its size changes, or it would run past the window's edge.
|
|
214
|
+
const sized = typeof ResizeObserver === 'function' ? new ResizeObserver(() => place()) : null;
|
|
215
|
+
|
|
216
|
+
const layer = {
|
|
217
|
+
el,
|
|
218
|
+
anchor,
|
|
219
|
+
place,
|
|
220
|
+
owns,
|
|
221
|
+
close(reason = 'closed') {
|
|
222
|
+
if (closed) return;
|
|
223
|
+
while (LAYERS[LAYERS.length - 1] !== layer) LAYERS[LAYERS.length - 1].close('parent');
|
|
224
|
+
closed = true;
|
|
225
|
+
const active = doc.activeElement;
|
|
226
|
+
const held = holds(el, active) || active === doc.body;
|
|
227
|
+
doc.removeEventListener('pointerdown', onPress, true);
|
|
228
|
+
doc.removeEventListener('mousedown', onPress, true);
|
|
229
|
+
doc.removeEventListener('keydown', onDocKey, true);
|
|
230
|
+
el.removeEventListener('keydown', onKey);
|
|
231
|
+
view.removeEventListener('resize', place);
|
|
232
|
+
view.removeEventListener('scroll', onScroll, true);
|
|
233
|
+
sized?.disconnect();
|
|
234
|
+
el.remove();
|
|
235
|
+
LAYERS.pop();
|
|
236
|
+
if (returnFocus && held && reason !== 'blur' && anchor?.isConnected) anchor.focus?.({ preventScroll: true });
|
|
237
|
+
onClose?.(reason);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
LAYERS.push(layer);
|
|
241
|
+
|
|
242
|
+
// The press that OPENED it is still being dispatched; listen from the next turn.
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
if (closed) return;
|
|
245
|
+
doc.addEventListener('pointerdown', onPress, true);
|
|
246
|
+
doc.addEventListener('mousedown', onPress, true);
|
|
247
|
+
}, 0);
|
|
248
|
+
doc.addEventListener('keydown', onDocKey, true);
|
|
249
|
+
el.addEventListener('keydown', onKey);
|
|
250
|
+
view.addEventListener('resize', place);
|
|
251
|
+
view.addEventListener('scroll', onScroll, true);
|
|
252
|
+
sized?.observe(el);
|
|
253
|
+
place();
|
|
254
|
+
return layer;
|
|
255
|
+
}
|