@gemboss/ui 0.12.0 → 0.21.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 +395 -1
- package/README.md +321 -6
- package/catalog/editor.html +611 -0
- package/catalog/index.html +3114 -13
- package/gemboss-ui.css +3 -4682
- package/package.json +16 -4
- package/src/app-shell.css +1 -256
- package/src/assistant.css +1 -558
- package/src/auth.css +1 -606
- package/src/board-move.ts +194 -0
- package/src/chat-composer.css +1 -110
- package/src/chat.css +1 -432
- package/src/collections.css +1 -0
- package/src/collections.tsx +543 -0
- package/src/editor/controls.tsx +206 -0
- package/src/editor/css-url.ts +11 -0
- package/src/editor/frame.tsx +242 -0
- package/src/editor/index.ts +43 -0
- package/src/editor/panel.tsx +188 -0
- package/src/editor/panes.tsx +44 -0
- package/src/editor/swatch.ts +45 -0
- package/src/editor-plain.js +1 -0
- package/src/editor.css +1 -1099
- package/src/gemboss-field.css +1 -112
- package/src/gemboss-tokens.css +1 -164
- package/src/icons.tsx +2 -0
- package/src/index-table.tsx +179 -0
- package/src/index.ts +19 -0
- package/src/kit-plain.css +1 -483
- package/src/kit-plain.js +1 -440
- package/src/menu-layer.ts +87 -0
- package/src/menu.tsx +28 -2
- package/src/portal.css +1 -139
- package/src/reactions-data.ts +59 -0
- package/src/reactions.css +1 -0
- package/src/reactions.tsx +139 -0
- package/src/resource-list-select.tsx +72 -0
- package/src/segmented-click.ts +20 -0
- package/src/segmented.tsx +98 -0
- package/src/selection.tsx +78 -0
- package/src/shell.css +1 -1079
- package/src/theme-dark.css +1 -66
- package/src/ui.css +1 -610
package/src/kit-plain.js
CHANGED
|
@@ -1,440 +1 @@
|
|
|
1
|
-
/* kit-plain.js: the BEHAVIOUR half of kit-plain.css, for surfaces that render HTML on the server and have no React
|
|
2
|
-
(GemFactory, gemcare, gempress, partner-radar). Each of them wrote its own popup, toast and menu-closing code; the
|
|
3
|
-
popup's Tab trap, stacking and "ask before closing" were each found missing by a review on some surface first.
|
|
4
|
-
GemFactory's copy (server/ui-kit.mjs, 02/10/2026) had all three, so it is the source.
|
|
5
|
-
|
|
6
|
-
A classic script, not a module: a server-rendered page can inline it (readFileSync(require.resolve(
|
|
7
|
-
"@gemboss/ui/kit-plain.js"))) or load it with <script src>, and use `globalThis.gembossUI` from its own inline code.
|
|
8
|
-
Strings it writes are the caller's: `title`, `body` and `foot` are HTML (escape your text), toast text is TEXT.
|
|
9
|
-
|
|
10
|
-
const { openModal, toast, initMenus } = gembossUI;
|
|
11
|
-
initMenus(); // details.ui-menu: outside click, item click, window blur and Escape close it
|
|
12
|
-
const m = openModal({ title: "Delete #12?", body: '<div class="ui-modal__body">…</div>', foot: "…" });
|
|
13
|
-
m.close(value); // onClose(value) runs once; Escape / backdrop / × close with null
|
|
14
|
-
toast("Saved"); toast("Could not save", { error: true });
|
|
15
|
-
if (await confirm({ title: "Delete #12?", body: "It leaves the board.", confirmLabel: "Delete", tone: "critical" })) …
|
|
16
|
-
initShell(); // a hand-written AppShell: ☰ drawer, backdrop, Escape, followed link
|
|
17
|
-
initPasswordFields(); // .gb-auth-pw-toggle reveal; resetPasswordFields() on sign-out
|
|
18
|
-
initSearch(); // TopbarSearch: ⌘K / Ctrl+K and "/" focus the box, Escape leaves it
|
|
19
|
-
// initNav() runs by itself (0.10.0): a followed rail link shows a spinner, a rested-on link is prefetched
|
|
20
|
-
*/
|
|
21
|
-
(function (g) {
|
|
22
|
-
"use strict";
|
|
23
|
-
if (g.gembossUI) return;
|
|
24
|
-
var CLOSE_SVG = '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M5 5l10 10M15 5L5 15" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" fill="none"/></svg>';
|
|
25
|
-
// `summary` is a control (a kit ui-menu's ⋯ button); anything with no box (inside a closed <details>, display:none,
|
|
26
|
-
// [hidden]) is not, or Shift+Tab would "focus" a hidden menu item and stick (review #3633).
|
|
27
|
-
var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, iframe, [tabindex]:not([tabindex="-1"])';
|
|
28
|
-
var focusables = function (root) {
|
|
29
|
-
// checkVisibility knows a closed <details>' content (content-visibility: hidden still has boxes, so
|
|
30
|
-
// getClientRects alone counts its items); the box test is the fallback for browsers without it.
|
|
31
|
-
return Array.prototype.slice.call(root.querySelectorAll(FOCUSABLE)).filter(function (el) {
|
|
32
|
-
return el.checkVisibility ? el.checkVisibility({ visibilityProperty: true }) : el.getClientRects().length > 0;
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
var esc = function (s) { return String(s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); };
|
|
36
|
-
var MODALS = [];
|
|
37
|
-
var seq = 0;
|
|
38
|
-
var savedOverflow = null; // the page's own body overflow, put back when the LAST popup closes
|
|
39
|
-
|
|
40
|
-
/* Popup: .ui-overlay + .ui-modal, the same markup as the admin's Modal.tsx. Escape, a click on the backdrop and the ×
|
|
41
|
-
close it; Tab stays inside; popups stack and only the TOP one takes keys. `title` empty = no header bar (then
|
|
42
|
-
`label` names the dialog for screen readers). `beforeClose(value)` may return a Promise<boolean>: false keeps it
|
|
43
|
-
open (unsaved changes); it gets the close value, so a Save that calls m.close(data) can skip the question, and a
|
|
44
|
-
guard that THROWS keeps the popup open (closing would throw away what it guards). Focus returns to whatever had it
|
|
45
|
-
when the popup opened, or to the popup below when that element is gone or sits under it. */
|
|
46
|
-
function openModal(o) {
|
|
47
|
-
var title = o.title || "", label = o.label || "", closeLabel = o.closeLabel || "Close";
|
|
48
|
-
var opener = document.activeElement;
|
|
49
|
-
var overlay = document.createElement("div");
|
|
50
|
-
overlay.className = "ui-overlay" + (o.overlayClass ? " " + o.overlayClass : "");
|
|
51
|
-
overlay.setAttribute("role", "presentation");
|
|
52
|
-
var tid = "gb-modal-title-" + ++seq;
|
|
53
|
-
overlay.innerHTML = '<div class="ui-modal' + (o.className ? " " + o.className : "") + '" role="dialog" aria-modal="true" ' +
|
|
54
|
-
(title ? 'aria-labelledby="' + tid + '"' : 'aria-label="' + esc(label) + '"') + ' tabindex="-1">' +
|
|
55
|
-
(title ? '<div class="ui-modal__head"><div class="ui-modal__title" id="' + tid + '">' + title + '</div><button type="button" class="ui-modal__close" aria-label="' + esc(closeLabel) + '">' + CLOSE_SVG + "</button></div>" : "") +
|
|
56
|
-
(o.body || "") + (o.foot ? '<div class="ui-modal__foot">' + o.foot + "</div>" : "") + "</div>";
|
|
57
|
-
var panel = overlay.firstChild;
|
|
58
|
-
var asking = false;
|
|
59
|
-
var m = {
|
|
60
|
-
panel: panel,
|
|
61
|
-
overlay: overlay,
|
|
62
|
-
close: function (value, opts) {
|
|
63
|
-
var force = opts && opts.force;
|
|
64
|
-
if (MODALS.indexOf(m) < 0) return Promise.resolve();
|
|
65
|
-
if (!force && o.beforeClose) {
|
|
66
|
-
if (asking) return Promise.resolve();
|
|
67
|
-
asking = true;
|
|
68
|
-
return Promise.resolve().then(function () { return o.beforeClose(value); }).catch(function (err) { console.error(err); return false; }).then(function (go) {
|
|
69
|
-
asking = false;
|
|
70
|
-
// Closed by force while asking (a navigation): closing again would splice the NEWEST popup instead.
|
|
71
|
-
if (go && MODALS.indexOf(m) >= 0) finish(value);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
finish(value);
|
|
75
|
-
return Promise.resolve();
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
function finish(value) {
|
|
79
|
-
MODALS.splice(MODALS.indexOf(m), 1);
|
|
80
|
-
removeEventListener("keydown", onKey);
|
|
81
|
-
overlay.remove();
|
|
82
|
-
var top = MODALS[MODALS.length - 1];
|
|
83
|
-
if (!top) { document.body.style.overflow = savedOverflow || ""; savedOverflow = null; }
|
|
84
|
-
if (opener && opener.isConnected && opener.focus && (!top || top.panel.contains(opener))) opener.focus();
|
|
85
|
-
else if (top) top.panel.focus();
|
|
86
|
-
if (o.onClose) o.onClose(value);
|
|
87
|
-
}
|
|
88
|
-
function onKey(e) {
|
|
89
|
-
if (e === opening || MODALS[MODALS.length - 1] !== m) return;
|
|
90
|
-
// Escape while an IME is composing (zh, ja, vi telex) cancels the composition, not the dialog.
|
|
91
|
-
if (e.key === "Escape") { if (e.isComposing || e.keyCode === 229) return; e.preventDefault(); m.close(null); return; }
|
|
92
|
-
if (e.key !== "Tab") return;
|
|
93
|
-
var f = focusables(panel);
|
|
94
|
-
if (!f.length) return;
|
|
95
|
-
var first = f[0], last = f[f.length - 1];
|
|
96
|
-
if (!panel.contains(document.activeElement)) { e.preventDefault(); first.focus(); }
|
|
97
|
-
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
98
|
-
else if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
99
|
-
}
|
|
100
|
-
// Close on the backdrop only when the press STARTED there: selecting text in a field and releasing past the panel
|
|
101
|
-
// edge fires click on the overlay (the common ancestor) and would throw the form away.
|
|
102
|
-
var downOnBackdrop = false;
|
|
103
|
-
overlay.addEventListener("mousedown", function (e) { downOnBackdrop = e.target === overlay; });
|
|
104
|
-
overlay.addEventListener("click", function (e) { if (e.target === overlay && downOnBackdrop) m.close(null); downOnBackdrop = false; });
|
|
105
|
-
var x = panel.querySelector(".ui-modal__head .ui-modal__close");
|
|
106
|
-
if (x) x.onclick = function () { m.close(null); };
|
|
107
|
-
// Keys belong to the popup from the moment it opens (0.6.4). 0.5.1 attached this listener a task later, and on a busy
|
|
108
|
-
// page the browser runs input before timers: Escape or Tab pressed right after opening was lost, the popup stayed open
|
|
109
|
-
// or Tab left the trap (CI lost "stacked popups" three runs in a row, 03/10/2026). The one key it must NOT take is the
|
|
110
|
-
// keydown that is opening it: a popup opened from a keydown handler (content-radar's composer asks "close without
|
|
111
|
-
// saving?" on Escape) would receive that same Escape when it bubbles up to window — a listener added during a dispatch
|
|
112
|
-
// still runs if its target has not been reached yet — and close at once. `window.event` is that event while it runs.
|
|
113
|
-
var opening = window.event;
|
|
114
|
-
addEventListener("keydown", onKey);
|
|
115
|
-
if (!MODALS.length) savedOverflow = document.body.style.overflow;
|
|
116
|
-
MODALS.push(m);
|
|
117
|
-
// Inside the top open MODAL <dialog>, not <body> (0.6.9, 03/10/2026). A `<dialog class="ui-modal">` opened with
|
|
118
|
-
// showModal() sits in the top layer and makes the rest of the page inert, so a confirm appended to <body> drew
|
|
119
|
-
// BEHIND it and could not be clicked: GemCommunity's lesson editor asked "Discard changes?" into a dead layer and
|
|
120
|
-
// had to move the overlay by hand. Appended inside that dialog, the overlay rides the top layer with it.
|
|
121
|
-
var host = document.body;
|
|
122
|
-
try {
|
|
123
|
-
var open = document.querySelectorAll("dialog[open]");
|
|
124
|
-
for (var di = open.length - 1; di >= 0; di--) { if (open[di].matches(":modal")) { host = open[di]; break; } }
|
|
125
|
-
} catch (_) { /* a browser without :modal keeps <body> */ }
|
|
126
|
-
host.append(overlay);
|
|
127
|
-
document.body.style.overflow = "hidden";
|
|
128
|
-
setTimeout(function () { (panel.querySelector("[autofocus]") || panel).focus(); }, 0);
|
|
129
|
-
return m;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/* Toast: the admin's pill (.gemboss-toast), bottom centre, red when `error`. Text, not HTML. The viewport is a polite
|
|
133
|
-
live region; an ERROR toast is role="alert" so it is announced at once. `duration: 0` = stays until its × is
|
|
134
|
-
pressed (`dismissLabel` names it). When the region is created by this very toast, the toast goes in one frame
|
|
135
|
-
later: a live region that appears together with its first message is often not announced at all. */
|
|
136
|
-
function toast(text, opts) {
|
|
137
|
-
opts = opts || {};
|
|
138
|
-
var vp = document.querySelector(".gemboss-toast-viewport"), fresh = !vp;
|
|
139
|
-
if (!vp) {
|
|
140
|
-
vp = document.createElement("div");
|
|
141
|
-
vp.className = "gemboss-toast-viewport";
|
|
142
|
-
vp.setAttribute("role", "status");
|
|
143
|
-
vp.setAttribute("aria-live", "polite");
|
|
144
|
-
document.body.append(vp);
|
|
145
|
-
}
|
|
146
|
-
var t = document.createElement("div");
|
|
147
|
-
t.className = "gemboss-toast" + (opts.error ? " is-error" : "");
|
|
148
|
-
if (opts.error) t.setAttribute("role", "alert");
|
|
149
|
-
var span = document.createElement("span");
|
|
150
|
-
span.className = "gemboss-toast-text";
|
|
151
|
-
span.textContent = String(text);
|
|
152
|
-
t.append(span);
|
|
153
|
-
var sticky = opts.duration === 0;
|
|
154
|
-
if (sticky) {
|
|
155
|
-
var x = document.createElement("button");
|
|
156
|
-
x.type = "button";
|
|
157
|
-
x.className = "gemboss-toast-dismiss";
|
|
158
|
-
x.setAttribute("aria-label", opts.dismissLabel || "Dismiss");
|
|
159
|
-
x.innerHTML = CLOSE_SVG;
|
|
160
|
-
x.onclick = function () { t.remove(); };
|
|
161
|
-
t.append(x);
|
|
162
|
-
}
|
|
163
|
-
// Toasts sent while the new region is still waiting queue behind the first, so they keep their order.
|
|
164
|
-
if (fresh) vp.dataset.readyAt = String(Date.now() + 50);
|
|
165
|
-
var wait = Math.max(0, Number(vp.dataset.readyAt || 0) - Date.now());
|
|
166
|
-
var put = function () { vp.append(t); };
|
|
167
|
-
// ONE timer drains the waiting toasts in the order they were sent. A setTimeout per toast did not: the second one
|
|
168
|
-
// computed a slightly shorter wait and could land first (CI, 03/10/2026: "down" before "<b>hi</b>"). Anything sent
|
|
169
|
-
// after the region is ready still goes straight in, behind the queue.
|
|
170
|
-
if (wait || vp._gbQueue) {
|
|
171
|
-
(vp._gbQueue = vp._gbQueue || []).push(put);
|
|
172
|
-
if (!vp._gbFlush) vp._gbFlush = setTimeout(function () { var q = vp._gbQueue; vp._gbQueue = null; vp._gbFlush = null; q.forEach(function (f) { f(); }); }, wait);
|
|
173
|
-
} else put();
|
|
174
|
-
if (!sticky) setTimeout(function () { t.remove(); }, wait + (opts.duration || (opts.error ? 6000 : 3500)));
|
|
175
|
-
return t;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/* Menus (details.ui-menu, kit-plain.css) open with no script; closing is the page's job: an outside click, a click on
|
|
179
|
-
an item, the window losing focus (a click inside an iframe never reaches this document), and Escape. Escape closes
|
|
180
|
-
the MENU first and stops there, so a menu inside a popup does not close the popup with it. Idempotent.
|
|
181
|
-
The account menu (AccountMenu, `details.gb-account`) is a menu too since 0.5.1: GemFactory kept four lines of its
|
|
182
|
-
own for it. */
|
|
183
|
-
var menusOn = false;
|
|
184
|
-
var MENU = "details.ui-menu, details.gb-account";
|
|
185
|
-
var MENU_ITEM = ".ui-menu__item, .gemboss-account-menu-item";
|
|
186
|
-
function closeMenus(except) {
|
|
187
|
-
document.querySelectorAll("details.ui-menu[open], details.gb-account[open]").forEach(function (d) { if (d !== except) d.open = false; });
|
|
188
|
-
}
|
|
189
|
-
function initMenus() {
|
|
190
|
-
if (menusOn) return;
|
|
191
|
-
menusOn = true;
|
|
192
|
-
document.addEventListener("click", function (e) {
|
|
193
|
-
var d = e.target.closest && e.target.closest(MENU);
|
|
194
|
-
closeMenus(d);
|
|
195
|
-
if (d && e.target.closest(MENU_ITEM)) d.open = false;
|
|
196
|
-
});
|
|
197
|
-
addEventListener("blur", function () { closeMenus(); });
|
|
198
|
-
addEventListener("keydown", function (e) {
|
|
199
|
-
var d = e.key === "Escape" && !e.isComposing && document.querySelector("details.ui-menu[open], details.gb-account[open]");
|
|
200
|
-
if (!d) return;
|
|
201
|
-
e.preventDefault();
|
|
202
|
-
e.stopImmediatePropagation();
|
|
203
|
-
d.open = false;
|
|
204
|
-
var s = d.querySelector("summary");
|
|
205
|
-
if (s) s.focus();
|
|
206
|
-
}, true);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/* Confirm: the kit's answer to `window.confirm()` (four surfaces used the browser's box for deletes, 02/10/2026).
|
|
210
|
-
Resolves true on the confirm button, false on cancel / Escape / backdrop / ×. Strings are TEXT. `tone: "critical"`
|
|
211
|
-
paints the confirm button red (a delete); focus starts on Cancel then, so Enter never destroys by accident. */
|
|
212
|
-
function confirm(o) {
|
|
213
|
-
o = o || {};
|
|
214
|
-
var critical = o.tone === "critical";
|
|
215
|
-
return new Promise(function (resolve) {
|
|
216
|
-
var m = openModal({
|
|
217
|
-
title: esc(o.title || ""),
|
|
218
|
-
label: o.title ? "" : (o.label || "Confirm"),
|
|
219
|
-
// The × is a second Cancel: named in the caller's language unless told otherwise (review #3665).
|
|
220
|
-
closeLabel: o.closeLabel || o.cancelLabel,
|
|
221
|
-
body: o.body ? '<div class="ui-modal__body">' + esc(o.body) + "</div>" : "",
|
|
222
|
-
foot: '<button type="button" class="ui-btn" data-gb-confirm="0"' + (critical ? " autofocus" : "") + ">" + esc(o.cancelLabel || "Cancel") + "</button>" +
|
|
223
|
-
'<button type="button" class="ui-btn ui-btn--primary' + (critical ? " ui-btn--critical" : "") + '" data-gb-confirm="1"' + (critical ? "" : " autofocus") + ">" +
|
|
224
|
-
esc(o.confirmLabel || "OK") + "</button>",
|
|
225
|
-
onClose: function (v) { resolve(v === true); },
|
|
226
|
-
});
|
|
227
|
-
m.panel.querySelectorAll("[data-gb-confirm]").forEach(function (b) {
|
|
228
|
-
b.onclick = function () { m.close(b.getAttribute("data-gb-confirm") === "1"); };
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/* The shell's drawer, for a page that wrote AppShell's markup by hand (renderToStaticMarkup, or the README's
|
|
234
|
-
tree). The same behaviour as the React AppShell: the ☰ button toggles it, the backdrop, Escape and a followed
|
|
235
|
-
rail link close it, and so does `gemboss:assistant-docked`; the backdrop leaves the Tab order while closed (it is
|
|
236
|
-
a full-screen invisible button at phone width otherwise), and the ☰ names the rail it controls when the rail has
|
|
237
|
-
an id. Labels: `data-label-open` / `data-label-close` on the button, else its current aria-label is kept.
|
|
238
|
-
Idempotent. */
|
|
239
|
-
var shellOn = false;
|
|
240
|
-
function initShell() {
|
|
241
|
-
if (shellOn) return;
|
|
242
|
-
shellOn = true;
|
|
243
|
-
var shell = function () { return document.querySelector(".gemboss-platform-shell"); };
|
|
244
|
-
var set = function (open) {
|
|
245
|
-
var s = shell();
|
|
246
|
-
if (!s) return;
|
|
247
|
-
s.classList.toggle("is-mobile-nav-open", open);
|
|
248
|
-
var bd = s.querySelector(".gemboss-mobile-nav-backdrop");
|
|
249
|
-
if (bd) bd.tabIndex = open ? 0 : -1;
|
|
250
|
-
var b = s.querySelector(".gemboss-mobile-menu-btn");
|
|
251
|
-
if (!b) return;
|
|
252
|
-
var rail = s.querySelector(".icon-nav[id]");
|
|
253
|
-
if (rail && !b.hasAttribute("aria-controls")) b.setAttribute("aria-controls", rail.id);
|
|
254
|
-
b.setAttribute("aria-expanded", String(open));
|
|
255
|
-
var l = b.getAttribute(open ? "data-label-close" : "data-label-open");
|
|
256
|
-
if (l) b.setAttribute("aria-label", l);
|
|
257
|
-
};
|
|
258
|
-
var isOpen = function () { var s = shell(); return !!s && s.classList.contains("is-mobile-nav-open"); };
|
|
259
|
-
document.addEventListener("click", function (e) {
|
|
260
|
-
var t = e.target;
|
|
261
|
-
if (!t.closest) return;
|
|
262
|
-
if (t.closest(".gemboss-mobile-menu-btn")) set(!isOpen());
|
|
263
|
-
else if (t.closest(".gemboss-mobile-nav-backdrop") || t.closest(".icon-nav .gnav-item, .icon-nav .gnav-subitem")) set(false);
|
|
264
|
-
});
|
|
265
|
-
// Escape while an IME is composing (vi telex, zh, ja) cancels the composition, not the drawer.
|
|
266
|
-
addEventListener("keydown", function (e) { if (e.key === "Escape" && !e.isComposing && e.keyCode !== 229 && isOpen() && !MODALS.length) set(false); });
|
|
267
|
-
addEventListener("gemboss:assistant-docked", function () { set(false); });
|
|
268
|
-
set(isOpen());
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/* The sign-in screen's reveal button (PasswordField's `.gb-auth-pw-toggle`) on a page with no React: flips the
|
|
272
|
-
input between password and text, its name between `data-label-show` / `data-label-hide` (English defaults), and
|
|
273
|
-
the eye's slash, as the component does. `reset()` puts every field back to hidden (call it on sign-out, or the
|
|
274
|
-
next person sees the password in plain text). Idempotent. */
|
|
275
|
-
var pwOn = false;
|
|
276
|
-
var SLASH = "M3 17 17 3";
|
|
277
|
-
// ONE lookup for the button's field: its aria-controls, else the input beside it in `.gb-auth-pw`.
|
|
278
|
-
function pwInput(b) {
|
|
279
|
-
return document.getElementById(b.getAttribute("aria-controls") || "") || (b.parentNode && b.parentNode.querySelector("input"));
|
|
280
|
-
}
|
|
281
|
-
function pwSet(b, shown) {
|
|
282
|
-
var input = pwInput(b);
|
|
283
|
-
if (!input) return;
|
|
284
|
-
input.type = shown ? "text" : "password";
|
|
285
|
-
var name = shown ? (b.getAttribute("data-label-hide") || "Hide password") : (b.getAttribute("data-label-show") || "Show password");
|
|
286
|
-
b.setAttribute("aria-label", name);
|
|
287
|
-
b.setAttribute("title", name);
|
|
288
|
-
var svg = b.querySelector("svg"), slash = svg && svg.querySelector('path[d="' + SLASH + '"]');
|
|
289
|
-
if (svg && shown && !slash) { var p = document.createElementNS("http://www.w3.org/2000/svg", "path"); p.setAttribute("d", SLASH); svg.append(p); }
|
|
290
|
-
if (slash && !shown) slash.remove();
|
|
291
|
-
}
|
|
292
|
-
function initPasswordFields() {
|
|
293
|
-
if (pwOn) return;
|
|
294
|
-
pwOn = true;
|
|
295
|
-
document.addEventListener("click", function (e) {
|
|
296
|
-
var b = e.target.closest && e.target.closest(".gb-auth-pw-toggle");
|
|
297
|
-
var input = b && pwInput(b);
|
|
298
|
-
if (input) pwSet(b, input.type === "password");
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
function resetPasswordFields() {
|
|
302
|
-
document.querySelectorAll(".gb-auth-pw-toggle").forEach(function (b) { pwSet(b, false); });
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// TopbarSearch (search.tsx, 0.6.3): the admin's ⌘K, for a page without React. "/" only when no field has focus, so it
|
|
306
|
-
// never eats a slash typed into a form; Escape blurs the box and gives the page its keys back.
|
|
307
|
-
var searchBound = false;
|
|
308
|
-
function initSearch() {
|
|
309
|
-
if (searchBound) return;
|
|
310
|
-
searchBound = true;
|
|
311
|
-
document.addEventListener("keydown", function (e) {
|
|
312
|
-
var box = document.querySelector(".gemboss-topbar-search-input");
|
|
313
|
-
if (!box) return;
|
|
314
|
-
var t = e.target, typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
|
|
315
|
-
if ((e.key === "k" || e.key === "K") && (e.metaKey || e.ctrlKey) && !e.altKey) { e.preventDefault(); box.focus(); box.select(); }
|
|
316
|
-
else if (e.key === "/" && !typing && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); box.focus(); }
|
|
317
|
-
else if (e.key === "Escape" && t === box) box.blur();
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/* Navigation pending (0.10.0), the same behaviour as `@gemboss/ui/next` NavLink for a page with no router: a followed
|
|
322
|
-
rail link answers on the first frame (its icon becomes a spinner: `aria-busy="true"`, drawn by app-shell.css), and a
|
|
323
|
-
link the pointer or keyboard focus RESTS on for 120 ms, or a finger touches, is prefetched once (speculation rules in Chromium, else
|
|
324
|
-
`<link rel="prefetch">`; see navPrefetch), so the click that follows does not wait for the server. Runs by itself
|
|
325
|
-
when the script loads (delegated on the document, so links
|
|
326
|
-
rendered later are covered); `initNav()` is idempotent.
|
|
327
|
-
Which links: `.gnav-item` / `.gnav-subitem` anchors, and any `a[data-gb-pending]` ("inline" | "corner": a page link
|
|
328
|
-
that gets the small spinner). Same origin, a plain left click (no modifier key), no `target` other than _self, no
|
|
329
|
-
`download`, and not a jump to `#hash` on this same page. Opt out: `data-gb-nav="off"` on the link or any ancestor
|
|
330
|
-
(`<html>` for the whole page); `data-gb-prefetch="off"` keeps the spinner and skips the prefetch (a GET with an
|
|
331
|
-
effect, e.g. a sign-out link). A link marked `data-gb-nav="next"` belongs to NavLink and is left alone, and in a
|
|
332
|
-
Next app, or once a rail click was taken over by a client router, nothing is prefetched here (the router owns that).
|
|
333
|
-
The mark comes off on `pageshow` (back/forward cache: the page you return to must not still be "loading"), when the
|
|
334
|
-
navigation is cancelled (Escape / the browser's stop: the Navigation API's `navigateerror`), on the next followed
|
|
335
|
-
link, and after 15 s at the latest (a 204, a download served as a page link). */
|
|
336
|
-
var NAV_DWELL = 120, NAV_NET = 15000;
|
|
337
|
-
var SPEC_RULES = typeof HTMLScriptElement !== "undefined" && !!HTMLScriptElement.supports && HTMLScriptElement.supports("speculationrules");
|
|
338
|
-
var navOn = false, navTimer = 0, navNet = 0, navArmed = null, prefetched = {}, clientRouted = false;
|
|
339
|
-
function navLink(t) {
|
|
340
|
-
var a = t && t.closest ? t.closest("a[href]") : null;
|
|
341
|
-
if (!a || !(a.matches(".gnav-item, .gnav-subitem") || a.hasAttribute("data-gb-pending"))) return null;
|
|
342
|
-
if (a.getAttribute("data-gb-nav") === "next" || a.closest('[data-gb-nav="off"]')) return null;
|
|
343
|
-
if ((a.target && a.target !== "_self") || a.hasAttribute("download")) return null;
|
|
344
|
-
var u;
|
|
345
|
-
try { u = new URL(a.href, location.href); } catch (_) { return null; }
|
|
346
|
-
if (u.origin !== location.origin || !/^https?:$/.test(u.protocol)) return null;
|
|
347
|
-
// A #hash on this same page scrolls; nothing loads. `href="#"` too: its URL's .hash is "" (review #3777).
|
|
348
|
-
if (u.pathname === location.pathname && u.search === location.search && (u.hash || (a.getAttribute("href") || "").indexOf("#") > -1)) return null;
|
|
349
|
-
return a;
|
|
350
|
-
}
|
|
351
|
-
function navClear() {
|
|
352
|
-
clearTimeout(navNet);
|
|
353
|
-
document.querySelectorAll('[data-gb-busy]').forEach(function (a) {
|
|
354
|
-
a.removeAttribute("data-gb-busy");
|
|
355
|
-
a.removeAttribute("aria-busy");
|
|
356
|
-
var m = a.querySelector(":scope > .gb-pending-mark");
|
|
357
|
-
if (m) m.remove();
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
function navMark(a) {
|
|
361
|
-
navClear();
|
|
362
|
-
a.setAttribute("data-gb-busy", "");
|
|
363
|
-
a.setAttribute("aria-busy", "true");
|
|
364
|
-
if (a.hasAttribute("data-gb-pending")) {
|
|
365
|
-
var m = document.createElement("span");
|
|
366
|
-
m.className = "gb-pending-mark";
|
|
367
|
-
m.setAttribute("role", "status");
|
|
368
|
-
m.innerHTML = '<span class="ui-sr-only">' + esc(a.getAttribute("data-gb-pending-label") || "Loading") + "</span>";
|
|
369
|
-
a.appendChild(m);
|
|
370
|
-
}
|
|
371
|
-
navNet = setTimeout(navClear, NAV_NET);
|
|
372
|
-
}
|
|
373
|
-
function navPrefetch(a) {
|
|
374
|
-
// A page whose router owns its links fetches them itself: Next (detected), or any app seen taking a rail click over
|
|
375
|
-
// (preventDefault: react-router, a SPA). A document prefetch there is a server request the click never uses.
|
|
376
|
-
if (clientRouted || g.next || g.__next_f || g.__NEXT_DATA__ || a.closest('[data-gb-prefetch="off"]')) return;
|
|
377
|
-
var c = navigator.connection;
|
|
378
|
-
if (c && c.saveData) return;
|
|
379
|
-
var u = new URL(a.href, location.href);
|
|
380
|
-
u.hash = "";
|
|
381
|
-
var href = u.href;
|
|
382
|
-
if (prefetched[href] || (u.pathname === location.pathname && u.search === location.search)) return;
|
|
383
|
-
prefetched[href] = true;
|
|
384
|
-
// Speculation rules where the browser has them (Chromium): its prefetch is what the click then USES. A plain
|
|
385
|
-
// `<link rel="prefetch">` of a page is reused only when that page is cacheable (max-age > 0); a server-rendered page
|
|
386
|
-
// sends max-age=0 / no-cache, so the click fetched it a second time, and one still in flight made the click SLOWER
|
|
387
|
-
// (measured 05/10/2026 through a 280 ms proxy: 1121 ms to the new page against 737 ms with no prefetch at all).
|
|
388
|
-
// Speculation rules were reused under every Cache-Control tried. Other browsers get the link, which helps there
|
|
389
|
-
// only for cacheable pages.
|
|
390
|
-
var el;
|
|
391
|
-
if (SPEC_RULES) {
|
|
392
|
-
el = document.createElement("script");
|
|
393
|
-
el.type = "speculationrules";
|
|
394
|
-
el.textContent = JSON.stringify({ prefetch: [{ source: "list", urls: [href] }] });
|
|
395
|
-
} else {
|
|
396
|
-
el = document.createElement("link");
|
|
397
|
-
el.rel = "prefetch";
|
|
398
|
-
el.href = href;
|
|
399
|
-
}
|
|
400
|
-
el.setAttribute("data-gb-prefetched", "");
|
|
401
|
-
document.head.appendChild(el);
|
|
402
|
-
}
|
|
403
|
-
function initNav() {
|
|
404
|
-
// A bundled app also evaluates this file on the server (a client module is rendered there too): no document.
|
|
405
|
-
if (navOn || typeof document === "undefined") return;
|
|
406
|
-
navOn = true;
|
|
407
|
-
// On the WINDOW, bubble phase: the last stop of a click, after every element and document listener the page adds
|
|
408
|
-
// (this script loads early, so a document listener here would run before the page's own delegated handlers). A
|
|
409
|
-
// handler that took the click over (a client router, a popup) has called preventDefault by now.
|
|
410
|
-
addEventListener("click", function (e) {
|
|
411
|
-
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
412
|
-
var a = navLink(e.target);
|
|
413
|
-
if (!a) return;
|
|
414
|
-
if (e.defaultPrevented) { clientRouted = true; return; }
|
|
415
|
-
navMark(a);
|
|
416
|
-
});
|
|
417
|
-
// Pointer and keyboard focus both have to REST: a sweep down the rail, or Tab run down it, fetches nothing.
|
|
418
|
-
var arm = function (a) {
|
|
419
|
-
if (a === navArmed) return; // moving between the icon and the label of the same link is not a new rest
|
|
420
|
-
clearTimeout(navTimer);
|
|
421
|
-
navArmed = a;
|
|
422
|
-
if (a) navTimer = setTimeout(function () { if (navArmed === a) navPrefetch(a); }, NAV_DWELL);
|
|
423
|
-
};
|
|
424
|
-
// The focus a press gives a link is not keyboard intent: that click is already navigating.
|
|
425
|
-
var pressed = null;
|
|
426
|
-
document.addEventListener("mousedown", function (e) { pressed = navLink(e.target); if (pressed && pressed === navArmed) clearTimeout(navTimer); });
|
|
427
|
-
document.addEventListener("mouseover", function (e) { arm(navLink(e.target)); });
|
|
428
|
-
document.addEventListener("focusin", function (e) { var a = navLink(e.target); if (a && a === pressed) return; arm(a); });
|
|
429
|
-
// A touch is already deliberate (its click follows in ~100 ms).
|
|
430
|
-
document.addEventListener("touchstart", function (e) { var a = navLink(e.target); if (a) navPrefetch(a); }, { passive: true });
|
|
431
|
-
addEventListener("pageshow", navClear);
|
|
432
|
-
addEventListener("keydown", function (e) { if (e.key === "Escape") navClear(); });
|
|
433
|
-
if (g.navigation && g.navigation.addEventListener) g.navigation.addEventListener("navigateerror", navClear);
|
|
434
|
-
}
|
|
435
|
-
initNav();
|
|
436
|
-
|
|
437
|
-
g.gembossUI = { openModal: openModal, toast: toast, confirm: confirm, initMenus: initMenus, closeMenus: closeMenus,
|
|
438
|
-
initSearch: initSearch, initShell: initShell, initPasswordFields: initPasswordFields, resetPasswordFields: resetPasswordFields,
|
|
439
|
-
initNav: initNav, openModals: function () { return MODALS.length; } };
|
|
440
|
-
})(globalThis);
|
|
1
|
+
(function(h){"use strict";if(h.gembossUI)return;var X='<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M5 5l10 10M15 5L5 15" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" fill="none"/></svg>',Se='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, iframe, [tabindex]:not([tabindex="-1"])',ke=function(e){return Array.prototype.slice.call(e.querySelectorAll(Se)).filter(function(t){return t.checkVisibility?t.checkVisibility({visibilityProperty:!0}):t.getClientRects().length>0})},E=function(e){return String(e).replace(/[&<>"']/g,function(t){return{"&":"&","<":"<",">":">",'"':""","'":"'"}[t]})},p=[],xe=0,H=null;function z(e){var t=e.title||"",r=e.label||"",n=e.closeLabel||"Close",a=document.activeElement,i=document.createElement("div");i.className="ui-overlay"+(e.overlayClass?" "+e.overlayClass:""),i.setAttribute("role","presentation");var l="gb-modal-title-"+ ++xe;i.innerHTML='<div class="ui-modal'+(e.className?" "+e.className:"")+'" role="dialog" aria-modal="true" '+(t?'aria-labelledby="'+l+'"':'aria-label="'+E(r)+'"')+' tabindex="-1">'+(t?'<div class="ui-modal__head"><div class="ui-modal__title" id="'+l+'">'+t+'</div><button type="button" class="ui-modal__close" aria-label="'+E(n)+'">'+X+"</button></div>":"")+(e.body||"")+(e.foot?'<div class="ui-modal__foot">'+e.foot+"</div>":"")+"</div>";var u=i.firstChild,s=!1,c={panel:u,overlay:i,close:function(f,b){var A=b&&b.force;return p.indexOf(c)<0?Promise.resolve():!A&&e.beforeClose?s?Promise.resolve():(s=!0,Promise.resolve().then(function(){return e.beforeClose(f)}).catch(function(x){return console.error(x),!1}).then(function(x){s=!1,x&&p.indexOf(c)>=0&&d(f)})):(d(f),Promise.resolve())}};function d(f){p.splice(p.indexOf(c),1),removeEventListener("keydown",v),i.remove();var b=p[p.length-1];b||(document.body.style.overflow=H||"",H=null),a&&a.isConnected&&a.focus&&(!b||b.panel.contains(a))?a.focus():b&&b.panel.focus(),e.onClose&&e.onClose(f)}function v(f){if(!(f===S||p[p.length-1]!==c)){if(f.key==="Escape"){if(f.isComposing||f.keyCode===229)return;f.preventDefault(),c.close(null);return}if(f.key==="Tab"){var b=ke(u);if(b.length){var A=b[0],x=b[b.length-1];u.contains(document.activeElement)?!f.shiftKey&&document.activeElement===x?(f.preventDefault(),A.focus()):f.shiftKey&&document.activeElement===A&&(f.preventDefault(),x.focus()):(f.preventDefault(),A.focus())}}}}var m=!1;i.addEventListener("mousedown",function(f){m=f.target===i}),i.addEventListener("click",function(f){f.target===i&&m&&c.close(null),m=!1});var g=u.querySelector(".ui-modal__head .ui-modal__close");g&&(g.onclick=function(){c.close(null)});var S=window.event;addEventListener("keydown",v),p.length||(H=document.body.style.overflow),p.push(c);var O=document.body;try{for(var k=document.querySelectorAll("dialog[open]"),y=k.length-1;y>=0;y--)if(k[y].matches(":modal")){O=k[y];break}}catch{}return O.append(i),document.body.style.overflow="hidden",setTimeout(function(){(u.querySelector("[autofocus]")||u).focus()},0),c}function Le(e,t){t=t||{};var r=document.querySelector(".gemboss-toast-viewport"),n=!r;r||(r=document.createElement("div"),r.className="gemboss-toast-viewport",r.setAttribute("role","status"),r.setAttribute("aria-live","polite"),document.body.append(r));var a=document.createElement("div");a.className="gemboss-toast"+(t.error?" is-error":""),t.error&&a.setAttribute("role","alert");var i=document.createElement("span");i.className="gemboss-toast-text",i.textContent=String(e),a.append(i);var l=t.duration===0;if(l){var u=document.createElement("button");u.type="button",u.className="gemboss-toast-dismiss",u.setAttribute("aria-label",t.dismissLabel||"Dismiss"),u.innerHTML=X,u.onclick=function(){a.remove()},a.append(u)}n&&(r.dataset.readyAt=String(Date.now()+50));var s=Math.max(0,Number(r.dataset.readyAt||0)-Date.now()),c=function(){r.append(a)};return s||r._gbQueue?((r._gbQueue=r._gbQueue||[]).push(c),r._gbFlush||(r._gbFlush=setTimeout(function(){var d=r._gbQueue;r._gbQueue=null,r._gbFlush=null,d.forEach(function(v){v()})},s))):c(),l||setTimeout(function(){a.remove()},s+(t.duration||(t.error?6e3:3500))),a}var G=!1,we="details.ui-menu, details.gb-account",qe=".ui-menu__item, .gemboss-account-menu-item";function R(e){document.querySelectorAll("details.ui-menu[open], details.gb-account[open]").forEach(function(t){t!==e&&(t.open=!1)})}function Ce(){G||(G=!0,document.addEventListener("click",function(e){var t=e.target.closest&&e.target.closest(we);R(t),t&&e.target.closest(qe)&&(t.open=!1)}),addEventListener("blur",function(){R()}),addEventListener("keydown",function(e){var t=e.key==="Escape"&&!e.isComposing&&document.querySelector("details.ui-menu[open], details.gb-account[open]");if(t){e.preventDefault(),e.stopImmediatePropagation(),t.open=!1;var r=t.querySelector("summary");r&&r.focus()}},!0))}var J=!1,Y=typeof HTMLElement<"u"&&Object.prototype.hasOwnProperty.call(HTMLElement.prototype,"popover");function B(e){return{list:e.querySelector(":scope > .ui-menu__list"),button:e.querySelector(":scope > summary")}}function F(e){var t=B(e),r=t.list,n=t.button;if(!(!r||!n||!e.open||!Y)){r.getAttribute("popover")!=="manual"&&r.setAttribute("popover","manual"),r.matches(":popover-open")||r.showPopover(),r.style.maxHeight="",r.style.left="0px",r.style.top="0px";var a=n.getBoundingClientRect(),i=document.documentElement.clientWidth,l=h.innerHeight,u=r.offsetWidth,s=r.offsetHeight,c=l-a.bottom-4-8,d=a.top-4-8,v;s<=c||c>=d?(v=a.bottom+4,s>c&&(r.style.maxHeight=Math.max(c,0)+"px"),r.setAttribute("data-side","bottom")):(s>d&&(r.style.maxHeight=Math.max(d,0)+"px"),v=a.top-4-Math.min(s,d),r.setAttribute("data-side","top"));var m=e.classList.contains("ui-menu--end")?a.right-u:a.left;m=Math.max(8,Math.min(m,i-u-8)),r.style.left=Math.round(m)+"px",r.style.top=Math.round(v)+"px"}}function Me(e){var t=B(e).list;t&&t.hasAttribute("popover")&&t.matches(":popover-open")&&t.hidePopover()}function j(e){e&&e.closest&&e.closest(".ui-menu__list")||document.querySelectorAll("details.ui-menu[open]").forEach(function(t){var r=B(t).list;!r||!r.matches(":popover-open")||(!e||e.nodeType===9||e.contains&&e.contains(t))&&(t.open=!1)})}function Z(){J||!Y||typeof document>"u"||(J=!0,document.documentElement.setAttribute("data-gb-menus","float"),document.querySelectorAll("details.ui-menu[open]").forEach(F),document.addEventListener("toggle",function(e){var t=e.target;!t||!t.matches||!t.matches("details.ui-menu")||(t.open?F(t):Me(t))},!0),document.addEventListener("scroll",function(e){j(e.target)},!0),addEventListener("resize",function(){j(null)}))}Z();function Te(e){e=e||{};var t=e.tone==="critical";return new Promise(function(r){var n=z({title:E(e.title||""),label:e.title?"":e.label||"Confirm",closeLabel:e.closeLabel||e.cancelLabel,body:e.body?'<div class="ui-modal__body">'+E(e.body)+"</div>":"",foot:'<button type="button" class="ui-btn" data-gb-confirm="0"'+(t?" autofocus":"")+">"+E(e.cancelLabel||"Cancel")+'</button><button type="button" class="ui-btn ui-btn--primary'+(t?" ui-btn--critical":"")+'" data-gb-confirm="1"'+(t?"":" autofocus")+">"+E(e.confirmLabel||"OK")+"</button>",onClose:function(a){r(a===!0)}});n.panel.querySelectorAll("[data-gb-confirm]").forEach(function(a){a.onclick=function(){n.close(a.getAttribute("data-gb-confirm")==="1")}})})}var ee=!1;function Ne(){if(!ee){ee=!0;var e=function(){return document.querySelector(".gemboss-platform-shell")},t=function(n){var a=e();if(a){a.classList.toggle("is-mobile-nav-open",n);var i=a.querySelector(".gemboss-mobile-nav-backdrop");i&&(i.tabIndex=n?0:-1);var l=a.querySelector(".gemboss-mobile-menu-btn");if(l){var u=a.querySelector(".icon-nav[id]");u&&!l.hasAttribute("aria-controls")&&l.setAttribute("aria-controls",u.id),l.setAttribute("aria-expanded",String(n));var s=l.getAttribute(n?"data-label-close":"data-label-open");s&&l.setAttribute("aria-label",s)}}},r=function(){var n=e();return!!n&&n.classList.contains("is-mobile-nav-open")};document.addEventListener("click",function(n){var a=n.target;a.closest&&(a.closest(".gemboss-mobile-menu-btn")?t(!r()):(a.closest(".gemboss-mobile-nav-backdrop")||a.closest(".icon-nav .gnav-item, .icon-nav .gnav-subitem"))&&t(!1))}),addEventListener("keydown",function(n){n.key==="Escape"&&!n.isComposing&&n.keyCode!==229&&r()&&!p.length&&t(!1)}),addEventListener("gemboss:assistant-docked",function(){t(!1)}),t(r())}}var te=!1,re="M3 17 17 3";function ne(e){return document.getElementById(e.getAttribute("aria-controls")||"")||e.parentNode&&e.parentNode.querySelector("input")}function ae(e,t){var r=ne(e);if(r){r.type=t?"text":"password";var n=t?e.getAttribute("data-label-hide")||"Hide password":e.getAttribute("data-label-show")||"Show password";e.setAttribute("aria-label",n),e.setAttribute("title",n);var a=e.querySelector("svg"),i=a&&a.querySelector('path[d="'+re+'"]');if(a&&t&&!i){var l=document.createElementNS("http://www.w3.org/2000/svg","path");l.setAttribute("d",re),a.append(l)}i&&!t&&i.remove()}}function Oe(){te||(te=!0,document.addEventListener("click",function(e){var t=e.target.closest&&e.target.closest(".gb-auth-pw-toggle"),r=t&&ne(t);r&&ae(t,r.type==="password")}))}function De(){document.querySelectorAll(".gb-auth-pw-toggle").forEach(function(e){ae(e,!1)})}var ie=!1;function Pe(){ie||(ie=!0,document.addEventListener("keydown",function(e){var t=document.querySelector(".gemboss-topbar-search-input");if(t){var r=e.target,n=r&&(r.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(r.tagName));(e.key==="k"||e.key==="K")&&(e.metaKey||e.ctrlKey)&&!e.altKey?(e.preventDefault(),t.focus(),t.select()):e.key==="/"&&!n&&!e.metaKey&&!e.ctrlKey&&!e.altKey?(e.preventDefault(),t.focus()):e.key==="Escape"&&r===t&&t.blur()}}))}var Ke=120,He=15e3,Re=typeof HTMLScriptElement<"u"&&!!HTMLScriptElement.supports&&HTMLScriptElement.supports("speculationrules"),oe=!1,I=0,le=0,D=null,ue={},ce=!1;function L(e){var t=e&&e.closest?e.closest("a[href]"):null;if(!t||!(t.matches(".gnav-item, .gnav-subitem")||t.hasAttribute("data-gb-pending"))||t.getAttribute("data-gb-nav")==="next"||t.closest('[data-gb-nav="off"]')||t.target&&t.target!=="_self"||t.hasAttribute("download"))return null;var r;try{r=new URL(t.href,location.href)}catch{return null}return r.origin!==location.origin||!/^https?:$/.test(r.protocol)||r.pathname===location.pathname&&r.search===location.search&&(r.hash||(t.getAttribute("href")||"").indexOf("#")>-1)?null:t}function w(){clearTimeout(le),document.querySelectorAll("[data-gb-busy]").forEach(function(e){e.removeAttribute("data-gb-busy"),e.removeAttribute("aria-busy");var t=e.querySelector(":scope > .gb-pending-mark");t&&t.remove()})}function Be(e){if(w(),e.setAttribute("data-gb-busy",""),e.setAttribute("aria-busy","true"),e.hasAttribute("data-gb-pending")){var t=document.createElement("span");t.className="gb-pending-mark",t.setAttribute("role","status"),t.innerHTML='<span class="ui-sr-only">'+E(e.getAttribute("data-gb-pending-label")||"Loading")+"</span>",e.appendChild(t)}le=setTimeout(w,He)}function se(e){if(!(ce||h.next||h.__next_f||h.__NEXT_DATA__||e.closest('[data-gb-prefetch="off"]'))){var t=navigator.connection;if(!(t&&t.saveData)){var r=new URL(e.href,location.href);r.hash="";var n=r.href;if(!(ue[n]||r.pathname===location.pathname&&r.search===location.search)){ue[n]=!0;var a;Re?(a=document.createElement("script"),a.type="speculationrules",a.textContent=JSON.stringify({prefetch:[{source:"list",urls:[n]}]})):(a=document.createElement("link"),a.rel="prefetch",a.href=n),a.setAttribute("data-gb-prefetched",""),document.head.appendChild(a)}}}}function de(){if(!(oe||typeof document>"u")){oe=!0,addEventListener("click",function(r){if(!(r.button!==0||r.metaKey||r.ctrlKey||r.shiftKey||r.altKey)){var n=L(r.target);if(n){if(r.defaultPrevented){ce=!0;return}Be(n)}}});var e=function(r){r!==D&&(clearTimeout(I),D=r,r&&(I=setTimeout(function(){D===r&&se(r)},Ke)))},t=null;document.addEventListener("mousedown",function(r){t=L(r.target),t&&t===D&&clearTimeout(I)}),document.addEventListener("mouseover",function(r){e(L(r.target))}),document.addEventListener("focusin",function(r){var n=L(r.target);n&&n===t||e(n)}),document.addEventListener("touchstart",function(r){var n=L(r.target);n&&se(n)},{passive:!0}),addEventListener("pageshow",w),addEventListener("keydown",function(r){r.key==="Escape"&&w()}),h.navigation&&h.navigation.addEventListener&&h.navigation.addEventListener("navigateerror",w)}}de();var Fe='a[href], button, input, select, textarea, label, summary, details, dialog, [role="dialog"], [role="menu"], [role="button"], [role="menuitem"], [role="checkbox"], [contenteditable="true"], [contenteditable=""], [data-row-ignore], .ui-table__check',fe='details[open], dialog[open], [role="dialog"], [role="menu"]';function Ie(e,t){return t&&t.closest&&t.closest(fe)?!0:Array.prototype.some.call(e.querySelectorAll(fe),function(r){return r.getClientRects().length>0})}var be=!1,q=null,U=".ui-index, .ui-resource";function Ue(e){for(var t=e.getAttribute("data-href"),r=e.querySelectorAll("a[href]"),n=0;n<r.length;n++)if(r[n].getAttribute("href")===t)return r[n];return null}function ve(e){if(!(e.defaultPrevented||e.button>1||!e.target.closest)){var t=e.target.closest("[data-href]");if(t){var r=e.target.closest(Fe);if(!(r&&t.contains(r))){var n=h.getSelection?h.getSelection():null;if(!(n&&!n.isCollapsed&&n.anchorNode&&t.contains(n.anchorNode))){var a=Ue(t),i=a?a.href:new URL(t.getAttribute("data-href"),location.href).href;if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1){h.open(i,"_blank","noopener");return}a?a.click():location.assign(i)}}}}}function me(e){var t=[];return e.querySelectorAll("input[data-gb-select]").forEach(function(r){r.checked&&t.push(r.value)}),t}function C(e){var t=e.querySelectorAll("input[data-gb-select]"),r=0;t.forEach(function(i){var l=i.closest("tr, .ui-list__item, [data-href]");i.checked&&r++,l&&(l.classList.toggle("is-selected",i.checked),l.matches(".ui-list__item")||l.setAttribute("aria-selected",i.checked?"true":"false"))}),e.querySelectorAll("input[data-gb-select-all]").forEach(function(i){i.checked=r>0&&r===t.length,i.indeterminate=r>0&&r<t.length});var n=e.querySelector(".ui-bulkbar");n&&e.matches(".ui-resource")?n.querySelectorAll(".ui-bulkbar__actions button").forEach(function(i){i.disabled=r===0}):n&&(n.hidden=r===0);var a=e.querySelector("[data-gb-count]");a&&(a.textContent=(a.getAttribute("data-gb-count")||"{n} selected").replace("{n}",r)),e.dispatchEvent(new CustomEvent("gb:selection",{bubbles:!0,detail:{ids:me(e),total:t.length}}))}function pe(e){e.querySelectorAll("input[data-gb-select]").forEach(function(t){t.checked=!1}),C(e)}function ge(e){if(!e)return"";var t=e.getAttribute("data-sort-value");return t??e.textContent.trim()}function Ve(e){var t=e.closest("th"),r=e.closest("table");if(!(!t||!r)){var n=t.getAttribute("aria-sort")==="ascending"?"descending":"ascending";if(r.querySelectorAll("thead th[aria-sort]").forEach(function(c){c!==t&&c.setAttribute("aria-sort","none")}),t.setAttribute("aria-sort",n),r.getAttribute("data-gb-sort")!=="client"){r.dispatchEvent(new CustomEvent("gb:sort",{bubbles:!0,detail:{key:t.getAttribute("data-key"),direction:n}}));return}var a=Array.prototype.indexOf.call(t.parentElement.children,t),i=r.tBodies[0];if(i){var l=Array.prototype.slice.call(i.rows),u=n==="ascending"?1:-1,s=function(c){var d=String(c).replace(/[\s,%$€£¥]/g,"");return d!==""&&!isNaN(+d)?+d:null};l.sort(function(c,d){var v=ge(c.cells[a]),m=ge(d.cells[a]),g=s(v),S=s(m);return u*(g!=null&&S!=null?g-S:String(v).localeCompare(String(m),void 0,{numeric:!0,sensitivity:"base"}))}),l.forEach(function(c){i.appendChild(c)})}}}function We(e){return Array.prototype.slice.call(e.querySelectorAll(":scope > .ui-board__col[data-col]"))}function M(e){return Array.prototype.slice.call(e.querySelectorAll("li[data-id]")).filter(function(t){return t.closest(".ui-board__col")===e})}function V(e){var t=e.getAttribute("data-name")||(e.querySelector(".ui-card__link, h2, h3, h4")||e).textContent||"";return t.trim().replace(/\s+/g," ").slice(0,80)||"Card"}function P(e){var t=e.querySelector(".ui-board__name");return(t&&t.textContent||e.getAttribute("data-col")||"").trim()}function _(e,t){var r=e.querySelector(":scope > [data-gb-live]");r||(r=document.createElement("div"),r.className="ui-sr-only",r.setAttribute("aria-live","assertive"),r.setAttribute("data-gb-live",""),e.appendChild(r)),r.textContent="",setTimeout(function(){r.textContent=t},30)}function he(e,t){var r=e.querySelector(".ui-board__count"),n=r?parseInt(r.textContent,10):NaN;r&&String(n)===r.textContent.trim()&&(r.textContent=String(n+t))}function Qe(e,t,r,n){var a=t.closest(".ui-board__col"),i=M(r).filter(function(d){return d!==t});if(n<i.length)i[n].parentNode.insertBefore(t,i[n]);else if(i.length){var l=i[i.length-1];l.parentNode.insertBefore(t,l.nextSibling)}else{var u=r.querySelector("ol.ui-board__list");if(!u){u=document.createElement("ol"),u.className="ui-board__list";var s=r.querySelector(":scope > .ui-board__empty");s?s.replaceWith(u):r.querySelector(".ui-board__head").after(u)}u.appendChild(t)}if(a!==r&&(he(a,-1),he(r,1),!M(a).length&&!a.querySelector(".ui-board__next a[data-gb-more]"))){var c=document.createElement("p");c.className="ui-board__empty",c.textContent=e.getAttribute("data-empty-label")||"Nothing here",a.querySelectorAll(":scope > ol.ui-board__list, :scope > details.ui-board__more").forEach(function(d){d.remove()}),a.querySelector(".ui-board__head").after(c)}}var o=null;function T(e){return M(e).filter(function(t){return!o||t!==o.li})}function W(e){e.querySelectorAll("[data-gb-drop]").forEach(function(t){t.removeAttribute("data-gb-drop")}),e.querySelectorAll("[data-gb-over]").forEach(function(t){t.removeAttribute("data-gb-over")})}function Q(){if(o){W(o.b),o.to.setAttribute("data-gb-over","");var e=T(o.to),t=o.index<e.length?e[o.index]:e[e.length-1];if(t){t.setAttribute("data-gb-drop",o.index<e.length?"before":"after");var r=t.closest("details:not([open])");r&&o.b.contains(r)&&(r.open=!0),o.keys&&t.scrollIntoView&&t.scrollIntoView({block:"nearest",inline:"nearest"})}}}function K(){return o?P(o.to)+", position "+(o.index+1)+" of "+(T(o.to).length+1):""}function $(){o&&(o.li.removeAttribute("data-gb-lifted"),o.b.removeAttribute("data-gb-dragging"),W(o.b),o=null)}function ye(e,t,r){var n=t.closest(".ui-board__col");if(!(!n||!t.getAttribute("data-id"))){var a=M(n).indexOf(t);o={b:e,li:t,id:t.getAttribute("data-id"),from:n,to:n,index:a,origin:a,keys:r},t.setAttribute("data-gb-lifted",r?"key":"drag"),r&&(Q(),_(e,"Picked up "+V(t)+". "+K()+". Arrow keys move it, Space drops it, Escape cancels."))}}function Ae(){if(o){var e=o,t=V(e.li),r=e.to===e.from&&e.index===e.origin,n=K();if($(),r){_(e.b,t+" dropped where it was, in "+P(e.from)+".");return}var a=new CustomEvent("gb:move",{bubbles:!0,cancelable:!0,detail:{id:e.id,from:e.from.getAttribute("data-col"),to:e.to.getAttribute("data-col"),index:e.index}}),i=e.b.dispatchEvent(a);if(i&&Qe(e.b,e.li,e.to,e.index),_(e.b,"Moved "+t+" to "+n+"."),e.keys){var l=10,u=function(){var s=Array.prototype.slice.call(e.b.querySelectorAll("li[data-id]")).filter(function(c){return c.getAttribute("data-id")===e.id})[0];s&&document.activeElement!==s?s.focus():!s&&l-- >0&&requestAnimationFrame(u)};requestAnimationFrame(u)}}}function N(e){if(o){var t=o;$(),t.keys&&_(t.b,(e||"")+V(t.li)+" stays in "+P(t.from)+".")}}function Ee(e){var t=e&&e.closest?e.closest(".ui-board[data-gb-move]"):null;return t}function $e(e){var t=Ee(e.target),r=t&&e.target.closest("li[data-id]");if(!(!r||!t.contains(r)||t.__gbMoveOn)){if(!o){(e.key===" "||e.key==="Spacebar")&&e.target===r&&(e.preventDefault(),ye(t,r,!0));return}if(!(o.li!==r||!o.keys)){var n=We(t);if(e.key==="ArrowUp"||e.key==="ArrowDown")e.preventDefault(),o.index=Math.max(0,Math.min(T(o.to).length,o.index+(e.key==="ArrowUp"?-1:1)));else if(e.key==="ArrowLeft"||e.key==="ArrowRight"){e.preventDefault();var a=n.indexOf(o.to)+(e.key==="ArrowLeft"?-1:1);if(a<0||a>=n.length){_(t,K()+". No column further "+(e.key==="ArrowLeft"?"left":"right")+".");return}o.to=n[a],o.index=Math.min(o.index,T(o.to).length)}else if(e.key===" "||e.key==="Spacebar"||e.key==="Enter"){e.preventDefault(),Ae();return}else if(e.key==="Escape"){e.preventDefault(),e.stopPropagation(),N("Move cancelled. ");return}else if(e.key==="Tab"){N("Move cancelled. ");return}else return;Q(),_(t,K())}}}function Xe(e){var t=e.closest(".ui-board__col"),r=e.closest(".ui-board"),n=e.closest(".ui-board__next")||e.parentNode;if(!(!t||!r||n.getAttribute("aria-busy")==="true")){var a=t.getAttribute("data-col"),i=e.href;n.setAttribute("aria-busy","true"),n.innerHTML='<span class="ui-spinner" aria-hidden="true"></span><span role="status">Loading\u2026</span>',fetch(i,{headers:{Accept:"text/html"},credentials:"same-origin"}).then(function(l){if(!l.ok)throw new Error(String(l.status));return l.text()}).then(function(l){var u=new DOMParser().parseFromString(l,"text/html"),s=r.id?u.getElementById(r.id):u,c=Array.prototype.slice.call((s||u).querySelectorAll(".ui-board__col[data-col]")).filter(function(b){return b.getAttribute("data-col")===a})[0];if(!c)throw new Error("no column "+a);var d={};r.querySelectorAll("li[data-id]").forEach(function(b){d[b.getAttribute("data-id")]=1});var v=t.querySelector(":scope > ol.ui-board__list");if(!v){v=document.createElement("ol"),v.className="ui-board__list";var m=t.querySelector(":scope > .ui-board__empty");m?m.replaceWith(v):n.before(v)}var g=M(c).filter(function(b){return!d[b.getAttribute("data-id")]}).map(function(b){var A=document.importNode(b,!0);return v.appendChild(A),A}),S=c.querySelector(".ui-board__count"),O=t.querySelector(".ui-board__count");S&&O&&(O.textContent=S.textContent);var k=c.querySelector(".ui-board__next");k&&k.querySelector("a[data-gb-more]")?(n.innerHTML=k.innerHTML,n.removeAttribute("aria-busy")):n.remove();try{history.replaceState(history.state,"",i)}catch{}_(r,g.length+" more "+(g.length===1?"card":"cards")+" in "+P(t)+".");var y=g[0];if(y){var f=y.matches("[tabindex]")?y:y.querySelector("a[href], button, [tabindex]");f&&f.focus({preventScroll:!1})}}).catch(function(){n.removeAttribute("aria-busy"),n.innerHTML=`<span role="alert" class="ui-more__error">Couldn't load.</span>`;var l=document.createElement("a");l.className="ui-btn ui-btn--sm",l.href=i,l.setAttribute("data-gb-more",""),l.textContent="Try again",n.appendChild(l),l.focus()})}}function _e(){if(!(be||typeof document>"u")){be=!0,document.addEventListener("keydown",$e),document.addEventListener("focusout",function(t){o&&o.keys&&t.target===o.li&&!o.li.contains(t.relatedTarget)&&N("Move cancelled. ")}),document.addEventListener("dragstart",function(t){var r=Ee(t.target),n=r&&t.target.closest("li[data-id]");!n||!r.contains(n)||r.__gbMoveOn||!t.dataTransfer||(o&&$(),ye(r,n,!1),t.dataTransfer.effectAllowed="move",t.dataTransfer.setData("text/plain",n.getAttribute("data-id")||""),requestAnimationFrame(function(){o&&r.setAttribute("data-gb-dragging","")}))});var e=function(t){var r=t.target.closest?t.target.closest(".ui-board__col[data-col]"):null;if(!r||!o||!o.b.contains(r))return null;for(var n=T(r),a=n.length,i=0;i<n.length;i++){var l=n[i].getBoundingClientRect();if(l.height){if(t.clientY<l.top+l.height/2){a=i;break}a=i+1}}return{col:r,index:a}};document.addEventListener("dragover",function(t){if(!(!o||o.keys)){var r=e(t);r&&(t.preventDefault(),t.dataTransfer&&(t.dataTransfer.dropEffect="move"),(r.col!==o.to||r.index!==o.index||!o.b.querySelector("[data-gb-over]"))&&(o.to=r.col,o.index=r.index,Q()))}}),document.addEventListener("dragleave",function(t){o&&!o.keys&&!o.b.contains(t.relatedTarget)&&W(o.b)}),document.addEventListener("drop",function(t){if(!(!o||o.keys)){t.preventDefault();var r=e(t);if(!r){N();return}o.to=r.col,o.index=r.index,Ae()}}),document.addEventListener("dragend",function(){o&&!o.keys&&N()}),document.addEventListener("click",function(t){var r=t.target;if(!(!r||!r.closest)){var n=r.closest(".ui-board a[data-gb-more][href]");if(n){!t.defaultPrevented&&t.button===0&&!t.metaKey&&!t.ctrlKey&&!t.shiftKey&&!t.altKey&&typeof fetch=="function"&&(t.preventDefault(),Xe(n));return}var a=r.closest("table[data-gb-sort] button.ui-table__sort");if(a){Ve(a);return}var i=r.closest("td.ui-table__check, th.ui-table__check");if(i&&r===i){var l=i.querySelector("input[data-gb-select], input[data-gb-select-all]");if(l){l.click();return}}var u=r.closest("input[data-gb-select]");if(u){var s=u.closest(U);if(s&&t.shiftKey&&q&&q!==u&&s.contains(q)){for(var c=Array.prototype.slice.call(s.querySelectorAll("input[data-gb-select]")),d=c.indexOf(q),v=c.indexOf(u),m=u.checked,g=Math.min(d,v);g<=Math.max(d,v);g++)c[g].checked=m;C(s)}q=u;return}ve(t)}}),document.addEventListener("auxclick",function(t){t.button===1&&ve(t)}),document.addEventListener("change",function(t){var r=t.target;if(!(!r||!r.matches)){var n=r.closest(U);n&&(r.matches("input[data-gb-select-all]")?(n.querySelectorAll("input[data-gb-select]").forEach(function(a){a.checked=r.checked}),C(n)):r.matches("input[data-gb-select]")&&C(n))}}),document.addEventListener("keydown",function(t){if(!(t.key!=="Escape"||t.defaultPrevented||p.length)){var r=document.activeElement&&document.activeElement.closest?document.activeElement.closest(U):null;r&&Ie(r,document.activeElement)||r&&r.querySelector("input[data-gb-select]:checked")&&pe(r)}})}}_e(),h.gembossUI={openModal:z,toast:Le,confirm:Te,initMenus:Ce,closeMenus:R,initSearch:Pe,initShell:Ne,initPasswordFields:Oe,resetPasswordFields:De,initNav:de,openModals:function(){return p.length},initCollections:_e,selection:me,clearSelection:pe,syncSelection:C,initMenuLayer:Z,floatMenu:F}})(globalThis);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu layer (0.18.0): an open `details.ui-menu` shows its list in the browser's TOP LAYER (the `popover` API), placed
|
|
3
|
+
* with `position: fixed` against its ⋯ button, so no scrolling frame can cut it off. Before, the list was
|
|
4
|
+
* `position: absolute` inside the row, and a table's frame (`.ui-table-wrap`, overflow auto) clipped the menu of the
|
|
5
|
+
* last rows: GemFactory Members (#197) at 820 / 900 / 1280 showed half a menu and asked for a scroll INSIDE the table.
|
|
6
|
+
*
|
|
7
|
+
* - Below its button when it fits, above it when the room below is short and the room above is larger (a last row near
|
|
8
|
+
* the bottom of the screen); `ui-menu--end` aligns its right edge with the button's; never closer than 8px to a
|
|
9
|
+
* viewport edge; taller than the room → it scrolls inside (`max-height`).
|
|
10
|
+
* - It stays the details' DOM child: Tab order (button, then the items), Escape (initMenus / Menu close the details and
|
|
11
|
+
* put focus back on the button), "a click on the menu is not a click on the row" (ROW_CONTROLS) all keep working.
|
|
12
|
+
* - It CLOSES when a scroll moves its button (the page, or a frame around the menu) or the window resizes: a fixed list
|
|
13
|
+
* would otherwise float away from it. A scroll inside the list, or of something elsewhere (a log following its
|
|
14
|
+
* output, a carousel), does not count.
|
|
15
|
+
* - No popover API (Safari < 17): nothing changes, the list stays absolute as before 0.18.0. No script: the same.
|
|
16
|
+
*
|
|
17
|
+
* kit-plain.js carries the same code for plain-HTML surfaces (`initMenuLayer()`, run by itself); collections-board.test.ts
|
|
18
|
+
* runs the same geometry tests against both, so they cannot drift apart.
|
|
19
|
+
*/
|
|
20
|
+
const GAP = 4;
|
|
21
|
+
const EDGE = 8;
|
|
22
|
+
|
|
23
|
+
export function canFloatMenus(): boolean {
|
|
24
|
+
return typeof HTMLElement !== "undefined" && Object.prototype.hasOwnProperty.call(HTMLElement.prototype, "popover");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const partsOf = (d: HTMLDetailsElement) => ({
|
|
28
|
+
list: d.querySelector<HTMLElement>(":scope > .ui-menu__list"),
|
|
29
|
+
button: d.querySelector<HTMLElement>(":scope > summary"),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Puts an OPEN menu's list in the top layer and places it against its button. Idempotent (a second call re-places). */
|
|
33
|
+
export function floatMenu(d: HTMLDetailsElement): void {
|
|
34
|
+
const { list, button } = partsOf(d);
|
|
35
|
+
if (!list || !button || !d.open || !canFloatMenus()) return;
|
|
36
|
+
if (list.getAttribute("popover") !== "manual") list.setAttribute("popover", "manual");
|
|
37
|
+
if (!list.matches(":popover-open")) list.showPopover();
|
|
38
|
+
list.style.maxHeight = "";
|
|
39
|
+
list.style.left = "0px";
|
|
40
|
+
list.style.top = "0px";
|
|
41
|
+
const r = button.getBoundingClientRect();
|
|
42
|
+
const vw = document.documentElement.clientWidth, vh = window.innerHeight;
|
|
43
|
+
const w = list.offsetWidth, h = list.offsetHeight;
|
|
44
|
+
const below = vh - r.bottom - GAP - EDGE, above = r.top - GAP - EDGE;
|
|
45
|
+
let top: number;
|
|
46
|
+
if (h <= below || below >= above) {
|
|
47
|
+
top = r.bottom + GAP;
|
|
48
|
+
if (h > below) list.style.maxHeight = `${Math.max(below, 0)}px`;
|
|
49
|
+
list.dataset.side = "bottom";
|
|
50
|
+
} else {
|
|
51
|
+
if (h > above) list.style.maxHeight = `${Math.max(above, 0)}px`;
|
|
52
|
+
top = r.top - GAP - Math.min(h, above);
|
|
53
|
+
list.dataset.side = "top";
|
|
54
|
+
}
|
|
55
|
+
let left = d.classList.contains("ui-menu--end") ? r.right - w : r.left;
|
|
56
|
+
left = Math.max(EDGE, Math.min(left, vw - w - EDGE));
|
|
57
|
+
list.style.left = `${Math.round(left)}px`;
|
|
58
|
+
list.style.top = `${Math.round(top)}px`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Takes a closed menu's list out of the top layer. The `popover` attribute stays: hidden until the next float. */
|
|
62
|
+
export function sinkMenu(d: HTMLDetailsElement): void {
|
|
63
|
+
const { list } = partsOf(d);
|
|
64
|
+
if (list && list.hasAttribute("popover") && list.matches(":popover-open")) list.hidePopover();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Closes the floating menus a scroll or a resize would leave behind: a scroll closes only the menus whose button it
|
|
68
|
+
* moves (the document, or a scroller that contains the menu), never because of a scroll inside a list, a log that
|
|
69
|
+
* follows its output or a carousel elsewhere on the page. `target` null = a resize: every floating menu. */
|
|
70
|
+
export function closeDetachedMenus(target: EventTarget | null): void {
|
|
71
|
+
const t = target as (Node & { closest?: (s: string) => Element | null }) | null;
|
|
72
|
+
if (t && t.closest && t.closest(".ui-menu__list")) return;
|
|
73
|
+
document.querySelectorAll<HTMLDetailsElement>("details.ui-menu[open]").forEach((d) => {
|
|
74
|
+
const list = d.querySelector<HTMLElement>(":scope > .ui-menu__list");
|
|
75
|
+
if (!list || !list.matches(":popover-open")) return;
|
|
76
|
+
if (!t || t === document || (t as Node).nodeType === 9 || (t as Node).contains(d)) d.open = false;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let listening = false;
|
|
81
|
+
/** One scroll (capture) and one resize listener for every floating menu on the page, whatever the number of Menus. */
|
|
82
|
+
export function listenForDetachedMenus(): void {
|
|
83
|
+
if (listening || typeof document === "undefined") return;
|
|
84
|
+
listening = true;
|
|
85
|
+
document.addEventListener("scroll", (e) => closeDetachedMenus(e.target), true);
|
|
86
|
+
window.addEventListener("resize", () => closeDetachedMenus(null));
|
|
87
|
+
}
|