@gardenerim/css 1.0.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 +121 -0
- package/LICENSE +21 -0
- package/README.md +591 -0
- package/dist/components/ai-compositions.min.css +2 -0
- package/dist/components/ai-extended.min.css +2 -0
- package/dist/components/ai.min.css +2 -0
- package/dist/components/auth-compositions.min.css +2 -0
- package/dist/components/basic.min.css +2 -0
- package/dist/components/catalog.min.css +2 -0
- package/dist/components/commerce-compositions.min.css +2 -0
- package/dist/components/content-compositions.min.css +2 -0
- package/dist/components/data-compositions.min.css +2 -0
- package/dist/components/data.min.css +2 -0
- package/dist/components/desktop-compositions.min.css +2 -0
- package/dist/components/desktop.min.css +2 -0
- package/dist/components/extended.min.css +2 -0
- package/dist/components/feedback.min.css +2 -0
- package/dist/components/form-compositions.min.css +2 -0
- package/dist/components/forms.min.css +2 -0
- package/dist/components/grid.min.css +2 -0
- package/dist/components/help.min.css +2 -0
- package/dist/components/layouts.min.css +2 -0
- package/dist/components/mobile-compositions.min.css +2 -0
- package/dist/components/navigation-compositions.min.css +2 -0
- package/dist/components/navigation.min.css +2 -0
- package/dist/components/page.min.css +2 -0
- package/dist/components/primitives.min.css +2 -0
- package/dist/components/recipes.min.css +2 -0
- package/dist/components/regions.min.css +2 -0
- package/dist/components/selection-compositions.min.css +2 -0
- package/dist/components/solution-compositions.min.css +2 -0
- package/dist/gardener.ai.css +236 -0
- package/dist/gardener.ai.min.css +3 -0
- package/dist/gardener.ai.min.css.map +7 -0
- package/dist/gardener.builds.json +7491 -0
- package/dist/gardener.capabilities.json +3784 -0
- package/dist/gardener.compatibility.json +1209 -0
- package/dist/gardener.components.css +3496 -0
- package/dist/gardener.components.min.css +3 -0
- package/dist/gardener.components.min.css.map +7 -0
- package/dist/gardener.core.css +1486 -0
- package/dist/gardener.core.min.css +3 -0
- package/dist/gardener.core.min.css.map +7 -0
- package/dist/gardener.css +7153 -0
- package/dist/gardener.d.ts +42 -0
- package/dist/gardener.electron.d.ts +5 -0
- package/dist/gardener.electron.js +24 -0
- package/dist/gardener.electron.min.js +3 -0
- package/dist/gardener.electron.min.js.map +7 -0
- package/dist/gardener.manifest.json +14557 -0
- package/dist/gardener.min.css +3 -0
- package/dist/gardener.min.css.map +7 -0
- package/dist/gardener.performance.json +1628 -0
- package/dist/gardener.public-api.json +2169 -0
- package/dist/gardener.recipes.json +1444 -0
- package/dist/gardener.runtime.js +2224 -0
- package/dist/gardener.runtime.min.js +3 -0
- package/dist/gardener.runtime.min.js.map +7 -0
- package/dist/gardener.tauri.d.ts +5 -0
- package/dist/gardener.tauri.js +28 -0
- package/dist/gardener.tauri.min.js +3 -0
- package/dist/gardener.tauri.min.js.map +7 -0
- package/dist/gardener.themes.css +1415 -0
- package/dist/gardener.themes.min.css +3 -0
- package/dist/gardener.themes.min.css.map +7 -0
- package/dist/gardener.utilities.css +1940 -0
- package/dist/gardener.utilities.json +92279 -0
- package/dist/gardener.utilities.min.css +3 -0
- package/dist/gardener.utilities.min.css.map +7 -0
- package/dist/platforms/gardener.desktop.min.css +2 -0
- package/dist/platforms/gardener.electron.min.css +2 -0
- package/dist/platforms/gardener.mobile.min.css +2 -0
- package/dist/platforms/gardener.tauri.min.css +2 -0
- package/dist/platforms/gardener.web.min.css +2 -0
- package/metadata/builds.schema.json +148 -0
- package/metadata/capabilities.json +3784 -0
- package/metadata/capabilities.schema.json +61 -0
- package/metadata/compatibility.json +1209 -0
- package/metadata/compatibility.schema.json +85 -0
- package/metadata/components.json +9082 -0
- package/metadata/components.schema.json +58 -0
- package/metadata/custom-build.schema.json +46 -0
- package/metadata/manifest.schema.json +47 -0
- package/metadata/performance-budgets.schema.json +103 -0
- package/metadata/performance-report.schema.json +194 -0
- package/metadata/public-api.json +2169 -0
- package/metadata/public-api.schema.json +165 -0
- package/metadata/recipes.json +1444 -0
- package/metadata/recipes.schema.json +39 -0
- package/metadata/schema.json +7 -0
- package/metadata/utilities.schema.json +29 -0
- package/package.json +177 -0
|
@@ -0,0 +1,2224 @@
|
|
|
1
|
+
/** Gardener runtime v1.0.0 — framework-agnostic, accessible component behavior. */
|
|
2
|
+
|
|
3
|
+
const instanceStores = new WeakMap();
|
|
4
|
+
const registry = new Map();
|
|
5
|
+
const focusableSelector = [
|
|
6
|
+
"a[href]", "button:not([disabled])", "input:not([disabled])", "select:not([disabled])",
|
|
7
|
+
"textarea:not([disabled])", "[tabindex]:not([tabindex='-1'])", "[contenteditable='true']"
|
|
8
|
+
].join(",");
|
|
9
|
+
|
|
10
|
+
const uid = (prefix = "g") => `${prefix}-${Math.random().toString(36).slice(2, 9)}`;
|
|
11
|
+
const numeric = (value, fallback) => value !== "" && value != null && Number.isFinite(Number(value)) ? Number(value) : fallback;
|
|
12
|
+
const targetById = (value) => value ? document.getElementById(value.replace(/^#/, "")) : null;
|
|
13
|
+
const visibleFocusable = (container) => [...container.querySelectorAll(focusableSelector)].filter((item) => item.getClientRects().length > 0);
|
|
14
|
+
|
|
15
|
+
function emit(element, name, detail = {}) {
|
|
16
|
+
return element.dispatchEvent(new CustomEvent(`gardener:${name}`, { bubbles: true, cancelable: true, detail }));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function storeFor(element) {
|
|
20
|
+
if (!instanceStores.has(element)) instanceStores.set(element, new Map());
|
|
21
|
+
return instanceStores.get(element);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function lockScroll() {
|
|
25
|
+
const root = document.documentElement;
|
|
26
|
+
const count = Number(root.dataset.gScrollLocks || 0) + 1;
|
|
27
|
+
root.dataset.gScrollLocks = String(count);
|
|
28
|
+
if (count === 1) {
|
|
29
|
+
root.dataset.gPreviousOverflow = root.style.overflow;
|
|
30
|
+
root.style.overflow = "hidden";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unlockScroll() {
|
|
35
|
+
const root = document.documentElement;
|
|
36
|
+
const count = Math.max(0, Number(root.dataset.gScrollLocks || 0) - 1);
|
|
37
|
+
root.dataset.gScrollLocks = String(count);
|
|
38
|
+
if (count === 0) {
|
|
39
|
+
root.style.overflow = root.dataset.gPreviousOverflow || "";
|
|
40
|
+
delete root.dataset.gPreviousOverflow;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createOverlay(element, type) {
|
|
45
|
+
let trigger = null;
|
|
46
|
+
let open = false;
|
|
47
|
+
let closeTimer = null;
|
|
48
|
+
let closeFinished = true;
|
|
49
|
+
const panel = element.querySelector(type === "dialog" ? ".g-dialog" : type === "mobile-sheet" ? ".g-mobile-sheet-panel" : ".g-drawer") || element;
|
|
50
|
+
panel.tabIndex = panel.tabIndex < 0 ? -1 : panel.tabIndex;
|
|
51
|
+
if (type === "dialog" || type === "mobile-sheet") {
|
|
52
|
+
panel.setAttribute("role", panel.getAttribute("role") || "dialog");
|
|
53
|
+
panel.setAttribute("aria-modal", "true");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function onKeydown(event) {
|
|
57
|
+
if (event.key === "Escape" && element.dataset.gDismissible !== "false") close("escape");
|
|
58
|
+
if (event.key !== "Tab") return;
|
|
59
|
+
const items = visibleFocusable(panel);
|
|
60
|
+
if (!items.length) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
panel.focus();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const first = items[0];
|
|
66
|
+
const last = items.at(-1);
|
|
67
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
68
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function finishClose(reason) {
|
|
72
|
+
if (open || closeFinished) return;
|
|
73
|
+
closeFinished = true;
|
|
74
|
+
clearTimeout(closeTimer);
|
|
75
|
+
element.hidden = true;
|
|
76
|
+
element.setAttribute("aria-hidden", "true");
|
|
77
|
+
trigger?.focus?.({ preventScroll: true });
|
|
78
|
+
emit(element, "close", { reason });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function openOverlay(source) {
|
|
82
|
+
if (open || !emit(element, "beforeopen", { source })) return;
|
|
83
|
+
clearTimeout(closeTimer);
|
|
84
|
+
open = true;
|
|
85
|
+
closeFinished = false;
|
|
86
|
+
trigger = source instanceof HTMLElement ? source : document.activeElement;
|
|
87
|
+
element.hidden = false;
|
|
88
|
+
element.setAttribute("aria-hidden", "false");
|
|
89
|
+
lockScroll();
|
|
90
|
+
requestAnimationFrame(() => {
|
|
91
|
+
element.classList.add("is-open");
|
|
92
|
+
(visibleFocusable(panel)[0] || panel).focus({ preventScroll: true });
|
|
93
|
+
});
|
|
94
|
+
document.addEventListener("keydown", onKeydown);
|
|
95
|
+
emit(element, "open", { source });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function close(reason = "api") {
|
|
99
|
+
if (!open || !emit(element, "beforeclose", { reason })) return;
|
|
100
|
+
open = false;
|
|
101
|
+
element.classList.remove("is-open");
|
|
102
|
+
document.removeEventListener("keydown", onKeydown);
|
|
103
|
+
unlockScroll();
|
|
104
|
+
const onTransitionEnd = (event) => {
|
|
105
|
+
if (event.target === element) finishClose(reason);
|
|
106
|
+
};
|
|
107
|
+
element.addEventListener("transitionend", onTransitionEnd, { once: true });
|
|
108
|
+
closeTimer = window.setTimeout(() => finishClose(reason), 350);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function onClick(event) {
|
|
112
|
+
if (event.target === element && element.dataset.gDismissible !== "false") close("backdrop");
|
|
113
|
+
if (event.target.closest("[data-g-close]")) close("button");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const startsOpen = !element.hidden;
|
|
117
|
+
element.hidden = true;
|
|
118
|
+
element.setAttribute("aria-hidden", "true");
|
|
119
|
+
element.addEventListener("click", onClick);
|
|
120
|
+
if (startsOpen) queueMicrotask(() => openOverlay());
|
|
121
|
+
return { open: openOverlay, close, toggle: (source) => open ? close("toggle") : openOverlay(source), isOpen: () => open, destroy: () => { clearTimeout(closeTimer); if (open) unlockScroll(); open = false; element.classList.remove("is-open"); element.hidden = true; element.removeEventListener("click", onClick); document.removeEventListener("keydown", onKeydown); } };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createDropdown(element) {
|
|
125
|
+
const trigger = element.querySelector("[data-g-dropdown-trigger]");
|
|
126
|
+
const menu = element.querySelector("[data-g-dropdown-menu]") || element.querySelector(".g-dropdown-menu");
|
|
127
|
+
if (!trigger || !menu) return null;
|
|
128
|
+
let search = "";
|
|
129
|
+
let searchTimer;
|
|
130
|
+
const startsOpen = !menu.hidden;
|
|
131
|
+
const items = () => visibleFocusable(menu).filter((item) => item.getAttribute("aria-disabled") !== "true");
|
|
132
|
+
|
|
133
|
+
function open() {
|
|
134
|
+
if (!menu.hidden) return;
|
|
135
|
+
menu.hidden = false;
|
|
136
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
137
|
+
document.addEventListener("pointerdown", outside, true);
|
|
138
|
+
document.addEventListener("keydown", keydown);
|
|
139
|
+
emit(element, "open");
|
|
140
|
+
}
|
|
141
|
+
function close(focus = false) {
|
|
142
|
+
if (menu.hidden) return;
|
|
143
|
+
menu.hidden = true;
|
|
144
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
145
|
+
document.removeEventListener("pointerdown", outside, true);
|
|
146
|
+
document.removeEventListener("keydown", keydown);
|
|
147
|
+
if (focus) trigger.focus();
|
|
148
|
+
emit(element, "close");
|
|
149
|
+
}
|
|
150
|
+
function outside(event) { if (!element.contains(event.target)) close(); }
|
|
151
|
+
function keydown(event) {
|
|
152
|
+
const options = items();
|
|
153
|
+
const index = options.indexOf(document.activeElement);
|
|
154
|
+
if (event.key === "Escape") return close(true);
|
|
155
|
+
if (event.key === "ArrowDown") { event.preventDefault(); (options[index + 1] || options[0])?.focus(); }
|
|
156
|
+
else if (event.key === "ArrowUp") { event.preventDefault(); (options[index - 1] || options.at(-1))?.focus(); }
|
|
157
|
+
else if (event.key === "Home") { event.preventDefault(); options[0]?.focus(); }
|
|
158
|
+
else if (event.key === "End") { event.preventDefault(); options.at(-1)?.focus(); }
|
|
159
|
+
else if (event.key.length === 1 && /\S/.test(event.key)) {
|
|
160
|
+
search += event.key.toLocaleLowerCase();
|
|
161
|
+
clearTimeout(searchTimer);
|
|
162
|
+
searchTimer = setTimeout(() => { search = ""; }, 500);
|
|
163
|
+
options.find((item) => item.textContent.trim().toLocaleLowerCase().startsWith(search))?.focus();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function toggle() { menu.hidden ? open() : close(); }
|
|
167
|
+
function menuClick(event) { if (event.target.closest("[role='menuitem']") && !event.target.closest("[aria-haspopup='menu']")) close(); }
|
|
168
|
+
trigger.setAttribute("aria-haspopup", trigger.getAttribute("aria-haspopup") || "menu");
|
|
169
|
+
menu.setAttribute("role", menu.getAttribute("role") || "menu");
|
|
170
|
+
menu.hidden = true;
|
|
171
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
172
|
+
trigger.addEventListener("click", toggle);
|
|
173
|
+
menu.addEventListener("click", menuClick);
|
|
174
|
+
if (startsOpen) queueMicrotask(open);
|
|
175
|
+
return { open, close, toggle, destroy: () => { clearTimeout(searchTimer); trigger.removeEventListener("click", toggle); menu.removeEventListener("click", menuClick); document.removeEventListener("pointerdown", outside, true); document.removeEventListener("keydown", keydown); } };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function createTabs(element) {
|
|
179
|
+
const tabs = [...element.querySelectorAll("[role='tab']")];
|
|
180
|
+
if (!tabs.length) return null;
|
|
181
|
+
const vertical = () => element.getAttribute("aria-orientation") === "vertical";
|
|
182
|
+
function select(tab, focus = false) {
|
|
183
|
+
if (!tab || tab.getAttribute("aria-disabled") === "true") return;
|
|
184
|
+
tabs.forEach((item) => {
|
|
185
|
+
const selected = item === tab;
|
|
186
|
+
item.setAttribute("aria-selected", String(selected));
|
|
187
|
+
item.tabIndex = selected ? 0 : -1;
|
|
188
|
+
const panel = targetById(item.getAttribute("aria-controls"));
|
|
189
|
+
if (panel) panel.hidden = !selected;
|
|
190
|
+
});
|
|
191
|
+
if (focus) tab.focus();
|
|
192
|
+
emit(element, "change", { tab });
|
|
193
|
+
}
|
|
194
|
+
function click(event) { const tab = event.target.closest("[role='tab']"); if (tab) select(tab); }
|
|
195
|
+
function keydown(event) {
|
|
196
|
+
const current = tabs.indexOf(event.target.closest("[role='tab']"));
|
|
197
|
+
if (current < 0) return;
|
|
198
|
+
const forward = vertical() ? "ArrowDown" : "ArrowRight";
|
|
199
|
+
const backward = vertical() ? "ArrowUp" : "ArrowLeft";
|
|
200
|
+
const enabled = tabs.filter((tab) => tab.getAttribute("aria-disabled") !== "true" && !tab.disabled);
|
|
201
|
+
if (!enabled.length) return;
|
|
202
|
+
const enabledIndex = enabled.indexOf(tabs[current]);
|
|
203
|
+
let next;
|
|
204
|
+
if (event.key === forward) next = enabled[(enabledIndex + 1 + enabled.length) % enabled.length];
|
|
205
|
+
else if (event.key === backward) next = enabled[(enabledIndex - 1 + enabled.length) % enabled.length];
|
|
206
|
+
else if (event.key === "Home") next = enabled[0];
|
|
207
|
+
else if (event.key === "End") next = enabled.at(-1);
|
|
208
|
+
else return;
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
select(next, true);
|
|
211
|
+
}
|
|
212
|
+
element.addEventListener("click", click);
|
|
213
|
+
element.addEventListener("keydown", keydown);
|
|
214
|
+
select(tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || tabs[0]);
|
|
215
|
+
return { select, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function createAccordion(element) {
|
|
219
|
+
const triggerSelector = ".g-accordion-trigger, .g-faq-question, .g-thinking-trigger, [data-g-tool-call-toggle]";
|
|
220
|
+
const triggers = [...element.querySelectorAll(triggerSelector)];
|
|
221
|
+
function set(trigger, expanded) {
|
|
222
|
+
trigger.setAttribute("aria-expanded", String(expanded));
|
|
223
|
+
const panel = targetById(trigger.getAttribute("aria-controls"));
|
|
224
|
+
if (panel) panel.hidden = !expanded;
|
|
225
|
+
emit(element, "change", { trigger, expanded });
|
|
226
|
+
}
|
|
227
|
+
function toggle(trigger) {
|
|
228
|
+
if (trigger.disabled || trigger.getAttribute("aria-disabled") === "true") return;
|
|
229
|
+
const expanded = trigger.getAttribute("aria-expanded") === "true";
|
|
230
|
+
if (!expanded && element.dataset.gMultiple !== "true") triggers.forEach((other) => { if (other !== trigger) set(other, false); });
|
|
231
|
+
set(trigger, !expanded);
|
|
232
|
+
}
|
|
233
|
+
function click(event) { const trigger = event.target.closest(triggerSelector); if (trigger) toggle(trigger); }
|
|
234
|
+
element.addEventListener("click", click);
|
|
235
|
+
triggers.forEach((trigger) => set(trigger, trigger.getAttribute("aria-expanded") === "true"));
|
|
236
|
+
return { toggle, destroy: () => element.removeEventListener("click", click) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function createAutoResize(element) {
|
|
240
|
+
function resize() {
|
|
241
|
+
element.style.height = "auto";
|
|
242
|
+
element.style.height = `${Math.min(element.scrollHeight, Number(element.dataset.gMaxHeight || 224))}px`;
|
|
243
|
+
}
|
|
244
|
+
element.addEventListener("input", resize);
|
|
245
|
+
resize();
|
|
246
|
+
return { resize, destroy: () => element.removeEventListener("input", resize) };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createPasswordToggle(element) {
|
|
250
|
+
const input = element.querySelector("input");
|
|
251
|
+
const button = element.querySelector("[data-g-password-button]");
|
|
252
|
+
if (!input || !button) return null;
|
|
253
|
+
const showLabel = button.dataset.gShowLabel || "显示密码";
|
|
254
|
+
const hideLabel = button.dataset.gHideLabel || "隐藏密码";
|
|
255
|
+
function update() {
|
|
256
|
+
const visible = input.type === "text";
|
|
257
|
+
button.setAttribute("aria-pressed", String(visible));
|
|
258
|
+
button.setAttribute("aria-label", visible ? hideLabel : showLabel);
|
|
259
|
+
const label = button.querySelector("[data-g-password-label]");
|
|
260
|
+
if (label) label.textContent = visible ? hideLabel : showLabel;
|
|
261
|
+
}
|
|
262
|
+
function toggle() {
|
|
263
|
+
const start = input.selectionStart;
|
|
264
|
+
const end = input.selectionEnd;
|
|
265
|
+
input.type = input.type === "password" ? "text" : "password";
|
|
266
|
+
update();
|
|
267
|
+
input.focus({ preventScroll: true });
|
|
268
|
+
if (start != null && end != null) input.setSelectionRange(start, end);
|
|
269
|
+
emit(element, "change", { visible: input.type === "text" });
|
|
270
|
+
}
|
|
271
|
+
button.addEventListener("click", toggle);
|
|
272
|
+
update();
|
|
273
|
+
return { toggle, destroy: () => button.removeEventListener("click", toggle) };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function createOtpInput(element) {
|
|
277
|
+
const cells = [...element.querySelectorAll("[data-g-otp-cell], .g-otp-cell")];
|
|
278
|
+
const output = element.querySelector("[data-g-otp-output]");
|
|
279
|
+
if (!cells.length) return null;
|
|
280
|
+
const digits = (value) => String(value || "").replace(/\D/g, "");
|
|
281
|
+
function sync(reason = "api") {
|
|
282
|
+
const value = cells.map((cell) => digits(cell.value).slice(-1)).join("");
|
|
283
|
+
cells.forEach((cell, index) => {
|
|
284
|
+
const digit = digits(cell.value).slice(-1);
|
|
285
|
+
if (cell.value !== digit) cell.value = digit;
|
|
286
|
+
cell.setAttribute("aria-label", cell.getAttribute("aria-label") || `验证码第 ${index + 1} 位,共 ${cells.length} 位`);
|
|
287
|
+
});
|
|
288
|
+
element.dataset.gOtpValue = value;
|
|
289
|
+
const valueOutput = element.querySelector("[data-g-otp-output]") || output;
|
|
290
|
+
if (valueOutput) { valueOutput.value = value; valueOutput.defaultValue = value; valueOutput.setAttribute("value", value); }
|
|
291
|
+
const complete = value.length === cells.length;
|
|
292
|
+
element.classList.toggle("is-complete", complete);
|
|
293
|
+
element.setAttribute("aria-invalid", String(element.classList.contains("is-invalid")));
|
|
294
|
+
emit(element, "otpchange", { value, complete, reason });
|
|
295
|
+
if (complete) emit(element, "otpcomplete", { value, reason });
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
function fill(value, start = 0, reason = "api") {
|
|
299
|
+
const values = digits(value).slice(0, cells.length - start);
|
|
300
|
+
[...values].forEach((digit, offset) => { cells[start + offset].value = digit; });
|
|
301
|
+
const next = Math.min(start + values.length, cells.length - 1);
|
|
302
|
+
cells[next]?.focus({ preventScroll: true });
|
|
303
|
+
sync(reason);
|
|
304
|
+
}
|
|
305
|
+
function input(event) {
|
|
306
|
+
const index = cells.indexOf(event.target);
|
|
307
|
+
if (index < 0) return;
|
|
308
|
+
const value = digits(event.target.value);
|
|
309
|
+
if (value.length > 1) fill(value, index, "input");
|
|
310
|
+
else { event.target.value = value.slice(-1); if (value && index < cells.length - 1) cells[index + 1].focus({ preventScroll: true }); sync("input"); }
|
|
311
|
+
}
|
|
312
|
+
function keydown(event) {
|
|
313
|
+
const index = cells.indexOf(event.target);
|
|
314
|
+
if (index < 0) return;
|
|
315
|
+
if (event.key === "Backspace" && !event.target.value && index > 0) { event.preventDefault(); cells[index - 1].value = ""; cells[index - 1].focus({ preventScroll: true }); sync("backspace"); }
|
|
316
|
+
if (event.key === "ArrowLeft" && index > 0) { event.preventDefault(); cells[index - 1].focus({ preventScroll: true }); }
|
|
317
|
+
if (event.key === "ArrowRight" && index < cells.length - 1) { event.preventDefault(); cells[index + 1].focus({ preventScroll: true }); }
|
|
318
|
+
if (event.key === "Home") { event.preventDefault(); cells[0].focus({ preventScroll: true }); }
|
|
319
|
+
if (event.key === "End") { event.preventDefault(); cells.at(-1).focus({ preventScroll: true }); }
|
|
320
|
+
}
|
|
321
|
+
function paste(event) { const index = cells.indexOf(event.target); if (index >= 0) { event.preventDefault(); fill(event.clipboardData?.getData("text") || "", index, "paste"); } }
|
|
322
|
+
function clear() { cells.forEach((cell) => { cell.value = ""; }); element.classList.remove("is-invalid"); cells[0].focus({ preventScroll: true }); sync("clear"); }
|
|
323
|
+
element.addEventListener("input", input); element.addEventListener("keydown", keydown); element.addEventListener("paste", paste); sync("init");
|
|
324
|
+
return { value: () => sync("read"), fill, clear, destroy: () => { element.removeEventListener("input", input); element.removeEventListener("keydown", keydown); element.removeEventListener("paste", paste); } };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function createPasswordStrength(element) {
|
|
328
|
+
const input = targetById(element.dataset.gPasswordTarget) || element.querySelector("input[type='password'], input");
|
|
329
|
+
const label = element.querySelector("[data-g-password-strength-label]");
|
|
330
|
+
const meter = element.querySelector("[data-g-password-strength-meter], .g-password-strength-meter");
|
|
331
|
+
const rules = [...element.querySelectorAll("[data-g-password-rule]")];
|
|
332
|
+
if (!input) return null;
|
|
333
|
+
const tests = {
|
|
334
|
+
length: (value) => value.length >= Number(element.dataset.gMinLength || input.minLength || 8),
|
|
335
|
+
upper: (value) => /[A-Z]/.test(value), lower: (value) => /[a-z]/.test(value),
|
|
336
|
+
number: (value) => /\d/.test(value), symbol: (value) => /[^\w\s]/.test(value)
|
|
337
|
+
};
|
|
338
|
+
function update(reason = "input") {
|
|
339
|
+
const value = input.value; const checks = Object.fromEntries(Object.entries(tests).map(([key, test]) => [key, test(value)]));
|
|
340
|
+
const passed = Object.values(checks).filter(Boolean).length;
|
|
341
|
+
const score = value ? Math.min(4, Math.max(1, Math.ceil((passed / Object.keys(checks).length) * 4))) : 0;
|
|
342
|
+
const labels = ["未设置", "较弱", "一般", "良好", "强"];
|
|
343
|
+
element.dataset.gStrength = String(score); element.style.setProperty("--g-password-strength", `${score * 25}%`);
|
|
344
|
+
if (label) label.textContent = labels[score];
|
|
345
|
+
if (meter) { meter.setAttribute("role", "progressbar"); meter.setAttribute("aria-label", "密码强度"); meter.setAttribute("aria-valuemin", "0"); meter.setAttribute("aria-valuemax", "4"); meter.setAttribute("aria-valuenow", String(score)); meter.setAttribute("aria-valuetext", labels[score]); }
|
|
346
|
+
rules.forEach((rule) => { const valid = Boolean(checks[rule.dataset.gPasswordRule]); rule.classList.toggle("is-valid", valid); rule.setAttribute("aria-label", `${rule.textContent.trim()}:${valid ? "已满足" : "未满足"}`); });
|
|
347
|
+
emit(element, "passwordstrength", { score, checks, reason }); return { score, checks };
|
|
348
|
+
}
|
|
349
|
+
input.addEventListener("input", update); update("init");
|
|
350
|
+
return { update, destroy: () => input.removeEventListener("input", update) };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function createAuthTimer(element) {
|
|
354
|
+
const output = element.querySelector("[data-g-auth-time]") || element;
|
|
355
|
+
const resetButton = element.querySelector("[data-g-auth-timer-reset]");
|
|
356
|
+
const duration = Math.max(0, Number(element.dataset.gDuration || 60));
|
|
357
|
+
let remaining = duration; let timer = 0;
|
|
358
|
+
function format(seconds) { const minutes = Math.floor(seconds / 60); const rest = seconds % 60; return minutes ? `${minutes}:${String(rest).padStart(2, "0")}` : `${rest} 秒`; }
|
|
359
|
+
function render(reason = "tick") { output.textContent = format(remaining); const expired = remaining <= 0; element.dataset.gTimerState = expired ? "expired" : "active"; element.setAttribute("aria-live", expired ? "polite" : "off"); if (expired) { window.clearInterval(timer); timer = 0; emit(element, "authtimerexpired", { reason }); } }
|
|
360
|
+
function start(seconds = duration, reason = "start") { window.clearInterval(timer); remaining = Math.max(0, Number(seconds)); render(reason); if (remaining > 0) timer = window.setInterval(() => { remaining -= 1; render(); }, 1000); emit(element, "authtimerstart", { remaining, reason }); }
|
|
361
|
+
function reset() { start(duration, "reset"); }
|
|
362
|
+
function click(event) { if (event.target.closest("[data-g-auth-timer-reset]")) { event.preventDefault(); reset(); } }
|
|
363
|
+
element.addEventListener("click", click); start(duration, "init");
|
|
364
|
+
return { start, reset, expire: () => start(0, "api"), remaining: () => remaining, destroy: () => { window.clearInterval(timer); element.removeEventListener("click", click); } };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function createClearInput(element) {
|
|
368
|
+
const input = element.querySelector("input, textarea");
|
|
369
|
+
const button = element.querySelector("[data-g-clear-button]");
|
|
370
|
+
if (!input || !button) return null;
|
|
371
|
+
function update() { button.hidden = !input.value || input.disabled || input.readOnly; }
|
|
372
|
+
function clear() {
|
|
373
|
+
if (input.disabled || input.readOnly) return;
|
|
374
|
+
input.value = "";
|
|
375
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
376
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
377
|
+
input.focus({ preventScroll: true });
|
|
378
|
+
emit(element, "clear", { input });
|
|
379
|
+
}
|
|
380
|
+
if (!button.hasAttribute("aria-label")) button.setAttribute("aria-label", "清空输入");
|
|
381
|
+
input.addEventListener("input", update);
|
|
382
|
+
button.addEventListener("click", clear);
|
|
383
|
+
update();
|
|
384
|
+
return { clear, update, destroy: () => { input.removeEventListener("input", update); button.removeEventListener("click", clear); } };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function createCharacterCount(element) {
|
|
388
|
+
const input = element.querySelector("textarea, input");
|
|
389
|
+
const output = element.querySelector("[data-g-character-output], .g-character-count");
|
|
390
|
+
if (!input || !output) return null;
|
|
391
|
+
const maximum = Number(element.dataset.gCharacterCount || input.maxLength || 0);
|
|
392
|
+
function update() {
|
|
393
|
+
const count = [...input.value].length;
|
|
394
|
+
output.textContent = maximum > 0 ? `${count} / ${maximum}` : String(count);
|
|
395
|
+
output.classList.toggle("is-near-limit", maximum > 0 && count >= maximum * .85 && count <= maximum);
|
|
396
|
+
output.classList.toggle("is-over-limit", maximum > 0 && count > maximum);
|
|
397
|
+
output.setAttribute("aria-label", maximum > 0 ? `已输入 ${count} 个字符,最多 ${maximum} 个` : `已输入 ${count} 个字符`);
|
|
398
|
+
emit(element, "count", { count, maximum });
|
|
399
|
+
}
|
|
400
|
+
input.addEventListener("input", update);
|
|
401
|
+
update();
|
|
402
|
+
return { update, destroy: () => input.removeEventListener("input", update) };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function createConditionalField(element) {
|
|
406
|
+
const selector = element.dataset.gController;
|
|
407
|
+
if (!selector) return null;
|
|
408
|
+
let controllers;
|
|
409
|
+
try { controllers = [...document.querySelectorAll(selector)]; }
|
|
410
|
+
catch { return null; }
|
|
411
|
+
if (!controllers.length) return null;
|
|
412
|
+
const expected = new Set(String(element.dataset.gShowWhen ?? "true").split(",").map((value) => value.trim()));
|
|
413
|
+
function value() {
|
|
414
|
+
const checkedRadio = controllers.find((controller) => controller.type === "radio" && controller.checked);
|
|
415
|
+
if (checkedRadio) return checkedRadio.value;
|
|
416
|
+
const controller = controllers[0];
|
|
417
|
+
if (controller.type === "checkbox") return controller.checked ? (controller.value || "true") : "false";
|
|
418
|
+
return controller.value;
|
|
419
|
+
}
|
|
420
|
+
function update() {
|
|
421
|
+
const visible = expected.has(String(value()));
|
|
422
|
+
element.hidden = !visible;
|
|
423
|
+
element.toggleAttribute("inert", !visible);
|
|
424
|
+
element.setAttribute("aria-hidden", String(!visible));
|
|
425
|
+
emit(element, "change", { visible, value: value() });
|
|
426
|
+
}
|
|
427
|
+
controllers.forEach((controller) => controller.addEventListener("change", update));
|
|
428
|
+
update();
|
|
429
|
+
return { update, destroy: () => controllers.forEach((controller) => controller.removeEventListener("change", update)) };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function createRepeatableField(element) {
|
|
433
|
+
const list = element.querySelector("[data-g-repeatable-list], .g-repeatable-list");
|
|
434
|
+
const template = element.querySelector("template[data-g-repeatable-template]");
|
|
435
|
+
const addButton = element.querySelector("[data-g-repeatable-add]");
|
|
436
|
+
if (!list || !template || !addButton) return null;
|
|
437
|
+
const minimum = Math.max(0, Number(element.dataset.gMinItems || 0));
|
|
438
|
+
const maximum = Math.max(minimum, Number(element.dataset.gMaxItems || Infinity));
|
|
439
|
+
const items = () => [...list.querySelectorAll(":scope > .g-repeatable-item")];
|
|
440
|
+
function replaceIndex(node, attribute, index) {
|
|
441
|
+
const current = node.getAttribute(attribute);
|
|
442
|
+
if (current == null) return;
|
|
443
|
+
const dataKey = `g${attribute[0].toUpperCase()}${attribute.slice(1)}Template`;
|
|
444
|
+
if (current.includes("__INDEX__")) node.dataset[dataKey] = current;
|
|
445
|
+
const source = node.dataset[dataKey];
|
|
446
|
+
if (source) node.setAttribute(attribute, source.replaceAll("__INDEX__", String(index)));
|
|
447
|
+
}
|
|
448
|
+
function sync() {
|
|
449
|
+
const currentItems = items();
|
|
450
|
+
currentItems.forEach((item, index) => {
|
|
451
|
+
item.dataset.gIndex = String(index);
|
|
452
|
+
item.querySelectorAll("[data-g-item-number]").forEach((node) => { node.textContent = String(index + 1); });
|
|
453
|
+
item.querySelectorAll("[name], [id], [for]").forEach((node) => { replaceIndex(node, "name", index); replaceIndex(node, "id", index); replaceIndex(node, "for", index); });
|
|
454
|
+
item.querySelectorAll("[data-g-repeatable-remove]").forEach((button) => { button.disabled = currentItems.length <= minimum; });
|
|
455
|
+
});
|
|
456
|
+
addButton.disabled = currentItems.length >= maximum;
|
|
457
|
+
}
|
|
458
|
+
function add() {
|
|
459
|
+
if (items().length >= maximum) return;
|
|
460
|
+
const fragment = template.content.cloneNode(true);
|
|
461
|
+
list.append(fragment);
|
|
462
|
+
sync();
|
|
463
|
+
const item = items().at(-1);
|
|
464
|
+
init(item);
|
|
465
|
+
visibleFocusable(item)[0]?.focus({ preventScroll: true });
|
|
466
|
+
emit(element, "add", { item, index: items().length - 1 });
|
|
467
|
+
}
|
|
468
|
+
function click(event) {
|
|
469
|
+
const removeButton = event.target.closest("[data-g-repeatable-remove]");
|
|
470
|
+
if (!removeButton) return;
|
|
471
|
+
const item = removeButton.closest(".g-repeatable-item");
|
|
472
|
+
if (!item || items().length <= minimum) return;
|
|
473
|
+
const index = items().indexOf(item);
|
|
474
|
+
item.remove();
|
|
475
|
+
sync();
|
|
476
|
+
addButton.focus({ preventScroll: true });
|
|
477
|
+
emit(element, "remove", { index });
|
|
478
|
+
}
|
|
479
|
+
addButton.addEventListener("click", add);
|
|
480
|
+
list.addEventListener("click", click);
|
|
481
|
+
sync();
|
|
482
|
+
return { add, sync, destroy: () => { addButton.removeEventListener("click", add); list.removeEventListener("click", click); } };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function createCombobox(element) {
|
|
486
|
+
const input = element.querySelector("[role='combobox'], input");
|
|
487
|
+
const list = element.querySelector("[role='listbox'], .g-combobox-list");
|
|
488
|
+
if (!input || !list) return null;
|
|
489
|
+
const options = () => [...list.querySelectorAll("[role='option'], .g-combobox-option")].filter((option) => !option.hidden && option.getAttribute("aria-disabled") !== "true");
|
|
490
|
+
let activeIndex = -1;
|
|
491
|
+
if (!list.id) list.id = uid("g-listbox");
|
|
492
|
+
input.setAttribute("role", "combobox");
|
|
493
|
+
input.setAttribute("aria-controls", list.id);
|
|
494
|
+
input.setAttribute("aria-autocomplete", input.getAttribute("aria-autocomplete") || "list");
|
|
495
|
+
|
|
496
|
+
function open() { if (!list.hidden) return; list.hidden = false; input.setAttribute("aria-expanded", "true"); emit(element, "open"); }
|
|
497
|
+
function close() { if (list.hidden) return; list.hidden = true; input.setAttribute("aria-expanded", "false"); input.removeAttribute("aria-activedescendant"); activeIndex = -1; emit(element, "close"); }
|
|
498
|
+
function activate(index) {
|
|
499
|
+
const items = options();
|
|
500
|
+
if (!items.length) { activeIndex = -1; input.removeAttribute("aria-activedescendant"); return; }
|
|
501
|
+
activeIndex = (index + items.length) % items.length;
|
|
502
|
+
items.forEach((option, optionIndex) => {
|
|
503
|
+
if (!option.id) option.id = uid("g-option");
|
|
504
|
+
option.classList.toggle("is-active", optionIndex === activeIndex);
|
|
505
|
+
});
|
|
506
|
+
input.setAttribute("aria-activedescendant", items[activeIndex].id);
|
|
507
|
+
items[activeIndex].scrollIntoView({ block: "nearest" });
|
|
508
|
+
}
|
|
509
|
+
function choose(option) {
|
|
510
|
+
input.value = option.dataset.gValue ?? option.textContent.trim();
|
|
511
|
+
options().forEach((item) => item.setAttribute("aria-selected", String(item === option)));
|
|
512
|
+
emit(element, "change", { option, value: input.value });
|
|
513
|
+
close();
|
|
514
|
+
}
|
|
515
|
+
function filter() {
|
|
516
|
+
const query = input.value.trim().toLocaleLowerCase();
|
|
517
|
+
[...list.querySelectorAll("[role='option'], .g-combobox-option")].forEach((option) => {
|
|
518
|
+
option.hidden = Boolean(query) && !option.textContent.toLocaleLowerCase().includes(query);
|
|
519
|
+
});
|
|
520
|
+
open();
|
|
521
|
+
activate(0);
|
|
522
|
+
}
|
|
523
|
+
function keydown(event) {
|
|
524
|
+
if (event.key === "ArrowDown") { event.preventDefault(); open(); activate(activeIndex + 1); }
|
|
525
|
+
else if (event.key === "ArrowUp") { event.preventDefault(); open(); activate(activeIndex - 1); }
|
|
526
|
+
else if (event.key === "Enter" && activeIndex >= 0) { event.preventDefault(); choose(options()[activeIndex]); }
|
|
527
|
+
else if (event.key === "Escape") close();
|
|
528
|
+
}
|
|
529
|
+
function click(event) { const option = event.target.closest("[role='option'], .g-combobox-option"); if (option) choose(option); }
|
|
530
|
+
function outside(event) { if (!element.contains(event.target)) close(); }
|
|
531
|
+
input.addEventListener("input", filter);
|
|
532
|
+
input.addEventListener("keydown", keydown);
|
|
533
|
+
input.addEventListener("focus", open);
|
|
534
|
+
list.addEventListener("pointerdown", click);
|
|
535
|
+
document.addEventListener("pointerdown", outside, true);
|
|
536
|
+
list.hidden = true;
|
|
537
|
+
input.setAttribute("aria-expanded", "false");
|
|
538
|
+
return { open, close, choose, destroy: () => { input.removeEventListener("input", filter); input.removeEventListener("keydown", keydown); input.removeEventListener("focus", open); list.removeEventListener("pointerdown", click); document.removeEventListener("pointerdown", outside, true); } };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function positionFloating(trigger, panel, requestedPlacement = "bottom-start") {
|
|
542
|
+
const rect = trigger.getBoundingClientRect();
|
|
543
|
+
const panelRect = panel.getBoundingClientRect();
|
|
544
|
+
const gap = 8;
|
|
545
|
+
const rtl = getComputedStyle(trigger).direction === "rtl";
|
|
546
|
+
const [rawSide, rawAlign = "start"] = String(requestedPlacement || "bottom-start").toLowerCase().split("-");
|
|
547
|
+
let side = rawSide === "start" ? (rtl ? "right" : "left") : rawSide === "end" ? (rtl ? "left" : "right") : rawSide;
|
|
548
|
+
if (!["top", "bottom", "left", "right"].includes(side)) side = "bottom";
|
|
549
|
+
const align = ["start", "center", "end"].includes(rawAlign) ? rawAlign : "start";
|
|
550
|
+
|
|
551
|
+
function coordinates(candidate) {
|
|
552
|
+
let top;
|
|
553
|
+
let left;
|
|
554
|
+
if (candidate === "top" || candidate === "bottom") {
|
|
555
|
+
top = candidate === "top" ? rect.top - panelRect.height - gap : rect.bottom + gap;
|
|
556
|
+
if (align === "center") left = rect.left + (rect.width - panelRect.width) / 2;
|
|
557
|
+
else if (align === "end") left = rtl ? rect.left : rect.right - panelRect.width;
|
|
558
|
+
else left = rtl ? rect.right - panelRect.width : rect.left;
|
|
559
|
+
} else {
|
|
560
|
+
left = candidate === "left" ? rect.left - panelRect.width - gap : rect.right + gap;
|
|
561
|
+
if (align === "center") top = rect.top + (rect.height - panelRect.height) / 2;
|
|
562
|
+
else if (align === "end") top = rect.bottom - panelRect.height;
|
|
563
|
+
else top = rect.top;
|
|
564
|
+
}
|
|
565
|
+
return { top, left };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let position = coordinates(side);
|
|
569
|
+
if (side === "bottom" && position.top + panelRect.height > window.innerHeight - gap && rect.top >= panelRect.height + gap) side = "top";
|
|
570
|
+
else if (side === "top" && position.top < gap && window.innerHeight - rect.bottom >= panelRect.height + gap) side = "bottom";
|
|
571
|
+
else if (side === "right" && position.left + panelRect.width > window.innerWidth - gap && rect.left >= panelRect.width + gap) side = "left";
|
|
572
|
+
else if (side === "left" && position.left < gap && window.innerWidth - rect.right >= panelRect.width + gap) side = "right";
|
|
573
|
+
position = coordinates(side);
|
|
574
|
+
const top = Math.max(gap, Math.min(position.top, window.innerHeight - panelRect.height - gap));
|
|
575
|
+
const left = Math.max(gap, Math.min(position.left, window.innerWidth - panelRect.width - gap));
|
|
576
|
+
panel.style.position = "fixed";
|
|
577
|
+
panel.style.inset = `${top}px auto auto ${left}px`;
|
|
578
|
+
panel.dataset.gPlacement = `${side}-${align}`;
|
|
579
|
+
panel.style.setProperty("--g-floating-arrow-inline", `${Math.max(10, Math.min(panelRect.width - 18, rect.left + rect.width / 2 - left - 6))}px`);
|
|
580
|
+
panel.style.setProperty("--g-floating-arrow-block", `${Math.max(10, Math.min(panelRect.height - 18, rect.top + rect.height / 2 - top - 6))}px`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function createFloating(element, type) {
|
|
584
|
+
const trigger = element.querySelector(`[data-g-${type}-trigger]`) || element.previousElementSibling;
|
|
585
|
+
const panel = element.querySelector(`[data-g-${type}-content]`) || element.querySelector(`.g-${type}, .g-${type}-rich, .g-help-${type}`) || element;
|
|
586
|
+
if (!trigger || !panel) return null;
|
|
587
|
+
const startsOpen = !panel.hidden;
|
|
588
|
+
let open = false;
|
|
589
|
+
const interactive = type === "popover";
|
|
590
|
+
if (!panel.id) panel.id = uid(`g-${type}`);
|
|
591
|
+
if (interactive) {
|
|
592
|
+
trigger.setAttribute("aria-haspopup", trigger.getAttribute("aria-haspopup") || "dialog");
|
|
593
|
+
trigger.setAttribute("aria-controls", panel.id);
|
|
594
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
595
|
+
} else {
|
|
596
|
+
panel.setAttribute("role", "tooltip");
|
|
597
|
+
const describedBy = new Set((trigger.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean));
|
|
598
|
+
describedBy.add(panel.id);
|
|
599
|
+
trigger.setAttribute("aria-describedby", [...describedBy].join(" "));
|
|
600
|
+
}
|
|
601
|
+
panel.hidden = true;
|
|
602
|
+
const placement = () => element.dataset.gPlacement || panel.dataset.gPlacement || trigger.dataset.gPlacement || "bottom-start";
|
|
603
|
+
function show() { if (open) return; open = true; panel.hidden = false; if (interactive) trigger.setAttribute("aria-expanded", "true"); requestAnimationFrame(() => positionFloating(trigger, panel, placement())); emit(element, "open"); }
|
|
604
|
+
function hide() { if (!open) return; open = false; panel.hidden = true; if (interactive) trigger.setAttribute("aria-expanded", "false"); emit(element, "close"); }
|
|
605
|
+
function toggle() { open ? hide() : show(); }
|
|
606
|
+
function outside(event) { if (!element.contains(event.target) && !trigger.contains(event.target)) hide(); }
|
|
607
|
+
function keydown(event) { if (event.key === "Escape") { hide(); trigger.focus(); } }
|
|
608
|
+
function reposition() { if (open) positionFloating(trigger, panel, placement()); }
|
|
609
|
+
if (interactive) trigger.addEventListener("click", toggle);
|
|
610
|
+
else {
|
|
611
|
+
trigger.addEventListener("pointerenter", show);
|
|
612
|
+
trigger.addEventListener("pointerleave", hide);
|
|
613
|
+
trigger.addEventListener("focus", show);
|
|
614
|
+
trigger.addEventListener("blur", hide);
|
|
615
|
+
}
|
|
616
|
+
document.addEventListener("pointerdown", outside, true);
|
|
617
|
+
document.addEventListener("keydown", keydown);
|
|
618
|
+
window.addEventListener("resize", reposition);
|
|
619
|
+
window.addEventListener("scroll", reposition, true);
|
|
620
|
+
if (startsOpen) queueMicrotask(show);
|
|
621
|
+
return { open: show, close: hide, toggle, destroy: () => { trigger.removeEventListener("click", toggle); trigger.removeEventListener("pointerenter", show); trigger.removeEventListener("pointerleave", hide); trigger.removeEventListener("focus", show); trigger.removeEventListener("blur", hide); document.removeEventListener("pointerdown", outside, true); document.removeEventListener("keydown", keydown); window.removeEventListener("resize", reposition); window.removeEventListener("scroll", reposition, true); } };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function createTour(element) {
|
|
625
|
+
const steps = [...element.querySelectorAll(".g-tour-step")];
|
|
626
|
+
if (!steps.length) return null;
|
|
627
|
+
if (!element.id) element.id = uid("g-tour");
|
|
628
|
+
const startsOpen = !element.hidden || element.dataset.gStartOpen === "true";
|
|
629
|
+
const progress = element.querySelector("[data-g-tour-progress], .g-tour-progress");
|
|
630
|
+
const spotlight = document.createElement("div");
|
|
631
|
+
spotlight.className = "g-spotlight";
|
|
632
|
+
spotlight.hidden = true;
|
|
633
|
+
spotlight.setAttribute("aria-hidden", "true");
|
|
634
|
+
document.body.append(spotlight);
|
|
635
|
+
element.setAttribute("role", element.getAttribute("role") || "dialog");
|
|
636
|
+
element.setAttribute("aria-modal", "false");
|
|
637
|
+
element.hidden = true;
|
|
638
|
+
element.setAttribute("aria-hidden", "true");
|
|
639
|
+
let index = 0;
|
|
640
|
+
let open = false;
|
|
641
|
+
let trigger = null;
|
|
642
|
+
|
|
643
|
+
function targetFor(step) {
|
|
644
|
+
try { return step.dataset.gTarget ? document.querySelector(step.dataset.gTarget) : null; }
|
|
645
|
+
catch { return null; }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function position() {
|
|
649
|
+
if (!open) return;
|
|
650
|
+
const step = steps[index];
|
|
651
|
+
const target = targetFor(step);
|
|
652
|
+
if (!target) {
|
|
653
|
+
spotlight.hidden = true;
|
|
654
|
+
element.style.position = "fixed";
|
|
655
|
+
element.style.inset = "50% auto auto 50%";
|
|
656
|
+
element.style.transform = "translate(-50%, -50%)";
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const rect = target.getBoundingClientRect();
|
|
660
|
+
const padding = Number(step.dataset.gSpotlightPadding || element.dataset.gSpotlightPadding || 6);
|
|
661
|
+
spotlight.hidden = false;
|
|
662
|
+
spotlight.style.inset = `${Math.max(0, rect.top - padding)}px auto auto ${Math.max(0, rect.left - padding)}px`;
|
|
663
|
+
spotlight.style.inlineSize = `${Math.min(window.innerWidth, rect.width + padding * 2)}px`;
|
|
664
|
+
spotlight.style.blockSize = `${Math.min(window.innerHeight, rect.height + padding * 2)}px`;
|
|
665
|
+
element.style.transform = "none";
|
|
666
|
+
positionFloating(target, element, step.dataset.gPlacement || element.dataset.gPlacement || "bottom-start");
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function showStep(nextIndex, source = "api") {
|
|
670
|
+
index = Math.max(0, Math.min(steps.length - 1, nextIndex));
|
|
671
|
+
steps.forEach((step, stepIndex) => {
|
|
672
|
+
step.hidden = stepIndex !== index;
|
|
673
|
+
step.setAttribute("aria-hidden", String(stepIndex !== index));
|
|
674
|
+
});
|
|
675
|
+
if (progress) progress.textContent = `${index + 1} / ${steps.length}`;
|
|
676
|
+
element.querySelectorAll("[data-g-tour-prev]").forEach((button) => { button.disabled = index === 0; });
|
|
677
|
+
element.querySelectorAll("[data-g-tour-next]").forEach((button) => { button.textContent = index === steps.length - 1 ? (button.dataset.gFinishLabel || "完成") : (button.dataset.gNextLabel || "下一步"); });
|
|
678
|
+
requestAnimationFrame(position);
|
|
679
|
+
emit(element, "change", { index, step: steps[index], source });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function openTour(source) {
|
|
683
|
+
if (open) return;
|
|
684
|
+
open = true;
|
|
685
|
+
trigger = source instanceof HTMLElement ? source : document.activeElement;
|
|
686
|
+
element.hidden = false;
|
|
687
|
+
element.setAttribute("aria-hidden", "false");
|
|
688
|
+
window.addEventListener("resize", position);
|
|
689
|
+
window.addEventListener("scroll", position, true);
|
|
690
|
+
document.addEventListener("keydown", keydown);
|
|
691
|
+
showStep(Number(element.dataset.gInitialStep || 0), "open");
|
|
692
|
+
emit(element, "open", { source });
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function close(reason = "api") {
|
|
696
|
+
if (!open) return;
|
|
697
|
+
open = false;
|
|
698
|
+
element.hidden = true;
|
|
699
|
+
element.setAttribute("aria-hidden", "true");
|
|
700
|
+
spotlight.hidden = true;
|
|
701
|
+
window.removeEventListener("resize", position);
|
|
702
|
+
window.removeEventListener("scroll", position, true);
|
|
703
|
+
document.removeEventListener("keydown", keydown);
|
|
704
|
+
trigger?.focus?.({ preventScroll: true });
|
|
705
|
+
emit(element, "close", { reason, index });
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function click(event) {
|
|
709
|
+
if (event.target.closest("[data-g-tour-prev]")) showStep(index - 1, "previous");
|
|
710
|
+
else if (event.target.closest("[data-g-tour-next]")) index === steps.length - 1 ? close("complete") : showStep(index + 1, "next");
|
|
711
|
+
else if (event.target.closest("[data-g-tour-close], [data-g-tour-skip]")) close(event.target.closest("[data-g-tour-skip]") ? "skip" : "close");
|
|
712
|
+
}
|
|
713
|
+
function keydown(event) { if (event.key === "Escape" && element.dataset.gDismissible !== "false") close("escape"); }
|
|
714
|
+
element.addEventListener("click", click);
|
|
715
|
+
if (startsOpen) queueMicrotask(openTour);
|
|
716
|
+
return { open: openTour, close, next: () => showStep(index + 1, "api"), previous: () => showStep(index - 1, "api"), go: (stepIndex) => showStep(stepIndex, "api"), isOpen: () => open, current: () => index, destroy: () => { close("destroy"); spotlight.remove(); element.removeEventListener("click", click); } };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function createCarousel(element) {
|
|
720
|
+
const slides = [...element.querySelectorAll(".g-carousel-slide, [data-g-carousel-slide]")];
|
|
721
|
+
const previous = element.querySelector(".g-carousel-prev, [data-g-carousel-prev]");
|
|
722
|
+
const next = element.querySelector(".g-carousel-next, [data-g-carousel-next]");
|
|
723
|
+
const dots = [...element.querySelectorAll(".g-carousel-dot, [data-g-carousel-dot]")];
|
|
724
|
+
if (!slides.length) return null;
|
|
725
|
+
let index = Math.max(0, slides.findIndex((slide) => slide.getAttribute("aria-hidden") !== "true"));
|
|
726
|
+
let timer;
|
|
727
|
+
const interval = Number(element.dataset.gInterval || 0);
|
|
728
|
+
const dotHandlers = dots.map((dot, dotIndex) => [dot, () => go(dotIndex, "dot")]);
|
|
729
|
+
element.setAttribute("aria-roledescription", "carousel");
|
|
730
|
+
function go(nextIndex, source = "api") {
|
|
731
|
+
index = (nextIndex + slides.length) % slides.length;
|
|
732
|
+
element.style.setProperty("--g-slide-index", index);
|
|
733
|
+
slides.forEach((slide, slideIndex) => { slide.setAttribute("aria-hidden", String(slideIndex !== index)); slide.inert = slideIndex !== index; });
|
|
734
|
+
dots.forEach((dot, dotIndex) => dot.setAttribute("aria-current", String(dotIndex === index)));
|
|
735
|
+
emit(element, "change", { index, source });
|
|
736
|
+
}
|
|
737
|
+
function start() { clearInterval(timer); if (interval > 0 && !matchMedia("(prefers-reduced-motion: reduce)").matches) timer = setInterval(() => go(index + 1, "auto"), interval); }
|
|
738
|
+
function stop() { clearInterval(timer); }
|
|
739
|
+
const prevClick = () => go(index - 1, "previous");
|
|
740
|
+
const nextClick = () => go(index + 1, "next");
|
|
741
|
+
previous?.addEventListener("click", prevClick);
|
|
742
|
+
next?.addEventListener("click", nextClick);
|
|
743
|
+
dotHandlers.forEach(([dot, handler]) => dot.addEventListener("click", handler));
|
|
744
|
+
const focusOut = (event) => { if (!element.contains(event.relatedTarget)) start(); };
|
|
745
|
+
element.addEventListener("focusin", stop);
|
|
746
|
+
element.addEventListener("focusout", focusOut);
|
|
747
|
+
element.addEventListener("pointerenter", stop);
|
|
748
|
+
element.addEventListener("pointerleave", start);
|
|
749
|
+
go(index, "init");
|
|
750
|
+
start();
|
|
751
|
+
return { go, next: nextClick, previous: prevClick, start, stop, destroy: () => { stop(); previous?.removeEventListener("click", prevClick); next?.removeEventListener("click", nextClick); dotHandlers.forEach(([dot, handler]) => dot.removeEventListener("click", handler)); element.removeEventListener("focusin", stop); element.removeEventListener("focusout", focusOut); element.removeEventListener("pointerenter", stop); element.removeEventListener("pointerleave", start); } };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function createSplitter(element) {
|
|
755
|
+
const handle = element.querySelector(".g-split-handle, [role='separator']");
|
|
756
|
+
const panels = [...element.querySelectorAll(":scope > .g-split-panel")];
|
|
757
|
+
if (!handle || panels.length < 2) return null;
|
|
758
|
+
const vertical = () => element.dataset.gOrientation === "vertical";
|
|
759
|
+
handle.setAttribute("role", "separator");
|
|
760
|
+
handle.tabIndex = 0;
|
|
761
|
+
handle.setAttribute("aria-orientation", vertical() ? "horizontal" : "vertical");
|
|
762
|
+
handle.setAttribute("aria-valuemin", "10");
|
|
763
|
+
handle.setAttribute("aria-valuemax", "90");
|
|
764
|
+
let activeMove = null;
|
|
765
|
+
let activeUp = null;
|
|
766
|
+
function setPercent(percent) {
|
|
767
|
+
const value = Math.max(10, Math.min(90, percent));
|
|
768
|
+
panels[0].style.flex = `0 0 ${value}%`;
|
|
769
|
+
panels[1].style.flex = "1 1 0";
|
|
770
|
+
handle.setAttribute("aria-valuenow", String(Math.round(value)));
|
|
771
|
+
emit(element, "resize", { value });
|
|
772
|
+
}
|
|
773
|
+
function pointerDown(event) {
|
|
774
|
+
event.preventDefault();
|
|
775
|
+
handle.setPointerCapture?.(event.pointerId);
|
|
776
|
+
handle.classList.add("is-dragging");
|
|
777
|
+
const rect = element.getBoundingClientRect();
|
|
778
|
+
activeMove = (moveEvent) => setPercent(vertical() ? (moveEvent.clientY - rect.top) / rect.height * 100 : (moveEvent.clientX - rect.left) / rect.width * 100);
|
|
779
|
+
activeUp = () => { handle.classList.remove("is-dragging"); window.removeEventListener("pointermove", activeMove); window.removeEventListener("pointerup", activeUp); window.removeEventListener("pointercancel", activeUp); activeMove = null; activeUp = null; };
|
|
780
|
+
window.addEventListener("pointermove", activeMove);
|
|
781
|
+
window.addEventListener("pointerup", activeUp, { once: true });
|
|
782
|
+
window.addEventListener("pointercancel", activeUp, { once: true });
|
|
783
|
+
}
|
|
784
|
+
function keydown(event) {
|
|
785
|
+
const delta = event.shiftKey ? 10 : 2;
|
|
786
|
+
const current = Number(handle.getAttribute("aria-valuenow") || 50);
|
|
787
|
+
if (event.key === (vertical() ? "ArrowUp" : "ArrowLeft")) { event.preventDefault(); setPercent(current - delta); }
|
|
788
|
+
else if (event.key === (vertical() ? "ArrowDown" : "ArrowRight")) { event.preventDefault(); setPercent(current + delta); }
|
|
789
|
+
else if (event.key === "Home") { event.preventDefault(); setPercent(10); }
|
|
790
|
+
else if (event.key === "End") { event.preventDefault(); setPercent(90); }
|
|
791
|
+
}
|
|
792
|
+
handle.addEventListener("pointerdown", pointerDown);
|
|
793
|
+
handle.addEventListener("keydown", keydown);
|
|
794
|
+
setPercent(Number(element.dataset.gSplit || 50));
|
|
795
|
+
return { set: setPercent, destroy: () => { activeUp?.(); handle.removeEventListener("pointerdown", pointerDown); handle.removeEventListener("keydown", keydown); } };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function createTree(element) {
|
|
799
|
+
const items = () => [...element.querySelectorAll("[role='treeitem']")].filter((item) => item.getClientRects().length > 0);
|
|
800
|
+
element.setAttribute("role", "tree");
|
|
801
|
+
function focusItem(item) { items().forEach((node) => { node.tabIndex = node === item ? 0 : -1; }); item?.focus(); }
|
|
802
|
+
function keydown(event) {
|
|
803
|
+
const item = event.target.closest("[role='treeitem']");
|
|
804
|
+
if (!item) return;
|
|
805
|
+
const nodes = items();
|
|
806
|
+
const index = nodes.indexOf(item);
|
|
807
|
+
if (event.key === "ArrowDown") { event.preventDefault(); focusItem(nodes[index + 1] || nodes[0]); }
|
|
808
|
+
else if (event.key === "ArrowUp") { event.preventDefault(); focusItem(nodes[index - 1] || nodes.at(-1)); }
|
|
809
|
+
else if (event.key === "Home") { event.preventDefault(); focusItem(nodes[0]); }
|
|
810
|
+
else if (event.key === "End") { event.preventDefault(); focusItem(nodes.at(-1)); }
|
|
811
|
+
else if (event.key === "ArrowRight") {
|
|
812
|
+
if (item.getAttribute("aria-expanded") === "false") { event.preventDefault(); item.setAttribute("aria-expanded", "true"); emit(element, "expand", { item }); }
|
|
813
|
+
else { const child = item.querySelector(":scope > [role='group'] [role='treeitem']"); if (child) { event.preventDefault(); focusItem(child); } }
|
|
814
|
+
}
|
|
815
|
+
else if (event.key === "ArrowLeft") {
|
|
816
|
+
if (item.getAttribute("aria-expanded") === "true") { event.preventDefault(); item.setAttribute("aria-expanded", "false"); emit(element, "collapse", { item }); }
|
|
817
|
+
else { const parent = item.parentElement?.closest("[role='treeitem']"); if (parent) { event.preventDefault(); focusItem(parent); } }
|
|
818
|
+
}
|
|
819
|
+
else if (event.key === "Enter" || event.key === " ") { event.preventDefault(); item.setAttribute("aria-selected", String(item.getAttribute("aria-selected") !== "true")); emit(element, "change", { item }); }
|
|
820
|
+
}
|
|
821
|
+
const first = items()[0];
|
|
822
|
+
items().forEach((item) => { item.tabIndex = item === first ? 0 : -1; });
|
|
823
|
+
element.addEventListener("keydown", keydown);
|
|
824
|
+
return { focus: focusItem, destroy: () => element.removeEventListener("keydown", keydown) };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function createDataGrid(element) {
|
|
828
|
+
const cells = () => [...element.querySelectorAll("[role='gridcell'], [role='columnheader'], [role='rowheader']")].filter((cell) => cell.getClientRects().length > 0);
|
|
829
|
+
element.setAttribute("role", "grid");
|
|
830
|
+
function focusCell(cell) { cells().forEach((item) => { item.tabIndex = item === cell ? 0 : -1; }); cell?.focus(); }
|
|
831
|
+
function keydown(event) {
|
|
832
|
+
const cell = event.target.closest("[role='gridcell'], [role='columnheader'], [role='rowheader']");
|
|
833
|
+
if (!cell) return;
|
|
834
|
+
const row = cell.closest("[role='row']");
|
|
835
|
+
const rowCells = [...row.querySelectorAll("[role='gridcell'], [role='columnheader'], [role='rowheader']")];
|
|
836
|
+
const rows = [...element.querySelectorAll("[role='row']")];
|
|
837
|
+
const columnIndex = rowCells.indexOf(cell);
|
|
838
|
+
const rowIndex = rows.indexOf(row);
|
|
839
|
+
let target;
|
|
840
|
+
if (event.key === "ArrowRight") target = rowCells[columnIndex + 1];
|
|
841
|
+
else if (event.key === "ArrowLeft") target = rowCells[columnIndex - 1];
|
|
842
|
+
else if (event.key === "ArrowDown") target = rows[rowIndex + 1]?.querySelectorAll("[role='gridcell'], [role='columnheader'], [role='rowheader']")[columnIndex];
|
|
843
|
+
else if (event.key === "ArrowUp") target = rows[rowIndex - 1]?.querySelectorAll("[role='gridcell'], [role='columnheader'], [role='rowheader']")[columnIndex];
|
|
844
|
+
else if (event.key === "Home") target = event.ctrlKey ? cells()[0] : rowCells[0];
|
|
845
|
+
else if (event.key === "End") target = event.ctrlKey ? cells().at(-1) : rowCells.at(-1);
|
|
846
|
+
if (target) { event.preventDefault(); focusCell(target); }
|
|
847
|
+
}
|
|
848
|
+
const first = cells()[0];
|
|
849
|
+
cells().forEach((cell) => { cell.tabIndex = cell === first ? 0 : -1; });
|
|
850
|
+
element.addEventListener("keydown", keydown);
|
|
851
|
+
return { focus: focusCell, destroy: () => element.removeEventListener("keydown", keydown) };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function createTableSort(element) {
|
|
855
|
+
const table = element.matches("table") ? element : element.querySelector("table");
|
|
856
|
+
const body = table?.tBodies?.[0];
|
|
857
|
+
const buttons = table ? [...table.querySelectorAll("[data-g-sort-key]")] : [];
|
|
858
|
+
if (!body || !buttons.length) return null;
|
|
859
|
+
const collator = new Intl.Collator(element.lang || document.documentElement.lang || undefined, { numeric: true, sensitivity: "base" });
|
|
860
|
+
|
|
861
|
+
function valueFor(row, key, index, type) {
|
|
862
|
+
const cell = row.querySelector(`[data-g-column="${CSS.escape(key)}"]`) || row.cells[index];
|
|
863
|
+
const raw = cell?.dataset.gSortValue ?? cell?.textContent?.trim() ?? "";
|
|
864
|
+
if (type === "number") return Number(String(raw).replace(/[^\d+-.]/g, "")) || 0;
|
|
865
|
+
if (type === "date") return Date.parse(raw) || 0;
|
|
866
|
+
return raw;
|
|
867
|
+
}
|
|
868
|
+
function sort(button, direction) {
|
|
869
|
+
const key = button.dataset.gSortKey;
|
|
870
|
+
const header = button.closest("th, [role='columnheader']");
|
|
871
|
+
const headers = [...table.querySelectorAll("th, [role='columnheader']")];
|
|
872
|
+
const index = headers.indexOf(header);
|
|
873
|
+
const type = button.dataset.gSortType || "text";
|
|
874
|
+
const rows = [...body.rows].filter((row) => !row.hasAttribute("data-g-detail-row") && !row.hasAttribute("data-g-group-row"));
|
|
875
|
+
const details = new Map([...body.querySelectorAll("[data-g-detail-row]")].map((row) => [row.dataset.gDetailRow, row]));
|
|
876
|
+
const stable = rows.map((row, order) => ({ row, order, value: valueFor(row, key, index, type) }));
|
|
877
|
+
stable.sort((left, right) => {
|
|
878
|
+
const result = type === "text" ? collator.compare(String(left.value), String(right.value)) : left.value - right.value;
|
|
879
|
+
return (direction === "descending" ? -result : result) || left.order - right.order;
|
|
880
|
+
});
|
|
881
|
+
stable.forEach(({ row }) => {
|
|
882
|
+
body.append(row);
|
|
883
|
+
const detail = details.get(row.id);
|
|
884
|
+
if (detail) body.append(detail);
|
|
885
|
+
});
|
|
886
|
+
headers.forEach((item) => item.setAttribute("aria-sort", item === header ? direction : "none"));
|
|
887
|
+
buttons.forEach((item) => item.dataset.gSortDirection = item === button ? direction : "none");
|
|
888
|
+
emit(element, "sort", { key, direction, type, button });
|
|
889
|
+
}
|
|
890
|
+
function click(event) {
|
|
891
|
+
const button = event.target.closest("[data-g-sort-key]");
|
|
892
|
+
if (!button || !element.contains(button)) return;
|
|
893
|
+
const current = button.dataset.gSortDirection || button.closest("th, [role='columnheader']")?.getAttribute("aria-sort") || "none";
|
|
894
|
+
sort(button, current === "ascending" ? "descending" : "ascending");
|
|
895
|
+
}
|
|
896
|
+
table.querySelectorAll("th, [role='columnheader']").forEach((header) => {
|
|
897
|
+
if (header.querySelector("[data-g-sort-key]") && !header.hasAttribute("aria-sort")) header.setAttribute("aria-sort", "none");
|
|
898
|
+
});
|
|
899
|
+
element.addEventListener("click", click);
|
|
900
|
+
return { sort, destroy: () => element.removeEventListener("click", click) };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function createRowSelect(element) {
|
|
904
|
+
const master = element.querySelector("[data-g-select-all]");
|
|
905
|
+
const boxes = () => [...element.querySelectorAll("[data-g-select-row]")].filter((box) => !box.disabled);
|
|
906
|
+
const outputs = () => [...element.querySelectorAll("[data-g-selection-count]")];
|
|
907
|
+
const totals = () => [...element.querySelectorAll("[data-g-selection-total]")];
|
|
908
|
+
const summaries = () => [...element.querySelectorAll("[data-g-selection-summary], [data-g-batch-toolbar]")];
|
|
909
|
+
let lastIndex = -1;
|
|
910
|
+
|
|
911
|
+
function sync(reason = "api") {
|
|
912
|
+
const items = boxes();
|
|
913
|
+
const selected = items.filter((box) => box.checked);
|
|
914
|
+
items.forEach((box) => {
|
|
915
|
+
const row = box.closest("tr, [role='row'], .g-list-item, [data-g-selectable-row]");
|
|
916
|
+
row?.classList.toggle("is-selected", box.checked);
|
|
917
|
+
if (row?.matches("tr, [role='row'], [role='option'], [role='tab'], [role='treeitem']")) row.setAttribute("aria-selected", String(box.checked));
|
|
918
|
+
else row?.removeAttribute("aria-selected");
|
|
919
|
+
});
|
|
920
|
+
if (master) {
|
|
921
|
+
master.checked = items.length > 0 && selected.length === items.length;
|
|
922
|
+
master.indeterminate = selected.length > 0 && selected.length < items.length;
|
|
923
|
+
}
|
|
924
|
+
outputs().forEach((output) => { output.textContent = String(selected.length); });
|
|
925
|
+
totals().forEach((output) => { output.textContent = String(items.length); });
|
|
926
|
+
summaries().forEach((summary) => { summary.hidden = selected.length === 0; });
|
|
927
|
+
element.querySelectorAll("[data-g-selection-requires]").forEach((control) => { control.disabled = selected.length === 0; });
|
|
928
|
+
emit(element, "selectionchange", { selected: selected.map((box) => box.value), count: selected.length, reason });
|
|
929
|
+
}
|
|
930
|
+
function change(event) {
|
|
931
|
+
if (event.target === master) boxes().forEach((box) => { box.checked = master.checked; });
|
|
932
|
+
if (event.target.matches?.("[data-g-select-row]")) lastIndex = boxes().indexOf(event.target);
|
|
933
|
+
sync(event.target === master ? "all" : "item");
|
|
934
|
+
}
|
|
935
|
+
function click(event) {
|
|
936
|
+
const action = event.target.closest?.("[data-g-select-all-action], [data-g-select-visible], [data-g-invert-selection], [data-g-clear-selection]");
|
|
937
|
+
if (action) {
|
|
938
|
+
event.preventDefault();
|
|
939
|
+
if (action.hasAttribute("data-g-clear-selection")) boxes().forEach((item) => { item.checked = false; });
|
|
940
|
+
else if (action.hasAttribute("data-g-invert-selection")) boxes().forEach((item) => { item.checked = !item.checked; });
|
|
941
|
+
else if (action.hasAttribute("data-g-select-visible")) boxes().filter((item) => !item.closest("[hidden]")).forEach((item) => { item.checked = true; });
|
|
942
|
+
else boxes().forEach((item) => { item.checked = true; });
|
|
943
|
+
sync(action.hasAttribute("data-g-invert-selection") ? "invert" : action.hasAttribute("data-g-clear-selection") ? "clear" : "all-action");
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const box = event.target.closest?.("[data-g-select-row]");
|
|
947
|
+
if (!box || !event.shiftKey || lastIndex < 0) return;
|
|
948
|
+
const items = boxes();
|
|
949
|
+
const current = items.indexOf(box);
|
|
950
|
+
if (current < 0) return;
|
|
951
|
+
const [start, end] = [lastIndex, current].sort((a, b) => a - b);
|
|
952
|
+
items.slice(start, end + 1).forEach((item) => { item.checked = box.checked; });
|
|
953
|
+
sync("range");
|
|
954
|
+
}
|
|
955
|
+
element.addEventListener("change", change);
|
|
956
|
+
element.addEventListener("click", click);
|
|
957
|
+
sync("init");
|
|
958
|
+
return {
|
|
959
|
+
sync,
|
|
960
|
+
clear: () => { boxes().forEach((box) => { box.checked = false; }); sync("clear"); },
|
|
961
|
+
selectAll: () => { boxes().forEach((box) => { box.checked = true; }); sync("all-api"); },
|
|
962
|
+
invert: () => { boxes().forEach((box) => { box.checked = !box.checked; }); sync("invert-api"); },
|
|
963
|
+
selected: () => boxes().filter((box) => box.checked),
|
|
964
|
+
destroy: () => { element.removeEventListener("change", change); element.removeEventListener("click", click); }
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function createTransfer(element) {
|
|
969
|
+
const source = element.querySelector("[data-g-transfer-source]");
|
|
970
|
+
const target = element.querySelector("[data-g-transfer-target]");
|
|
971
|
+
if (!source || !target) return null;
|
|
972
|
+
const optionSelector = "[data-g-transfer-option]";
|
|
973
|
+
const options = (pane) => [...pane.querySelectorAll(optionSelector)];
|
|
974
|
+
const checked = (pane) => options(pane).filter((option) => option.querySelector("input[type='checkbox']")?.checked || option.getAttribute("aria-selected") === "true");
|
|
975
|
+
function setChecked(option, value) {
|
|
976
|
+
const input = option.querySelector("input[type='checkbox']");
|
|
977
|
+
if (input) input.checked = value;
|
|
978
|
+
if (input) option.removeAttribute("aria-selected");
|
|
979
|
+
else { option.setAttribute("role", option.getAttribute("role") || "option"); option.setAttribute("aria-selected", String(value)); }
|
|
980
|
+
}
|
|
981
|
+
function sync(reason = "api") {
|
|
982
|
+
[[source, "source"], [target, "target"]].forEach(([pane, name]) => {
|
|
983
|
+
const list = options(pane);
|
|
984
|
+
const count = element.querySelector(`[data-g-transfer-count="${name}"]`);
|
|
985
|
+
const empty = pane.querySelector("[data-g-transfer-empty]");
|
|
986
|
+
if (count) count.textContent = String(list.length);
|
|
987
|
+
if (empty) empty.hidden = list.length !== 0;
|
|
988
|
+
});
|
|
989
|
+
element.querySelectorAll("[data-g-transfer-move]").forEach((button) => {
|
|
990
|
+
const direction = button.dataset.gTransferMove;
|
|
991
|
+
const from = direction.endsWith("target") ? source : target;
|
|
992
|
+
button.disabled = direction.startsWith("all") ? options(from).length === 0 : checked(from).length === 0;
|
|
993
|
+
});
|
|
994
|
+
emit(element, "transferchange", { source: options(source).map((item) => item.dataset.gValue), target: options(target).map((item) => item.dataset.gValue), reason });
|
|
995
|
+
}
|
|
996
|
+
function move(direction) {
|
|
997
|
+
const toTarget = direction.endsWith("target");
|
|
998
|
+
const from = toTarget ? source : target;
|
|
999
|
+
const to = toTarget ? target : source;
|
|
1000
|
+
const moving = direction.startsWith("all") ? options(from) : checked(from);
|
|
1001
|
+
moving.forEach((option) => { setChecked(option, false); to.append(option); });
|
|
1002
|
+
sync(direction);
|
|
1003
|
+
moving[0]?.focus?.();
|
|
1004
|
+
}
|
|
1005
|
+
function click(event) {
|
|
1006
|
+
const button = event.target.closest("[data-g-transfer-move]");
|
|
1007
|
+
if (button) { event.preventDefault(); move(button.dataset.gTransferMove); return; }
|
|
1008
|
+
const option = event.target.closest(optionSelector);
|
|
1009
|
+
if (option && !option.querySelector("input[type='checkbox']") && !event.target.matches("button,a")) { setChecked(option, option.getAttribute("aria-selected") !== "true"); sync("option"); }
|
|
1010
|
+
}
|
|
1011
|
+
function change(event) {
|
|
1012
|
+
const option = event.target.closest(optionSelector);
|
|
1013
|
+
if (option) { setChecked(option, event.target.checked); sync("option"); }
|
|
1014
|
+
}
|
|
1015
|
+
function input(event) {
|
|
1016
|
+
const field = event.target.closest("[data-g-transfer-search]");
|
|
1017
|
+
if (!field) return;
|
|
1018
|
+
const pane = field.dataset.gTransferSearch === "target" ? target : source;
|
|
1019
|
+
const query = field.value.normalize("NFKD").toLocaleLowerCase().trim();
|
|
1020
|
+
options(pane).forEach((option) => { option.hidden = Boolean(query) && !String(option.dataset.gFilterText || option.textContent).normalize("NFKD").toLocaleLowerCase().includes(query); });
|
|
1021
|
+
}
|
|
1022
|
+
function keydown(event) {
|
|
1023
|
+
const option = event.target.closest(optionSelector);
|
|
1024
|
+
if (!option || !(event.ctrlKey || event.metaKey)) return;
|
|
1025
|
+
if (event.key === "ArrowRight" && source.contains(option)) { event.preventDefault(); setChecked(option, true); move("selected-to-target"); }
|
|
1026
|
+
if (event.key === "ArrowLeft" && target.contains(option)) { event.preventDefault(); setChecked(option, true); move("selected-to-source"); }
|
|
1027
|
+
}
|
|
1028
|
+
element.addEventListener("click", click);
|
|
1029
|
+
element.addEventListener("change", change);
|
|
1030
|
+
element.addEventListener("input", input);
|
|
1031
|
+
element.addEventListener("keydown", keydown);
|
|
1032
|
+
sync("init");
|
|
1033
|
+
return { move, sync, values: () => options(target).map((item) => item.dataset.gValue), destroy: () => { element.removeEventListener("click", click); element.removeEventListener("change", change); element.removeEventListener("input", input); element.removeEventListener("keydown", keydown); } };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function createPicker(element) {
|
|
1037
|
+
const panel = element.querySelector("[data-g-picker-panel]");
|
|
1038
|
+
const trigger = element.querySelector("[data-g-picker-trigger]");
|
|
1039
|
+
const input = element.querySelector("[data-g-picker-input]");
|
|
1040
|
+
const valuesHost = element.querySelector("[data-g-picker-values]");
|
|
1041
|
+
const output = element.querySelector("[data-g-picker-output]");
|
|
1042
|
+
const options = () => [...element.querySelectorAll("[data-g-picker-option]")];
|
|
1043
|
+
const multiple = element.hasAttribute("data-g-picker-multiple");
|
|
1044
|
+
if (!panel || (!trigger && !input) || !options().length) return null;
|
|
1045
|
+
let open = !panel.hidden;
|
|
1046
|
+
function selected() { return options().filter((option) => option.getAttribute("aria-selected") === "true"); }
|
|
1047
|
+
function sync(reason = "api") {
|
|
1048
|
+
const current = selected();
|
|
1049
|
+
options().forEach((option) => option.classList.toggle("is-selected", current.includes(option)));
|
|
1050
|
+
if (output) {
|
|
1051
|
+
const value = multiple ? current.map((option) => option.dataset.gValue || option.textContent.trim()).join(",") : current[0]?.dataset.gValue || "";
|
|
1052
|
+
output.value = value;
|
|
1053
|
+
output.setAttribute("value", value);
|
|
1054
|
+
}
|
|
1055
|
+
element.querySelectorAll("[data-g-picker-count]").forEach((node) => { node.textContent = String(current.length); });
|
|
1056
|
+
if (valuesHost) {
|
|
1057
|
+
valuesHost.replaceChildren(...current.map((option) => {
|
|
1058
|
+
const chip = document.createElement("span"); chip.className = "g-picker-value"; chip.dataset.gValue = option.dataset.gValue || option.textContent.trim();
|
|
1059
|
+
const label = document.createElement("span"); label.className = "g-picker-value-label"; label.textContent = option.dataset.gLabel || option.querySelector(".g-picker-option-title")?.textContent || option.textContent.trim(); chip.append(label);
|
|
1060
|
+
const remove = document.createElement("button"); remove.type = "button"; remove.className = "g-picker-value-remove"; remove.dataset.gPickerRemove = chip.dataset.gValue; remove.setAttribute("aria-label", `移除 ${label.textContent}`); remove.textContent = "×"; chip.append(remove); return chip;
|
|
1061
|
+
}));
|
|
1062
|
+
}
|
|
1063
|
+
emit(element, "pickerchange", { values: current.map((option) => option.dataset.gValue), reason });
|
|
1064
|
+
}
|
|
1065
|
+
function setOpen(value, reason = "api") {
|
|
1066
|
+
open = value; panel.hidden = !open; trigger?.setAttribute("aria-expanded", String(open)); input?.setAttribute("aria-expanded", String(open));
|
|
1067
|
+
if (open) queueMicrotask(() => input?.focus());
|
|
1068
|
+
emit(element, open ? "open" : "close", { reason });
|
|
1069
|
+
}
|
|
1070
|
+
function choose(option, reason = "option") {
|
|
1071
|
+
const next = option.getAttribute("aria-selected") !== "true";
|
|
1072
|
+
if (!multiple) options().forEach((item) => item.setAttribute("aria-selected", "false"));
|
|
1073
|
+
option.setAttribute("aria-selected", String(next)); sync(reason); if (!multiple) setOpen(false, "selection");
|
|
1074
|
+
}
|
|
1075
|
+
function click(event) {
|
|
1076
|
+
const remove = event.target.closest("[data-g-picker-remove]");
|
|
1077
|
+
if (remove) { const option = options().find((item) => (item.dataset.gValue || item.textContent.trim()) === remove.dataset.gPickerRemove); option?.setAttribute("aria-selected", "false"); sync("remove"); return; }
|
|
1078
|
+
const option = event.target.closest("[data-g-picker-option]");
|
|
1079
|
+
if (option) { event.preventDefault(); choose(option); return; }
|
|
1080
|
+
if (event.target.closest("[data-g-picker-trigger]")) { const inputTarget = event.target.closest("[data-g-picker-input]"); if (!inputTarget) event.preventDefault(); setOpen(inputTarget ? true : !open, "trigger"); }
|
|
1081
|
+
}
|
|
1082
|
+
function filter() {
|
|
1083
|
+
const query = String(input?.value || "").normalize("NFKD").toLocaleLowerCase().trim(); let visible = 0;
|
|
1084
|
+
options().forEach((option) => { const match = !query || String(option.dataset.gFilterText || option.textContent).normalize("NFKD").toLocaleLowerCase().includes(query); option.hidden = !match; if (match) visible += 1; });
|
|
1085
|
+
element.querySelectorAll("[data-g-picker-empty]").forEach((empty) => { empty.hidden = visible !== 0; });
|
|
1086
|
+
if (!open) setOpen(true, "input");
|
|
1087
|
+
}
|
|
1088
|
+
function keydown(event) {
|
|
1089
|
+
if (event.key === "Escape" && open) { event.preventDefault(); setOpen(false, "escape"); trigger?.focus(); return; }
|
|
1090
|
+
if (!["ArrowDown", "ArrowUp", "Home", "End", "Enter", " "].includes(event.key)) return;
|
|
1091
|
+
const visible = options().filter((option) => !option.hidden); if (!visible.length) return;
|
|
1092
|
+
const current = visible.indexOf(document.activeElement);
|
|
1093
|
+
if ((event.key === "Enter" || event.key === " ") && current >= 0) { event.preventDefault(); choose(visible[current], "keyboard"); return; }
|
|
1094
|
+
if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) { event.preventDefault(); setOpen(true, "keyboard"); const index = event.key === "Home" ? 0 : event.key === "End" ? visible.length - 1 : event.key === "ArrowDown" ? (current + 1 + visible.length) % visible.length : (current - 1 + visible.length) % visible.length; visible[index].focus(); }
|
|
1095
|
+
}
|
|
1096
|
+
function outside(event) { if (open && !element.contains(event.target)) setOpen(false, "outside"); }
|
|
1097
|
+
element.addEventListener("click", click); element.addEventListener("keydown", keydown); input?.addEventListener("input", filter); document.addEventListener("pointerdown", outside);
|
|
1098
|
+
options().forEach((option) => { option.tabIndex ||= -1; option.setAttribute("role", option.getAttribute("role") || "option"); if (!option.hasAttribute("aria-selected")) option.setAttribute("aria-selected", "false"); });
|
|
1099
|
+
sync("init"); setOpen(open, "init");
|
|
1100
|
+
return { open: () => setOpen(true), close: () => setOpen(false), selected, choose, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); input?.removeEventListener("input", filter); document.removeEventListener("pointerdown", outside); } };
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function createCascader(element) {
|
|
1104
|
+
const columns = () => [...element.querySelectorAll("[data-g-cascade-column]")];
|
|
1105
|
+
const output = element.querySelector("[data-g-cascade-output]");
|
|
1106
|
+
const path = element.querySelector("[data-g-cascade-path]");
|
|
1107
|
+
if (!columns().length) return null;
|
|
1108
|
+
function choose(option, reason = "option") {
|
|
1109
|
+
const column = option.closest("[data-g-cascade-column]"); const index = columns().indexOf(column);
|
|
1110
|
+
[...column.querySelectorAll("[data-g-cascade-option]")].forEach((item) => { item.setAttribute("aria-current", String(item === option)); });
|
|
1111
|
+
columns().slice(index + 1).forEach((next) => { next.hidden = next.id !== option.dataset.gCascadeNext; });
|
|
1112
|
+
const values = columns().filter((item) => !item.hidden).map((item) => item.querySelector('[data-g-cascade-option][aria-current="true"]')).filter(Boolean);
|
|
1113
|
+
if (path) path.textContent = values.map((item) => item.dataset.gLabel || item.textContent.trim()).join(" / ");
|
|
1114
|
+
if (output) { const value = values.at(-1)?.dataset.gValue || ""; output.value = value; output.setAttribute("value", value); }
|
|
1115
|
+
emit(element, "cascadechange", { value: output?.value, path: values.map((item) => item.dataset.gValue), reason });
|
|
1116
|
+
}
|
|
1117
|
+
function click(event) { const option = event.target.closest("[data-g-cascade-option]"); if (option) { event.preventDefault(); choose(option); } }
|
|
1118
|
+
function keydown(event) { const option = event.target.closest("[data-g-cascade-option]"); if (!option) return; if (["Enter", " ", "ArrowRight"].includes(event.key)) { event.preventDefault(); choose(option, "keyboard"); const next = option.dataset.gCascadeNext && targetById(option.dataset.gCascadeNext); next?.querySelector("[data-g-cascade-option]")?.focus(); } if (event.key === "ArrowLeft") { event.preventDefault(); const column = option.closest("[data-g-cascade-column]"); columns()[Math.max(0, columns().indexOf(column) - 1)]?.querySelector('[data-g-cascade-option][aria-current="true"], [data-g-cascade-option]')?.focus(); } }
|
|
1119
|
+
element.addEventListener("click", click); element.addEventListener("keydown", keydown);
|
|
1120
|
+
return { choose, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function createSavedChoice(element) {
|
|
1124
|
+
const items = () => [...element.querySelectorAll("[data-g-saved-choice-item]")];
|
|
1125
|
+
if (!items().length) return null;
|
|
1126
|
+
function select(item, reason = "option") { const multiple = element.hasAttribute("data-g-multiple"); if (!multiple) items().forEach((option) => option.setAttribute("aria-pressed", "false")); item.setAttribute("aria-pressed", String(multiple ? item.getAttribute("aria-pressed") !== "true" : true)); emit(element, "savedchoice", { values: items().filter((option) => option.getAttribute("aria-pressed") === "true").map((option) => option.dataset.gValue), reason }); }
|
|
1127
|
+
function click(event) { const item = event.target.closest("[data-g-saved-choice-item]"); if (item) { event.preventDefault(); select(item); } }
|
|
1128
|
+
element.addEventListener("click", click); return { select, destroy: () => element.removeEventListener("click", click) };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function createBuilderList(element) {
|
|
1132
|
+
const list = element.querySelector("[data-g-builder-items]"); const template = targetById(element.dataset.gBuilderTemplate); if (!list || !template) return null;
|
|
1133
|
+
function sync(reason = "api") { [...list.children].forEach((item, index) => { item.dataset.gBuilderIndex = String(index); item.querySelectorAll("[data-g-builder-position]").forEach((output) => { output.textContent = String(index + 1); }); }); emit(element, "builderchange", { count: list.children.length, reason }); }
|
|
1134
|
+
function add() { const fragment = template.content ? template.content.cloneNode(true) : template.cloneNode(true); list.append(fragment); sync("add"); list.lastElementChild?.querySelector(focusableSelector)?.focus(); }
|
|
1135
|
+
function click(event) { const control = event.target.closest("[data-g-builder-add], [data-g-builder-remove], [data-g-builder-up], [data-g-builder-down]"); if (!control) return; event.preventDefault(); if (control.hasAttribute("data-g-builder-add")) { add(); return; } const item = control.closest("[data-g-builder-item]"); if (!item) return; if (control.hasAttribute("data-g-builder-remove")) item.remove(); else if (control.hasAttribute("data-g-builder-up") && item.previousElementSibling) list.insertBefore(item, item.previousElementSibling); else if (control.hasAttribute("data-g-builder-down") && item.nextElementSibling) list.insertBefore(item.nextElementSibling, item); sync(control.hasAttribute("data-g-builder-remove") ? "remove" : "move"); }
|
|
1136
|
+
element.addEventListener("click", click); sync("init"); return { add, sync, destroy: () => element.removeEventListener("click", click) };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function createRowDisclosure(element) {
|
|
1140
|
+
const triggers = [...element.querySelectorAll("[data-g-row-toggle]")];
|
|
1141
|
+
if (!triggers.length) return null;
|
|
1142
|
+
function descendants(parent) { return [...element.querySelectorAll(`[data-g-parent="${CSS.escape(parent)}"]`)]; }
|
|
1143
|
+
function set(trigger, expanded, reason = "api") {
|
|
1144
|
+
const value = trigger.dataset.gRowToggle;
|
|
1145
|
+
const target = targetById(value);
|
|
1146
|
+
const children = target ? [target] : descendants(value);
|
|
1147
|
+
trigger.setAttribute("aria-expanded", String(expanded));
|
|
1148
|
+
if (target?.id) trigger.setAttribute("aria-controls", target.id);
|
|
1149
|
+
children.forEach((child) => {
|
|
1150
|
+
child.hidden = !expanded;
|
|
1151
|
+
child.setAttribute("aria-hidden", String(!expanded));
|
|
1152
|
+
if (!expanded && child.id) {
|
|
1153
|
+
descendants(child.id).forEach((nested) => { nested.hidden = true; nested.setAttribute("aria-hidden", "true"); });
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
emit(element, "disclosure", { trigger, targets: children, expanded, reason });
|
|
1157
|
+
}
|
|
1158
|
+
function click(event) {
|
|
1159
|
+
const trigger = event.target.closest("[data-g-row-toggle]");
|
|
1160
|
+
if (trigger) set(trigger, trigger.getAttribute("aria-expanded") !== "true", "toggle");
|
|
1161
|
+
}
|
|
1162
|
+
function keydown(event) {
|
|
1163
|
+
const trigger = event.target.closest("[data-g-row-toggle]");
|
|
1164
|
+
if (trigger && event.key === "Escape" && trigger.getAttribute("aria-expanded") === "true") { event.preventDefault(); set(trigger, false, "escape"); trigger.focus(); }
|
|
1165
|
+
}
|
|
1166
|
+
triggers.forEach((trigger) => {
|
|
1167
|
+
const expanded = trigger.getAttribute("aria-expanded") === "true";
|
|
1168
|
+
set(trigger, expanded, "init");
|
|
1169
|
+
});
|
|
1170
|
+
element.addEventListener("click", click);
|
|
1171
|
+
element.addEventListener("keydown", keydown);
|
|
1172
|
+
return { set, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function createColumnToggle(element) {
|
|
1176
|
+
const table = element.querySelector("table, [role='grid']");
|
|
1177
|
+
const toggles = [...element.querySelectorAll("[data-g-toggle-column]")];
|
|
1178
|
+
if (!table || !toggles.length) return null;
|
|
1179
|
+
function set(key, visible, source) {
|
|
1180
|
+
const active = toggles.filter((toggle) => toggle.dataset.gToggleColumn === key ? visible : toggle.checked);
|
|
1181
|
+
if (!visible && active.length === 0 && element.dataset.gAllowEmptyColumns !== "true") {
|
|
1182
|
+
if (source) source.checked = true;
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
table.querySelectorAll(`[data-g-column="${CSS.escape(key)}"]`).forEach((cell) => {
|
|
1186
|
+
cell.hidden = !visible;
|
|
1187
|
+
cell.setAttribute("aria-hidden", String(!visible));
|
|
1188
|
+
});
|
|
1189
|
+
element.querySelectorAll(`[data-g-toggle-column="${CSS.escape(key)}"]`).forEach((toggle) => { toggle.checked = visible; });
|
|
1190
|
+
emit(element, "columnchange", { key, visible });
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
function change(event) { if (event.target.matches("[data-g-toggle-column]")) set(event.target.dataset.gToggleColumn, event.target.checked, event.target); }
|
|
1194
|
+
toggles.forEach((toggle) => set(toggle.dataset.gToggleColumn, toggle.checked, toggle));
|
|
1195
|
+
element.addEventListener("change", change);
|
|
1196
|
+
return { set, destroy: () => element.removeEventListener("change", change) };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function createDataFilter(element) {
|
|
1200
|
+
const input = element.querySelector("[data-g-filter-input]") || (element.matches("input") ? element : null);
|
|
1201
|
+
const scope = targetById(element.dataset.gFilterTarget) || element;
|
|
1202
|
+
const items = () => [...scope.querySelectorAll("[data-g-filter-item]")];
|
|
1203
|
+
const empty = scope.querySelector("[data-g-filter-empty]");
|
|
1204
|
+
const output = element.querySelector("[data-g-filter-count]") || scope.querySelector("[data-g-filter-count]");
|
|
1205
|
+
if (!input || !items().length) return null;
|
|
1206
|
+
const normalize = (value) => String(value).normalize("NFKD").toLocaleLowerCase().trim();
|
|
1207
|
+
function filter(value = input.value) {
|
|
1208
|
+
const terms = normalize(value).split(/\s+/).filter(Boolean);
|
|
1209
|
+
let visible = 0;
|
|
1210
|
+
items().forEach((item) => {
|
|
1211
|
+
const haystack = normalize(item.dataset.gFilterText || item.textContent);
|
|
1212
|
+
const match = terms.every((term) => haystack.includes(term));
|
|
1213
|
+
item.hidden = !match;
|
|
1214
|
+
const related = item.id
|
|
1215
|
+
? [...scope.querySelectorAll(`[data-g-detail-row="${CSS.escape(item.id)}"], [data-g-parent="${CSS.escape(item.id)}"]`)]
|
|
1216
|
+
: [];
|
|
1217
|
+
related.forEach((row) => {
|
|
1218
|
+
if (!match && !row.hasAttribute("data-g-filter-previous-hidden")) {
|
|
1219
|
+
row.dataset.gFilterPreviousHidden = String(row.hidden);
|
|
1220
|
+
row.hidden = true;
|
|
1221
|
+
} else if (match && row.hasAttribute("data-g-filter-previous-hidden")) {
|
|
1222
|
+
row.hidden = row.dataset.gFilterPreviousHidden === "true";
|
|
1223
|
+
delete row.dataset.gFilterPreviousHidden;
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
if (match) visible += 1;
|
|
1227
|
+
});
|
|
1228
|
+
if (empty) empty.hidden = visible !== 0;
|
|
1229
|
+
if (output) output.textContent = String(visible);
|
|
1230
|
+
emit(element, "filter", { value, terms, count: visible });
|
|
1231
|
+
return visible;
|
|
1232
|
+
}
|
|
1233
|
+
const inputHandler = () => filter();
|
|
1234
|
+
input.addEventListener("input", inputHandler);
|
|
1235
|
+
filter();
|
|
1236
|
+
return { filter, clear: () => { input.value = ""; filter(); }, destroy: () => input.removeEventListener("input", inputHandler) };
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function createDataView(element) {
|
|
1240
|
+
const controls = [...element.querySelectorAll("[data-g-view]")];
|
|
1241
|
+
const panels = [...element.querySelectorAll("[data-g-view-panel]")];
|
|
1242
|
+
if (!controls.length) return null;
|
|
1243
|
+
function select(view, focus = false) {
|
|
1244
|
+
element.dataset.gCurrentView = view;
|
|
1245
|
+
controls.forEach((control) => {
|
|
1246
|
+
const active = control.dataset.gView === view;
|
|
1247
|
+
control.setAttribute("aria-pressed", String(active));
|
|
1248
|
+
control.tabIndex = active ? 0 : -1;
|
|
1249
|
+
if (active && focus) control.focus();
|
|
1250
|
+
});
|
|
1251
|
+
panels.forEach((panel) => { panel.hidden = panel.dataset.gViewPanel !== view; });
|
|
1252
|
+
emit(element, "viewchange", { view });
|
|
1253
|
+
}
|
|
1254
|
+
function click(event) { const control = event.target.closest("[data-g-view]"); if (control) select(control.dataset.gView); }
|
|
1255
|
+
function keydown(event) {
|
|
1256
|
+
const current = event.target.closest("[data-g-view]");
|
|
1257
|
+
if (!current) return;
|
|
1258
|
+
const index = controls.indexOf(current);
|
|
1259
|
+
let next;
|
|
1260
|
+
if (event.key === "ArrowRight" || event.key === "ArrowDown") next = controls[(index + 1) % controls.length];
|
|
1261
|
+
else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = controls[(index - 1 + controls.length) % controls.length];
|
|
1262
|
+
else if (event.key === "Home") next = controls[0];
|
|
1263
|
+
else if (event.key === "End") next = controls.at(-1);
|
|
1264
|
+
else return;
|
|
1265
|
+
event.preventDefault();
|
|
1266
|
+
select(next.dataset.gView, true);
|
|
1267
|
+
}
|
|
1268
|
+
element.addEventListener("click", click);
|
|
1269
|
+
element.addEventListener("keydown", keydown);
|
|
1270
|
+
select(element.dataset.gCurrentView || controls.find((control) => control.getAttribute("aria-pressed") === "true")?.dataset.gView || controls[0].dataset.gView);
|
|
1271
|
+
return { select, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function createToast(element) {
|
|
1275
|
+
let timer;
|
|
1276
|
+
let remaining = Number(element.dataset.gTimeout || 5000);
|
|
1277
|
+
let startedAt;
|
|
1278
|
+
const pauses = new Set();
|
|
1279
|
+
function dismiss(reason = "timeout") { clearTimeout(timer); if (emit(element, "dismiss", { reason })) element.remove(); }
|
|
1280
|
+
function schedule() { clearTimeout(timer); if (pauses.size || remaining <= 0 || element.dataset.gPersistent === "true") return; startedAt = Date.now(); timer = setTimeout(() => dismiss(), remaining); }
|
|
1281
|
+
function pause(reason = "api") { if (!pauses.size && startedAt) remaining = Math.max(0, remaining - (Date.now() - startedAt)); startedAt = null; pauses.add(reason); clearTimeout(timer); }
|
|
1282
|
+
function resume(reason = "api") { pauses.delete(reason); schedule(); }
|
|
1283
|
+
const pointerEnter = () => pause("pointer");
|
|
1284
|
+
const pointerLeave = () => resume("pointer");
|
|
1285
|
+
const focusIn = () => pause("focus");
|
|
1286
|
+
const focusOut = (event) => { if (!element.contains(event.relatedTarget)) resume("focus"); };
|
|
1287
|
+
element.addEventListener("pointerenter", pointerEnter);
|
|
1288
|
+
element.addEventListener("pointerleave", pointerLeave);
|
|
1289
|
+
element.addEventListener("focusin", focusIn);
|
|
1290
|
+
element.addEventListener("focusout", focusOut);
|
|
1291
|
+
schedule();
|
|
1292
|
+
return { dismiss, pause, resume, destroy: () => { clearTimeout(timer); element.removeEventListener("pointerenter", pointerEnter); element.removeEventListener("pointerleave", pointerLeave); element.removeEventListener("focusin", focusIn); element.removeEventListener("focusout", focusOut); } };
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function createCopy(element) {
|
|
1296
|
+
async function copy() {
|
|
1297
|
+
const target = targetById(element.dataset.gCopy);
|
|
1298
|
+
const value = element.dataset.gCopyValue ?? target?.value ?? target?.textContent ?? "";
|
|
1299
|
+
try {
|
|
1300
|
+
const text = String(value);
|
|
1301
|
+
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text);
|
|
1302
|
+
else {
|
|
1303
|
+
const fallback = document.createElement("textarea");
|
|
1304
|
+
fallback.value = text;
|
|
1305
|
+
fallback.setAttribute("readonly", "");
|
|
1306
|
+
fallback.style.cssText = "position:fixed;opacity:0;pointer-events:none";
|
|
1307
|
+
document.body.append(fallback);
|
|
1308
|
+
fallback.select();
|
|
1309
|
+
const copied = document.execCommand("copy");
|
|
1310
|
+
fallback.remove();
|
|
1311
|
+
if (!copied) throw new Error("Clipboard API is unavailable.");
|
|
1312
|
+
}
|
|
1313
|
+
element.dataset.gCopyState = "success";
|
|
1314
|
+
emit(element, "copy", { value: text });
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
element.dataset.gCopyState = "error";
|
|
1317
|
+
emit(element, "error", { action: "copy", error });
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
element.addEventListener("click", copy);
|
|
1321
|
+
return { copy, destroy: () => element.removeEventListener("click", copy) };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function createFullscreen(element) {
|
|
1325
|
+
const target = targetById(element.dataset.gFullscreen) || document.documentElement;
|
|
1326
|
+
async function toggle() {
|
|
1327
|
+
try {
|
|
1328
|
+
if (document.fullscreenElement) await document.exitFullscreen?.();
|
|
1329
|
+
else if (target.requestFullscreen) await target.requestFullscreen();
|
|
1330
|
+
else throw new Error("Fullscreen API is unavailable.");
|
|
1331
|
+
} catch (error) { emit(element, "error", { action: "fullscreen", error }); }
|
|
1332
|
+
}
|
|
1333
|
+
function change() {
|
|
1334
|
+
const active = document.fullscreenElement === target;
|
|
1335
|
+
element.setAttribute("aria-pressed", String(active));
|
|
1336
|
+
emit(element, "fullscreenchange", { active, target });
|
|
1337
|
+
}
|
|
1338
|
+
element.setAttribute("aria-pressed", String(document.fullscreenElement === target));
|
|
1339
|
+
element.addEventListener("click", toggle);
|
|
1340
|
+
document.addEventListener("fullscreenchange", change);
|
|
1341
|
+
return { toggle, destroy: () => { element.removeEventListener("click", toggle); document.removeEventListener("fullscreenchange", change); } };
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function createScrollTop(element) {
|
|
1345
|
+
const threshold = Number(element.dataset.gScrollThreshold || 320);
|
|
1346
|
+
function update() { element.hidden = window.scrollY < threshold; }
|
|
1347
|
+
function scroll() { window.scrollTo({ top: 0, behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" }); }
|
|
1348
|
+
element.addEventListener("click", scroll);
|
|
1349
|
+
window.addEventListener("scroll", update, { passive: true });
|
|
1350
|
+
update();
|
|
1351
|
+
return { update, scroll, destroy: () => { element.removeEventListener("click", scroll); window.removeEventListener("scroll", update); } };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function createNavToggle(element) {
|
|
1355
|
+
const target = targetById(element.dataset.gNavToggle);
|
|
1356
|
+
if (!target) return null;
|
|
1357
|
+
let closeTimer;
|
|
1358
|
+
if (!target.hasAttribute("data-g-nav-open")) target.dataset.gNavOpen = String(!target.hidden || target.classList.contains("is-open"));
|
|
1359
|
+
const isOpen = () => target.dataset.gNavOpen === "true";
|
|
1360
|
+
const syncExpanded = (value) => document.querySelectorAll("[data-g-nav-toggle]").forEach((trigger) => {
|
|
1361
|
+
if (trigger.dataset.gNavToggle?.replace(/^#/, "") === target.id) trigger.setAttribute("aria-expanded", String(value));
|
|
1362
|
+
});
|
|
1363
|
+
element.setAttribute("aria-controls", target.id);
|
|
1364
|
+
syncExpanded(isOpen());
|
|
1365
|
+
target.setAttribute("aria-hidden", String(!isOpen()));
|
|
1366
|
+
|
|
1367
|
+
function show() {
|
|
1368
|
+
if (isOpen() || !emit(target, "beforeopen", { trigger: element })) return;
|
|
1369
|
+
clearTimeout(closeTimer);
|
|
1370
|
+
target.dataset.gNavOpen = "true";
|
|
1371
|
+
target.hidden = false;
|
|
1372
|
+
target.setAttribute("aria-hidden", "false");
|
|
1373
|
+
syncExpanded(true);
|
|
1374
|
+
requestAnimationFrame(() => target.classList.add("is-open"));
|
|
1375
|
+
document.addEventListener("keydown", keydown);
|
|
1376
|
+
if (element.dataset.gDismissOutside === "true") document.addEventListener("pointerdown", outside, true);
|
|
1377
|
+
emit(target, "open", { trigger: element });
|
|
1378
|
+
}
|
|
1379
|
+
function hide(reason = "api") {
|
|
1380
|
+
if (!isOpen() || !emit(target, "beforeclose", { reason, trigger: element })) return;
|
|
1381
|
+
target.dataset.gNavOpen = "false";
|
|
1382
|
+
target.classList.remove("is-open");
|
|
1383
|
+
target.setAttribute("aria-hidden", "true");
|
|
1384
|
+
syncExpanded(false);
|
|
1385
|
+
document.removeEventListener("keydown", keydown);
|
|
1386
|
+
document.removeEventListener("pointerdown", outside, true);
|
|
1387
|
+
closeTimer = window.setTimeout(() => { if (!isOpen()) target.hidden = true; }, 300);
|
|
1388
|
+
if (reason === "escape") element.focus({ preventScroll: true });
|
|
1389
|
+
emit(target, "close", { reason, trigger: element });
|
|
1390
|
+
}
|
|
1391
|
+
function toggle() { isOpen() ? hide("toggle") : show(); }
|
|
1392
|
+
function keydown(event) { if (event.key === "Escape") { event.preventDefault(); hide("escape"); } }
|
|
1393
|
+
function outside(event) { if (!target.contains(event.target) && !element.contains(event.target)) hide("outside"); }
|
|
1394
|
+
function targetClose() {
|
|
1395
|
+
document.removeEventListener("keydown", keydown);
|
|
1396
|
+
document.removeEventListener("pointerdown", outside, true);
|
|
1397
|
+
}
|
|
1398
|
+
element.addEventListener("click", toggle);
|
|
1399
|
+
target.addEventListener("gardener:close", targetClose);
|
|
1400
|
+
return { open: show, close: hide, toggle, isOpen, destroy: () => { clearTimeout(closeTimer); element.removeEventListener("click", toggle); target.removeEventListener("gardener:close", targetClose); targetClose(); } };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function createRovingNav(element) {
|
|
1404
|
+
const selector = ".g-nav-item, .g-nav-rail-item, .g-bottom-nav-item, .g-mobile-tab-item, .g-command-nav-item, [role='menuitem']";
|
|
1405
|
+
const items = () => [...element.querySelectorAll(selector)].filter((item) => !item.disabled && item.getAttribute("aria-disabled") !== "true" && !item.hidden);
|
|
1406
|
+
const vertical = () => element.getAttribute("aria-orientation") === "vertical" || element.dataset.gOrientation === "vertical";
|
|
1407
|
+
function select(item, focus = false) {
|
|
1408
|
+
if (!item) return;
|
|
1409
|
+
items().forEach((node) => { node.tabIndex = node === item ? 0 : -1; });
|
|
1410
|
+
if (element.dataset.gSelection === "true") {
|
|
1411
|
+
items().forEach((node) => node.setAttribute("aria-selected", String(node === item)));
|
|
1412
|
+
emit(element, "change", { item });
|
|
1413
|
+
}
|
|
1414
|
+
if (focus) item.focus();
|
|
1415
|
+
}
|
|
1416
|
+
function click(event) { const item = event.target.closest(selector); if (item && element.contains(item)) select(item); }
|
|
1417
|
+
function keydown(event) {
|
|
1418
|
+
const current = event.target.closest(selector);
|
|
1419
|
+
if (!current) return;
|
|
1420
|
+
const nodes = items();
|
|
1421
|
+
const index = nodes.indexOf(current);
|
|
1422
|
+
const rtl = getComputedStyle(element).direction === "rtl";
|
|
1423
|
+
const forward = vertical() ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
|
|
1424
|
+
const backward = vertical() ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
|
|
1425
|
+
let next;
|
|
1426
|
+
if (event.key === forward) next = nodes[(index + 1 + nodes.length) % nodes.length];
|
|
1427
|
+
else if (event.key === backward) next = nodes[(index - 1 + nodes.length) % nodes.length];
|
|
1428
|
+
else if (event.key === "Home") next = nodes[0];
|
|
1429
|
+
else if (event.key === "End") next = nodes.at(-1);
|
|
1430
|
+
else return;
|
|
1431
|
+
event.preventDefault();
|
|
1432
|
+
select(next, true);
|
|
1433
|
+
}
|
|
1434
|
+
const initial = items().find((item) => item.matches("[aria-current], [aria-selected='true'], .is-active")) || items()[0];
|
|
1435
|
+
select(initial);
|
|
1436
|
+
element.addEventListener("click", click);
|
|
1437
|
+
element.addEventListener("keydown", keydown);
|
|
1438
|
+
return { select, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function createContextMenu(element) {
|
|
1442
|
+
const target = element.querySelector("[data-g-context-target]") || element;
|
|
1443
|
+
const menu = element.querySelector("[data-g-context-menu-panel]") || element.querySelector(".g-context-menu");
|
|
1444
|
+
if (!menu || menu === target) return null;
|
|
1445
|
+
let open = !menu.hidden;
|
|
1446
|
+
let previousFocus = null;
|
|
1447
|
+
const items = () => visibleFocusable(menu).filter((item) => item.getAttribute("aria-disabled") !== "true");
|
|
1448
|
+
menu.setAttribute("role", menu.getAttribute("role") || "menu");
|
|
1449
|
+
menu.hidden = true;
|
|
1450
|
+
|
|
1451
|
+
function position(x, y) {
|
|
1452
|
+
menu.style.inset = "auto";
|
|
1453
|
+
menu.style.left = `${Math.max(4, x)}px`;
|
|
1454
|
+
menu.style.top = `${Math.max(4, y)}px`;
|
|
1455
|
+
const rect = menu.getBoundingClientRect();
|
|
1456
|
+
menu.style.left = `${Math.max(4, Math.min(x, window.innerWidth - rect.width - 4))}px`;
|
|
1457
|
+
menu.style.top = `${Math.max(4, Math.min(y, window.innerHeight - rect.height - 4))}px`;
|
|
1458
|
+
}
|
|
1459
|
+
function show(x, y, source = target) {
|
|
1460
|
+
if (!emit(element, "beforeopen", { source })) return;
|
|
1461
|
+
open = true;
|
|
1462
|
+
previousFocus = document.activeElement;
|
|
1463
|
+
menu.hidden = false;
|
|
1464
|
+
menu.setAttribute("aria-hidden", "false");
|
|
1465
|
+
position(x, y);
|
|
1466
|
+
document.addEventListener("pointerdown", outside, true);
|
|
1467
|
+
document.addEventListener("keydown", keydown);
|
|
1468
|
+
items()[0]?.focus();
|
|
1469
|
+
emit(element, "open", { source, x, y });
|
|
1470
|
+
}
|
|
1471
|
+
function hide(reason = "api", restore = false) {
|
|
1472
|
+
if (!open) return;
|
|
1473
|
+
open = false;
|
|
1474
|
+
menu.hidden = true;
|
|
1475
|
+
menu.setAttribute("aria-hidden", "true");
|
|
1476
|
+
document.removeEventListener("pointerdown", outside, true);
|
|
1477
|
+
document.removeEventListener("keydown", keydown);
|
|
1478
|
+
if (restore) previousFocus?.focus?.({ preventScroll: true });
|
|
1479
|
+
emit(element, "close", { reason });
|
|
1480
|
+
}
|
|
1481
|
+
function context(event) { event.preventDefault(); show(event.clientX, event.clientY, event.target); }
|
|
1482
|
+
function targetKeydown(event) {
|
|
1483
|
+
if ((event.shiftKey && event.key === "F10") || event.key === "ContextMenu") {
|
|
1484
|
+
event.preventDefault();
|
|
1485
|
+
const rect = target.getBoundingClientRect();
|
|
1486
|
+
show(rect.left + 8, rect.top + 8, target);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function outside(event) { if (!menu.contains(event.target)) hide("outside"); }
|
|
1490
|
+
function keydown(event) {
|
|
1491
|
+
const nodes = items();
|
|
1492
|
+
const index = nodes.indexOf(document.activeElement);
|
|
1493
|
+
if (event.key === "Escape") { event.preventDefault(); hide("escape", true); }
|
|
1494
|
+
else if (event.key === "ArrowDown") { event.preventDefault(); (nodes[index + 1] || nodes[0])?.focus(); }
|
|
1495
|
+
else if (event.key === "ArrowUp") { event.preventDefault(); (nodes[index - 1] || nodes.at(-1))?.focus(); }
|
|
1496
|
+
else if (event.key === "Home") { event.preventDefault(); nodes[0]?.focus(); }
|
|
1497
|
+
else if (event.key === "End") { event.preventDefault(); nodes.at(-1)?.focus(); }
|
|
1498
|
+
}
|
|
1499
|
+
function menuClick(event) { if (event.target.closest("[role='menuitem'], .g-menu-item")) hide("select"); }
|
|
1500
|
+
target.addEventListener("contextmenu", context);
|
|
1501
|
+
target.addEventListener("keydown", targetKeydown);
|
|
1502
|
+
menu.addEventListener("click", menuClick);
|
|
1503
|
+
return { open: show, close: hide, isOpen: () => open, destroy: () => { target.removeEventListener("contextmenu", context); target.removeEventListener("keydown", targetKeydown); menu.removeEventListener("click", menuClick); document.removeEventListener("pointerdown", outside, true); document.removeEventListener("keydown", keydown); } };
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function createScrollspy(element) {
|
|
1507
|
+
const links = [...element.querySelectorAll("a[href^='#']")];
|
|
1508
|
+
const entries = links.map((link) => ({ link, section: targetById(link.getAttribute("href")) })).filter((entry) => entry.section);
|
|
1509
|
+
if (!entries.length) return null;
|
|
1510
|
+
const offset = Number(element.dataset.gScrollspyOffset || 96);
|
|
1511
|
+
const scrollRoot = targetById(element.dataset.gScrollspyRoot) || window;
|
|
1512
|
+
let current = null;
|
|
1513
|
+
let frame;
|
|
1514
|
+
function update() {
|
|
1515
|
+
frame = null;
|
|
1516
|
+
let active = entries[0];
|
|
1517
|
+
const rootTop = scrollRoot === window ? 0 : scrollRoot.getBoundingClientRect().top;
|
|
1518
|
+
for (const entry of entries) if (entry.section.getBoundingClientRect().top <= rootTop + offset) active = entry;
|
|
1519
|
+
const atEnd = scrollRoot === window
|
|
1520
|
+
? window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 1
|
|
1521
|
+
: scrollRoot.scrollTop + scrollRoot.clientHeight >= scrollRoot.scrollHeight - 1;
|
|
1522
|
+
if (atEnd) active = entries.at(-1);
|
|
1523
|
+
entries.forEach((entry) => {
|
|
1524
|
+
if (entry === active) entry.link.setAttribute("aria-current", "location");
|
|
1525
|
+
else entry.link.removeAttribute("aria-current");
|
|
1526
|
+
});
|
|
1527
|
+
if (current !== active) {
|
|
1528
|
+
current = active;
|
|
1529
|
+
emit(element, "change", { link: active.link, section: active.section });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function schedule() { if (!frame) frame = requestAnimationFrame(update); }
|
|
1533
|
+
scrollRoot.addEventListener("scroll", schedule, { passive: true });
|
|
1534
|
+
window.addEventListener("resize", schedule);
|
|
1535
|
+
update();
|
|
1536
|
+
return { update, destroy: () => { if (frame) cancelAnimationFrame(frame); scrollRoot.removeEventListener("scroll", schedule); window.removeEventListener("resize", schedule); } };
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function createJumpNav(element) {
|
|
1540
|
+
const select = element.matches("select") ? element : element.querySelector("select");
|
|
1541
|
+
if (!select) return null;
|
|
1542
|
+
function jump() {
|
|
1543
|
+
const target = targetById(select.value);
|
|
1544
|
+
if (!target) return;
|
|
1545
|
+
target.scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "start" });
|
|
1546
|
+
if (target.id && history.replaceState) history.replaceState(null, "", `#${target.id}`);
|
|
1547
|
+
emit(element, "change", { target, value: select.value });
|
|
1548
|
+
}
|
|
1549
|
+
select.addEventListener("change", jump);
|
|
1550
|
+
return { jump, destroy: () => select.removeEventListener("change", jump) };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function createDropzone(element) {
|
|
1554
|
+
let depth = 0;
|
|
1555
|
+
const input = element.querySelector("input[type='file']");
|
|
1556
|
+
const nativeLabel = element.matches("label") && Boolean(input);
|
|
1557
|
+
function enter(event) { event.preventDefault(); depth += 1; element.classList.add("is-dragging"); }
|
|
1558
|
+
function over(event) { event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = "copy"; }
|
|
1559
|
+
function leave(event) { event.preventDefault(); depth = Math.max(0, depth - 1); if (!depth) element.classList.remove("is-dragging"); }
|
|
1560
|
+
function drop(event) {
|
|
1561
|
+
event.preventDefault();
|
|
1562
|
+
depth = 0;
|
|
1563
|
+
element.classList.remove("is-dragging");
|
|
1564
|
+
const files = [...(event.dataTransfer?.files || [])];
|
|
1565
|
+
emit(element, "drop", { files, dataTransfer: event.dataTransfer });
|
|
1566
|
+
}
|
|
1567
|
+
function choose() { const files = [...(input?.files || [])]; emit(element, "files", { files, input }); }
|
|
1568
|
+
function activate(event) {
|
|
1569
|
+
if (!input || event.target === input || event.target.closest("button, a, input, select, textarea")) return;
|
|
1570
|
+
if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") return;
|
|
1571
|
+
event.preventDefault();
|
|
1572
|
+
input.click();
|
|
1573
|
+
}
|
|
1574
|
+
if (input) {
|
|
1575
|
+
if (!nativeLabel && !element.hasAttribute("role")) element.setAttribute("role", "button");
|
|
1576
|
+
if (!nativeLabel && !element.hasAttribute("tabindex")) element.tabIndex = 0;
|
|
1577
|
+
input.addEventListener("change", choose);
|
|
1578
|
+
if (!nativeLabel) {
|
|
1579
|
+
element.addEventListener("click", activate);
|
|
1580
|
+
element.addEventListener("keydown", activate);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
element.addEventListener("dragenter", enter);
|
|
1584
|
+
element.addEventListener("dragover", over);
|
|
1585
|
+
element.addEventListener("dragleave", leave);
|
|
1586
|
+
element.addEventListener("drop", drop);
|
|
1587
|
+
return { destroy: () => { element.removeEventListener("dragenter", enter); element.removeEventListener("dragover", over); element.removeEventListener("dragleave", leave); element.removeEventListener("drop", drop); input?.removeEventListener("change", choose); element.removeEventListener("click", activate); element.removeEventListener("keydown", activate); } };
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function createCommandPalette(element) {
|
|
1591
|
+
let trigger = null;
|
|
1592
|
+
let open = !element.hidden;
|
|
1593
|
+
if (!element.id) element.id = uid("g-command-palette");
|
|
1594
|
+
const panel = element.querySelector("[role='dialog'], .g-command-palette") || element;
|
|
1595
|
+
const triggers = [...document.querySelectorAll("[data-g-command-trigger], [aria-controls]")].filter((button) => (button.dataset.gCommandTrigger || button.getAttribute("aria-controls"))?.replace(/^#/, "") === element.id);
|
|
1596
|
+
panel.setAttribute("role", "dialog");
|
|
1597
|
+
panel.setAttribute("aria-modal", "true");
|
|
1598
|
+
element.setAttribute("aria-hidden", String(!open));
|
|
1599
|
+
triggers.forEach((button) => {
|
|
1600
|
+
button.setAttribute("aria-haspopup", "dialog");
|
|
1601
|
+
button.setAttribute("aria-controls", element.id);
|
|
1602
|
+
button.setAttribute("aria-expanded", String(open));
|
|
1603
|
+
});
|
|
1604
|
+
function syncExpanded(value) { triggers.forEach((button) => button.setAttribute("aria-expanded", String(value))); }
|
|
1605
|
+
function show(source = document.activeElement) {
|
|
1606
|
+
if (open || !emit(element, "beforeopen", { source })) return;
|
|
1607
|
+
open = true;
|
|
1608
|
+
trigger = source;
|
|
1609
|
+
if (trigger instanceof HTMLElement && trigger.hasAttribute("data-g-command-trigger")) trigger.setAttribute("aria-expanded", "true");
|
|
1610
|
+
element.hidden = false;
|
|
1611
|
+
element.setAttribute("aria-hidden", "false");
|
|
1612
|
+
element.classList.add("is-open");
|
|
1613
|
+
syncExpanded(true);
|
|
1614
|
+
(visibleFocusable(panel)[0] || panel).focus();
|
|
1615
|
+
emit(element, "open", { source });
|
|
1616
|
+
}
|
|
1617
|
+
function hide(reason = "api") {
|
|
1618
|
+
if (!open || !emit(element, "beforeclose", { reason })) return;
|
|
1619
|
+
open = false;
|
|
1620
|
+
element.hidden = true;
|
|
1621
|
+
element.setAttribute("aria-hidden", "true");
|
|
1622
|
+
element.classList.remove("is-open");
|
|
1623
|
+
syncExpanded(false);
|
|
1624
|
+
if (trigger instanceof HTMLElement && trigger.hasAttribute("data-g-command-trigger")) trigger.setAttribute("aria-expanded", "false");
|
|
1625
|
+
trigger?.focus?.();
|
|
1626
|
+
emit(element, "close", { reason });
|
|
1627
|
+
}
|
|
1628
|
+
function keydown(event) {
|
|
1629
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLocaleLowerCase() === "k") { event.preventDefault(); open ? hide("shortcut") : show(document.activeElement); }
|
|
1630
|
+
else if (open && event.key === "Escape") { event.preventDefault(); hide("escape"); }
|
|
1631
|
+
else if (open && event.key === "Tab") {
|
|
1632
|
+
const items = visibleFocusable(panel);
|
|
1633
|
+
if (!items.length) { event.preventDefault(); panel.focus(); return; }
|
|
1634
|
+
const first = items[0];
|
|
1635
|
+
const last = items.at(-1);
|
|
1636
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
1637
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function backdrop(event) { if (event.target === element) hide("backdrop"); }
|
|
1641
|
+
if (open) { open = false; queueMicrotask(show); }
|
|
1642
|
+
document.addEventListener("keydown", keydown);
|
|
1643
|
+
element.addEventListener("pointerdown", backdrop);
|
|
1644
|
+
return { open: show, close: hide, toggle: (source) => open ? hide("toggle") : show(source), isOpen: () => open, destroy: () => { document.removeEventListener("keydown", keydown); element.removeEventListener("pointerdown", backdrop); open = false; element.hidden = true; element.setAttribute("aria-hidden", "true"); element.classList.remove("is-open"); syncExpanded(false); } };
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function createUploadManager(element) {
|
|
1648
|
+
const list = element.querySelector("[data-g-upload-list]") || element;
|
|
1649
|
+
const input = element.querySelector("input[type='file'][data-g-upload-input]");
|
|
1650
|
+
const template = targetById(element.dataset.gUploadTemplate);
|
|
1651
|
+
const items = () => [...list.querySelectorAll("[data-g-upload-item]")];
|
|
1652
|
+
function statusText(item) {
|
|
1653
|
+
const state = item.dataset.gStatus || "queued";
|
|
1654
|
+
const progress = Math.max(0, Math.min(100, Number(item.dataset.gProgress || 0)));
|
|
1655
|
+
return state === "success" ? "已完成" : state === "error" ? "上传失败" : state === "paused" ? "已暂停" : state === "uploading" ? `上传中 ${progress}%` : "等待上传";
|
|
1656
|
+
}
|
|
1657
|
+
function sync(reason = "api") {
|
|
1658
|
+
const current = items();
|
|
1659
|
+
current.forEach((item) => {
|
|
1660
|
+
const progress = Math.max(0, Math.min(100, Number(item.dataset.gProgress || 0)));
|
|
1661
|
+
item.style.setProperty("--g-progress", `${progress}%`);
|
|
1662
|
+
const bar = item.querySelector("[data-g-upload-progress]");
|
|
1663
|
+
if (bar) { bar.style.setProperty("--g-progress", `${progress}%`); bar.setAttribute("aria-valuenow", String(progress)); }
|
|
1664
|
+
const status = item.querySelector("[data-g-upload-status]");
|
|
1665
|
+
if (status) status.textContent = statusText(item);
|
|
1666
|
+
item.querySelectorAll("[data-g-upload-action='pause']").forEach((button) => { button.textContent = item.dataset.gStatus === "paused" ? "继续" : "暂停"; });
|
|
1667
|
+
});
|
|
1668
|
+
const counts = { total: current.length, success: current.filter((item) => item.dataset.gStatus === "success").length, error: current.filter((item) => item.dataset.gStatus === "error").length, active: current.filter((item) => ["queued", "uploading", "paused"].includes(item.dataset.gStatus || "queued")).length };
|
|
1669
|
+
Object.entries(counts).forEach(([name, value]) => element.querySelectorAll(`[data-g-upload-count="${name}"]`).forEach((node) => { node.textContent = String(value); }));
|
|
1670
|
+
emit(element, "uploadchange", { items: current, counts, reason });
|
|
1671
|
+
}
|
|
1672
|
+
function addFile(file) {
|
|
1673
|
+
let item;
|
|
1674
|
+
if (template?.content) { const fragment = template.content.cloneNode(true); item = fragment.querySelector("[data-g-upload-item]") || fragment.firstElementChild; list.append(fragment); }
|
|
1675
|
+
if (!item) { item = document.createElement("div"); item.className = "g-upload-item"; item.dataset.gUploadItem = ""; item.innerHTML = '<span class="g-file-icon">FILE</span><span class="g-file-main"><strong class="g-file-name"></strong><span class="g-file-meta"></span><span class="g-upload-status" data-g-upload-status></span></span><span class="g-upload-progress" data-g-upload-progress role="progressbar" aria-valuemin="0" aria-valuemax="100"><i class="g-upload-progress-bar"></i></span><span class="g-upload-actions"><button class="g-btn g-btn-ghost g-btn-sm" type="button" data-g-upload-action="remove">移除</button></span>'; list.append(item); }
|
|
1676
|
+
item.dataset.gStatus = "queued"; item.dataset.gProgress = "0"; item.querySelector(".g-file-name")?.replaceChildren(document.createTextNode(file.name));
|
|
1677
|
+
const meta = item.querySelector(".g-file-meta"); if (meta) meta.textContent = file.size ? `${Math.max(1, Math.round(file.size / 1024))} KB` : "等待计算";
|
|
1678
|
+
emit(element, "uploadadd", { file, item });
|
|
1679
|
+
}
|
|
1680
|
+
function action(item, name) {
|
|
1681
|
+
if (name === "remove") { item.remove(); sync("remove"); return; }
|
|
1682
|
+
if (name === "retry") { item.dataset.gStatus = "queued"; item.dataset.gProgress = "0"; }
|
|
1683
|
+
if (name === "pause") item.dataset.gStatus = item.dataset.gStatus === "paused" ? "uploading" : "paused";
|
|
1684
|
+
if (name === "advance" && item.dataset.gStatus !== "paused") { const progress = Math.min(100, Number(item.dataset.gProgress || 0) + 25); item.dataset.gProgress = String(progress); item.dataset.gStatus = progress >= 100 ? "success" : "uploading"; }
|
|
1685
|
+
sync(name);
|
|
1686
|
+
}
|
|
1687
|
+
function click(event) { const button = event.target.closest("[data-g-upload-action]"); const item = button?.closest("[data-g-upload-item]"); if (button && item) { event.preventDefault(); action(item, button.dataset.gUploadAction); } }
|
|
1688
|
+
function change() { [...(input?.files || [])].forEach(addFile); if (input) input.value = ""; sync("files"); }
|
|
1689
|
+
element.addEventListener("click", click); input?.addEventListener("change", change); sync("init");
|
|
1690
|
+
return { sync, addFile, items, destroy: () => { element.removeEventListener("click", click); input?.removeEventListener("change", change); } };
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function createFileBrowser(element) {
|
|
1694
|
+
const items = () => [...element.querySelectorAll("[data-g-file-item]")];
|
|
1695
|
+
const search = element.querySelector("[data-g-file-search]");
|
|
1696
|
+
let selected = items().find((item) => item.getAttribute("aria-selected") === "true") || null;
|
|
1697
|
+
function select(item, reason = "api") {
|
|
1698
|
+
items().forEach((entry) => { const active = entry === item; entry.setAttribute("aria-selected", String(active)); entry.tabIndex = active ? 0 : -1; });
|
|
1699
|
+
selected = item;
|
|
1700
|
+
element.querySelectorAll("[data-g-file-selection]").forEach((node) => { node.textContent = item?.dataset.gFileName || item?.querySelector(".g-file-name, .g-media-label")?.textContent || "未选择"; });
|
|
1701
|
+
emit(element, "fileselect", { item, value: item?.dataset.gValue, reason });
|
|
1702
|
+
}
|
|
1703
|
+
function filter() {
|
|
1704
|
+
const query = String(search?.value || "").normalize("NFKD").toLocaleLowerCase().trim();
|
|
1705
|
+
let visible = 0;
|
|
1706
|
+
items().forEach((item) => { const match = !query || String(item.dataset.gFilterText || item.dataset.gFileName || item.textContent).normalize("NFKD").toLocaleLowerCase().includes(query); item.hidden = !match; if (match) visible += 1; });
|
|
1707
|
+
element.querySelectorAll("[data-g-file-count]").forEach((node) => { node.textContent = String(visible); });
|
|
1708
|
+
emit(element, "filefilter", { query, visible });
|
|
1709
|
+
}
|
|
1710
|
+
function click(event) {
|
|
1711
|
+
const view = event.target.closest("[data-g-file-view]");
|
|
1712
|
+
if (view) { element.dataset.gView = view.dataset.gFileView; element.querySelectorAll("[data-g-file-view]").forEach((button) => button.setAttribute("aria-pressed", String(button === view))); emit(element, "fileview", { view: element.dataset.gView }); return; }
|
|
1713
|
+
const item = event.target.closest("[data-g-file-item]"); if (item) select(item, "pointer");
|
|
1714
|
+
}
|
|
1715
|
+
function keydown(event) {
|
|
1716
|
+
const item = event.target.closest("[data-g-file-item]"); if (!item || !["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Home", "End", "Enter", " "].includes(event.key)) return;
|
|
1717
|
+
const visible = items().filter((entry) => !entry.hidden); if (!visible.length) return; event.preventDefault();
|
|
1718
|
+
if (["Enter", " "].includes(event.key)) { select(item, "keyboard"); return; }
|
|
1719
|
+
const current = visible.indexOf(item); const forward = ["ArrowDown", "ArrowRight"].includes(event.key); const index = event.key === "Home" ? 0 : event.key === "End" ? visible.length - 1 : (current + (forward ? 1 : -1) + visible.length) % visible.length; visible[index].focus();
|
|
1720
|
+
}
|
|
1721
|
+
element.addEventListener("click", click); element.addEventListener("keydown", keydown); search?.addEventListener("input", filter);
|
|
1722
|
+
items().forEach((item, index) => {
|
|
1723
|
+
item.tabIndex = item === selected || (!selected && index === 0) ? 0 : -1;
|
|
1724
|
+
item.setAttribute("role", item.getAttribute("role") || "option");
|
|
1725
|
+
if (!item.hasAttribute("aria-selected")) item.setAttribute("aria-selected", "false");
|
|
1726
|
+
const collection = item.parentElement;
|
|
1727
|
+
if (collection) {
|
|
1728
|
+
if (!collection.hasAttribute("role")) collection.setAttribute("role", "listbox");
|
|
1729
|
+
if (!collection.hasAttribute("aria-label") && !collection.hasAttribute("aria-labelledby")) collection.setAttribute("aria-label", element.getAttribute("aria-label") || "文件列表");
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
if (selected) select(selected, "init"); filter();
|
|
1733
|
+
return { select, filter, selected: () => selected, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); search?.removeEventListener("input", filter); } };
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function createEditorShell(element) {
|
|
1737
|
+
const surface = element.querySelector("[data-g-editor-surface]");
|
|
1738
|
+
if (!surface) return null;
|
|
1739
|
+
const tools = [...element.querySelectorAll("[data-g-editor-command]")];
|
|
1740
|
+
function counts() {
|
|
1741
|
+
const text = String("value" in surface ? surface.value : surface.innerText || surface.textContent || "");
|
|
1742
|
+
const trimmed = text.trim(); const words = trimmed ? (trimmed.match(/[\p{L}\p{N}]+/gu) || []).length : 0;
|
|
1743
|
+
element.querySelectorAll("[data-g-editor-count='characters']").forEach((node) => { node.textContent = String(text.length); });
|
|
1744
|
+
element.querySelectorAll("[data-g-editor-count='words']").forEach((node) => { node.textContent = String(words); });
|
|
1745
|
+
emit(element, "editorchange", { text, characters: text.length, words });
|
|
1746
|
+
}
|
|
1747
|
+
function run(tool) {
|
|
1748
|
+
const command = tool.dataset.gEditorCommand; const value = tool.dataset.gEditorValue || null;
|
|
1749
|
+
surface.focus();
|
|
1750
|
+
if (command === "undo" || command === "redo" || surface.isContentEditable) document.execCommand?.(command, false, value);
|
|
1751
|
+
if (tool.hasAttribute("aria-pressed")) tool.setAttribute("aria-pressed", String(tool.getAttribute("aria-pressed") !== "true"));
|
|
1752
|
+
counts(); emit(element, "editorcommand", { command, value, tool });
|
|
1753
|
+
}
|
|
1754
|
+
function click(event) { const tool = event.target.closest("[data-g-editor-command]"); if (tool) { event.preventDefault(); run(tool); } }
|
|
1755
|
+
function keydown(event) { if ((event.ctrlKey || event.metaKey) && ["b", "i", "u"].includes(event.key.toLocaleLowerCase())) { event.preventDefault(); const map = { b: "bold", i: "italic", u: "underline" }; const tool = tools.find((item) => item.dataset.gEditorCommand === map[event.key.toLocaleLowerCase()]); if (tool) run(tool); } }
|
|
1756
|
+
element.addEventListener("click", click); surface.addEventListener("input", counts); surface.addEventListener("keydown", keydown); counts();
|
|
1757
|
+
return { run, counts, surface, destroy: () => { element.removeEventListener("click", click); surface.removeEventListener("input", counts); surface.removeEventListener("keydown", keydown); } };
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function createRevisionCompare(element) {
|
|
1761
|
+
const controls = [...element.querySelectorAll("[data-g-revision-view]")];
|
|
1762
|
+
function setView(view, reason = "api") { element.dataset.gRevisionView = view; controls.forEach((button) => button.setAttribute("aria-pressed", String(button.dataset.gRevisionView === view))); emit(element, "revisionview", { view, reason }); }
|
|
1763
|
+
function click(event) { const button = event.target.closest("[data-g-revision-view]"); if (button) { event.preventDefault(); setView(button.dataset.gRevisionView, "pointer"); } }
|
|
1764
|
+
element.addEventListener("click", click); setView(element.dataset.gRevisionView || controls.find((button) => button.getAttribute("aria-pressed") === "true")?.dataset.gRevisionView || "split", "init");
|
|
1765
|
+
return { setView, view: () => element.dataset.gRevisionView, destroy: () => element.removeEventListener("click", click) };
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function createAutosave(element) {
|
|
1769
|
+
const scope = targetById(element.dataset.gAutosaveScope) || element.closest("form, [data-g-editor-shell]") || element.parentElement;
|
|
1770
|
+
const delay = Math.max(150, Number(element.dataset.gAutosaveDelay || 900));
|
|
1771
|
+
let timer = 0; let saveTimer = 0;
|
|
1772
|
+
function setState(state, reason = "api") { element.dataset.gAutosaveState = state; const label = element.querySelector("[data-g-autosave-label]"); if (label) label.textContent = state === "dirty" ? "有未保存更改" : state === "saving" ? "正在保存…" : state === "error" ? "保存失败" : "已保存"; emit(element, "autosavestate", { state, reason }); }
|
|
1773
|
+
function save(reason = "api") { clearTimeout(timer); clearTimeout(saveTimer); setState("saving", reason); saveTimer = window.setTimeout(() => setState("saved", "complete"), 260); }
|
|
1774
|
+
function dirty() { clearTimeout(timer); setState("dirty", "input"); timer = window.setTimeout(() => save("delay"), delay); }
|
|
1775
|
+
function click(event) { if (event.target.closest("[data-g-autosave-now]")) { event.preventDefault(); save("button"); } }
|
|
1776
|
+
scope?.addEventListener("input", dirty); scope?.addEventListener("change", dirty); element.addEventListener("click", click); setState(element.dataset.gAutosaveState || "saved", "init");
|
|
1777
|
+
return { save, state: () => element.dataset.gAutosaveState, destroy: () => { clearTimeout(timer); clearTimeout(saveTimer); scope?.removeEventListener("input", dirty); scope?.removeEventListener("change", dirty); element.removeEventListener("click", click); } };
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function createQuantityStepper(element) {
|
|
1781
|
+
const input = element.querySelector("[data-g-quantity-input]") || element.querySelector("input[type='number']");
|
|
1782
|
+
const decrement = element.querySelector("[data-g-quantity-decrement]");
|
|
1783
|
+
const increment = element.querySelector("[data-g-quantity-increment]");
|
|
1784
|
+
if (!input) return null;
|
|
1785
|
+
const limits = () => ({ min: numeric(input.min, 0), max: input.max === "" ? Infinity : numeric(input.max, Infinity), step: Math.max(.000001, numeric(input.step, 1)) });
|
|
1786
|
+
function set(value, reason = "api") {
|
|
1787
|
+
const { min, max, step } = limits();
|
|
1788
|
+
const next = Math.min(max, Math.max(min, numeric(value, min)));
|
|
1789
|
+
const precision = Math.max(0, String(step).split(".")[1]?.length || 0);
|
|
1790
|
+
input.value = String(Number(next.toFixed(precision)));
|
|
1791
|
+
decrement?.toggleAttribute("disabled", next <= min);
|
|
1792
|
+
increment?.toggleAttribute("disabled", next >= max);
|
|
1793
|
+
element.dataset.gQuantityValue = input.value;
|
|
1794
|
+
emit(element, "quantitychange", { value: next, reason });
|
|
1795
|
+
return next;
|
|
1796
|
+
}
|
|
1797
|
+
function click(event) {
|
|
1798
|
+
if (event.target.closest("[data-g-quantity-decrement]")) set(numeric(input.value, 0) - limits().step, "decrement");
|
|
1799
|
+
if (event.target.closest("[data-g-quantity-increment]")) set(numeric(input.value, 0) + limits().step, "increment");
|
|
1800
|
+
}
|
|
1801
|
+
function change() { set(input.value, "input"); }
|
|
1802
|
+
element.addEventListener("click", click);
|
|
1803
|
+
input.addEventListener("change", change);
|
|
1804
|
+
set(input.value, "init");
|
|
1805
|
+
return { value: () => numeric(input.value, 0), set, increment: () => set(numeric(input.value, 0) + limits().step, "api"), decrement: () => set(numeric(input.value, 0) - limits().step, "api"), destroy: () => { element.removeEventListener("click", click); input.removeEventListener("change", change); } };
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
function createSkuSelector(element) {
|
|
1809
|
+
const groups = [...element.querySelectorAll("[data-g-sku-group]")];
|
|
1810
|
+
const output = element.querySelector("[data-g-sku-output]");
|
|
1811
|
+
const summary = element.querySelector("[data-g-sku-selection]");
|
|
1812
|
+
if (!groups.length) return null;
|
|
1813
|
+
function options(group) { return group ? [...group.querySelectorAll("[data-g-sku-option]")] : []; }
|
|
1814
|
+
function selection() {
|
|
1815
|
+
return Object.fromEntries(groups.map((group, index) => {
|
|
1816
|
+
const chosen = options(group).find((option) => option.getAttribute("aria-pressed") === "true");
|
|
1817
|
+
return [group.dataset.gSkuGroup || `option${index + 1}`, chosen?.dataset.gValue || chosen?.textContent.trim() || ""];
|
|
1818
|
+
}));
|
|
1819
|
+
}
|
|
1820
|
+
function sync(reason = "api") {
|
|
1821
|
+
const value = selection();
|
|
1822
|
+
const complete = Object.values(value).every(Boolean);
|
|
1823
|
+
element.classList.toggle("is-complete", complete);
|
|
1824
|
+
element.dataset.gSkuComplete = String(complete);
|
|
1825
|
+
if (output) output.value = JSON.stringify(value);
|
|
1826
|
+
if (summary) summary.textContent = Object.values(value).filter(Boolean).join(" / ") || element.dataset.gEmptyLabel || "请选择规格";
|
|
1827
|
+
emit(element, "skuchange", { value, complete, reason });
|
|
1828
|
+
return value;
|
|
1829
|
+
}
|
|
1830
|
+
function choose(option, reason = "pointer") {
|
|
1831
|
+
if (!option || option.disabled || option.getAttribute("aria-disabled") === "true") return;
|
|
1832
|
+
const group = option.closest("[data-g-sku-group]");
|
|
1833
|
+
options(group).forEach((item) => { item.setAttribute("aria-pressed", String(item === option)); item.tabIndex = item === option ? 0 : -1; });
|
|
1834
|
+
sync(reason);
|
|
1835
|
+
}
|
|
1836
|
+
function click(event) { const option = event.target.closest("[data-g-sku-option]"); if (option && element.contains(option)) choose(option); }
|
|
1837
|
+
function keydown(event) {
|
|
1838
|
+
const current = event.target.closest("[data-g-sku-option]");
|
|
1839
|
+
if (!current) return;
|
|
1840
|
+
if (["Enter", " "].includes(event.key)) { event.preventDefault(); choose(current, "keyboard"); return; }
|
|
1841
|
+
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
|
|
1842
|
+
event.preventDefault();
|
|
1843
|
+
const enabled = options(current.closest("[data-g-sku-group]")).filter((item) => !item.disabled && item.getAttribute("aria-disabled") !== "true");
|
|
1844
|
+
const index = enabled.indexOf(current);
|
|
1845
|
+
const next = event.key === "Home" ? enabled[0] : event.key === "End" ? enabled.at(-1) : ["ArrowRight", "ArrowDown"].includes(event.key) ? enabled[index + 1] || enabled[0] : enabled[index - 1] || enabled.at(-1);
|
|
1846
|
+
next?.focus(); choose(next, "keyboard");
|
|
1847
|
+
}
|
|
1848
|
+
groups.forEach((group) => { const items = options(group); const selected = items.find((item) => item.getAttribute("aria-pressed") === "true") || items.find((item) => !item.disabled && item.getAttribute("aria-disabled") !== "true"); items.forEach((item) => { item.setAttribute("aria-pressed", String(item === selected)); item.tabIndex = item === selected ? 0 : -1; }); });
|
|
1849
|
+
element.addEventListener("click", click); element.addEventListener("keydown", keydown); sync("init");
|
|
1850
|
+
return { value: selection, select: (groupName, value) => { const group = groups.find((item) => item.dataset.gSkuGroup === groupName); choose(options(group).find((item) => (item.dataset.gValue || item.textContent.trim()) === value), "api"); }, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function createCart(element) {
|
|
1854
|
+
const locale = element.dataset.gLocale || document.documentElement.lang || "zh-CN";
|
|
1855
|
+
const currency = element.dataset.gCurrency || "CNY";
|
|
1856
|
+
const formatter = new Intl.NumberFormat(locale, { style: "currency", currency });
|
|
1857
|
+
const number = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
|
1858
|
+
function update(reason = "api") {
|
|
1859
|
+
const items = [...element.querySelectorAll("[data-g-cart-item]")];
|
|
1860
|
+
let subtotal = 0; let count = 0;
|
|
1861
|
+
items.forEach((item) => {
|
|
1862
|
+
const input = item.querySelector("[data-g-quantity-input]") || item.querySelector("input[type='number']");
|
|
1863
|
+
const quantity = Math.max(0, number(input?.value || item.dataset.gQuantity || 1));
|
|
1864
|
+
const total = number(item.dataset.gUnitPrice) * quantity;
|
|
1865
|
+
subtotal += total; count += quantity;
|
|
1866
|
+
const line = item.querySelector("[data-g-line-total]");
|
|
1867
|
+
if (line) line.textContent = formatter.format(total / 100);
|
|
1868
|
+
});
|
|
1869
|
+
const discount = number(element.dataset.gDiscount);
|
|
1870
|
+
const shipping = number(element.dataset.gShipping);
|
|
1871
|
+
const tax = number(element.dataset.gTax);
|
|
1872
|
+
const total = Math.max(0, subtotal - discount + shipping + tax);
|
|
1873
|
+
const values = { "[data-g-cart-subtotal]": subtotal, "[data-g-cart-discount]": -discount, "[data-g-cart-shipping]": shipping, "[data-g-cart-tax]": tax, "[data-g-cart-total]": total };
|
|
1874
|
+
Object.entries(values).forEach(([selector, value]) => { element.querySelectorAll(selector).forEach((node) => { node.textContent = formatter.format(value / 100); }); });
|
|
1875
|
+
element.querySelectorAll("[data-g-cart-count]").forEach((node) => { node.textContent = String(count); });
|
|
1876
|
+
element.querySelector("[data-g-cart-empty]")?.toggleAttribute("hidden", items.length > 0);
|
|
1877
|
+
element.classList.toggle("is-empty", items.length === 0);
|
|
1878
|
+
emit(element, "cartchange", { subtotal, discount, shipping, tax, total, count, reason });
|
|
1879
|
+
return { subtotal, discount, shipping, tax, total, count };
|
|
1880
|
+
}
|
|
1881
|
+
function click(event) { const remove = event.target.closest("[data-g-cart-remove]"); if (remove) { const item = remove.closest("[data-g-cart-item]"); if (item && emit(item, "cartremove", { item })) { item.remove(); update("remove"); } } }
|
|
1882
|
+
function change(event) { if (event.target.matches("[data-g-quantity-input], input[type='number']")) update("quantity"); }
|
|
1883
|
+
function quantity(event) { if (element.contains(event.target)) update("quantity"); }
|
|
1884
|
+
element.addEventListener("click", click); element.addEventListener("change", change); element.addEventListener("gardener:quantitychange", quantity); update("init");
|
|
1885
|
+
return { update, summary: () => update("api"), destroy: () => { element.removeEventListener("click", click); element.removeEventListener("change", change); element.removeEventListener("gardener:quantitychange", quantity); } };
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function createCoupon(element) {
|
|
1889
|
+
const input = element.querySelector("[data-g-coupon-input]") || element.querySelector("input");
|
|
1890
|
+
const status = element.querySelector("[data-g-coupon-status]");
|
|
1891
|
+
if (!input) return null;
|
|
1892
|
+
const validCodes = (element.dataset.gValidCodes || "").split(",").map((code) => code.trim().toLocaleUpperCase()).filter(Boolean);
|
|
1893
|
+
function setState(state, code = "", reason = "api") {
|
|
1894
|
+
element.classList.toggle("is-applied", state === "applied"); element.classList.toggle("is-invalid", state === "invalid");
|
|
1895
|
+
element.dataset.gCouponState = state; input.setAttribute("aria-invalid", String(state === "invalid"));
|
|
1896
|
+
if (status) { status.textContent = state === "applied" ? element.dataset.gAppliedMessage || `优惠码 ${code} 已使用` : state === "invalid" ? element.dataset.gInvalidMessage || "优惠码无效或已过期" : element.dataset.gIdleMessage || ""; status.setAttribute("role", state === "invalid" ? "alert" : "status"); }
|
|
1897
|
+
emit(element, "couponchange", { state, code, reason });
|
|
1898
|
+
}
|
|
1899
|
+
function apply(reason = "button") { const code = input.value.trim().toLocaleUpperCase(); if (!code || (validCodes.length && !validCodes.includes(code))) setState("invalid", code, reason); else { input.value = code; setState("applied", code, reason); } }
|
|
1900
|
+
function clear() { input.value = ""; setState("idle", "", "clear"); input.focus(); }
|
|
1901
|
+
function click(event) { if (event.target.closest("[data-g-coupon-apply]")) { event.preventDefault(); apply(); } if (event.target.closest("[data-g-coupon-clear]")) { event.preventDefault(); clear(); } }
|
|
1902
|
+
function keydown(event) { if (event.key === "Enter") { event.preventDefault(); apply("keyboard"); } }
|
|
1903
|
+
element.addEventListener("click", click); input.addEventListener("keydown", keydown); setState("idle", "", "init");
|
|
1904
|
+
return { apply, clear, state: () => element.dataset.gCouponState, destroy: () => { element.removeEventListener("click", click); input.removeEventListener("keydown", keydown); } };
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function register(name, factory) {
|
|
1908
|
+
if (!name || typeof factory !== "function") throw new TypeError("Gardener.register requires a name and factory function.");
|
|
1909
|
+
registry.set(name, factory);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function createPullRefresh(element) {
|
|
1913
|
+
const content = element.querySelector(".g-mobile-pull-refresh-content") || element.firstElementChild;
|
|
1914
|
+
const indicator = element.querySelector("[data-g-pull-indicator]");
|
|
1915
|
+
const threshold = numeric(element.dataset.gPullThreshold, 72);
|
|
1916
|
+
let startY = 0; let distance = 0; let tracking = false; let refreshing = false;
|
|
1917
|
+
function paint(value) { distance = Math.max(0, Math.min(value, threshold * 1.5)); element.style.setProperty("--g-pull-distance", `${distance}px`); element.classList.toggle("is-pulling", tracking); if (indicator) indicator.textContent = distance >= threshold ? element.dataset.gReleaseLabel || "松开刷新" : element.dataset.gPullLabel || "下拉刷新"; }
|
|
1918
|
+
function begin(event) { if (refreshing || element.scrollTop > 0 || event.pointerType === "mouse" && event.button !== 0) return; startY = event.clientY; tracking = true; element.setPointerCapture?.(event.pointerId); }
|
|
1919
|
+
function move(event) { if (!tracking) return; const delta = event.clientY - startY; if (delta <= 0) return paint(0); if (delta > 8) event.preventDefault(); paint(Math.sqrt(delta) * 8); }
|
|
1920
|
+
function finish() { if (!refreshing) return; refreshing = false; element.classList.remove("is-refreshing"); element.setAttribute("aria-busy", "false"); paint(0); if (indicator) indicator.textContent = element.dataset.gPullLabel || "下拉刷新"; emit(element, "refreshcomplete"); }
|
|
1921
|
+
function refresh(reason = "gesture") { if (refreshing || !emit(element, "refresh", { reason, complete: finish })) return; refreshing = true; tracking = false; element.classList.add("is-refreshing"); element.setAttribute("aria-busy", "true"); paint(threshold / 2); if (indicator) indicator.textContent = element.dataset.gRefreshingLabel || "正在刷新…"; const timeout = numeric(element.dataset.gRefreshTimeout, 0); if (timeout > 0) window.setTimeout(finish, timeout); }
|
|
1922
|
+
function end() { if (!tracking) return; tracking = false; element.classList.remove("is-pulling"); distance >= threshold ? refresh() : paint(0); }
|
|
1923
|
+
function click(event) { if (event.target.closest("[data-g-refresh]")) refresh("button"); }
|
|
1924
|
+
element.addEventListener("pointerdown", begin); element.addEventListener("pointermove", move, { passive: false }); element.addEventListener("pointerup", end); element.addEventListener("pointercancel", end); element.addEventListener("click", click);
|
|
1925
|
+
return { refresh, complete: finish, isRefreshing: () => refreshing, destroy: () => { element.removeEventListener("pointerdown", begin); element.removeEventListener("pointermove", move); element.removeEventListener("pointerup", end); element.removeEventListener("pointercancel", end); element.removeEventListener("click", click); content?.style.removeProperty("transform"); element.style.removeProperty("--g-pull-distance"); } };
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function createInfiniteLoad(element) {
|
|
1929
|
+
const sentinel = element.querySelector("[data-g-infinite-sentinel]") || element;
|
|
1930
|
+
let loading = false; let done = false; let observer;
|
|
1931
|
+
function complete(options = {}) { loading = false; done = Boolean(options.done); element.classList.toggle("is-loading", false); element.classList.toggle("is-done", done); element.setAttribute("aria-busy", "false"); if (done) observer?.disconnect(); emit(element, "loadcomplete", { done }); }
|
|
1932
|
+
function load(reason = "observer") { if (loading || done || !emit(element, "loadmore", { reason, complete })) return; loading = true; element.classList.add("is-loading"); element.setAttribute("aria-busy", "true"); }
|
|
1933
|
+
function click(event) { if (event.target.closest("[data-g-infinite-load-more]")) load("button"); }
|
|
1934
|
+
if (typeof IntersectionObserver !== "undefined") { observer = new IntersectionObserver((entries) => { if (entries.some((entry) => entry.isIntersecting)) load(); }, { rootMargin: element.dataset.gRootMargin || "200px" }); observer.observe(sentinel); }
|
|
1935
|
+
element.addEventListener("click", click);
|
|
1936
|
+
return { load, complete, isLoading: () => loading, destroy: () => { observer?.disconnect(); element.removeEventListener("click", click); } };
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function createSwipeActions(element) {
|
|
1940
|
+
let startX = 0; let startY = 0; let tracking = false;
|
|
1941
|
+
function set(revealed, reason = "api") { element.classList.toggle("is-revealed", revealed); element.querySelector("[data-g-swipe-toggle]")?.setAttribute("aria-expanded", String(revealed)); emit(element, "swipechange", { revealed, reason }); }
|
|
1942
|
+
function begin(event) { if (event.pointerType === "mouse" && event.button !== 0) return; startX = event.clientX; startY = event.clientY; tracking = true; element.setPointerCapture?.(event.pointerId); }
|
|
1943
|
+
function end(event) { if (!tracking) return; tracking = false; const dx = event.clientX - startX; const dy = event.clientY - startY; if (Math.abs(dx) < 36 || Math.abs(dx) < Math.abs(dy)) return; const rtl = getComputedStyle(element).direction === "rtl"; set(rtl ? dx > 0 : dx < 0, "gesture"); }
|
|
1944
|
+
function click(event) { if (event.target.closest("[data-g-swipe-toggle]")) set(!element.classList.contains("is-revealed"), "button"); else if (event.target.closest("[data-g-swipe-close]")) set(false, "action"); }
|
|
1945
|
+
function keydown(event) { if (event.key === "Escape") set(false, "keyboard"); }
|
|
1946
|
+
element.addEventListener("pointerdown", begin); element.addEventListener("pointerup", end); element.addEventListener("pointercancel", () => { tracking = false; }); element.addEventListener("click", click); element.addEventListener("keydown", keydown);
|
|
1947
|
+
return { reveal: () => set(true), close: () => set(false), toggle: () => set(!element.classList.contains("is-revealed")), destroy: () => { element.removeEventListener("pointerdown", begin); element.removeEventListener("pointerup", end); element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function createWheelPicker(element) {
|
|
1951
|
+
const columns = [...element.querySelectorAll("[data-g-wheel-column]")]; const output = element.querySelector("[data-g-wheel-output]"); const timers = new WeakMap();
|
|
1952
|
+
function options(column) { return [...column.querySelectorAll("[data-g-wheel-option]")]; }
|
|
1953
|
+
function select(option, reason = "option") { const column = option.closest("[data-g-wheel-column]"); if (!column) return; options(column).forEach((item) => item.setAttribute("aria-selected", String(item === option))); option.scrollIntoView({ block: "center", behavior: reason === "scroll" ? "auto" : "smooth" }); const values = columns.map((item) => item.querySelector('[data-g-wheel-option][aria-selected="true"]')?.dataset.gValue || ""); if (output) output.value = JSON.stringify(values); emit(element, "wheelchange", { values, reason }); }
|
|
1954
|
+
function click(event) { const option = event.target.closest("[data-g-wheel-option]"); if (option) select(option); }
|
|
1955
|
+
function keydown(event) { const option = event.target.closest("[data-g-wheel-option]"); if (!option || !["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return; event.preventDefault(); const list = options(option.closest("[data-g-wheel-column]")); const index = list.indexOf(option); const next = event.key === "Home" ? list[0] : event.key === "End" ? list.at(-1) : list[index + (event.key === "ArrowDown" ? 1 : -1)] || option; next.focus(); select(next, "keyboard"); }
|
|
1956
|
+
function scroll(event) { const column = event.currentTarget; clearTimeout(timers.get(column)); timers.set(column, setTimeout(() => { const center = column.getBoundingClientRect().top + column.clientHeight / 2; const nearest = options(column).sort((a, b) => Math.abs(a.getBoundingClientRect().top + a.clientHeight / 2 - center) - Math.abs(b.getBoundingClientRect().top + b.clientHeight / 2 - center))[0]; if (nearest) select(nearest, "scroll"); }, 90)); }
|
|
1957
|
+
columns.forEach((column) => { column.addEventListener("scroll", scroll, { passive: true }); const selected = column.querySelector('[data-g-wheel-option][aria-selected="true"]') || options(column)[0]; if (selected) select(selected, "init"); }); element.addEventListener("click", click); element.addEventListener("keydown", keydown);
|
|
1958
|
+
return { select, values: () => columns.map((column) => column.querySelector('[data-g-wheel-option][aria-selected="true"]')?.dataset.gValue || ""), destroy: () => { columns.forEach((column) => { clearTimeout(timers.get(column)); column.removeEventListener("scroll", scroll); }); element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function createAIComposer(element) {
|
|
1962
|
+
const input = element.querySelector("[data-g-composer-input], .g-composer-input");
|
|
1963
|
+
const send = element.querySelector("[data-g-composer-send]");
|
|
1964
|
+
const stop = element.querySelector("[data-g-composer-stop]");
|
|
1965
|
+
const status = element.querySelector("[data-g-composer-status]");
|
|
1966
|
+
if (!input) return null;
|
|
1967
|
+
let streaming = element.classList.contains("is-streaming");
|
|
1968
|
+
function resize() { input.style.height = "auto"; input.style.height = `${Math.min(input.scrollHeight, Number(element.dataset.gMaxHeight || 224))}px`; }
|
|
1969
|
+
function sync() { const empty = !input.value.trim(); if (send) send.disabled = empty || streaming; if (stop) stop.hidden = !streaming; element.classList.toggle("is-empty", empty); if (status && !streaming) status.textContent = empty ? element.dataset.gEmptyLabel || "输入消息,Enter 发送,Shift+Enter 换行" : element.dataset.gReadyLabel || "已准备发送"; resize(); }
|
|
1970
|
+
function setStreaming(value, reason = "api") { streaming = Boolean(value); element.classList.toggle("is-streaming", streaming); element.setAttribute("aria-busy", String(streaming)); if (status) status.textContent = streaming ? element.dataset.gStreamingLabel || "正在生成;可停止" : element.dataset.gReadyLabel || "已准备发送"; sync(); emit(element, "composerstate", { streaming, reason }); }
|
|
1971
|
+
function submit(reason = "api") { const value = input.value.trim(); if (!value || streaming || !emit(element, "beforepromptsubmit", { value, reason, input })) return; emit(element, "promptsubmit", { value, reason, input }); if (element.dataset.gClearOnSend !== "false") input.value = ""; sync(); }
|
|
1972
|
+
function stopGeneration(reason = "api") { if (!streaming || !emit(element, "beforepromptstop", { reason })) return; setStreaming(false, reason); emit(element, "promptstop", { reason }); }
|
|
1973
|
+
function keydown(event) { if (event.key === "Enter" && !event.shiftKey && !event.altKey && !event.isComposing) { event.preventDefault(); submit("keyboard"); } }
|
|
1974
|
+
function click(event) { if (event.target.closest("[data-g-composer-send]")) submit("button"); else if (event.target.closest("[data-g-composer-stop]")) stopGeneration("button"); else { const remove = event.target.closest("[data-g-attachment-remove]"); if (remove) { const attachment = remove.closest(".g-attachment"); if (attachment && emit(element, "attachmentremove", { attachment })) attachment.remove(); } } }
|
|
1975
|
+
input.addEventListener("input", sync); input.addEventListener("keydown", keydown); element.addEventListener("click", click); sync();
|
|
1976
|
+
return { submit, stop: stopGeneration, setStreaming, value: () => input.value, focus: () => input.focus(), destroy: () => { input.removeEventListener("input", sync); input.removeEventListener("keydown", keydown); element.removeEventListener("click", click); } };
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function createPromptFill(element) {
|
|
1980
|
+
function click(event) { const item = event.target.closest("[data-g-prompt-value]"); if (!item) return; const target = targetById(item.dataset.gPromptTarget || element.dataset.gPromptTarget); if (!target) return; const value = item.dataset.gPromptValue || item.dataset.gValue || item.textContent.trim(); target.value = value; target.dispatchEvent(new Event("input", { bubbles: true })); target.focus(); emit(element, "promptfill", { value, item, target }); }
|
|
1981
|
+
element.addEventListener("click", click);
|
|
1982
|
+
return { fill: (item) => click({ target: item }), destroy: () => element.removeEventListener("click", click) };
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function createAIApproval(element) {
|
|
1986
|
+
const choices = [...element.querySelectorAll("[data-g-approval-choice]")];
|
|
1987
|
+
function choose(choice, reason = "api") { if (!choice || choice.disabled) return; const value = choice.dataset.gApprovalChoice; if (!emit(element, "beforeapproval", { value, choice, reason })) return; element.dataset.gApprovalState = value; ["approved", "rejected", "cancelled"].forEach((state) => element.classList.toggle(`is-${state}`, value === state)); choices.forEach((item) => { item.setAttribute("aria-pressed", String(item === choice)); if (element.dataset.gKeepEnabled !== "true") item.disabled = true; }); emit(element, "approval", { value, choice, reason }); }
|
|
1988
|
+
function click(event) { choose(event.target.closest("[data-g-approval-choice]"), "pointer"); }
|
|
1989
|
+
element.addEventListener("click", click);
|
|
1990
|
+
return { choose, reset: () => { delete element.dataset.gApprovalState; element.classList.remove("is-approved", "is-rejected", "is-cancelled"); choices.forEach((item) => { item.disabled = false; item.setAttribute("aria-pressed", "false"); }); }, destroy: () => element.removeEventListener("click", click) };
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function createAIFeedback(element) {
|
|
1994
|
+
const options = [...element.querySelectorAll("[data-g-feedback-value]")];
|
|
1995
|
+
const detail = element.querySelector("[data-g-feedback-detail]");
|
|
1996
|
+
const comment = element.querySelector("[data-g-feedback-comment]");
|
|
1997
|
+
const output = element.querySelector("[data-g-feedback-output]");
|
|
1998
|
+
let value = output?.value || "";
|
|
1999
|
+
function select(option, reason = "api") { if (!option) return; value = option.dataset.gFeedbackValue; options.forEach((item) => item.setAttribute("aria-pressed", String(item === option))); if (output) output.value = value; if (detail) detail.hidden = false; emit(element, "feedbackchange", { value, option, reason }); }
|
|
2000
|
+
function submit(reason = "api") { if (!value) return; emit(element, "feedbacksubmit", { value, comment: comment?.value || "", reason }); element.classList.add("is-submitted"); }
|
|
2001
|
+
function click(event) { const option = event.target.closest("[data-g-feedback-value]"); if (option) select(option, "pointer"); else if (event.target.closest("[data-g-feedback-submit]")) submit("button"); }
|
|
2002
|
+
element.addEventListener("click", click); if (value) select(options.find((item) => item.dataset.gFeedbackValue === value), "init");
|
|
2003
|
+
return { select, submit, value: () => value, destroy: () => element.removeEventListener("click", click) };
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
function createShortcutRecorder(element) {
|
|
2007
|
+
const control = element.querySelector("[data-g-shortcut-control]") || element;
|
|
2008
|
+
const output = element.querySelector("[data-g-shortcut-output]");
|
|
2009
|
+
const display = element.querySelector("[data-g-shortcut-display]");
|
|
2010
|
+
const status = element.querySelector("[data-g-shortcut-status]");
|
|
2011
|
+
let recording = false; let value = output?.value || element.dataset.gValue || "";
|
|
2012
|
+
const modifierOrder = ["Control", "Alt", "Shift", "Meta"];
|
|
2013
|
+
function format(shortcut) { return shortcut ? shortcut.replaceAll("Control", navigator.platform.includes("Mac") ? "⌃" : "Ctrl").replaceAll("Meta", navigator.platform.includes("Mac") ? "⌘" : "Meta").replaceAll("Alt", navigator.platform.includes("Mac") ? "⌥" : "Alt").replaceAll("Shift", navigator.platform.includes("Mac") ? "⇧" : "Shift") : element.dataset.gEmptyLabel || "未设置"; }
|
|
2014
|
+
function sync(reason = "api") { if (output) output.value = value; if (display) display.textContent = format(value); element.dataset.gValue = value; emit(element, "shortcutchange", { value, reason }); }
|
|
2015
|
+
function stop(reason = "cancel") { recording = false; control.classList.remove("is-recording"); control.setAttribute("aria-pressed", "false"); if (status) status.textContent = reason === "recorded" ? element.dataset.gSavedLabel || "快捷键已记录" : element.dataset.gIdleLabel || "点击后按下组合键"; }
|
|
2016
|
+
function start() { recording = true; control.classList.add("is-recording"); control.setAttribute("aria-pressed", "true"); if (status) status.textContent = element.dataset.gRecordingLabel || "请按下快捷键;Esc 取消"; control.focus(); }
|
|
2017
|
+
function keydown(event) { if (!recording) return; event.preventDefault(); event.stopPropagation(); if (event.key === "Escape") return stop(); if (["Backspace", "Delete"].includes(event.key)) { value = ""; sync("clear"); return stop("recorded"); } if (["Control", "Alt", "Shift", "Meta"].includes(event.key)) return; const pressed = { Control: event.ctrlKey, Alt: event.altKey, Shift: event.shiftKey, Meta: event.metaKey }; const modifiers = modifierOrder.filter((name) => pressed[name]); const key = event.key.length === 1 ? event.key.toLocaleUpperCase() : event.key; value = [...modifiers, key].join("+"); sync("keyboard"); stop("recorded"); }
|
|
2018
|
+
function click(event) { if (event.target.closest("[data-g-shortcut-clear]")) { value = ""; sync("clear"); stop("recorded"); } else start(); }
|
|
2019
|
+
control.addEventListener("click", click); control.addEventListener("keydown", keydown); sync("init"); stop();
|
|
2020
|
+
return { start, stop, clear: () => { value = ""; sync("api"); }, value: () => value, destroy: () => { control.removeEventListener("click", click); control.removeEventListener("keydown", keydown); } };
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function createDesktopTabs(element) {
|
|
2024
|
+
const tabs = () => [...element.querySelectorAll("[data-g-desktop-tab]")];
|
|
2025
|
+
function select(tab, reason = "api", focus = false) { if (!tab || tab.getAttribute("aria-disabled") === "true") return; tabs().forEach((item) => { const active = item === tab; item.setAttribute("aria-selected", String(active)); item.classList.toggle("is-active", active); item.tabIndex = active ? 0 : -1; const panel = targetById(item.getAttribute("aria-controls")); if (panel) panel.hidden = !active; }); if (focus) tab.focus(); emit(element, "desktoptabchange", { tab, reason }); }
|
|
2026
|
+
function close(tab, reason = "api") { if (!tab || !emit(element, "beforetabclose", { tab, reason, dirty: tab.classList.contains("is-dirty") })) return; const list = tabs(); const index = list.indexOf(tab); const wasActive = tab.getAttribute("aria-selected") === "true" || tab.classList.contains("is-active"); const panel = targetById(tab.getAttribute("aria-controls")); panel?.remove(); tab.remove(); const remaining = tabs(); if (wasActive) select(remaining[Math.min(index, remaining.length - 1)], "close", true); emit(element, "desktoptabclose", { reason }); }
|
|
2027
|
+
function click(event) { const closeButton = event.target.closest("[data-g-desktop-tab-close]"); const tab = event.target.closest("[data-g-desktop-tab]"); if (closeButton) { event.stopPropagation(); close(closeButton.closest("[data-g-desktop-tab]"), "button"); } else if (tab) select(tab, "pointer"); }
|
|
2028
|
+
function pointerdown(event) { if (event.button === 1) { const tab = event.target.closest("[data-g-desktop-tab]"); if (tab) { event.preventDefault(); close(tab, "middle-click"); } } }
|
|
2029
|
+
function keydown(event) { const list = tabs(); const current = event.target.closest("[data-g-desktop-tab]") || list.find((tab) => tab.getAttribute("aria-selected") === "true"); if (!current) return; const index = list.indexOf(current); if ((event.ctrlKey || event.metaKey) && event.key === "Tab") { event.preventDefault(); select(list[(index + (event.shiftKey ? -1 : 1) + list.length) % list.length], "shortcut", true); } else if ((event.ctrlKey || event.metaKey) && event.key.toLocaleLowerCase() === "w") { event.preventDefault(); close(current, "shortcut"); } else if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) { event.preventDefault(); const next = event.key === "Home" ? list[0] : event.key === "End" ? list.at(-1) : list[(index + (event.key === "ArrowRight" ? 1 : -1) + list.length) % list.length]; select(next, "keyboard", true); } }
|
|
2030
|
+
element.setAttribute("role", element.getAttribute("role") || "tablist"); tabs().forEach((tab) => tab.setAttribute("role", tab.getAttribute("role") || "tab")); element.addEventListener("click", click); element.addEventListener("pointerdown", pointerdown); element.addEventListener("keydown", keydown); const initial = tabs().find((tab) => tab.getAttribute("aria-selected") === "true") || tabs()[0]; if (initial) select(initial, "init");
|
|
2031
|
+
return { select, close, tabs, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("pointerdown", pointerdown); element.removeEventListener("keydown", keydown); } };
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function createNativeFilePicker(element) {
|
|
2035
|
+
const input = element.querySelector("input[type='file']"); const trigger = element.querySelector("[data-g-native-file-trigger]"); const value = element.querySelector("[data-g-native-file-value]"); if (!input || !trigger) return null;
|
|
2036
|
+
function sync(reason = "change") { const files = [...input.files]; if (value) value.textContent = files.length ? files.map((file) => file.name).join(", ") : element.dataset.gEmptyLabel || "未选择文件"; emit(element, "nativefiles", { files, input, reason }); }
|
|
2037
|
+
function open() { if (!emit(element, "beforefilepicker", { input })) return; input.click(); }
|
|
2038
|
+
function change() { sync(); }
|
|
2039
|
+
trigger.addEventListener("click", open); input.addEventListener("change", change); sync("init");
|
|
2040
|
+
return { open, files: () => [...input.files], clear: () => { input.value = ""; sync("clear"); }, destroy: () => { trigger.removeEventListener("click", open); input.removeEventListener("change", change); } };
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function createWindowSwitcher(element) {
|
|
2044
|
+
const panel = element.querySelector(".g-desktop-window-switcher-panel") || element; const items = () => [...element.querySelectorAll("[data-g-window-item]")]; let open = !element.hidden; let trigger = null;
|
|
2045
|
+
function select(item, reason = "api") { if (!item) return; items().forEach((node) => node.setAttribute("aria-selected", String(node === item))); item.focus(); emit(element, "windowselect", { item, value: item.dataset.gValue, reason }); }
|
|
2046
|
+
function show(source = document.activeElement) { if (open) return; open = true; trigger = source; element.hidden = false; element.setAttribute("aria-hidden", "false"); select(items().find((item) => item.getAttribute("aria-selected") === "true") || items()[0], "open"); emit(element, "open", { source }); }
|
|
2047
|
+
function hide(reason = "api") { if (!open) return; open = false; element.hidden = true; element.setAttribute("aria-hidden", "true"); trigger?.focus?.(); emit(element, "close", { reason }); }
|
|
2048
|
+
function keydown(event) { const list = items(); const current = list.indexOf(document.activeElement); if ((event.ctrlKey || event.metaKey) && event.key === "`") { event.preventDefault(); open ? select(list[(current + 1) % list.length], "shortcut") : show(); } else if (!open) return; else if (event.key === "Escape") { event.preventDefault(); hide("escape"); } else if (["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(event.key)) { event.preventDefault(); select(list[(current + (["ArrowDown", "ArrowRight"].includes(event.key) ? 1 : -1) + list.length) % list.length], "keyboard"); } else if (event.key === "Enter") { event.preventDefault(); emit(element, "windowactivate", { item: document.activeElement, value: document.activeElement?.dataset.gValue }); hide("activate"); } }
|
|
2049
|
+
function click(event) { const item = event.target.closest("[data-g-window-item]"); if (item) { select(item, "pointer"); emit(element, "windowactivate", { item, value: item.dataset.gValue }); hide("activate"); } else if (event.target === element) hide("backdrop"); }
|
|
2050
|
+
if (open) { open = false; queueMicrotask(show); } else { element.hidden = true; element.setAttribute("aria-hidden", "true"); } document.addEventListener("keydown", keydown); element.addEventListener("click", click); panel.tabIndex = panel.tabIndex < 0 ? -1 : panel.tabIndex;
|
|
2051
|
+
return { open: show, close: hide, select, isOpen: () => open, destroy: () => { document.removeEventListener("keydown", keydown); element.removeEventListener("click", click); element.hidden = true; } };
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
[
|
|
2055
|
+
["dialog", (element) => createOverlay(element, "dialog")],
|
|
2056
|
+
["drawer", (element) => createOverlay(element, "drawer")],
|
|
2057
|
+
["mobile-sheet", (element) => createOverlay(element, "mobile-sheet")],
|
|
2058
|
+
["dropdown", createDropdown], ["tabs", createTabs], ["accordion", createAccordion],
|
|
2059
|
+
["auto-resize", createAutoResize], ["combobox", createCombobox],
|
|
2060
|
+
["password-toggle", createPasswordToggle], ["clear-input", createClearInput],
|
|
2061
|
+
["otp-input", createOtpInput], ["password-strength", createPasswordStrength], ["auth-timer", createAuthTimer],
|
|
2062
|
+
["quantity-stepper", createQuantityStepper], ["sku-selector", createSkuSelector], ["cart", createCart], ["coupon", createCoupon],
|
|
2063
|
+
["pull-refresh", createPullRefresh], ["infinite-load", createInfiniteLoad], ["swipe-actions", createSwipeActions], ["wheel-picker", createWheelPicker],
|
|
2064
|
+
["ai-composer", createAIComposer], ["prompt-fill", createPromptFill], ["ai-approval", createAIApproval], ["ai-feedback", createAIFeedback],
|
|
2065
|
+
["shortcut-recorder", createShortcutRecorder], ["desktop-tabs", createDesktopTabs], ["native-file-picker", createNativeFilePicker], ["window-switcher", createWindowSwitcher],
|
|
2066
|
+
["character-count", createCharacterCount], ["conditional-field", createConditionalField],
|
|
2067
|
+
["repeatable-field", createRepeatableField],
|
|
2068
|
+
["tooltip", (element) => createFloating(element, "tooltip")], ["popover", (element) => createFloating(element, "popover")],
|
|
2069
|
+
["tour", createTour],
|
|
2070
|
+
["carousel", createCarousel], ["split-pane", createSplitter], ["tree", createTree],
|
|
2071
|
+
["data-grid", createDataGrid], ["table-sort", createTableSort], ["row-select", createRowSelect],
|
|
2072
|
+
["row-disclosure", createRowDisclosure], ["column-toggle", createColumnToggle],
|
|
2073
|
+
["data-filter", createDataFilter], ["data-view", createDataView],
|
|
2074
|
+
["transfer", createTransfer], ["picker", createPicker], ["cascader", createCascader],
|
|
2075
|
+
["saved-choice", createSavedChoice], ["builder-list", createBuilderList],
|
|
2076
|
+
["toast", createToast], ["copy", createCopy],
|
|
2077
|
+
["fullscreen", createFullscreen], ["scroll-top", createScrollTop], ["dropzone", createDropzone],
|
|
2078
|
+
["nav-toggle", createNavToggle], ["roving-nav", createRovingNav], ["context-menu", createContextMenu],
|
|
2079
|
+
["scrollspy", createScrollspy], ["jump-nav", createJumpNav],
|
|
2080
|
+
["upload-manager", createUploadManager], ["file-browser", createFileBrowser],
|
|
2081
|
+
["editor-shell", createEditorShell], ["revision-compare", createRevisionCompare], ["autosave", createAutosave],
|
|
2082
|
+
["command-palette", createCommandPalette]
|
|
2083
|
+
].forEach(([name, factory]) => register(name, factory));
|
|
2084
|
+
|
|
2085
|
+
function initElement(element, name) {
|
|
2086
|
+
const store = storeFor(element);
|
|
2087
|
+
if (store.has(name)) return store.get(name);
|
|
2088
|
+
const factory = registry.get(name);
|
|
2089
|
+
if (!factory) return null;
|
|
2090
|
+
const instance = factory(element);
|
|
2091
|
+
if (instance) { store.set(name, instance); emit(element, "init", { name, instance }); }
|
|
2092
|
+
return instance;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function init(root = document) {
|
|
2096
|
+
for (const [name] of registry) {
|
|
2097
|
+
const selector = `[data-g-${name}]`;
|
|
2098
|
+
if (root instanceof Element && root.matches(selector)) initElement(root, name);
|
|
2099
|
+
root.querySelectorAll?.(selector).forEach((element) => initElement(element, name));
|
|
2100
|
+
}
|
|
2101
|
+
return Gardener;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function getInstance(elementOrSelector, name) {
|
|
2105
|
+
const element = typeof elementOrSelector === "string" ? document.querySelector(elementOrSelector) : elementOrSelector;
|
|
2106
|
+
const store = element ? instanceStores.get(element) : null;
|
|
2107
|
+
if (!store) return null;
|
|
2108
|
+
return name ? store.get(name) || null : store.size === 1 ? store.values().next().value : Object.fromEntries(store);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function destroy(root = document) {
|
|
2112
|
+
const candidates = [];
|
|
2113
|
+
if (root instanceof Element) candidates.push(root);
|
|
2114
|
+
root.querySelectorAll?.("*").forEach((element) => candidates.push(element));
|
|
2115
|
+
candidates.forEach((element) => {
|
|
2116
|
+
const store = instanceStores.get(element);
|
|
2117
|
+
if (!store) return;
|
|
2118
|
+
store.forEach((instance) => instance?.destroy?.());
|
|
2119
|
+
instanceStores.delete(element);
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function toast(options = {}) {
|
|
2124
|
+
let region = document.querySelector(".g-toast-region");
|
|
2125
|
+
if (!region) {
|
|
2126
|
+
region = document.createElement("div");
|
|
2127
|
+
region.className = "g-toast-region";
|
|
2128
|
+
region.setAttribute("aria-live", options.tone === "danger" ? "assertive" : "polite");
|
|
2129
|
+
document.body.append(region);
|
|
2130
|
+
}
|
|
2131
|
+
const element = document.createElement("div");
|
|
2132
|
+
element.className = `g-toast${options.tone ? ` g-toast-${options.tone}` : ""}`;
|
|
2133
|
+
element.setAttribute("role", options.tone === "danger" ? "alert" : "status");
|
|
2134
|
+
element.dataset.gToast = "";
|
|
2135
|
+
if (options.timeout != null) element.dataset.gTimeout = String(options.timeout);
|
|
2136
|
+
element.innerHTML = `<div class="g-toast-content"><div class="g-toast-title"></div><div class="g-toast-message"></div></div><button class="g-btn g-btn-ghost g-btn-icon" data-g-dismiss aria-label="Close">×</button>`;
|
|
2137
|
+
element.querySelector(".g-toast-title").textContent = options.title || "";
|
|
2138
|
+
element.querySelector(".g-toast-message").textContent = options.message || "";
|
|
2139
|
+
region.append(element);
|
|
2140
|
+
init(element);
|
|
2141
|
+
return element;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function delegateClick(event) {
|
|
2145
|
+
const commandTrigger = event.target.closest("[data-g-command-trigger]");
|
|
2146
|
+
if (commandTrigger) {
|
|
2147
|
+
const target = targetById(commandTrigger.dataset.gCommandTrigger);
|
|
2148
|
+
if (target) {
|
|
2149
|
+
commandTrigger.setAttribute("aria-haspopup", "dialog");
|
|
2150
|
+
commandTrigger.setAttribute("aria-controls", target.id);
|
|
2151
|
+
init(target);
|
|
2152
|
+
getInstance(target, "command-palette")?.toggle(commandTrigger);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
const opener = event.target.closest("[data-g-dialog-open], [data-g-drawer-open], [data-g-mobile-sheet-open]");
|
|
2156
|
+
if (opener) {
|
|
2157
|
+
const id = opener.dataset.gDialogOpen || opener.dataset.gDrawerOpen || opener.dataset.gMobileSheetOpen;
|
|
2158
|
+
const target = targetById(id);
|
|
2159
|
+
if (target) {
|
|
2160
|
+
init(target);
|
|
2161
|
+
getInstance(target, opener.dataset.gDialogOpen ? "dialog" : opener.dataset.gDrawerOpen ? "drawer" : "mobile-sheet")?.open(opener);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const tourOpener = event.target.closest("[data-g-tour-open]");
|
|
2165
|
+
if (tourOpener) {
|
|
2166
|
+
const target = targetById(tourOpener.dataset.gTourOpen);
|
|
2167
|
+
if (target) {
|
|
2168
|
+
tourOpener.setAttribute("aria-haspopup", "dialog");
|
|
2169
|
+
tourOpener.setAttribute("aria-controls", target.id);
|
|
2170
|
+
init(target);
|
|
2171
|
+
getInstance(target, "tour")?.open(tourOpener);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
const toggle = event.target.closest("[data-g-toggle]");
|
|
2175
|
+
if (toggle) {
|
|
2176
|
+
const target = targetById(toggle.dataset.gToggle);
|
|
2177
|
+
if (target) {
|
|
2178
|
+
const expanded = toggle.getAttribute("aria-expanded") !== "true";
|
|
2179
|
+
toggle.setAttribute("aria-expanded", String(expanded));
|
|
2180
|
+
target.hidden = !expanded;
|
|
2181
|
+
target.classList.toggle("is-open", expanded);
|
|
2182
|
+
emit(target, "toggle", { expanded, trigger: toggle });
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
const dismiss = event.target.closest("[data-g-dismiss]");
|
|
2186
|
+
if (dismiss) {
|
|
2187
|
+
const target = dismiss.closest(".g-alert, .g-toast, .g-banner, [data-g-dismissible]");
|
|
2188
|
+
const toastInstance = target ? getInstance(target, "toast") : null;
|
|
2189
|
+
if (toastInstance) toastInstance.dismiss("button");
|
|
2190
|
+
else if (target && emit(target, "dismiss", { reason: "button" })) target.remove();
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
let observer;
|
|
2195
|
+
function observe() {
|
|
2196
|
+
if (observer || typeof MutationObserver === "undefined") return;
|
|
2197
|
+
observer = new MutationObserver((records) => records.forEach((record) => {
|
|
2198
|
+
record.addedNodes.forEach((node) => { if (node instanceof Element) init(node); });
|
|
2199
|
+
record.removedNodes.forEach((node) => { if (node instanceof Element) destroy(node); });
|
|
2200
|
+
if (record.type === "attributes" && record.target instanceof Element && record.attributeName?.startsWith("data-g-")) {
|
|
2201
|
+
const name = record.attributeName.slice(7);
|
|
2202
|
+
if (registry.has(name) && record.target.hasAttribute(record.attributeName)) initElement(record.target, name);
|
|
2203
|
+
else if (registry.has(name)) {
|
|
2204
|
+
const store = instanceStores.get(record.target);
|
|
2205
|
+
store?.get(name)?.destroy?.();
|
|
2206
|
+
store?.delete(name);
|
|
2207
|
+
if (store?.size === 0) instanceStores.delete(record.target);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}));
|
|
2211
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
const Gardener = Object.freeze({ version: "1.0.0", get behaviors() { return Object.freeze([...registry.keys()]); }, init, destroy, register, getInstance, emit, toast, observe });
|
|
2215
|
+
|
|
2216
|
+
if (typeof document !== "undefined") {
|
|
2217
|
+
document.addEventListener("click", delegateClick);
|
|
2218
|
+
const start = () => { init(); observe(); };
|
|
2219
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
2220
|
+
else queueMicrotask(start);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
export { Gardener, init, destroy, register, getInstance, emit, toast, observe };
|
|
2224
|
+
export default Gardener;
|