gg-wf-scripts 2.5.0 → 2.8.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/README.md +33 -0
- package/dist/action-engine.d.ts +3 -0
- package/dist/action-engine.js +78 -0
- package/dist/action-engine.js.map +1 -0
- package/dist/actions.d.ts +14 -0
- package/dist/actions.js +12 -0
- package/dist/actions.js.map +1 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +27 -0
- package/dist/auth.js.map +1 -0
- package/dist/bridges.d.ts +1 -0
- package/dist/bridges.js +33 -0
- package/dist/bridges.js.map +1 -0
- package/dist/data-engine.d.ts +3 -0
- package/dist/data-engine.js +158 -0
- package/dist/data-engine.js.map +1 -0
- package/dist/dialog.d.ts +1 -0
- package/dist/dialog.js +89 -0
- package/dist/dialog.js.map +1 -0
- package/dist/form-action-engine.d.ts +3 -0
- package/dist/form-action-engine.js +156 -0
- package/dist/form-action-engine.js.map +1 -0
- package/dist/form-actions.d.ts +22 -0
- package/dist/form-actions.js +15 -0
- package/dist/form-actions.js.map +1 -0
- package/dist/form-visibility.d.ts +1 -0
- package/dist/form-visibility.js +108 -0
- package/dist/form-visibility.js.map +1 -0
- package/dist/helpers/dom.d.ts +16 -0
- package/dist/helpers/dom.js +39 -0
- package/dist/helpers/dom.js.map +1 -0
- package/dist/helpers/path.d.ts +2 -0
- package/dist/helpers/path.js +9 -0
- package/dist/helpers/path.js.map +1 -0
- package/dist/helpers/run-handler.d.ts +22 -0
- package/dist/helpers/run-handler.js +33 -0
- package/dist/helpers/run-handler.js.map +1 -0
- package/dist/helpers/run-with-loading.d.ts +6 -0
- package/dist/helpers/run-with-loading.js +28 -0
- package/dist/helpers/run-with-loading.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/queries.d.ts +10 -0
- package/dist/queries.js +12 -0
- package/dist/queries.js.map +1 -0
- package/dist/query-params.d.ts +19 -0
- package/dist/query-params.js +142 -0
- package/dist/query-params.js.map +1 -0
- package/dist/switch-engine.d.ts +1 -0
- package/dist/switch-engine.js +21 -0
- package/dist/switch-engine.js.map +1 -0
- package/package.json +20 -2
- package/src/action-engine.js +0 -64
- package/src/actions.js +0 -14
- package/src/auth.js +0 -28
- package/src/bridges.js +0 -31
- package/src/data-engine.js +0 -165
- package/src/dialog.js +0 -86
- package/src/form-action-engine.js +0 -149
- package/src/form-actions.js +0 -21
- package/src/form-visibility.js +0 -122
- package/src/helpers/dom.js +0 -35
- package/src/helpers/log.js +0 -34
- package/src/helpers/path.js +0 -4
- package/src/index.js +0 -60
- package/src/queries.js +0 -14
- package/src/query-params.js +0 -144
- package/src/switch-engine.js +0 -22
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { formActionRegistry } from "./form-actions.js";
|
|
2
|
-
import { populateFields } from "./helpers/dom.js";
|
|
3
|
-
import { withDebugLog } from "./helpers/log.js";
|
|
4
|
-
import { getParams } from "./query-params.js";
|
|
5
|
-
|
|
6
|
-
function findInputs(form, name) {
|
|
7
|
-
const escaped = CSS.escape(name);
|
|
8
|
-
return form.querySelectorAll(
|
|
9
|
-
`input[name="${escaped}"], select[name="${escaped}"], textarea[name="${escaped}"]`,
|
|
10
|
-
);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function clearFieldError(form, name) {
|
|
14
|
-
findInputs(form, name).forEach((el) => {
|
|
15
|
-
el.removeAttribute("gg-form-field-invalid");
|
|
16
|
-
});
|
|
17
|
-
const escaped = CSS.escape(name);
|
|
18
|
-
form
|
|
19
|
-
.querySelectorAll(`[gg-form-field-error="${escaped}"]`)
|
|
20
|
-
.forEach((el) => {
|
|
21
|
-
el.textContent = "";
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function clearFormErrors(form) {
|
|
26
|
-
form.querySelectorAll("[gg-form-field-invalid]").forEach((el) => {
|
|
27
|
-
el.removeAttribute("gg-form-field-invalid");
|
|
28
|
-
});
|
|
29
|
-
form.querySelectorAll("[gg-form-field-error]").forEach((el) => {
|
|
30
|
-
el.textContent = "";
|
|
31
|
-
});
|
|
32
|
-
form.querySelectorAll("[gg-form-error]").forEach((el) => {
|
|
33
|
-
el.textContent = "";
|
|
34
|
-
});
|
|
35
|
-
form.querySelectorAll("[gg-form-error-list]").forEach((list) => {
|
|
36
|
-
const template = list.querySelector("[gg-list-template]");
|
|
37
|
-
Array.from(list.children).forEach((child) => {
|
|
38
|
-
if (child !== template) child.remove();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function applyFieldErrors(form, fieldErrors) {
|
|
44
|
-
fieldErrors.forEach(({ name, message }) => {
|
|
45
|
-
if (!name) return;
|
|
46
|
-
findInputs(form, name).forEach((el) => {
|
|
47
|
-
el.setAttribute("gg-form-field-invalid", "true");
|
|
48
|
-
});
|
|
49
|
-
const escaped = CSS.escape(name);
|
|
50
|
-
form
|
|
51
|
-
.querySelectorAll(`[gg-form-field-error="${escaped}"]`)
|
|
52
|
-
.forEach((el) => {
|
|
53
|
-
el.textContent = message ?? "";
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function applyErrorList(form, fieldErrors) {
|
|
59
|
-
form.querySelectorAll("[gg-form-error-list]").forEach((list) => {
|
|
60
|
-
const template = list.querySelector("[gg-list-template]");
|
|
61
|
-
if (!template) {
|
|
62
|
-
console.warn(
|
|
63
|
-
"[gg-form-error-list] no [gg-list-template] inside list:",
|
|
64
|
-
list,
|
|
65
|
-
);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
fieldErrors.forEach((record) => {
|
|
69
|
-
const clone = template.cloneNode(true);
|
|
70
|
-
clone.removeAttribute("gg-list-template");
|
|
71
|
-
clone.style.display = "";
|
|
72
|
-
populateFields(clone, record);
|
|
73
|
-
list.appendChild(clone);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function applyFormError(form, error) {
|
|
79
|
-
if (error == null) return;
|
|
80
|
-
const message = typeof error === "string" ? error : String(error);
|
|
81
|
-
form.querySelectorAll("[gg-form-error]").forEach((el) => {
|
|
82
|
-
el.textContent = message;
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function bindFieldClearOnInput(form) {
|
|
87
|
-
if (form.__ggFormErrorBound) return;
|
|
88
|
-
form.__ggFormErrorBound = true;
|
|
89
|
-
form.addEventListener("input", (e) => {
|
|
90
|
-
const name = e.target?.getAttribute?.("name");
|
|
91
|
-
if (!name) return;
|
|
92
|
-
if (e.target.hasAttribute("gg-form-field-invalid")) {
|
|
93
|
-
clearFieldError(form, name);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function initFormActionEngine(context, { debug = false } = {}) {
|
|
99
|
-
async function handleSubmit(form, event) {
|
|
100
|
-
const id = form.getAttribute("gg-form-action");
|
|
101
|
-
const action = formActionRegistry[id];
|
|
102
|
-
if (!action) {
|
|
103
|
-
console.warn(`[gg-form-action] no form action registered for "${id}"`);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
event.preventDefault();
|
|
108
|
-
clearFormErrors(form);
|
|
109
|
-
|
|
110
|
-
const formData = new FormData(form);
|
|
111
|
-
const params = getParams();
|
|
112
|
-
|
|
113
|
-
const result = await withDebugLog(
|
|
114
|
-
"[gg-form-action]",
|
|
115
|
-
id,
|
|
116
|
-
{
|
|
117
|
-
form,
|
|
118
|
-
formData: Object.fromEntries(formData),
|
|
119
|
-
params: Object.fromEntries(params),
|
|
120
|
-
},
|
|
121
|
-
debug,
|
|
122
|
-
() => action(context, formData, params),
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
if (result?.ok === false) {
|
|
126
|
-
const fieldErrors = Array.isArray(result.field_errors)
|
|
127
|
-
? result.field_errors
|
|
128
|
-
: [];
|
|
129
|
-
applyFieldErrors(form, fieldErrors);
|
|
130
|
-
applyErrorList(form, fieldErrors);
|
|
131
|
-
applyFormError(form, result.error);
|
|
132
|
-
if (!fieldErrors.length && result.error == null) {
|
|
133
|
-
console.warn(
|
|
134
|
-
`[gg-form-action] "${id}" returned ok:false with no error or field_errors`,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
document.querySelectorAll("form[gg-form-action]").forEach(bindFieldClearOnInput);
|
|
141
|
-
|
|
142
|
-
document.addEventListener("submit", (e) => {
|
|
143
|
-
const form = e.target;
|
|
144
|
-
if (form?.tagName === "FORM" && form.hasAttribute("gg-form-action")) {
|
|
145
|
-
bindFieldClearOnInput(form);
|
|
146
|
-
handleSubmit(form, e);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
}
|
package/src/form-actions.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export const formActionRegistry = {};
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Register a form action triggered by submitting a form with gg-form-action="<id>".
|
|
5
|
-
*
|
|
6
|
-
* @param {string} id - The form action identifier, referenced by gg-form-action="<id>" on a form.
|
|
7
|
-
* @param {(context: object, formData: FormData, params: URLSearchParams) => Promise<{
|
|
8
|
-
* ok: boolean,
|
|
9
|
-
* error?: any,
|
|
10
|
-
* field_errors?: Array<{ name: string, message: string }>,
|
|
11
|
-
* }>} fn
|
|
12
|
-
* Receives the context object passed to init(), a FormData snapshot of the submitted form,
|
|
13
|
-
* and a URLSearchParams snapshot of the current URL query string. Return:
|
|
14
|
-
* - { ok: true } on success
|
|
15
|
-
* - { ok: false, field_errors: [{ name, message }, ...] } for validation errors
|
|
16
|
-
* - { ok: false, error: "..." } for a single form-level error
|
|
17
|
-
* - Both field_errors and error may be present together.
|
|
18
|
-
*/
|
|
19
|
-
export function registerFormAction(id, fn) {
|
|
20
|
-
formActionRegistry[id] = fn;
|
|
21
|
-
}
|
package/src/form-visibility.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
const TRANSITION_MS = 200;
|
|
2
|
-
|
|
3
|
-
function parseConditions(attr) {
|
|
4
|
-
return attr
|
|
5
|
-
.split(",")
|
|
6
|
-
.map((pair) => {
|
|
7
|
-
const [name, value] = pair.split(":");
|
|
8
|
-
return { name: name?.trim(), value: value?.trim() };
|
|
9
|
-
})
|
|
10
|
-
.filter((p) => p.name && p.value);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function getFieldValue(scope, name) {
|
|
14
|
-
const escaped = CSS.escape(name);
|
|
15
|
-
const inputs = scope.querySelectorAll(
|
|
16
|
-
`input[name="${escaped}"], select[name="${escaped}"], textarea[name="${escaped}"]`,
|
|
17
|
-
);
|
|
18
|
-
if (!inputs.length) return null;
|
|
19
|
-
|
|
20
|
-
const type = (inputs[0].getAttribute("type") || "").toLowerCase();
|
|
21
|
-
if (type === "radio" || type === "checkbox") {
|
|
22
|
-
for (const input of inputs) {
|
|
23
|
-
if (input.checked) return input.value;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
return inputs[0].value.trim();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function matchesAny(scope, conditions) {
|
|
31
|
-
return conditions.some(
|
|
32
|
-
({ name, value }) => getFieldValue(scope, name) === value,
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function initFormVisibility() {
|
|
37
|
-
// Group all [gg-visible-when] elements by their nearest scope —
|
|
38
|
-
// either an explicit [gg-form-scope] or a <form>.
|
|
39
|
-
const scopeTargets = new Map();
|
|
40
|
-
document.querySelectorAll("[gg-visible-when]").forEach((el) => {
|
|
41
|
-
if (!Boolean(el.getAttribute("gg-visible-when"))) {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const scope = el.closest("[gg-form-scope], form");
|
|
45
|
-
if (!scope) {
|
|
46
|
-
console.warn(
|
|
47
|
-
"[gg-visible-when] element is not inside a <form> or [gg-form-scope]:",
|
|
48
|
-
el,
|
|
49
|
-
);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (!scopeTargets.has(scope)) scopeTargets.set(scope, []);
|
|
53
|
-
scopeTargets.get(scope).push(el);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
scopeTargets.forEach((targets, scope) => {
|
|
57
|
-
const conditions = new WeakMap();
|
|
58
|
-
const lastState = new WeakMap();
|
|
59
|
-
const hideTimers = new WeakMap();
|
|
60
|
-
|
|
61
|
-
targets.forEach((el) => {
|
|
62
|
-
conditions.set(
|
|
63
|
-
el,
|
|
64
|
-
parseConditions(el.getAttribute("gg-visible-when")),
|
|
65
|
-
);
|
|
66
|
-
el.style.transition = "none";
|
|
67
|
-
el.style.opacity = "0";
|
|
68
|
-
el.style.display = "none";
|
|
69
|
-
el.style.pointerEvents = "none";
|
|
70
|
-
el.setAttribute("inert", "");
|
|
71
|
-
el.setAttribute("aria-hidden", "true");
|
|
72
|
-
lastState.set(el, false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
function show(el) {
|
|
76
|
-
const pending = hideTimers.get(el);
|
|
77
|
-
if (pending) {
|
|
78
|
-
clearTimeout(pending);
|
|
79
|
-
hideTimers.delete(el);
|
|
80
|
-
}
|
|
81
|
-
el.removeAttribute("inert");
|
|
82
|
-
el.removeAttribute("aria-hidden");
|
|
83
|
-
el.style.display = "";
|
|
84
|
-
el.style.pointerEvents = "";
|
|
85
|
-
requestAnimationFrame(() => {
|
|
86
|
-
el.style.opacity = "1";
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function hide(el) {
|
|
91
|
-
el.style.opacity = "0";
|
|
92
|
-
el.style.pointerEvents = "none";
|
|
93
|
-
el.setAttribute("inert", "");
|
|
94
|
-
el.setAttribute("aria-hidden", "true");
|
|
95
|
-
const t = setTimeout(() => {
|
|
96
|
-
el.style.display = "none";
|
|
97
|
-
hideTimers.delete(el);
|
|
98
|
-
}, TRANSITION_MS);
|
|
99
|
-
hideTimers.set(el, t);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function evaluate() {
|
|
103
|
-
targets.forEach((el) => {
|
|
104
|
-
const shouldShow = matchesAny(scope, conditions.get(el));
|
|
105
|
-
if (shouldShow === lastState.get(el)) return;
|
|
106
|
-
lastState.set(el, shouldShow);
|
|
107
|
-
if (shouldShow) show(el);
|
|
108
|
-
else hide(el);
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
requestAnimationFrame(() => {
|
|
113
|
-
targets.forEach((el) => {
|
|
114
|
-
el.style.transition = `opacity ${TRANSITION_MS}ms ease`;
|
|
115
|
-
});
|
|
116
|
-
evaluate();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
scope.addEventListener("change", evaluate);
|
|
120
|
-
scope.addEventListener("input", evaluate);
|
|
121
|
-
});
|
|
122
|
-
}
|
package/src/helpers/dom.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { getPath } from "./path.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Set textContent on every [gg-field] descendant of `root` to the value
|
|
5
|
-
* at that field's dot-path on `record`. Null / missing values are left
|
|
6
|
-
* as-is (keeps whatever the markup's default content was).
|
|
7
|
-
*/
|
|
8
|
-
export function populateFields(root, record) {
|
|
9
|
-
root.querySelectorAll("[gg-field]").forEach((el) => {
|
|
10
|
-
const path = el.getAttribute("gg-field");
|
|
11
|
-
const value = getPath(record, path);
|
|
12
|
-
if (value != null) el.textContent = String(value);
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Write a normalized switch state onto an element.
|
|
18
|
-
* null / undefined become "" so gg-case="" can serve as the empty/default.
|
|
19
|
-
*/
|
|
20
|
-
export function setSwitchState(el, value) {
|
|
21
|
-
el.setAttribute("gg-switch-state", value == null ? "" : String(value));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Show the [gg-case] child whose value matches the container's current
|
|
26
|
-
* gg-switch-state; hide the rest with display:none.
|
|
27
|
-
*/
|
|
28
|
-
export function applySwitchState(container) {
|
|
29
|
-
const state = container.getAttribute("gg-switch-state") ?? "";
|
|
30
|
-
Array.from(container.children).forEach((child) => {
|
|
31
|
-
if (!child.hasAttribute("gg-case")) return;
|
|
32
|
-
const match = child.getAttribute("gg-case") === state;
|
|
33
|
-
child.style.display = match ? "" : "none";
|
|
34
|
-
});
|
|
35
|
-
}
|
package/src/helpers/log.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wrap an async handler with optional debug logging and uniform throw handling.
|
|
3
|
-
*
|
|
4
|
-
* On success, returns the handler's resolved value. On throw, logs the error and
|
|
5
|
-
* returns `undefined` so callers can branch on it.
|
|
6
|
-
*
|
|
7
|
-
* @param {string} prefix - Log prefix, e.g. "[gg-action]".
|
|
8
|
-
* @param {string} id - Handler identifier shown in the log group.
|
|
9
|
-
* @param {Record<string, any>} fields - Key/value pairs logged when debug is on.
|
|
10
|
-
* @param {boolean} debug - Whether to emit grouped debug logs.
|
|
11
|
-
* @param {() => Promise<any>} run - The handler invocation.
|
|
12
|
-
*/
|
|
13
|
-
export async function withDebugLog(prefix, id, fields, debug, run) {
|
|
14
|
-
if (debug) {
|
|
15
|
-
console.groupCollapsed(`${prefix} "${id}"`);
|
|
16
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
17
|
-
console.log(`${key}:`, value);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const startedAt = debug ? performance.now() : 0;
|
|
21
|
-
try {
|
|
22
|
-
const result = await run();
|
|
23
|
-
if (debug) {
|
|
24
|
-
const ms = (performance.now() - startedAt).toFixed(1);
|
|
25
|
-
console.log(`result (${ms}ms):`, result);
|
|
26
|
-
}
|
|
27
|
-
return result;
|
|
28
|
-
} catch (err) {
|
|
29
|
-
console.error(`${prefix} "${id}" threw:`, err);
|
|
30
|
-
return undefined;
|
|
31
|
-
} finally {
|
|
32
|
-
if (debug) console.groupEnd();
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/helpers/path.js
DELETED
package/src/index.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { registerQuery } from "./queries.js";
|
|
2
|
-
import { registerAction } from "./actions.js";
|
|
3
|
-
import { registerFormAction } from "./form-actions.js";
|
|
4
|
-
import { setSwitchState, applySwitchState, populateFields } from "./helpers/dom.js";
|
|
5
|
-
import { setQueryParams, removeQueryParams, initQueryParams } from "./query-params.js";
|
|
6
|
-
import { initAuth } from "./auth.js";
|
|
7
|
-
import { initSwitchEngine } from "./switch-engine.js";
|
|
8
|
-
import { initDialog } from "./dialog.js";
|
|
9
|
-
import { initBridges } from "./bridges.js";
|
|
10
|
-
import { initFormVisibility } from "./form-visibility.js";
|
|
11
|
-
import { initDataEngine } from "./data-engine.js";
|
|
12
|
-
import { initActionEngine } from "./action-engine.js";
|
|
13
|
-
import { initFormActionEngine } from "./form-action-engine.js";
|
|
14
|
-
|
|
15
|
-
export { setSwitchState, applySwitchState, populateFields, setQueryParams, removeQueryParams };
|
|
16
|
-
export { getPath } from "./helpers/path.js";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Create a gg-scripts app instance.
|
|
20
|
-
*
|
|
21
|
-
* @param {object} [options]
|
|
22
|
-
* @param {object} [options.context] - Arbitrary object passed to every query and action.
|
|
23
|
-
* Put backend clients (Supabase, fetch wrappers, etc.) or anything else your queries need on it.
|
|
24
|
-
* @param {object} [options.auth] - Auth adapter. If omitted, gg-auth/gg-role attrs are never set.
|
|
25
|
-
* @param {() => (string|null) | Promise<string|null>} options.auth.getUser
|
|
26
|
-
* Returns the current user id, or null when signed out.
|
|
27
|
-
* @param {(cb: (userId: string|null) => void) => void} [options.auth.onChange]
|
|
28
|
-
* Subscribe to auth changes. Called with the new user id (or null) whenever it changes.
|
|
29
|
-
* @param {(context: object, userId: string) => Promise<string|null>} [options.auth.roleQuery]
|
|
30
|
-
* Returns the user's role string for gg-role gating. If omitted, gg-role is never set.
|
|
31
|
-
* @param {boolean} [options.debug=false] - When true, every query and action is logged to the
|
|
32
|
-
* console with its trigger/container, data, result, and duration.
|
|
33
|
-
* @returns {{ addQuery: function, addAction: function, start: function }}
|
|
34
|
-
*/
|
|
35
|
-
export function init({ context = {}, auth, debug = false } = {}) {
|
|
36
|
-
return {
|
|
37
|
-
addQuery: registerQuery,
|
|
38
|
-
addAction: registerAction,
|
|
39
|
-
addFormAction: registerFormAction,
|
|
40
|
-
start() {
|
|
41
|
-
function run() {
|
|
42
|
-
if (auth) initAuth(context, auth);
|
|
43
|
-
initSwitchEngine();
|
|
44
|
-
initQueryParams();
|
|
45
|
-
initDialog();
|
|
46
|
-
initBridges();
|
|
47
|
-
initFormVisibility();
|
|
48
|
-
initDataEngine(context, { debug });
|
|
49
|
-
initActionEngine(context, { debug });
|
|
50
|
-
initFormActionEngine(context, { debug });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (document.readyState === "loading") {
|
|
54
|
-
document.addEventListener("DOMContentLoaded", run);
|
|
55
|
-
} else {
|
|
56
|
-
run();
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
package/src/queries.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const queryRegistry = {};
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Register a data query for use with gg-data, gg-data-form, or gg-data-list.
|
|
5
|
-
*
|
|
6
|
-
* @param {string} id - The query identifier, referenced by gg-data="<id>" in markup.
|
|
7
|
-
* @param {(context: object, params: URLSearchParams) => Promise<object|object[]|null>} fn
|
|
8
|
-
* Receives the context object passed to init() and a URLSearchParams snapshot of the
|
|
9
|
-
* current URL query string. Return a single object (or null) for gg-data/gg-data-form,
|
|
10
|
-
* or an array for gg-data-list.
|
|
11
|
-
*/
|
|
12
|
-
export function registerQuery(id, fn) {
|
|
13
|
-
queryRegistry[id] = fn;
|
|
14
|
-
}
|
package/src/query-params.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
const subscribers = [];
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Snapshot of the current URL query string as a URLSearchParams instance.
|
|
5
|
-
*/
|
|
6
|
-
export function getParams() {
|
|
7
|
-
return new URL(window.location).searchParams;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function onQueryChanged(callback) {
|
|
11
|
-
subscribers.push(callback);
|
|
12
|
-
return () => {
|
|
13
|
-
const idx = subscribers.indexOf(callback);
|
|
14
|
-
if (idx >= 0) subscribers.splice(idx, 1);
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function notify(key, value) {
|
|
19
|
-
subscribers.forEach((cb) => cb(key, value));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Set one or more URL query params and notify subscribers.
|
|
24
|
-
*
|
|
25
|
-
* @param {Array<{key: string, value: string}>} params - Key/value pairs to set.
|
|
26
|
-
*/
|
|
27
|
-
export function setQueryParams(params) {
|
|
28
|
-
const url = new URL(window.location);
|
|
29
|
-
params.forEach(({ key, value }) => url.searchParams.set(key, value));
|
|
30
|
-
history.pushState({}, "", url);
|
|
31
|
-
params.forEach(({ key, value }) => notify(key, value));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Remove one or more URL query params and notify subscribers.
|
|
36
|
-
*
|
|
37
|
-
* @param {string[]} keys - Param keys to remove.
|
|
38
|
-
*/
|
|
39
|
-
export function removeQueryParams(keys) {
|
|
40
|
-
const url = new URL(window.location);
|
|
41
|
-
keys.forEach((key) => url.searchParams.delete(key));
|
|
42
|
-
history.pushState({}, "", url);
|
|
43
|
-
keys.forEach((key) => notify(key, null));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---- click delegation for gg-query-set / gg-query-remove ----
|
|
47
|
-
|
|
48
|
-
function handleQueryClick(target) {
|
|
49
|
-
const setTrigger = target.closest("[gg-query-set]");
|
|
50
|
-
if (setTrigger) {
|
|
51
|
-
const params = setTrigger
|
|
52
|
-
.getAttribute("gg-query-set")
|
|
53
|
-
.split(",")
|
|
54
|
-
.filter(Boolean)
|
|
55
|
-
.map((pair) => {
|
|
56
|
-
const [key, value] = pair.split(":");
|
|
57
|
-
return { key: key?.trim(), value: value?.trim() };
|
|
58
|
-
})
|
|
59
|
-
.filter((p) => p.key && p.value);
|
|
60
|
-
if (params.length) setQueryParams(params);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const removeTrigger = target.closest("[gg-query-remove]");
|
|
65
|
-
if (removeTrigger) {
|
|
66
|
-
const keys = removeTrigger
|
|
67
|
-
.getAttribute("gg-query-remove")
|
|
68
|
-
.split(",")
|
|
69
|
-
.map((k) => k.trim())
|
|
70
|
-
.filter(Boolean);
|
|
71
|
-
if (keys.length) removeQueryParams(keys);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function initQueryParams() {
|
|
77
|
-
document.addEventListener("click", (e) => handleQueryClick(e.target));
|
|
78
|
-
|
|
79
|
-
// Shadow-root click forwarder — shadow DOM swallows bubbling, so anything
|
|
80
|
-
// inside a shadow tree dispatches `gg:shadow:click` with the real target.
|
|
81
|
-
document.addEventListener("gg:shadow:click", (e) =>
|
|
82
|
-
handleQueryClick(e.detail.target),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
initQueryBindings();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ---- gg-query-bind: input/textarea/select <-> URL param ----
|
|
89
|
-
|
|
90
|
-
function syncBindInputFromUrl(el) {
|
|
91
|
-
const key = el.getAttribute("gg-query-bind");
|
|
92
|
-
if (!key) return;
|
|
93
|
-
const value = new URL(window.location).searchParams.get(key) ?? "";
|
|
94
|
-
if (el.value !== value) el.value = value;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function setupQueryBindInput(el) {
|
|
98
|
-
const key = el.getAttribute("gg-query-bind");
|
|
99
|
-
if (!key) return;
|
|
100
|
-
const debounceMs = parseInt(el.getAttribute("gg-query-debounce") ?? "0", 10) || 0;
|
|
101
|
-
|
|
102
|
-
syncBindInputFromUrl(el);
|
|
103
|
-
|
|
104
|
-
let timer;
|
|
105
|
-
let suppress = false;
|
|
106
|
-
el.addEventListener("input", () => {
|
|
107
|
-
if (suppress) return;
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
const fire = () => {
|
|
110
|
-
const value = el.value;
|
|
111
|
-
if (value === "") {
|
|
112
|
-
removeQueryParams([key]);
|
|
113
|
-
} else {
|
|
114
|
-
setQueryParams([{ key, value }]);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
if (debounceMs > 0) {
|
|
118
|
-
timer = setTimeout(fire, debounceMs);
|
|
119
|
-
} else {
|
|
120
|
-
fire();
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// When the URL changes from elsewhere (back button, programmatic), mirror
|
|
125
|
-
// it into the input without re-firing the input listener.
|
|
126
|
-
onQueryChanged((changedKey, value) => {
|
|
127
|
-
if (changedKey !== key) return;
|
|
128
|
-
const next = value ?? "";
|
|
129
|
-
if (el.value === next) return;
|
|
130
|
-
suppress = true;
|
|
131
|
-
el.value = next;
|
|
132
|
-
suppress = false;
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function initQueryBindings() {
|
|
137
|
-
document.querySelectorAll("[gg-query-bind]").forEach(setupQueryBindInput);
|
|
138
|
-
|
|
139
|
-
// Back/forward navigation doesn't fire pushState notifications, so
|
|
140
|
-
// re-sync all bound inputs from the URL on popstate.
|
|
141
|
-
window.addEventListener("popstate", () => {
|
|
142
|
-
document.querySelectorAll("[gg-query-bind]").forEach(syncBindInputFromUrl);
|
|
143
|
-
});
|
|
144
|
-
}
|
package/src/switch-engine.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { applySwitchState } from "./helpers/dom.js";
|
|
2
|
-
|
|
3
|
-
export function initSwitchEngine() {
|
|
4
|
-
// Re-apply whenever gg-switch-state changes on any element.
|
|
5
|
-
new MutationObserver((mutations) => {
|
|
6
|
-
mutations.forEach((m) => {
|
|
7
|
-
if (m.attributeName === "gg-switch-state") {
|
|
8
|
-
applySwitchState(m.target);
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
}).observe(document.body, {
|
|
12
|
-
attributes: true,
|
|
13
|
-
attributeFilter: ["gg-switch-state"],
|
|
14
|
-
subtree: true,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
// Apply initial state for any elements that already have gg-switch-state
|
|
18
|
-
// set at load time, so we don't flash all children before the first mutation.
|
|
19
|
-
document
|
|
20
|
-
.querySelectorAll("[gg-switch-state]")
|
|
21
|
-
.forEach(applySwitchState);
|
|
22
|
-
}
|