@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
package/src/resolvers.ts
ADDED
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
import type { ListenerOptionSource } from "./ast";
|
|
2
|
+
import type { DiagnosticLevel } from "./ast";
|
|
3
|
+
import { isNoop, isWipe, SYMBOL_NOOP } from "./constants";
|
|
4
|
+
import { getDevtoolsHook, publicize } from "./devtools-hook";
|
|
5
|
+
import { evaluateExpression, getExpressionAst } from "./evaluator";
|
|
6
|
+
import { isFormControlAttribute, syncTextControl } from "./form-controls";
|
|
7
|
+
import { type PaintEntry, type PaintTransition, schedulePaint } from "./paint";
|
|
8
|
+
import {
|
|
9
|
+
DYNAMIC_LISTENER_OPTIONS,
|
|
10
|
+
type Listener,
|
|
11
|
+
listenerHost,
|
|
12
|
+
type ResolvedListenerOptions,
|
|
13
|
+
} from "./properties";
|
|
14
|
+
import { getQuarkInternal } from "./quark-internal";
|
|
15
|
+
import { type SettleUntil, trackPending } from "./settle";
|
|
16
|
+
import type { ContextField, ExpressionResult, TransitionSpec } from "./types";
|
|
17
|
+
import { QuarkLogger } from "./utils";
|
|
18
|
+
import { createScope } from "./variables";
|
|
19
|
+
import {
|
|
20
|
+
getChildren,
|
|
21
|
+
isPojo,
|
|
22
|
+
LoopGuard,
|
|
23
|
+
objToAttrs,
|
|
24
|
+
Queue,
|
|
25
|
+
replaceNonTemplateChildren,
|
|
26
|
+
tc,
|
|
27
|
+
} from "@excom/kit-utils";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Write text only when it differs from what's painted: one text node
|
|
31
|
+
* holding `text` (or no children for `""`). Same-string re-runs skip
|
|
32
|
+
* the DOM. Anything else in the target (elements, several nodes) is
|
|
33
|
+
* replaced as before.
|
|
34
|
+
*/
|
|
35
|
+
const paintText = (target: Node, text: string) => {
|
|
36
|
+
const first = target.firstChild;
|
|
37
|
+
if (!(first === null ? text === "" : isSoleText(first, text))) {
|
|
38
|
+
target.textContent = text;
|
|
39
|
+
}
|
|
40
|
+
// a <textarea>'s text is only its default value: mirror it to .value
|
|
41
|
+
syncTextControl(target, text);
|
|
42
|
+
};
|
|
43
|
+
const isSoleText = (node: ChildNode, text: string) =>
|
|
44
|
+
node.nodeType === Node.TEXT_NODE &&
|
|
45
|
+
node.nextSibling === null &&
|
|
46
|
+
(node as Text).data === text;
|
|
47
|
+
|
|
48
|
+
/** Clear rendered content. `null` from expressions (e.g. `none`) lands here. */
|
|
49
|
+
const wipeContent = (element: Element) => {
|
|
50
|
+
const { templateChild } = getChildren(element);
|
|
51
|
+
if (templateChild) {
|
|
52
|
+
// Keep the source <template>; drop siblings (elements + text).
|
|
53
|
+
replaceNonTemplateChildren(element, []);
|
|
54
|
+
} else {
|
|
55
|
+
element.textContent = "";
|
|
56
|
+
}
|
|
57
|
+
syncTextControl(element, "");
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const resolveExpression = ({
|
|
61
|
+
element,
|
|
62
|
+
options,
|
|
63
|
+
value,
|
|
64
|
+
key,
|
|
65
|
+
}: ContextField) => {
|
|
66
|
+
if (!value) return undefined;
|
|
67
|
+
try {
|
|
68
|
+
const ast = getExpressionAst(value);
|
|
69
|
+
// names resolve lazily as the evaluator reaches them
|
|
70
|
+
const scope = createScope({ element, options });
|
|
71
|
+
return evaluateExpression(ast, { scope, element });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
QuarkLogger.error({
|
|
74
|
+
method: "resolveExpression",
|
|
75
|
+
message: "Could not resolve expression",
|
|
76
|
+
element,
|
|
77
|
+
script: value,
|
|
78
|
+
error: [error],
|
|
79
|
+
});
|
|
80
|
+
const err = error as { message?: string; name?: string } | null;
|
|
81
|
+
publicize(["quark", "error"], {
|
|
82
|
+
weakElement: new WeakRef(element),
|
|
83
|
+
tag: element.localName,
|
|
84
|
+
selector: options.rule?.selector ?? null,
|
|
85
|
+
ruleId: options.rule?.id ?? null,
|
|
86
|
+
sheetId: options.rule?.quarkInstance.id ?? null,
|
|
87
|
+
runId: options.runId ?? null,
|
|
88
|
+
key,
|
|
89
|
+
expression: value,
|
|
90
|
+
errorMessage: err?.message ? String(err.message) : String(error),
|
|
91
|
+
errorName: err?.name ? String(err.name) : undefined,
|
|
92
|
+
});
|
|
93
|
+
// failed evaluations never destroy state, they no-op
|
|
94
|
+
return SYMBOL_NOOP;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Bare-flag `@on` options, resolved once per match. */
|
|
99
|
+
const LISTENER_FLAGS: Record<string, keyof ResolvedListenerOptions> = {
|
|
100
|
+
once: "once",
|
|
101
|
+
self: "self",
|
|
102
|
+
passive: "passive",
|
|
103
|
+
capture: "capture",
|
|
104
|
+
"prevent-default": "preventDefault",
|
|
105
|
+
"stop-propagation": "stopPropagation",
|
|
106
|
+
"stop-immediate-propagation": "stopImmediatePropagation",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const warnOption = (args: ContextField, message: string) =>
|
|
110
|
+
QuarkLogger.warn({
|
|
111
|
+
method: "listener",
|
|
112
|
+
message: `Quark: ${args.key} — ${message}`,
|
|
113
|
+
element: args.element,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Split an `@on` options group for one matched element: flags and the
|
|
118
|
+
* `host` word resolve now (they configure the registration or act on the
|
|
119
|
+
* event); `target` / `key` / `debounce` / `throttle` / `handle` are kept
|
|
120
|
+
* as sources and evaluated by the wrapper when the event fires. Unknown
|
|
121
|
+
* options warn and are ignored.
|
|
122
|
+
*/
|
|
123
|
+
export const resolveListenerOptions = (
|
|
124
|
+
args: ContextField,
|
|
125
|
+
sources: ListenerOptionSource[]
|
|
126
|
+
): { opts: ResolvedListenerOptions; dynamic: ListenerOptionSource[] } => {
|
|
127
|
+
const opts: ResolvedListenerOptions = {};
|
|
128
|
+
let dynamic: ListenerOptionSource[] = [];
|
|
129
|
+
for (const source of sources) {
|
|
130
|
+
const { name, text, ident } = source;
|
|
131
|
+
if (name in LISTENER_FLAGS) {
|
|
132
|
+
if (text !== null) warnOption(args, `option "${name}" takes no value`);
|
|
133
|
+
opts[LISTENER_FLAGS[name]] = true as never;
|
|
134
|
+
} else if ((DYNAMIC_LISTENER_OPTIONS as readonly string[]).includes(name)) {
|
|
135
|
+
dynamic.push(source);
|
|
136
|
+
} else if (name === "host") {
|
|
137
|
+
if (ident === "window" || ident === "document") {
|
|
138
|
+
opts.host = ident;
|
|
139
|
+
} else {
|
|
140
|
+
warnOption(args, `option "host" must be window or document`);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
warnOption(args, `unknown option "${name}"`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
dynamic.some((o) => o.name === "debounce") &&
|
|
148
|
+
dynamic.some((o) => o.name === "throttle")
|
|
149
|
+
) {
|
|
150
|
+
warnOption(args, "debounce and throttle are exclusive; using debounce");
|
|
151
|
+
dynamic = dynamic.filter((o) => o.name !== "throttle");
|
|
152
|
+
}
|
|
153
|
+
return { opts, dynamic };
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** `@view-transition` options after evaluation (see `resolveTransitionOptions`). */
|
|
157
|
+
export interface ResolvedTransitionOptions {
|
|
158
|
+
types: string[];
|
|
159
|
+
timeout?: number;
|
|
160
|
+
delay?: number;
|
|
161
|
+
firstRender?: boolean;
|
|
162
|
+
ifActive?: "skip" | "replace";
|
|
163
|
+
until?: { selector: string } | { thenable: PromiseLike<unknown> };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `"a b"` / `("a", "b c")` → `["a", "b", "c"]`; `undefined` when not strings. */
|
|
167
|
+
const typeNames = (value: unknown): string[] | undefined => {
|
|
168
|
+
const items = Array.isArray(value) ? value.flat(Infinity) : [value];
|
|
169
|
+
if (!items.every((item) => typeof item === "string")) return undefined;
|
|
170
|
+
return (items as string[]).flatMap((item) =>
|
|
171
|
+
item.split(/\s+/).filter(Boolean)
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** Settle cap of a block's `update`: longer when it waits on `until`. */
|
|
176
|
+
export const DEFAULT_TRANSITION_TIMEOUT = 300;
|
|
177
|
+
export const DEFAULT_UNTIL_TIMEOUT = 1000;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Evaluate a `@view-transition` options group for one resolve, in the
|
|
181
|
+
* scope of the block's element (`args.element` is the owner, see
|
|
182
|
+
* `paintTransition`). `types` → names; `timeout` / `delay` →
|
|
183
|
+
* milliseconds; `first-render` flag; `if-active` → the bare word `skip` /
|
|
184
|
+
* `replace`; `until` → a selector string or a thenable. `scope` is
|
|
185
|
+
* reserved. Unknown or malformed options warn once per block and are
|
|
186
|
+
* ignored.
|
|
187
|
+
*/
|
|
188
|
+
export const resolveTransitionOptions = (
|
|
189
|
+
args: ContextField,
|
|
190
|
+
spec: TransitionSpec
|
|
191
|
+
): ResolvedTransitionOptions => {
|
|
192
|
+
const opts: ResolvedTransitionOptions = { types: [] };
|
|
193
|
+
const warn = (name: string, message: string) => {
|
|
194
|
+
if (spec.warned.has(name)) return;
|
|
195
|
+
spec.warned.add(name);
|
|
196
|
+
QuarkLogger.warn({
|
|
197
|
+
method: "viewTransition",
|
|
198
|
+
message: `Quark: ${spec.source} — ${message}`,
|
|
199
|
+
element: args.element,
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const evaluate = (text: string | null) =>
|
|
203
|
+
text === null
|
|
204
|
+
? undefined
|
|
205
|
+
: resolveExpression({
|
|
206
|
+
...args,
|
|
207
|
+
key: `${spec.source} option`,
|
|
208
|
+
value: text,
|
|
209
|
+
});
|
|
210
|
+
for (const { name, text, ident } of spec.optionSources) {
|
|
211
|
+
if (name === "types") {
|
|
212
|
+
const types = typeNames(evaluate(text));
|
|
213
|
+
if (types) opts.types = types;
|
|
214
|
+
else warn(name, `option "types" needs strings`);
|
|
215
|
+
} else if (name === "timeout" || name === "delay") {
|
|
216
|
+
const value = evaluate(text);
|
|
217
|
+
const ms = typeof value === "string" ? parseFloat(value) : value;
|
|
218
|
+
if (typeof ms === "number" && ms > 0) opts[name] = ms;
|
|
219
|
+
else
|
|
220
|
+
warn(name, `option "${name}" needs a positive number of milliseconds`);
|
|
221
|
+
} else if (name === "first-render") {
|
|
222
|
+
if (text !== null) warn(name, `option "first-render" takes no value`);
|
|
223
|
+
opts.firstRender = true;
|
|
224
|
+
} else if (name === "if-active") {
|
|
225
|
+
if (ident === "skip" || ident === "replace") opts.ifActive = ident;
|
|
226
|
+
else warn(name, `option "if-active" must be skip or replace`);
|
|
227
|
+
} else if (name === "until") {
|
|
228
|
+
const value = evaluate(text);
|
|
229
|
+
if (typeof value === "string" && value.trim()) {
|
|
230
|
+
if (tc(() => args.element.matches(value)) === undefined) {
|
|
231
|
+
warn(name, `until: "${value}" is not a valid selector`);
|
|
232
|
+
} else {
|
|
233
|
+
opts.until = { selector: value };
|
|
234
|
+
}
|
|
235
|
+
} else if (isThenable(value)) {
|
|
236
|
+
opts.until = { thenable: value };
|
|
237
|
+
} else if (value != null && !isNoop(value)) {
|
|
238
|
+
warn(name, `option "until" needs a selector or a promise`);
|
|
239
|
+
}
|
|
240
|
+
} else if (name === "scope") {
|
|
241
|
+
warn(name, `option "scope" is not supported yet`);
|
|
242
|
+
} else {
|
|
243
|
+
warn(name, `unknown option "${name}"`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return opts;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The block's element: what the rule it is written in matches (an
|
|
251
|
+
* ancestor-or-self of the painted element), or the host for a sheet-level
|
|
252
|
+
* or `:scope` block. Options are evaluated on it and `until: "<selector>"`
|
|
253
|
+
* is checked on it.
|
|
254
|
+
*/
|
|
255
|
+
const transitionOwner = (
|
|
256
|
+
element: HTMLElement,
|
|
257
|
+
spec: TransitionSpec,
|
|
258
|
+
host: HTMLElement | undefined
|
|
259
|
+
): Element | null => {
|
|
260
|
+
const owner = spec.ownerRule;
|
|
261
|
+
if (!owner || !owner.matchSelector) return host ?? null;
|
|
262
|
+
const selector = owner.isScoped
|
|
263
|
+
? owner.scopedSelector()
|
|
264
|
+
: owner.matchSelector;
|
|
265
|
+
return tc(() => element.closest(selector)) ?? null;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* How this resolve's paints commit when the declaration sits inside a
|
|
270
|
+
* `@view-transition` block; `undefined` otherwise, and during the sheet's
|
|
271
|
+
* first render unless the block says `first-render`.
|
|
272
|
+
*/
|
|
273
|
+
const paintTransition = (args: ContextField): PaintTransition | undefined => {
|
|
274
|
+
const property = args.options.property;
|
|
275
|
+
const spec = property?.transition;
|
|
276
|
+
if (!spec) return undefined;
|
|
277
|
+
const sheet = property.parent.quarkInstance;
|
|
278
|
+
// the first render costs nothing unless the block animates it
|
|
279
|
+
if ((args.options.isFirstRun || sheet.isFirstRender) && !spec.firstRender) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
// options belong to the block: `attr()` / `prop()` / `item` read its element
|
|
283
|
+
const owner = spec.optionSources.length
|
|
284
|
+
? ((transitionOwner(args.element, spec, sheet.host?.deref()) ??
|
|
285
|
+
args.element) as typeof args.element)
|
|
286
|
+
: args.element;
|
|
287
|
+
const opts: ResolvedTransitionOptions = spec.optionSources.length
|
|
288
|
+
? resolveTransitionOptions({ ...args, element: owner }, spec)
|
|
289
|
+
: { types: [] };
|
|
290
|
+
const until: SettleUntil | undefined =
|
|
291
|
+
opts.until && "selector" in opts.until
|
|
292
|
+
? { selector: opts.until.selector, owner: new WeakRef(owner) }
|
|
293
|
+
: opts.until;
|
|
294
|
+
return {
|
|
295
|
+
types: opts.types,
|
|
296
|
+
timeout:
|
|
297
|
+
opts.timeout ??
|
|
298
|
+
(until ? DEFAULT_UNTIL_TIMEOUT : DEFAULT_TRANSITION_TIMEOUT),
|
|
299
|
+
delay: opts.delay,
|
|
300
|
+
ifActive: opts.ifActive ?? "skip",
|
|
301
|
+
until,
|
|
302
|
+
source: spec.source,
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/** Paint extras for a flagged declaration, with its commit-time change check. */
|
|
307
|
+
const withTransition = (
|
|
308
|
+
transition: PaintTransition | undefined,
|
|
309
|
+
willChange: () => boolean
|
|
310
|
+
): Pick<PaintEntry, "transition" | "willChange"> | undefined =>
|
|
311
|
+
transition && { transition, willChange };
|
|
312
|
+
|
|
313
|
+
/** Would `QuarkInternal.setAttr(name, value)` change what is rendered? */
|
|
314
|
+
const attrWillChange = (
|
|
315
|
+
element: Element,
|
|
316
|
+
name: string,
|
|
317
|
+
value: unknown
|
|
318
|
+
): boolean => {
|
|
319
|
+
if (value === undefined) return false;
|
|
320
|
+
const next = typeof value === "boolean" ? (value ? "" : null) : value;
|
|
321
|
+
if (isFormControlAttribute(element, name)) {
|
|
322
|
+
// the live property may have drifted from the attribute (form-controls.ts)
|
|
323
|
+
const control = element as HTMLInputElement & Record<string, unknown>;
|
|
324
|
+
const drifted =
|
|
325
|
+
name === "value"
|
|
326
|
+
? control.value !== (next === null ? "" : String(next))
|
|
327
|
+
: control[name] !== (next !== null);
|
|
328
|
+
if (drifted) return true;
|
|
329
|
+
}
|
|
330
|
+
return next === null
|
|
331
|
+
? element.hasAttribute(name)
|
|
332
|
+
: element.getAttribute(name) !== String(next);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/** Would replacing `element`'s non-template children with `nodes` change them? */
|
|
336
|
+
const childrenWillChange = (element: Element, nodes: Node[]): boolean => {
|
|
337
|
+
const { otherChildren } = getChildren(element);
|
|
338
|
+
return (
|
|
339
|
+
otherChildren.length !== nodes.length ||
|
|
340
|
+
nodes.some((node, index) => otherChildren[index] !== node)
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/** Would `wipeContent(element)` change it? */
|
|
345
|
+
const wipeWillChange = (element: Element): boolean =>
|
|
346
|
+
getChildren(element).templateChild
|
|
347
|
+
? getChildren(element).otherChildren.length > 0
|
|
348
|
+
: element.firstChild !== null;
|
|
349
|
+
|
|
350
|
+
/** Would `paintText(target, text)` change it? */
|
|
351
|
+
const textWillChange = (target: Node, text: string): boolean => {
|
|
352
|
+
const first = target.firstChild;
|
|
353
|
+
return !(first === null ? text === "" : isSoleText(first, text));
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export const resolveField = (args: ContextField) => {
|
|
357
|
+
let key: string;
|
|
358
|
+
if (args.listener) {
|
|
359
|
+
key = "listener";
|
|
360
|
+
} else if (args.key.startsWith("$")) {
|
|
361
|
+
key = "variable";
|
|
362
|
+
} else if (args.key.startsWith("--")) {
|
|
363
|
+
key = "styleProperty";
|
|
364
|
+
} else if (args.key in FIELD_RESOLVERS) {
|
|
365
|
+
key = args.key;
|
|
366
|
+
} else {
|
|
367
|
+
key = "attribute";
|
|
368
|
+
}
|
|
369
|
+
const result = FIELD_RESOLVERS[key](args);
|
|
370
|
+
publishApply(args, key, result);
|
|
371
|
+
return result;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* DevTools: one publication per property resolve, Quark's version of a
|
|
376
|
+
* Neutron `effect`. Fires at resolve time (before the batched paint),
|
|
377
|
+
* same moment Neutron publishes before running an effect. No-op without
|
|
378
|
+
* a hook.
|
|
379
|
+
*
|
|
380
|
+
* Published as the author meant: render descriptors (`{ type: "nodes" |
|
|
381
|
+
* "html", value }` from `template()` / `iterate()` / `dangerous-html()`)
|
|
382
|
+
* unwrap to `value`, fragments / NodeLists expand to nodes, promises
|
|
383
|
+
* publish once settled, never as `Promise`.
|
|
384
|
+
*/
|
|
385
|
+
const publishApply = (
|
|
386
|
+
{ element, key, value, options }: ContextField,
|
|
387
|
+
kind: string,
|
|
388
|
+
result: unknown
|
|
389
|
+
) => {
|
|
390
|
+
if (!getDevtoolsHook()?.publicize) return;
|
|
391
|
+
const rule = options.rule;
|
|
392
|
+
const emit = (resolved: unknown, noop: boolean) =>
|
|
393
|
+
publicize(["quark", "apply"], {
|
|
394
|
+
weakElement: new WeakRef(element),
|
|
395
|
+
tag: element.localName,
|
|
396
|
+
selector: rule?.selector ?? null,
|
|
397
|
+
ruleId: rule?.id ?? null,
|
|
398
|
+
sheetId: rule?.quarkInstance.id ?? null,
|
|
399
|
+
runId: options.runId ?? null,
|
|
400
|
+
kind,
|
|
401
|
+
key,
|
|
402
|
+
expression: value,
|
|
403
|
+
result: noop ? undefined : presentResult(resolved),
|
|
404
|
+
isNoop: noop,
|
|
405
|
+
isWipe: !noop && isWipe(resolved),
|
|
406
|
+
isFirstRun: !!options.isFirstRun,
|
|
407
|
+
});
|
|
408
|
+
if (isNoop(result)) return emit(result, true);
|
|
409
|
+
if (isThenable(result)) {
|
|
410
|
+
result.then(
|
|
411
|
+
(settled) => emit(settled, isNoop(settled)),
|
|
412
|
+
() => emit(undefined, false)
|
|
413
|
+
);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
emit(result, false);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Report a `@warn` / `@debug` / `@error` statement: the console at the
|
|
421
|
+
* matching logger level (`@debug` is silent below debug level), and the
|
|
422
|
+
* DevTools hook as `quark/diagnostic` with the values presented the way
|
|
423
|
+
* `quark/apply` presents results.
|
|
424
|
+
*/
|
|
425
|
+
export const reportDiagnostic = (
|
|
426
|
+
{ element, key, value, options }: ContextField,
|
|
427
|
+
level: DiagnosticLevel,
|
|
428
|
+
values: unknown[]
|
|
429
|
+
) => {
|
|
430
|
+
const rule = options.rule;
|
|
431
|
+
const text = values
|
|
432
|
+
.map((v) => (typeof v === "string" ? v : presentResult(v)))
|
|
433
|
+
.map((v) =>
|
|
434
|
+
typeof v === "string"
|
|
435
|
+
? v
|
|
436
|
+
: v instanceof Node
|
|
437
|
+
? `<${v.nodeName.toLowerCase()}>`
|
|
438
|
+
: (JSON.stringify(v) ?? String(v))
|
|
439
|
+
)
|
|
440
|
+
.join(" ");
|
|
441
|
+
QuarkLogger[level]({
|
|
442
|
+
method: key,
|
|
443
|
+
message: `Quark ${key} (${rule?.selector ?? "?"}): ${text}`,
|
|
444
|
+
element,
|
|
445
|
+
values,
|
|
446
|
+
});
|
|
447
|
+
publicize(["quark", "diagnostic"], {
|
|
448
|
+
weakElement: new WeakRef(element),
|
|
449
|
+
tag: element.localName,
|
|
450
|
+
selector: rule?.selector ?? null,
|
|
451
|
+
ruleId: rule?.id ?? null,
|
|
452
|
+
sheetId: rule?.quarkInstance.id ?? null,
|
|
453
|
+
runId: options.runId ?? null,
|
|
454
|
+
level,
|
|
455
|
+
key,
|
|
456
|
+
expression: value,
|
|
457
|
+
values: values.map(presentResult),
|
|
458
|
+
message: text,
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const isThenable = (value: unknown): value is PromiseLike<unknown> =>
|
|
463
|
+
!!value &&
|
|
464
|
+
typeof value === "object" &&
|
|
465
|
+
typeof (value as PromiseLike<unknown>).then === "function";
|
|
466
|
+
|
|
467
|
+
/** The value an author intended, stripped of Quark's render plumbing. */
|
|
468
|
+
const presentResult = (result: unknown): unknown => {
|
|
469
|
+
if (typeof NodeList !== "undefined" && result instanceof NodeList) {
|
|
470
|
+
return Array.from(result).map(presentResult);
|
|
471
|
+
}
|
|
472
|
+
if (
|
|
473
|
+
typeof DocumentFragment !== "undefined" &&
|
|
474
|
+
result instanceof DocumentFragment
|
|
475
|
+
) {
|
|
476
|
+
return Array.from(result.childNodes)
|
|
477
|
+
.filter((n) => n.nodeType !== Node.TEXT_NODE || n.textContent?.trim())
|
|
478
|
+
.map(presentResult);
|
|
479
|
+
}
|
|
480
|
+
if (Array.isArray(result)) return result.map(presentResult);
|
|
481
|
+
if (
|
|
482
|
+
result &&
|
|
483
|
+
typeof result === "object" &&
|
|
484
|
+
"value" in result &&
|
|
485
|
+
((result as ExpressionResult)?.type === "nodes" ||
|
|
486
|
+
(result as ExpressionResult)?.type === "html")
|
|
487
|
+
) {
|
|
488
|
+
return presentResult((result as { value: unknown }).value);
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
export const FIELD_RESOLVERS = {
|
|
494
|
+
// Listeners and variables set sync
|
|
495
|
+
listener: (args: ContextField) => {
|
|
496
|
+
const { element, hash } = args;
|
|
497
|
+
const { eventTypes, slot, block, optionSources } = args.listener!;
|
|
498
|
+
const listener = args.options.property as Listener;
|
|
499
|
+
const ruleId = args.options.rule!.id;
|
|
500
|
+
const elementInternal = getQuarkInternal(element);
|
|
501
|
+
let fns: Array<(e: Event) => unknown>;
|
|
502
|
+
let target: EventTarget = element;
|
|
503
|
+
const addOptions: AddEventListenerOptions = {};
|
|
504
|
+
if (listener.hasOptions) {
|
|
505
|
+
const { opts, dynamic } = resolveListenerOptions(args, optionSources);
|
|
506
|
+
// one wrapper carries filters, flags, timing, `once`, `handle` and the block
|
|
507
|
+
fns = [
|
|
508
|
+
listener.wrapper(element, {
|
|
509
|
+
opts,
|
|
510
|
+
dynamic,
|
|
511
|
+
options: args.options,
|
|
512
|
+
hash,
|
|
513
|
+
ruleId,
|
|
514
|
+
internal: elementInternal,
|
|
515
|
+
}),
|
|
516
|
+
];
|
|
517
|
+
target = listenerHost(element, opts.host);
|
|
518
|
+
if (opts.capture) addOptions.capture = true;
|
|
519
|
+
if (opts.passive) addOptions.passive = true;
|
|
520
|
+
} else {
|
|
521
|
+
// `@on … { }` without options: the block listener, registered directly
|
|
522
|
+
fns = block ? [listener.blockListener(element, args.options)] : [];
|
|
523
|
+
}
|
|
524
|
+
elementInternal.setOrderedListeners(hash, ruleId, slot, fns, {
|
|
525
|
+
eventTypes,
|
|
526
|
+
target,
|
|
527
|
+
options: addOptions,
|
|
528
|
+
});
|
|
529
|
+
return fns;
|
|
530
|
+
},
|
|
531
|
+
variable: (args: ContextField) => {
|
|
532
|
+
const result = resolveExpression(args);
|
|
533
|
+
return result;
|
|
534
|
+
},
|
|
535
|
+
// all attrs and content set async in schedulePaint()
|
|
536
|
+
dataset: (args: ContextField) => {
|
|
537
|
+
const { element, hash } = args;
|
|
538
|
+
const elementInternal = getQuarkInternal(element);
|
|
539
|
+
const resolved = resolveExpression(args);
|
|
540
|
+
if (isNoop(resolved)) {
|
|
541
|
+
return resolved;
|
|
542
|
+
}
|
|
543
|
+
// wipe values (and other falsy) clear prior data-* attrs
|
|
544
|
+
const result = objToAttrs((isWipe(resolved) ? {} : resolved) || {}, {
|
|
545
|
+
prefix: "data-",
|
|
546
|
+
convertNonPrimitives: true,
|
|
547
|
+
preserveKey: false,
|
|
548
|
+
});
|
|
549
|
+
const entries = () =>
|
|
550
|
+
prefixedAttrEntries(elementInternal, hash, "data-", result);
|
|
551
|
+
schedulePaint(
|
|
552
|
+
() => {
|
|
553
|
+
entries().forEach(([k, val]) => {
|
|
554
|
+
elementInternal.setAttr(hash, k, val);
|
|
555
|
+
});
|
|
556
|
+
},
|
|
557
|
+
1,
|
|
558
|
+
withTransition(paintTransition(args), () =>
|
|
559
|
+
entries().some(([k, val]) => attrWillChange(element, k, val))
|
|
560
|
+
)
|
|
561
|
+
);
|
|
562
|
+
return result;
|
|
563
|
+
},
|
|
564
|
+
ariaset: (args: ContextField) => {
|
|
565
|
+
const { element, hash } = args;
|
|
566
|
+
const elementInternal = getQuarkInternal(element);
|
|
567
|
+
const resolved = resolveExpression(args);
|
|
568
|
+
if (isNoop(resolved)) {
|
|
569
|
+
return resolved;
|
|
570
|
+
}
|
|
571
|
+
const result = objToAttrs((isWipe(resolved) ? {} : resolved) || {}, {
|
|
572
|
+
prefix: "aria-",
|
|
573
|
+
convertNonPrimitives: true,
|
|
574
|
+
preserveKey: false,
|
|
575
|
+
});
|
|
576
|
+
const entries = () =>
|
|
577
|
+
prefixedAttrEntries(elementInternal, hash, "aria-", result);
|
|
578
|
+
schedulePaint(
|
|
579
|
+
() => {
|
|
580
|
+
entries().forEach(([k, val]) => {
|
|
581
|
+
elementInternal.setAttr(hash, k, val);
|
|
582
|
+
});
|
|
583
|
+
},
|
|
584
|
+
1,
|
|
585
|
+
withTransition(paintTransition(args), () =>
|
|
586
|
+
entries().some(([k, val]) => attrWillChange(element, k, val))
|
|
587
|
+
)
|
|
588
|
+
);
|
|
589
|
+
return result;
|
|
590
|
+
},
|
|
591
|
+
class: (args: ContextField) => {
|
|
592
|
+
const { element, hash, key } = args;
|
|
593
|
+
const elementInternal = getQuarkInternal(element);
|
|
594
|
+
const result = resolveExpression(args);
|
|
595
|
+
if (isNoop(result)) {
|
|
596
|
+
return result;
|
|
597
|
+
}
|
|
598
|
+
// a string, a list (joined) or a wipe is one attribute write; a map
|
|
599
|
+
// toggles single classes (read at commit time: class changes are
|
|
600
|
+
// observed, so only real flips may write, as one causal hop)
|
|
601
|
+
const value = Array.isArray(result)
|
|
602
|
+
? result.join(" ")
|
|
603
|
+
: isWipe(result)
|
|
604
|
+
? null
|
|
605
|
+
: typeof result === "string"
|
|
606
|
+
? result
|
|
607
|
+
: undefined;
|
|
608
|
+
const flips = (): [string, unknown][] =>
|
|
609
|
+
isPojo(result)
|
|
610
|
+
? Object.entries(result).filter(
|
|
611
|
+
([k, v]) => element.classList.contains(k) !== !!v
|
|
612
|
+
)
|
|
613
|
+
: [];
|
|
614
|
+
schedulePaint(
|
|
615
|
+
() => {
|
|
616
|
+
if (value !== undefined) {
|
|
617
|
+
elementInternal.setAttr(hash, key, value);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const toggles = flips();
|
|
621
|
+
if (toggles.length) {
|
|
622
|
+
LoopGuard.write(element, key, () =>
|
|
623
|
+
toggles.forEach(([k, v]) => element.classList.toggle(k, !!v))
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
1,
|
|
628
|
+
withTransition(paintTransition(args), () =>
|
|
629
|
+
value !== undefined
|
|
630
|
+
? attrWillChange(element, key, value)
|
|
631
|
+
: flips().length > 0
|
|
632
|
+
)
|
|
633
|
+
);
|
|
634
|
+
return result;
|
|
635
|
+
},
|
|
636
|
+
/*
|
|
637
|
+
* CSS custom properties (`--x:`). Inline-style writes so stylesheets
|
|
638
|
+
* can consume Quark state via `var()` (colors, progress, theming).
|
|
639
|
+
* Values are Quark expressions, not CSS value grammar: quote literals
|
|
640
|
+
* (`"red !important"`, `"#ccc"`). A trailing `!important` in the
|
|
641
|
+
* string maps to the priority arg. Wipe removes the property. Quark
|
|
642
|
+
* never reads CSS variables back.
|
|
643
|
+
*/
|
|
644
|
+
styleProperty: (args: ContextField) => {
|
|
645
|
+
const { element, hash, key } = args;
|
|
646
|
+
const elementInternal = getQuarkInternal(element);
|
|
647
|
+
const resolved = resolveExpression(args);
|
|
648
|
+
if (isNoop(resolved)) {
|
|
649
|
+
return resolved;
|
|
650
|
+
}
|
|
651
|
+
const wiped = isWipe(resolved);
|
|
652
|
+
if (
|
|
653
|
+
!wiped &&
|
|
654
|
+
typeof resolved !== "string" &&
|
|
655
|
+
typeof resolved !== "number"
|
|
656
|
+
) {
|
|
657
|
+
QuarkLogger.error({
|
|
658
|
+
method: "styleProperty",
|
|
659
|
+
message: `Quark: "${key}" must resolve to a string — quote CSS values (e.g. "#ccc")`,
|
|
660
|
+
element,
|
|
661
|
+
script: args.value,
|
|
662
|
+
});
|
|
663
|
+
return SYMBOL_NOOP;
|
|
664
|
+
}
|
|
665
|
+
const value = wiped ? null : String(resolved);
|
|
666
|
+
schedulePaint(
|
|
667
|
+
() => {
|
|
668
|
+
elementInternal.setStyleProperty(hash, key, value);
|
|
669
|
+
},
|
|
670
|
+
1,
|
|
671
|
+
withTransition(paintTransition(args), () => {
|
|
672
|
+
const style = element.style;
|
|
673
|
+
if (value === null) return style.getPropertyValue(key) !== "";
|
|
674
|
+
const important = IMPORTANT.test(value);
|
|
675
|
+
return (
|
|
676
|
+
style.getPropertyValue(key).trim() !==
|
|
677
|
+
value.replace(IMPORTANT, "").trim() ||
|
|
678
|
+
style.getPropertyPriority(key) !== (important ? "important" : "")
|
|
679
|
+
);
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
return resolved;
|
|
683
|
+
},
|
|
684
|
+
attribute: (args: ContextField) => {
|
|
685
|
+
const { element, hash, key } = args;
|
|
686
|
+
const elementInternal = getQuarkInternal(element);
|
|
687
|
+
const result = resolveExpression(args);
|
|
688
|
+
if (isNoop(result)) {
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
const value = isWipe(result) ? null : result;
|
|
692
|
+
schedulePaint(
|
|
693
|
+
() => {
|
|
694
|
+
elementInternal.setAttr(hash, key, value);
|
|
695
|
+
},
|
|
696
|
+
1,
|
|
697
|
+
withTransition(paintTransition(args), () =>
|
|
698
|
+
attrWillChange(element, key, value)
|
|
699
|
+
)
|
|
700
|
+
);
|
|
701
|
+
return result;
|
|
702
|
+
},
|
|
703
|
+
content: (args: ContextField) => {
|
|
704
|
+
const { element, hash, value } = args;
|
|
705
|
+
const elementInternal = getQuarkInternal(element);
|
|
706
|
+
const result = resolveExpression(args);
|
|
707
|
+
// `preserve` leaves rendered content alone; wipe values clear it
|
|
708
|
+
if (isNoop(result)) {
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
// `@view-transition`: evaluated now, in this run's scope, for any branch
|
|
712
|
+
const transition = paintTransition(args);
|
|
713
|
+
if (isWipe(result)) {
|
|
714
|
+
elementInternal.setContentAttr(hash);
|
|
715
|
+
schedulePaint(
|
|
716
|
+
() => {
|
|
717
|
+
wipeContent(element);
|
|
718
|
+
},
|
|
719
|
+
0,
|
|
720
|
+
withTransition(transition, () => wipeWillChange(element))
|
|
721
|
+
);
|
|
722
|
+
return result;
|
|
723
|
+
}
|
|
724
|
+
// a template's fragment is outside the observed tree and its nodes are
|
|
725
|
+
// never connected, so paints into it must not requeue rules either
|
|
726
|
+
const willRenderIntoTemplate = element instanceof HTMLTemplateElement;
|
|
727
|
+
const textTarget = willRenderIntoTemplate ? element.content : element;
|
|
728
|
+
if (typeof result === "object") {
|
|
729
|
+
/*
|
|
730
|
+
* Mark content applied before any async work so parallel
|
|
731
|
+
* calls still collapse to one render.
|
|
732
|
+
*/
|
|
733
|
+
elementInternal.setContentAttr(hash);
|
|
734
|
+
// a settle wait (view transitions, `Quark.whenSettled`) covers the
|
|
735
|
+
// paint this promise will schedule
|
|
736
|
+
if (isThenable(result)) trackPending(result, element);
|
|
737
|
+
new Queue()
|
|
738
|
+
.settle(result)
|
|
739
|
+
/*
|
|
740
|
+
* If this settles sync, dependents see the new value. If async,
|
|
741
|
+
* a dependent with a sync dep that changed at the same time
|
|
742
|
+
* renders once with stale / undefined, then again when this
|
|
743
|
+
* resolves. Fix would be: dependents check `item` / `index` for
|
|
744
|
+
* a pending async op and wait.
|
|
745
|
+
*/
|
|
746
|
+
.onResolved((state) => {
|
|
747
|
+
const res = state.value as ExpressionResult | Node;
|
|
748
|
+
// iterate() / template() / dangerous-html() settle to
|
|
749
|
+
// {type, value, after?}, or a wipe / no-op value
|
|
750
|
+
if (isNoop(res)) return;
|
|
751
|
+
if (isWipe(res)) {
|
|
752
|
+
schedulePaint(
|
|
753
|
+
() => {
|
|
754
|
+
wipeContent(element);
|
|
755
|
+
},
|
|
756
|
+
0,
|
|
757
|
+
withTransition(transition, () => wipeWillChange(element))
|
|
758
|
+
);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (res instanceof Node || res instanceof NodeList) {
|
|
762
|
+
// inserted elements reach rules through the childList observer;
|
|
763
|
+
// a NodeList may be live, so read it at commit time
|
|
764
|
+
const nodes = () => (res instanceof Node ? [res] : Array.from(res));
|
|
765
|
+
schedulePaint(
|
|
766
|
+
() => {
|
|
767
|
+
replaceNonTemplateChildren(element, nodes());
|
|
768
|
+
},
|
|
769
|
+
0,
|
|
770
|
+
withTransition(transition, () =>
|
|
771
|
+
childrenWillChange(element, nodes())
|
|
772
|
+
)
|
|
773
|
+
);
|
|
774
|
+
} else if (res?.type === "nodes") {
|
|
775
|
+
const nodeArray = res.value;
|
|
776
|
+
schedulePaint(
|
|
777
|
+
() => {
|
|
778
|
+
replaceNonTemplateChildren(element, nodeArray);
|
|
779
|
+
if (nodeArray.length && !willRenderIntoTemplate) {
|
|
780
|
+
res?.after?.();
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
0,
|
|
784
|
+
withTransition(transition, () =>
|
|
785
|
+
childrenWillChange(element, nodeArray)
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
} else if (res?.type === "html") {
|
|
789
|
+
schedulePaint(
|
|
790
|
+
() => {
|
|
791
|
+
// element insertions are one causal hop (see LoopGuard)
|
|
792
|
+
LoopGuard.write(element, "content", () => {
|
|
793
|
+
element.innerHTML = res.value as string;
|
|
794
|
+
if (!willRenderIntoTemplate) {
|
|
795
|
+
res?.after?.();
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
},
|
|
799
|
+
0,
|
|
800
|
+
withTransition(
|
|
801
|
+
transition,
|
|
802
|
+
() => element.innerHTML !== (res.value as string)
|
|
803
|
+
)
|
|
804
|
+
);
|
|
805
|
+
} else {
|
|
806
|
+
const text = res + "";
|
|
807
|
+
schedulePaint(
|
|
808
|
+
() => {
|
|
809
|
+
/*
|
|
810
|
+
* `textContent` / `innerText` do not write a template's
|
|
811
|
+
* document fragment (unlike `innerHTML`), so templates go
|
|
812
|
+
* through `.content`.
|
|
813
|
+
*/
|
|
814
|
+
paintText(textTarget, text);
|
|
815
|
+
},
|
|
816
|
+
0,
|
|
817
|
+
withTransition(transition, () => textWillChange(textTarget, text))
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
})
|
|
821
|
+
.onRejected((state) => {
|
|
822
|
+
QuarkLogger.error({
|
|
823
|
+
method: "content",
|
|
824
|
+
message: "Could not render content",
|
|
825
|
+
element,
|
|
826
|
+
expression: value,
|
|
827
|
+
value: [state.value],
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
} else {
|
|
831
|
+
// record that content was applied so re-run gating (propertyIsNew /
|
|
832
|
+
// nearness) works for plain string results too
|
|
833
|
+
elementInternal.setContentAttr(hash);
|
|
834
|
+
const text = result + "";
|
|
835
|
+
schedulePaint(
|
|
836
|
+
() => {
|
|
837
|
+
paintText(textTarget, text);
|
|
838
|
+
},
|
|
839
|
+
0,
|
|
840
|
+
withTransition(transition, () => textWillChange(textTarget, text))
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
return result;
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
const IMPORTANT = /\s*!important\s*$/i;
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* The attribute writes of a `dataset` / `ariaset` paint: every prefixed
|
|
851
|
+
* attribute this sheet set before is removed unless the new object sets
|
|
852
|
+
* it again. Read at commit time.
|
|
853
|
+
*/
|
|
854
|
+
const prefixedAttrEntries = (
|
|
855
|
+
elementInternal: ReturnType<typeof getQuarkInternal>,
|
|
856
|
+
hash: string,
|
|
857
|
+
prefix: string,
|
|
858
|
+
result: Record<string, unknown>
|
|
859
|
+
): [string, unknown][] =>
|
|
860
|
+
Object.entries({
|
|
861
|
+
...Object.fromEntries(
|
|
862
|
+
Object.keys(elementInternal.getAllAttrs(hash))
|
|
863
|
+
.map((k) => k.startsWith(prefix) && [k, null])
|
|
864
|
+
.filter(Boolean) as [string, null][]
|
|
865
|
+
),
|
|
866
|
+
...result,
|
|
867
|
+
});
|