@excom/quark 0.1.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/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +4 -0
- package/language.ts +7 -0
- package/package.json +45 -0
- package/rush-logs/quark.apply-exports.cache.log +1 -0
- package/rush-logs/quark.apply-exports.log +1 -0
- package/rush-logs/quark.build_package-metas.cache.log +1 -0
- package/rush-logs/quark.build_package-metas.log +1 -0
- package/src/actions.ts +445 -0
- package/src/ast.ts +187 -0
- package/src/bindings.ts +115 -0
- package/src/builtin-modules.ts +401 -0
- package/src/constants.ts +47 -0
- package/src/cycle-check.ts +121 -0
- package/src/devtools-hook.ts +303 -0
- package/src/element-api.ts +109 -0
- package/src/evaluator.ts +317 -0
- package/src/form-controls.ts +69 -0
- package/src/language-tables.ts +137 -0
- package/src/language.ts +1225 -0
- package/src/observer.ts +110 -0
- package/src/paint.ts +305 -0
- package/src/properties.ts +818 -0
- package/src/props.ts +94 -0
- package/src/quark-element.ts +7 -0
- package/src/quark-internal.ts +365 -0
- package/src/quark.ts +817 -0
- package/src/resolvers.ts +867 -0
- package/src/rule.ts +800 -0
- package/src/scope-id.ts +47 -0
- package/src/selector-utils.ts +430 -0
- package/src/settle.ts +117 -0
- package/src/types.ts +155 -0
- package/src/utils.ts +79 -0
- package/src/variables.ts +359 -0
- package/support/demos/css-variables.html +18 -0
- package/support/demos/dispatch.html +20 -0
- package/support/demos/events.html +9 -0
- package/support/demos/iterate.html +13 -0
- package/support/demos/js-api.html +17 -0
- package/support/demos/provider-list.html +16 -0
- package/support/demos/toggle-content.html +14 -0
- package/support/demos/unset.html +12 -0
- package/support/demos/view-transition.html +36 -0
- package/support/docs/ATTRIBUTES.md +37 -0
- package/support/docs/AT_RULES.md +45 -0
- package/support/docs/BUILTINS.md +57 -0
- package/support/docs/CONTENT.md +40 -0
- package/support/docs/CSS_VARIABLES.md +16 -0
- package/support/docs/DECLARATIONS.md +26 -0
- package/support/docs/DELAY.md +37 -0
- package/support/docs/DIAGNOSTICS.md +32 -0
- package/support/docs/DISPATCH.md +81 -0
- package/support/docs/ELEMENT_PROPERTIES.md +34 -0
- package/support/docs/EXPRESSIONS.md +31 -0
- package/support/docs/INTERNAL.md +8 -0
- package/support/docs/JS_API.md +53 -0
- package/support/docs/JS_WRITES.md +36 -0
- package/support/docs/LIMITATIONS.md +26 -0
- package/support/docs/LOOP_GUARD.md +30 -0
- package/support/docs/METHODS.md +43 -0
- package/support/docs/MODULES.md +111 -0
- package/support/docs/NO_REVERSION.md +18 -0
- package/support/docs/ON.md +87 -0
- package/support/docs/REACTIVITY.md +24 -0
- package/support/docs/README.md +143 -0
- package/support/docs/SELECTORS.md +87 -0
- package/support/docs/SHEETS.md +67 -0
- package/support/docs/SYNTAX.md +36 -0
- package/support/docs/USE.md +24 -0
- package/support/docs/VALUES.md +27 -0
- package/support/docs/VARIABLES.md +47 -0
- package/support/docs/VIEW_TRANSITION.md +64 -0
- package/support/docs-sections.json +65 -0
- package/support/package-meta.json +208 -0
- package/support/scripts/build-language-docs.mjs +196 -0
- package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
- package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
- package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
- package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
- package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
- package/support/tests/actions.test.ts +437 -0
- package/support/tests/builtin-modules.test.ts +337 -0
- package/support/tests/builtins.test.ts +189 -0
- package/support/tests/css-variables.view.test.ts +35 -0
- package/support/tests/delay.test.ts +244 -0
- package/support/tests/devtools-hook.test.ts +496 -0
- package/support/tests/diagnostics.test.ts +224 -0
- package/support/tests/dispatch.view.test.ts +43 -0
- package/support/tests/element-api.test.ts +202 -0
- package/support/tests/evaluator.test.ts +237 -0
- package/support/tests/events.view.test.ts +40 -0
- package/support/tests/form-controls.test.ts +452 -0
- package/support/tests/helpers.ts +354 -0
- package/support/tests/internals.test.ts +568 -0
- package/support/tests/iterate.view.test.ts +40 -0
- package/support/tests/js-api.view.test.ts +45 -0
- package/support/tests/language-docs.test.ts +147 -0
- package/support/tests/lifecycle.test.ts +440 -0
- package/support/tests/loop-guard.test.ts +323 -0
- package/support/tests/provider-list.view.test.ts +47 -0
- package/support/tests/quark-features.test.ts +2887 -0
- package/support/tests/quark-with-providers.test.ts +169 -0
- package/support/tests/quark.test.ts +359 -0
- package/support/tests/resolvers.test.ts +346 -0
- package/support/tests/selector-analysis.test.ts +281 -0
- package/support/tests/selectors.test.ts +670 -0
- package/support/tests/toggle-content.view.test.ts +40 -0
- package/support/tests/unset.view.test.ts +42 -0
- package/support/tests/view-helpers.ts +81 -0
- package/support/tests/view-transition.test.ts +1069 -0
- package/support/tests/view-transition.view.test.ts +76 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
package/config/rig.json
ADDED
package/index.ts
ADDED
package/language.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@excom/quark/language`: documented language metadata (keywords,
|
|
3
|
+
* declaration kinds, at-rules, built-ins, allowed methods, combinators,
|
|
4
|
+
* pseudo-classes) for the README language reference. Off the main entry
|
|
5
|
+
* so the prose stays out of app bundles.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./src/language";
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@excom/quark",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CSS-like DOM orchestration language for binding, rendering, and events",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.13.0"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@excom/kit-devtools": "^0.1.0",
|
|
12
|
+
"@excom/hash-object": "^0.1.0",
|
|
13
|
+
"@excom/kit-logger": "^0.1.0",
|
|
14
|
+
"@excom/kit-utils": "^0.1.0",
|
|
15
|
+
"@excom/quark-parser": "^0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@excom/heft-rig": "^0.1.0",
|
|
20
|
+
"@excom/provider-fetch": "^0.1.0",
|
|
21
|
+
"@excom/provider-storage": "^0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"url": "excom-dev/nucleus",
|
|
25
|
+
"directory": "packages/quark"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/quark/support/docs/README.md",
|
|
28
|
+
"bugs": "https://github.com/excom-dev/nucleus/issues",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"quark"
|
|
31
|
+
],
|
|
32
|
+
"excom": {
|
|
33
|
+
"packageType": "library"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
|
|
37
|
+
"build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
|
|
38
|
+
"format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
|
|
39
|
+
"test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
|
|
40
|
+
"coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
|
|
41
|
+
"dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
|
|
42
|
+
"preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
|
|
43
|
+
"build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Caching has been disabled for this project's "apply-exports" command.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
package/src/actions.ts
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dispatch` / `@command` statements: the outgoing half of `@on`.
|
|
3
|
+
* Accepted inside `@on … { }` blocks (and `@delay` blocks within them)
|
|
4
|
+
* only, so a dispatch is always caused by an event the sheet heard —
|
|
5
|
+
* never by a rule matching. Options are evaluated per event, in the
|
|
6
|
+
* block's scope (`event`, `target`, `element`, `$bindings`).
|
|
7
|
+
*
|
|
8
|
+
* @on click { @dispatch cart-add (detail: (sku: $sku), target: "cart-view"); }
|
|
9
|
+
* @on click { @command --refresh (target: "#feed"); }
|
|
10
|
+
*
|
|
11
|
+
* Dispatch is synchronous at the end of the block (after its writes are
|
|
12
|
+
* queued, before they paint): an event is an occurrence, not a delivery
|
|
13
|
+
* of State. Chains are one loop-guard hop per dispatch, so a cycle is cut
|
|
14
|
+
* like a write cycle.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
type EventName,
|
|
18
|
+
eventNamesText,
|
|
19
|
+
type ListenerOptionSource,
|
|
20
|
+
listenerOptionsText,
|
|
21
|
+
} from "./ast";
|
|
22
|
+
import { isNoop } from "./constants";
|
|
23
|
+
import { publicize } from "./devtools-hook";
|
|
24
|
+
import { resolveExpression } from "./resolvers";
|
|
25
|
+
import type { Rule } from "./rule";
|
|
26
|
+
import type { QuarkOptions } from "./types";
|
|
27
|
+
import { QuarkLogger } from "./utils";
|
|
28
|
+
import { formToJson, LoopGuard, selectAll, tc } from "@excom/kit-utils";
|
|
29
|
+
|
|
30
|
+
export type ActionKind = "dispatch" | "command";
|
|
31
|
+
|
|
32
|
+
/** Event-init flags an `@dispatch` may set; bare = `true`. */
|
|
33
|
+
const DISPATCH_FLAGS = ["bubbles", "cancelable", "composed"] as const;
|
|
34
|
+
type DispatchFlag = (typeof DISPATCH_FLAGS)[number];
|
|
35
|
+
const DISPATCH_DEFAULTS: Record<DispatchFlag, boolean> = {
|
|
36
|
+
bubbles: true,
|
|
37
|
+
cancelable: true,
|
|
38
|
+
composed: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Native commands the fallback invokes directly when the browser has no
|
|
43
|
+
* Invoker Commands API (`button.commandForElement`).
|
|
44
|
+
*/
|
|
45
|
+
const NATIVE_COMMANDS: Record<string, string> = {
|
|
46
|
+
"show-modal": "showModal",
|
|
47
|
+
close: "close",
|
|
48
|
+
"request-close": "requestClose",
|
|
49
|
+
"show-popover": "showPopover",
|
|
50
|
+
"hide-popover": "hidePopover",
|
|
51
|
+
"toggle-popover": "togglePopover",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const supportsInvokers = (): boolean =>
|
|
55
|
+
typeof HTMLButtonElement !== "undefined" &&
|
|
56
|
+
"commandForElement" in HTMLButtonElement.prototype;
|
|
57
|
+
|
|
58
|
+
interface ResolvedTargets {
|
|
59
|
+
/** Where to dispatch / invoke; empty when nothing matched. */
|
|
60
|
+
targets: EventTarget[];
|
|
61
|
+
/** `host: window | document` was used. */
|
|
62
|
+
onHost: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class Action {
|
|
66
|
+
kind: ActionKind;
|
|
67
|
+
/** Event / command names in source order. */
|
|
68
|
+
names: string[];
|
|
69
|
+
optionSources: ListenerOptionSource[];
|
|
70
|
+
/** Display key: `@dispatch cart-add (detail: $d)`. */
|
|
71
|
+
key: string;
|
|
72
|
+
parent: Rule;
|
|
73
|
+
/** Option / target problems already reported, per element. */
|
|
74
|
+
private warned = new WeakMap<Element, Set<string>>();
|
|
75
|
+
constructor({
|
|
76
|
+
kind,
|
|
77
|
+
names,
|
|
78
|
+
optionSources,
|
|
79
|
+
parent,
|
|
80
|
+
}: {
|
|
81
|
+
kind: ActionKind;
|
|
82
|
+
names: EventName[];
|
|
83
|
+
optionSources: ListenerOptionSource[];
|
|
84
|
+
parent: Rule;
|
|
85
|
+
}) {
|
|
86
|
+
this.kind = kind;
|
|
87
|
+
this.names = names.map((n) => n.name);
|
|
88
|
+
this.optionSources = optionSources;
|
|
89
|
+
this.key = `@${kind} ${eventNamesText(names)}${listenerOptionsText(optionSources)}`;
|
|
90
|
+
this.parent = parent;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private warnOnce(element: Element, topic: string, message: string) {
|
|
94
|
+
let topics = this.warned.get(element);
|
|
95
|
+
if (!topics) this.warned.set(element, (topics = new Set()));
|
|
96
|
+
if (topics.has(topic)) return;
|
|
97
|
+
topics.add(topic);
|
|
98
|
+
QuarkLogger.warn({
|
|
99
|
+
method: this.kind,
|
|
100
|
+
message: `Quark: ${this.key} — ${message} (${this.parent.selector})`,
|
|
101
|
+
element,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private selectTargets(
|
|
106
|
+
element: HTMLElement,
|
|
107
|
+
selector: string
|
|
108
|
+
): Element[] | undefined {
|
|
109
|
+
return tc(() => selectAll(selector, { scope: element }) ?? []);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `target:` / `host:` → the event targets; the matched element by default. */
|
|
113
|
+
private resolveTargets(
|
|
114
|
+
element: HTMLElement,
|
|
115
|
+
host: unknown,
|
|
116
|
+
target: unknown,
|
|
117
|
+
targetText: string | null | undefined
|
|
118
|
+
): ResolvedTargets {
|
|
119
|
+
if (host === "window" || host === "document") {
|
|
120
|
+
return {
|
|
121
|
+
targets: [
|
|
122
|
+
host === "window"
|
|
123
|
+
? ((element.ownerDocument.defaultView as EventTarget) ?? window)
|
|
124
|
+
: element.ownerDocument,
|
|
125
|
+
],
|
|
126
|
+
onHost: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (targetText === undefined) return { targets: [element], onHost: false };
|
|
130
|
+
let targets: Element[] | undefined;
|
|
131
|
+
if (typeof target === "string") {
|
|
132
|
+
targets = this.selectTargets(element, target);
|
|
133
|
+
if (!targets) {
|
|
134
|
+
this.warnOnce(
|
|
135
|
+
element,
|
|
136
|
+
"target",
|
|
137
|
+
`target "${target}" is not a valid selector`
|
|
138
|
+
);
|
|
139
|
+
return { targets: [], onHost: false };
|
|
140
|
+
}
|
|
141
|
+
} else if (target instanceof Element) {
|
|
142
|
+
targets = [target];
|
|
143
|
+
} else if (
|
|
144
|
+
target &&
|
|
145
|
+
typeof target === "object" &&
|
|
146
|
+
Symbol.iterator in target
|
|
147
|
+
) {
|
|
148
|
+
targets = Array.from(target as Iterable<unknown>).filter(
|
|
149
|
+
(t): t is Element => t instanceof Element
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
targets = [];
|
|
153
|
+
}
|
|
154
|
+
if (!targets.length) {
|
|
155
|
+
this.warnOnce(
|
|
156
|
+
element,
|
|
157
|
+
"target",
|
|
158
|
+
`target ${typeof target === "string" ? `"${target}"` : String(target)} matches no element`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return { targets, onHost: false };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
run(element: HTMLElement, options: QuarkOptions) {
|
|
165
|
+
const sheet = this.parent.quarkInstance;
|
|
166
|
+
const args = {
|
|
167
|
+
element: element as HTMLElement & { _q_: unknown },
|
|
168
|
+
key: this.key,
|
|
169
|
+
value: null as string | null,
|
|
170
|
+
options: { ...options, rule: this.parent },
|
|
171
|
+
hash: sheet.hash,
|
|
172
|
+
};
|
|
173
|
+
const evaluate = (text: string) =>
|
|
174
|
+
resolveExpression({
|
|
175
|
+
...args,
|
|
176
|
+
key: `${this.key} option`,
|
|
177
|
+
value: text,
|
|
178
|
+
} as Parameters<typeof resolveExpression>[0]);
|
|
179
|
+
let detail: unknown;
|
|
180
|
+
let hasDetail = false;
|
|
181
|
+
let form: unknown;
|
|
182
|
+
let hostOption: unknown;
|
|
183
|
+
let target: unknown;
|
|
184
|
+
let targetText: string | null | undefined;
|
|
185
|
+
const flags: Partial<Record<DispatchFlag, boolean>> = {};
|
|
186
|
+
for (const { name, text, ident } of this.optionSources) {
|
|
187
|
+
if (name === "target") {
|
|
188
|
+
targetText = text;
|
|
189
|
+
target = text === null ? undefined : evaluate(text);
|
|
190
|
+
if (text === null) {
|
|
191
|
+
this.warnOnce(
|
|
192
|
+
element,
|
|
193
|
+
name,
|
|
194
|
+
`option "target" needs a selector or an element`
|
|
195
|
+
);
|
|
196
|
+
targetText = undefined;
|
|
197
|
+
}
|
|
198
|
+
} else if (this.kind === "command") {
|
|
199
|
+
this.warnOnce(
|
|
200
|
+
element,
|
|
201
|
+
name,
|
|
202
|
+
`option "${name}" is not a @command option`
|
|
203
|
+
);
|
|
204
|
+
} else if (name === "detail") {
|
|
205
|
+
hasDetail = text !== null;
|
|
206
|
+
detail = text === null ? undefined : evaluate(text);
|
|
207
|
+
if (text === null)
|
|
208
|
+
this.warnOnce(element, name, `option "detail" needs a value`);
|
|
209
|
+
} else if (name === "form") {
|
|
210
|
+
form = text === null ? undefined : evaluate(text);
|
|
211
|
+
if (text === null)
|
|
212
|
+
this.warnOnce(
|
|
213
|
+
element,
|
|
214
|
+
name,
|
|
215
|
+
`option "form" needs a form or a selector`
|
|
216
|
+
);
|
|
217
|
+
} else if (name === "host") {
|
|
218
|
+
if (ident === "window" || ident === "document") hostOption = ident;
|
|
219
|
+
else
|
|
220
|
+
this.warnOnce(
|
|
221
|
+
element,
|
|
222
|
+
name,
|
|
223
|
+
`option "host" must be window or document`
|
|
224
|
+
);
|
|
225
|
+
} else if ((DISPATCH_FLAGS as readonly string[]).includes(name)) {
|
|
226
|
+
const value = text === null ? true : evaluate(text);
|
|
227
|
+
if (typeof value === "boolean") flags[name as DispatchFlag] = value;
|
|
228
|
+
else this.warnOnce(element, name, `option "${name}" needs a boolean`);
|
|
229
|
+
} else {
|
|
230
|
+
this.warnOnce(element, name, `unknown option "${name}"`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (isNoop(detail) || isNoop(target) || isNoop(form)) return;
|
|
234
|
+
const { targets, onHost } = this.resolveTargets(
|
|
235
|
+
element,
|
|
236
|
+
hostOption,
|
|
237
|
+
target,
|
|
238
|
+
targetText
|
|
239
|
+
);
|
|
240
|
+
if (this.kind === "command" && onHost) return;
|
|
241
|
+
const formValues = this.formValues(element, form);
|
|
242
|
+
if (formValues === false) return;
|
|
243
|
+
const publish = (
|
|
244
|
+
name: string,
|
|
245
|
+
phase: "dispatched" | "dropped",
|
|
246
|
+
extra: Record<string, unknown> = {}
|
|
247
|
+
) =>
|
|
248
|
+
publicize(["quark", this.kind], {
|
|
249
|
+
weakElement: new WeakRef(element),
|
|
250
|
+
tag: element.localName,
|
|
251
|
+
selector: this.parent.selector,
|
|
252
|
+
ruleId: this.parent.id,
|
|
253
|
+
sheetId: sheet.id,
|
|
254
|
+
runId: options.runId ?? null,
|
|
255
|
+
name,
|
|
256
|
+
targets: targets.length,
|
|
257
|
+
phase,
|
|
258
|
+
...extra,
|
|
259
|
+
});
|
|
260
|
+
for (const name of this.names) {
|
|
261
|
+
// the enclosing event re-dispatched from its own handler is a loop by construction
|
|
262
|
+
if (
|
|
263
|
+
this.kind === "dispatch" &&
|
|
264
|
+
options.event &&
|
|
265
|
+
options.event.type === name
|
|
266
|
+
) {
|
|
267
|
+
this.warnOnce(
|
|
268
|
+
element,
|
|
269
|
+
`same:${name}`,
|
|
270
|
+
`refusing to dispatch "${name}" from its own @on ${name} block`
|
|
271
|
+
);
|
|
272
|
+
publish(name, "dropped", { reason: "same-event" });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// an unmatched `target:` already warned; nothing to send to
|
|
276
|
+
targets.forEach((eventTarget) => {
|
|
277
|
+
const done =
|
|
278
|
+
this.kind === "dispatch"
|
|
279
|
+
? this.dispatch(eventTarget, name, {
|
|
280
|
+
detail: hasDetail ? detail : undefined,
|
|
281
|
+
formValues,
|
|
282
|
+
flags,
|
|
283
|
+
})
|
|
284
|
+
: this.command(eventTarget as Element, name, element);
|
|
285
|
+
if (done === false) {
|
|
286
|
+
publish(name, "dropped", { reason: "loop-guard" });
|
|
287
|
+
} else if (done === null) {
|
|
288
|
+
publish(name, "dropped", { reason: "unsupported" });
|
|
289
|
+
} else {
|
|
290
|
+
publish(name, "dispatched", done);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** `form:` → its values (`formToJson`); `false` when the option is unusable. */
|
|
297
|
+
private formValues(
|
|
298
|
+
element: HTMLElement,
|
|
299
|
+
form: unknown
|
|
300
|
+
): Record<string, unknown> | undefined | false {
|
|
301
|
+
if (form === undefined) return undefined;
|
|
302
|
+
let formElement: unknown = form;
|
|
303
|
+
if (typeof form === "string") {
|
|
304
|
+
const found = this.selectTargets(element, form);
|
|
305
|
+
if (!found) {
|
|
306
|
+
this.warnOnce(
|
|
307
|
+
element,
|
|
308
|
+
"form",
|
|
309
|
+
`form "${form}" is not a valid selector`
|
|
310
|
+
);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
formElement = found.find((el) => el instanceof HTMLFormElement);
|
|
314
|
+
}
|
|
315
|
+
if (!(formElement instanceof HTMLFormElement)) {
|
|
316
|
+
this.warnOnce(
|
|
317
|
+
element,
|
|
318
|
+
"form",
|
|
319
|
+
`form ${typeof form === "string" ? `"${form}"` : String(form)} is not a <form>`
|
|
320
|
+
);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
return formToJson(formElement) as Record<string, unknown>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Dispatch one `CustomEvent`; `false` when the loop guard dropped it. */
|
|
327
|
+
private dispatch(
|
|
328
|
+
target: EventTarget,
|
|
329
|
+
name: string,
|
|
330
|
+
{
|
|
331
|
+
detail,
|
|
332
|
+
formValues,
|
|
333
|
+
flags,
|
|
334
|
+
}: {
|
|
335
|
+
detail: unknown;
|
|
336
|
+
formValues: Record<string, unknown> | undefined;
|
|
337
|
+
flags: Partial<Record<DispatchFlag, boolean>>;
|
|
338
|
+
}
|
|
339
|
+
): Record<string, unknown> | false {
|
|
340
|
+
const init: CustomEventInit = {
|
|
341
|
+
bubbles: flags.bubbles ?? DISPATCH_DEFAULTS.bubbles,
|
|
342
|
+
cancelable: flags.cancelable ?? DISPATCH_DEFAULTS.cancelable,
|
|
343
|
+
composed: flags.composed ?? DISPATCH_DEFAULTS.composed,
|
|
344
|
+
};
|
|
345
|
+
// form values are the base detail; an explicit map merges over them
|
|
346
|
+
const merged =
|
|
347
|
+
formValues &&
|
|
348
|
+
detail &&
|
|
349
|
+
typeof detail === "object" &&
|
|
350
|
+
!Array.isArray(detail)
|
|
351
|
+
? { ...formValues, ...(detail as Record<string, unknown>) }
|
|
352
|
+
: (detail ?? formValues);
|
|
353
|
+
if (merged !== undefined) init.detail = merged;
|
|
354
|
+
const event = new CustomEvent(name, init);
|
|
355
|
+
// `dispatchEvent` returns false when canceled; the guard's false means dropped
|
|
356
|
+
const dispatched = LoopGuard.write(target, `@dispatch ${name}`, () =>
|
|
357
|
+
LoopGuard.run(LoopGuard.current() + 1, () => {
|
|
358
|
+
target.dispatchEvent(event);
|
|
359
|
+
return true;
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
if (dispatched === false) return false;
|
|
363
|
+
return {
|
|
364
|
+
bubbles: init.bubbles,
|
|
365
|
+
cancelable: init.cancelable,
|
|
366
|
+
composed: init.composed,
|
|
367
|
+
defaultPrevented: event.defaultPrevented,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Invoke one command on `target`: through a hidden invoker button
|
|
373
|
+
* where the browser has the Command API (native and custom commands
|
|
374
|
+
* alike), else a synthetic `command` event for custom `--names` and a
|
|
375
|
+
* direct method call for the native ones. `null` when unsupported.
|
|
376
|
+
*/
|
|
377
|
+
private command(
|
|
378
|
+
target: Element,
|
|
379
|
+
name: string,
|
|
380
|
+
source: HTMLElement
|
|
381
|
+
): Record<string, unknown> | false | null {
|
|
382
|
+
const invoke = (): "invoker" | "event" | "method" | null => {
|
|
383
|
+
if (supportsInvokers()) {
|
|
384
|
+
const button = target.ownerDocument.createElement(
|
|
385
|
+
"button"
|
|
386
|
+
) as HTMLButtonElement & {
|
|
387
|
+
commandForElement: Element;
|
|
388
|
+
command: string;
|
|
389
|
+
};
|
|
390
|
+
button.type = "button";
|
|
391
|
+
button.style.display = "none";
|
|
392
|
+
source.ownerDocument.body.appendChild(button);
|
|
393
|
+
button.commandForElement = target;
|
|
394
|
+
button.command = name;
|
|
395
|
+
try {
|
|
396
|
+
button.click();
|
|
397
|
+
} finally {
|
|
398
|
+
button.remove();
|
|
399
|
+
}
|
|
400
|
+
return "invoker";
|
|
401
|
+
}
|
|
402
|
+
if (name.startsWith("--")) {
|
|
403
|
+
const init = { bubbles: false, cancelable: true, composed: true };
|
|
404
|
+
const CommandEventCtor = (
|
|
405
|
+
globalThis as {
|
|
406
|
+
CommandEvent?: new (type: string, init: object) => Event;
|
|
407
|
+
}
|
|
408
|
+
).CommandEvent;
|
|
409
|
+
const event = CommandEventCtor
|
|
410
|
+
? new CommandEventCtor("command", { ...init, command: name, source })
|
|
411
|
+
: Object.defineProperties(new CustomEvent("command", init), {
|
|
412
|
+
command: { value: name },
|
|
413
|
+
source: { value: source },
|
|
414
|
+
});
|
|
415
|
+
target.dispatchEvent(event);
|
|
416
|
+
return "event";
|
|
417
|
+
}
|
|
418
|
+
const method = NATIVE_COMMANDS[name];
|
|
419
|
+
const fn =
|
|
420
|
+
method && (target as unknown as Record<string, unknown>)[method];
|
|
421
|
+
if (typeof fn === "function") {
|
|
422
|
+
fn.call(target);
|
|
423
|
+
return "method";
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
};
|
|
427
|
+
let via: "invoker" | "event" | "method" | null = null;
|
|
428
|
+
const done = LoopGuard.write(target, `@command ${name}`, () =>
|
|
429
|
+
LoopGuard.run(LoopGuard.current() + 1, () => {
|
|
430
|
+
via = invoke();
|
|
431
|
+
return true;
|
|
432
|
+
})
|
|
433
|
+
);
|
|
434
|
+
if (done === false) return false;
|
|
435
|
+
if (via === null) {
|
|
436
|
+
this.warnOnce(
|
|
437
|
+
source,
|
|
438
|
+
`command:${name}`,
|
|
439
|
+
`command "${name}" is not supported here (custom commands start with --)`
|
|
440
|
+
);
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
return { via };
|
|
444
|
+
}
|
|
445
|
+
}
|