@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,818 @@
|
|
|
1
|
+
export type * from "./types";
|
|
2
|
+
import type { DiagnosticLevel } from "./ast";
|
|
3
|
+
import { type ListenerOptionSource, listenerOptionsText } from "./ast";
|
|
4
|
+
import { writeBinding } from "./bindings";
|
|
5
|
+
import { ATTRIBUTE_BLACKLIST_REGEXES, isNoop } from "./constants";
|
|
6
|
+
import { publicize } from "./devtools-hook";
|
|
7
|
+
import {
|
|
8
|
+
collectAttrCalls,
|
|
9
|
+
collectPropCalls,
|
|
10
|
+
collectVariableNames,
|
|
11
|
+
getExpressionAst,
|
|
12
|
+
} from "./evaluator";
|
|
13
|
+
import { getQuarkInternal, TQuarkElement } from "./quark-internal";
|
|
14
|
+
import { reportDiagnostic, resolveExpression, resolveField } from "./resolvers";
|
|
15
|
+
import type { Rule } from "./rule";
|
|
16
|
+
import type { QuarkOptions, TransitionSpec } from "./types";
|
|
17
|
+
import { deref, QuarkLogger } from "./utils";
|
|
18
|
+
import { LoopGuard, tc } from "@excom/kit-utils";
|
|
19
|
+
import type { Expression } from "@excom/quark-parser";
|
|
20
|
+
|
|
21
|
+
/** How `QuarkInternal.propertyHasBeenSet` tells a property's kind apart. */
|
|
22
|
+
export type PropertyKind =
|
|
23
|
+
| "variable"
|
|
24
|
+
| "attribute"
|
|
25
|
+
| "listener"
|
|
26
|
+
| "diagnostic"
|
|
27
|
+
| "delay";
|
|
28
|
+
|
|
29
|
+
export class Property {
|
|
30
|
+
key: string;
|
|
31
|
+
value: string;
|
|
32
|
+
parsedValue: Expression | null;
|
|
33
|
+
/** `$name`s referenced by the expression (static analysis). */
|
|
34
|
+
referencedVarNames: string[] = [];
|
|
35
|
+
/** Literal `attr("x")` names referenced by the expression (static analysis). */
|
|
36
|
+
referencedAttrNames: string[] = [];
|
|
37
|
+
/**
|
|
38
|
+
* Literal `prop("x")` names in the expression. Subscribed on the
|
|
39
|
+
* matched element at first read (`props.ts`); change events re-run
|
|
40
|
+
* this property via the sheet's `propIndex`.
|
|
41
|
+
*/
|
|
42
|
+
referencedPropNames: string[] = [];
|
|
43
|
+
parent: Rule;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves from bindings ($vars) or `prop()`, so it re-runs on change
|
|
46
|
+
* events, not sync DOM runs. Reactivity is in the AST; no cross-sheet
|
|
47
|
+
* analysis needed.
|
|
48
|
+
*/
|
|
49
|
+
isReactive: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The `@view-transition` block this write sits in, if any: its paints
|
|
52
|
+
* commit inside a view transition (see `paint.ts`).
|
|
53
|
+
*/
|
|
54
|
+
transition: TransitionSpec | null;
|
|
55
|
+
constructor({
|
|
56
|
+
key,
|
|
57
|
+
value,
|
|
58
|
+
parent,
|
|
59
|
+
transition = null,
|
|
60
|
+
}: {
|
|
61
|
+
key: string;
|
|
62
|
+
value: string;
|
|
63
|
+
parent: Rule;
|
|
64
|
+
transition?: TransitionSpec | null;
|
|
65
|
+
}) {
|
|
66
|
+
this.key = key;
|
|
67
|
+
this.value = value;
|
|
68
|
+
this.parent = parent;
|
|
69
|
+
this.transition = transition;
|
|
70
|
+
if (transition) parent.quarkInstance.hasTransitions = true;
|
|
71
|
+
try {
|
|
72
|
+
// an `@on` block without prelude handlers has nothing to evaluate
|
|
73
|
+
this.parsedValue = value ? getExpressionAst(value) : null;
|
|
74
|
+
this.referencedVarNames = this.parsedValue
|
|
75
|
+
? collectVariableNames(this.parsedValue)
|
|
76
|
+
: [];
|
|
77
|
+
const attrCalls = collectAttrCalls(this.parsedValue);
|
|
78
|
+
this.referencedAttrNames = attrCalls.names.filter(
|
|
79
|
+
(name) => !ATTRIBUTE_BLACKLIST_REGEXES.some((regex) => regex.test(name))
|
|
80
|
+
);
|
|
81
|
+
this.referencedAttrNames.forEach((name) =>
|
|
82
|
+
this.parent.observedAttrs.add(name)
|
|
83
|
+
);
|
|
84
|
+
if (attrCalls.hasNonLiteral) {
|
|
85
|
+
QuarkLogger.warn({
|
|
86
|
+
method: "property",
|
|
87
|
+
message: `Quark: attr() only observes static string names; "${key}: ${value}" will not re-run when a dynamic name changes`,
|
|
88
|
+
script: value,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const propCalls = collectPropCalls(this.parsedValue);
|
|
92
|
+
this.referencedPropNames = propCalls.names;
|
|
93
|
+
if (propCalls.hasNonLiteral) {
|
|
94
|
+
QuarkLogger.warn({
|
|
95
|
+
method: "property",
|
|
96
|
+
message: `Quark: prop() only observes static string names; "${key}: ${value}" will not re-run when a dynamic name changes`,
|
|
97
|
+
script: value,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.parsedValue = null;
|
|
102
|
+
QuarkLogger.error({
|
|
103
|
+
method: "property",
|
|
104
|
+
message: `Quark: Invalid expression for "${key}"`,
|
|
105
|
+
script: value,
|
|
106
|
+
error: [error],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
this.isReactive =
|
|
110
|
+
this.referencedVarNames.length > 0 || this.referencedPropNames.length > 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The storage kind `propertyHasBeenSet` checks. Diagnostics and delays
|
|
115
|
+
* record nothing on the element, so they count as new on every
|
|
116
|
+
* application (they gate themselves).
|
|
117
|
+
*/
|
|
118
|
+
kind(): PropertyKind {
|
|
119
|
+
return this instanceof Variable
|
|
120
|
+
? "variable"
|
|
121
|
+
: this instanceof Attribute
|
|
122
|
+
? "attribute"
|
|
123
|
+
: "listener";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
run(element: HTMLElement, options: QuarkOptions): void {
|
|
127
|
+
const elementInternal = getQuarkInternal(element);
|
|
128
|
+
const propertyIsNew = !elementInternal.propertyHasBeenSet(
|
|
129
|
+
this.parent.quarkInstance.hash,
|
|
130
|
+
this.parent.id,
|
|
131
|
+
this.kind(),
|
|
132
|
+
this.key,
|
|
133
|
+
this.value
|
|
134
|
+
);
|
|
135
|
+
/*
|
|
136
|
+
* Nearness: skip an event re-run when the changed owner is farther
|
|
137
|
+
* up than the owner this element last read. The nearer one shadows.
|
|
138
|
+
*/
|
|
139
|
+
const { changedBinding } = options;
|
|
140
|
+
if (
|
|
141
|
+
!propertyIsNew &&
|
|
142
|
+
changedBinding &&
|
|
143
|
+
this.referencedVarNames.includes(changedBinding.name)
|
|
144
|
+
) {
|
|
145
|
+
const known = elementInternal.varOwners[changedBinding.name]?.deref();
|
|
146
|
+
const origin = deref(changedBinding.origin);
|
|
147
|
+
if (known && origin && origin !== known && origin.contains(known)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/*
|
|
152
|
+
* Deferred fan-out: this sheet wrote during the run and this
|
|
153
|
+
* property already ran on this element after it (new value). Nothing
|
|
154
|
+
* to refresh.
|
|
155
|
+
*/
|
|
156
|
+
if (changedBinding?.trace) {
|
|
157
|
+
const ranAt = changedBinding.trace.ran.get(element)?.get(this);
|
|
158
|
+
if (ranAt !== undefined && ranAt > (changedBinding.sinceSeq ?? -1)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const trace = this.parent.quarkInstance.runTrace;
|
|
163
|
+
if (trace) recordSeq(trace.visited, element, this, ++trace.seq);
|
|
164
|
+
const shouldRun =
|
|
165
|
+
propertyIsNew ||
|
|
166
|
+
(options.isAsyncRun
|
|
167
|
+
? this.isReactive
|
|
168
|
+
: !this.isReactive ||
|
|
169
|
+
// a binding this reads was written earlier in this run: refresh
|
|
170
|
+
// inline instead of through the deferred change event
|
|
171
|
+
(!!trace &&
|
|
172
|
+
this.referencedVarNames.some((name) => trace.written.has(name))));
|
|
173
|
+
if (shouldRun) {
|
|
174
|
+
if (trace) recordSeq(trace.ran, element, this, trace.seq);
|
|
175
|
+
this._run(element as TQuarkElement, options);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
_run(element: TQuarkElement, options: QuarkOptions) {
|
|
179
|
+
return resolveField({
|
|
180
|
+
element,
|
|
181
|
+
key: this.key,
|
|
182
|
+
value: this.value,
|
|
183
|
+
options: { ...options, rule: this.parent, property: this },
|
|
184
|
+
hash: this.parent.quarkInstance.hash,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Store `seq` for (element, property) in a run-trace map. */
|
|
190
|
+
export const recordSeq = (
|
|
191
|
+
map: Map<Element, Map<Variable | Attribute | Listener, number>>,
|
|
192
|
+
element: Element,
|
|
193
|
+
property: Variable | Attribute | Listener,
|
|
194
|
+
seq: number
|
|
195
|
+
) => {
|
|
196
|
+
let byProperty = map.get(element);
|
|
197
|
+
if (!byProperty) map.set(element, (byProperty = new Map()));
|
|
198
|
+
byProperty.set(property, seq);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export class Variable extends Property {
|
|
202
|
+
_run(element: TQuarkElement, options: QuarkOptions) {
|
|
203
|
+
const resolved = super._run(element, options);
|
|
204
|
+
// `preserve` leaves the existing binding untouched
|
|
205
|
+
if (isNoop(resolved)) return false;
|
|
206
|
+
/*
|
|
207
|
+
* Bindings live on the element (`_q_.vars`), shared across sheets
|
|
208
|
+
* like CSS custom properties. `unset` deletes; a real change fires
|
|
209
|
+
* bubbling `quark-binding-change`, which drives re-runs.
|
|
210
|
+
*/
|
|
211
|
+
const sheet = this.parent.quarkInstance;
|
|
212
|
+
return writeBinding(
|
|
213
|
+
element,
|
|
214
|
+
this.key,
|
|
215
|
+
resolved,
|
|
216
|
+
{
|
|
217
|
+
sheetId: sheet.id,
|
|
218
|
+
runId: options.runId,
|
|
219
|
+
isFirstRun: options.isFirstRun,
|
|
220
|
+
},
|
|
221
|
+
// mid-run writes are announced once the run completes
|
|
222
|
+
sheet.runTrace ? sheet.queueBindingChange : undefined
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export class Attribute extends Property {}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `--custom-prop: expr` writes a CSS custom property on matched elements
|
|
231
|
+
* (see the `styleProperty` resolver). Values are Quark expressions, not
|
|
232
|
+
* CSS value grammar: quote CSS literals. Enforced here for the two
|
|
233
|
+
* shapes that would silently become strings (`#ccc` colors, `10px` unit
|
|
234
|
+
* numbers). Bare ids and space lists (`red`, `1px solid red`) already
|
|
235
|
+
* fail at eval.
|
|
236
|
+
*/
|
|
237
|
+
export class StyleProperty extends Attribute {
|
|
238
|
+
private isValidValue: boolean;
|
|
239
|
+
constructor(args: ConstructorParameters<typeof Property>[0]) {
|
|
240
|
+
super(args);
|
|
241
|
+
const node = this.parsedValue as { type?: string; unit?: string } | null;
|
|
242
|
+
this.isValidValue = !(
|
|
243
|
+
node &&
|
|
244
|
+
(node.type === "color" || (node.type === "number" && node.unit))
|
|
245
|
+
);
|
|
246
|
+
if (!this.isValidValue) {
|
|
247
|
+
QuarkLogger.error({
|
|
248
|
+
method: "styleProperty",
|
|
249
|
+
message: `Quark: CSS literal values must be quoted — write ${this.key}: "${this.value}" (strings / expressions only)`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
_run(element: TQuarkElement, options: QuarkOptions) {
|
|
254
|
+
if (!this.isValidValue) return false;
|
|
255
|
+
return super._run(element, options);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `@warn` / `@debug` / `@error` statements: evaluate their value on the
|
|
261
|
+
* matched element and report it (console at that logger level, DevTools
|
|
262
|
+
* as `quark/diagnostic`). `@warn` and `@error` speak once per element and
|
|
263
|
+
* rule — a warning repeated on every re-run is noise; `@debug` speaks on
|
|
264
|
+
* every application, so it re-logs when a binding or `prop()` it reads
|
|
265
|
+
* changes. A top-level comma list (`@debug "size", attr("width")`)
|
|
266
|
+
* reports one value per item.
|
|
267
|
+
*/
|
|
268
|
+
export class Diagnostic extends Property {
|
|
269
|
+
level: DiagnosticLevel;
|
|
270
|
+
/** Elements already warned about (not kept for `@debug`). */
|
|
271
|
+
private spoken = new WeakSet<Element>();
|
|
272
|
+
constructor({
|
|
273
|
+
level,
|
|
274
|
+
value,
|
|
275
|
+
parent,
|
|
276
|
+
transition = null,
|
|
277
|
+
}: {
|
|
278
|
+
level: DiagnosticLevel;
|
|
279
|
+
value: string;
|
|
280
|
+
parent: Rule;
|
|
281
|
+
transition?: TransitionSpec | null;
|
|
282
|
+
}) {
|
|
283
|
+
super({ key: `@${level}`, value, parent, transition });
|
|
284
|
+
this.level = level;
|
|
285
|
+
}
|
|
286
|
+
kind(): PropertyKind {
|
|
287
|
+
return "diagnostic";
|
|
288
|
+
}
|
|
289
|
+
_run(element: TQuarkElement, options: QuarkOptions) {
|
|
290
|
+
if (this.level !== "debug") {
|
|
291
|
+
if (this.spoken.has(element)) return false;
|
|
292
|
+
this.spoken.add(element);
|
|
293
|
+
}
|
|
294
|
+
const args = {
|
|
295
|
+
element,
|
|
296
|
+
key: this.key,
|
|
297
|
+
value: this.value,
|
|
298
|
+
options: { ...options, rule: this.parent, property: this },
|
|
299
|
+
hash: this.parent.quarkInstance.hash,
|
|
300
|
+
};
|
|
301
|
+
const resolved = resolveExpression(args);
|
|
302
|
+
// a failed expression already logged and published an error
|
|
303
|
+
if (isNoop(resolved)) return false;
|
|
304
|
+
const node = this.parsedValue as {
|
|
305
|
+
type?: string;
|
|
306
|
+
separator?: string;
|
|
307
|
+
} | null;
|
|
308
|
+
const values =
|
|
309
|
+
node?.type === "list" && node.separator === "," && Array.isArray(resolved)
|
|
310
|
+
? resolved
|
|
311
|
+
: [resolved];
|
|
312
|
+
reportDiagnostic(args, this.level, values);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* `@delay <ms> { … }`: applies its block to the matched element once the
|
|
319
|
+
* duration elapses, provided the element is still in the document, the
|
|
320
|
+
* sheet is still registered and the rule still matches. Applying the rule
|
|
321
|
+
* again restarts the timer (one per element). The block runs like an
|
|
322
|
+
* `@on` block — a one-shot, with `event` / `target` kept when scheduled
|
|
323
|
+
* from one — inside the loop-guard depth of the run that scheduled it, so
|
|
324
|
+
* two delays cannot bounce forever. The sheet clears pending timers on
|
|
325
|
+
* unregister.
|
|
326
|
+
*/
|
|
327
|
+
export class Delay extends Property {
|
|
328
|
+
block: Rule;
|
|
329
|
+
private timers = new WeakMap<Element, ReturnType<typeof setTimeout>>();
|
|
330
|
+
constructor({
|
|
331
|
+
value,
|
|
332
|
+
parent,
|
|
333
|
+
transition = null,
|
|
334
|
+
block,
|
|
335
|
+
}: {
|
|
336
|
+
value: string;
|
|
337
|
+
parent: Rule;
|
|
338
|
+
transition?: TransitionSpec | null;
|
|
339
|
+
block: Rule;
|
|
340
|
+
}) {
|
|
341
|
+
super({ key: "@delay", value, parent, transition });
|
|
342
|
+
this.block = block;
|
|
343
|
+
}
|
|
344
|
+
kind(): PropertyKind {
|
|
345
|
+
return "delay";
|
|
346
|
+
}
|
|
347
|
+
/** Drop the pending timer for `element`, if any. */
|
|
348
|
+
cancel(element: Element) {
|
|
349
|
+
const timer = this.timers.get(element);
|
|
350
|
+
if (timer === undefined) return;
|
|
351
|
+
clearTimeout(timer);
|
|
352
|
+
this.timers.delete(element);
|
|
353
|
+
this.parent.quarkInstance.delayTimers.delete(timer);
|
|
354
|
+
}
|
|
355
|
+
_run(element: TQuarkElement, options: QuarkOptions) {
|
|
356
|
+
const sheet = this.parent.quarkInstance;
|
|
357
|
+
const resolved = resolveExpression({
|
|
358
|
+
element,
|
|
359
|
+
key: this.key,
|
|
360
|
+
value: this.value,
|
|
361
|
+
options: { ...options, rule: this.parent, property: this },
|
|
362
|
+
hash: sheet.hash,
|
|
363
|
+
});
|
|
364
|
+
if (isNoop(resolved)) return false;
|
|
365
|
+
const ms = typeof resolved === "string" ? parseFloat(resolved) : resolved;
|
|
366
|
+
if (typeof ms !== "number" || !(ms >= 0)) {
|
|
367
|
+
QuarkLogger.warn({
|
|
368
|
+
method: "delay",
|
|
369
|
+
message: `Quark: @delay ${this.value} needs a number of milliseconds (${this.parent.selector})`,
|
|
370
|
+
element,
|
|
371
|
+
});
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
this.cancel(element);
|
|
375
|
+
const publish = (phase: "scheduled" | "fired" | "dropped", extra = {}) =>
|
|
376
|
+
publicize(["quark", "delay"], {
|
|
377
|
+
weakElement: new WeakRef(element),
|
|
378
|
+
tag: element.localName,
|
|
379
|
+
selector: this.parent.selector,
|
|
380
|
+
ruleId: this.parent.id,
|
|
381
|
+
sheetId: sheet.id,
|
|
382
|
+
runId: options.runId ?? null,
|
|
383
|
+
expression: this.value,
|
|
384
|
+
ms,
|
|
385
|
+
phase,
|
|
386
|
+
...extra,
|
|
387
|
+
});
|
|
388
|
+
// the timer carries the causal depth of the run that scheduled it
|
|
389
|
+
const depth = LoopGuard.current();
|
|
390
|
+
const elRef = new WeakRef(element);
|
|
391
|
+
const timer = setTimeout(() => {
|
|
392
|
+
this.timers.delete(element);
|
|
393
|
+
sheet.delayTimers.delete(timer);
|
|
394
|
+
const el = elRef.deref();
|
|
395
|
+
const host = sheet.host?.deref();
|
|
396
|
+
const reason =
|
|
397
|
+
!el || !el.isConnected
|
|
398
|
+
? "disconnected"
|
|
399
|
+
: !sheet.isRegistered || !host
|
|
400
|
+
? "unregistered"
|
|
401
|
+
: !stillMatches(this.parent, el, host)
|
|
402
|
+
? "unmatched"
|
|
403
|
+
: null;
|
|
404
|
+
if (reason) {
|
|
405
|
+
publish("dropped", { reason });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
publish("fired");
|
|
409
|
+
LoopGuard.run(depth, () =>
|
|
410
|
+
this.block.runEvent(
|
|
411
|
+
el as HTMLElement,
|
|
412
|
+
options.event,
|
|
413
|
+
options,
|
|
414
|
+
options.eventTarget ?? null
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
}, ms);
|
|
418
|
+
this.timers.set(element, timer);
|
|
419
|
+
sheet.delayTimers.add(timer);
|
|
420
|
+
publish("scheduled");
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Whether `rule` (its host compound included) still matches `element`. */
|
|
426
|
+
const stillMatches = (
|
|
427
|
+
rule: Rule,
|
|
428
|
+
element: HTMLElement,
|
|
429
|
+
host: HTMLElement
|
|
430
|
+
): boolean =>
|
|
431
|
+
(rule.hostCompound === null || host.matches(rule.hostCompound)) &&
|
|
432
|
+
rule.matchesElement(element, host);
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* `@on` options that configure the registration or act on the event
|
|
436
|
+
* itself, resolved once per match (bare flags and the `host` word; see
|
|
437
|
+
* `resolveListenerOptions`). Everything else (`target`, `key`,
|
|
438
|
+
* `debounce`, `throttle`, `handle`) is evaluated when the event fires.
|
|
439
|
+
*/
|
|
440
|
+
export interface ResolvedListenerOptions {
|
|
441
|
+
self?: boolean;
|
|
442
|
+
once?: boolean;
|
|
443
|
+
passive?: boolean;
|
|
444
|
+
capture?: boolean;
|
|
445
|
+
preventDefault?: boolean;
|
|
446
|
+
stopPropagation?: boolean;
|
|
447
|
+
stopImmediatePropagation?: boolean;
|
|
448
|
+
host?: "window" | "document";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Option names evaluated per event, in the block's scope. */
|
|
452
|
+
export const DYNAMIC_LISTENER_OPTIONS = [
|
|
453
|
+
"target",
|
|
454
|
+
"key",
|
|
455
|
+
"debounce",
|
|
456
|
+
"throttle",
|
|
457
|
+
"handle",
|
|
458
|
+
] as const;
|
|
459
|
+
export type DynamicListenerOption = (typeof DYNAMIC_LISTENER_OPTIONS)[number];
|
|
460
|
+
|
|
461
|
+
/** What an option-aware listener needs at event time; refreshed per run. */
|
|
462
|
+
export interface ListenerState {
|
|
463
|
+
opts: ResolvedListenerOptions;
|
|
464
|
+
/** Per-event options as written (`target`, `key`, `debounce`, `throttle`, `handle`). */
|
|
465
|
+
dynamic: ListenerOptionSource[];
|
|
466
|
+
options: QuarkOptions;
|
|
467
|
+
hash: string;
|
|
468
|
+
ruleId: number;
|
|
469
|
+
internal: ReturnType<typeof getQuarkInternal>;
|
|
470
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
471
|
+
last?: number;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const KEY_MODIFIERS: Record<string, (e: KeyboardEvent) => boolean> = {
|
|
475
|
+
shift: (e) => e.shiftKey,
|
|
476
|
+
alt: (e) => e.altKey,
|
|
477
|
+
ctrl: (e) => e.ctrlKey,
|
|
478
|
+
control: (e) => e.ctrlKey,
|
|
479
|
+
meta: (e) => e.metaKey,
|
|
480
|
+
cmd: (e) => e.metaKey,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const modifierKeyName = (mod: string): string =>
|
|
484
|
+
mod === "ctrl" || mod === "control"
|
|
485
|
+
? "control"
|
|
486
|
+
: mod === "cmd"
|
|
487
|
+
? "meta"
|
|
488
|
+
: mod;
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* One chord token: `k`, `shift`, or `shift+k` / `k+shift`. Listed
|
|
492
|
+
* modifiers must be held; a modifier-only token matches that key's
|
|
493
|
+
* own keydown. Same rules as `event-handler`'s `keycode-filter`.
|
|
494
|
+
*/
|
|
495
|
+
const matchesKeyToken = (token: string, e: KeyboardEvent): boolean => {
|
|
496
|
+
const parts = token.toLowerCase().split("+").filter(Boolean);
|
|
497
|
+
if (!parts.length) return false;
|
|
498
|
+
const mods = parts.filter((p) => p in KEY_MODIFIERS);
|
|
499
|
+
const keys = parts.filter((p) => !(p in KEY_MODIFIERS));
|
|
500
|
+
if (!mods.every((m) => KEY_MODIFIERS[m](e))) return false;
|
|
501
|
+
const key = e.key?.toLowerCase();
|
|
502
|
+
if (!key) return false;
|
|
503
|
+
if (keys.length) return keys.every((k) => key === k);
|
|
504
|
+
return mods.some((m) => key === modifierKeyName(m));
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/** `key: "Escape Shift+K"`, space-separated alternatives, any may match. */
|
|
508
|
+
export const matchesKey = (filter: string, e: Event): boolean =>
|
|
509
|
+
filter
|
|
510
|
+
.split(/\s+/)
|
|
511
|
+
.filter(Boolean)
|
|
512
|
+
.some((token) => matchesKeyToken(token, e as KeyboardEvent));
|
|
513
|
+
|
|
514
|
+
/** The EventTarget an `@on (host: …)` listener registers on. */
|
|
515
|
+
export const listenerHost = (
|
|
516
|
+
element: Element,
|
|
517
|
+
host: ResolvedListenerOptions["host"]
|
|
518
|
+
): EventTarget =>
|
|
519
|
+
host === "window"
|
|
520
|
+
? ((element.ownerDocument.defaultView as EventTarget) ?? window)
|
|
521
|
+
: host === "document"
|
|
522
|
+
? element.ownerDocument
|
|
523
|
+
: element;
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* `@on <event>[, <event>] [(options)] { … }` / `@on <event> (options);`
|
|
527
|
+
* attaches listeners on matched elements. `key` is the display form
|
|
528
|
+
* (`@on click, submit (once, handle: save)`), unique per rule for one
|
|
529
|
+
* event list + options group and used as the storage slot; the same
|
|
530
|
+
* function is registered for every event type in the list.
|
|
531
|
+
*
|
|
532
|
+
* `block` is a rule from the block body (never run by sheet passes)
|
|
533
|
+
* applied to the matched element once per event. Without options the
|
|
534
|
+
* block listener is registered directly and created once per element
|
|
535
|
+
* (`blockFns`) so rule re-runs keep the same registration.
|
|
536
|
+
*
|
|
537
|
+
* With options, one per-element `wrapper` applies the filters (`self`,
|
|
538
|
+
* `target`, `key`), the event flags (`prevent-default`, …), timing
|
|
539
|
+
* (`debounce` / `throttle`), `once`, then runs the `handle` functions
|
|
540
|
+
* and the block. `target`, `key`, `debounce`, `throttle` and `handle` are
|
|
541
|
+
* expressions evaluated when the event fires, in the block's scope
|
|
542
|
+
* (`event`, `target`, `element`, current `$bindings`), so nothing about
|
|
543
|
+
* the listener is reactive: a re-run only refreshes `ListenerState`,
|
|
544
|
+
* never the DOM registration.
|
|
545
|
+
*/
|
|
546
|
+
export class Listener extends Property {
|
|
547
|
+
eventTypes: string[];
|
|
548
|
+
optionSources: ListenerOptionSource[];
|
|
549
|
+
block: Rule | null;
|
|
550
|
+
/** The `handle:` option as written, for DevTools (`""` when absent). */
|
|
551
|
+
handleText: string;
|
|
552
|
+
/** Per-element block listener, stable across rule re-runs. */
|
|
553
|
+
private blockFns = new WeakMap<Element, (e: Event) => void>();
|
|
554
|
+
/** Per-element option-aware wrapper + the state it reads at event time. */
|
|
555
|
+
private wrappers = new WeakMap<
|
|
556
|
+
Element,
|
|
557
|
+
{ fn: (e: Event) => void; state: ListenerState }
|
|
558
|
+
>();
|
|
559
|
+
/** Per-event option problems already reported, per element. */
|
|
560
|
+
private warned = new WeakMap<Element, Set<string>>();
|
|
561
|
+
constructor({
|
|
562
|
+
eventTypes,
|
|
563
|
+
eventsText,
|
|
564
|
+
optionSources = [],
|
|
565
|
+
parent,
|
|
566
|
+
block = null,
|
|
567
|
+
transition = null,
|
|
568
|
+
}: {
|
|
569
|
+
eventTypes: string[];
|
|
570
|
+
/** Event list as written (`click, "my:evt"`), for the display key. */
|
|
571
|
+
eventsText: string;
|
|
572
|
+
optionSources?: ListenerOptionSource[];
|
|
573
|
+
parent: Rule;
|
|
574
|
+
block?: Rule | null;
|
|
575
|
+
transition?: TransitionSpec | null;
|
|
576
|
+
}) {
|
|
577
|
+
super({
|
|
578
|
+
key: `@on ${eventsText}${listenerOptionsText(optionSources)}`,
|
|
579
|
+
value: "",
|
|
580
|
+
parent,
|
|
581
|
+
transition,
|
|
582
|
+
});
|
|
583
|
+
this.eventTypes = eventTypes;
|
|
584
|
+
this.optionSources = optionSources;
|
|
585
|
+
this.block = block;
|
|
586
|
+
this.handleText =
|
|
587
|
+
optionSources.find((o) => o.name === "handle")?.text ?? "";
|
|
588
|
+
}
|
|
589
|
+
get hasOptions(): boolean {
|
|
590
|
+
return this.optionSources.length > 0;
|
|
591
|
+
}
|
|
592
|
+
/** The listener that applies this `@on` block to `element`. */
|
|
593
|
+
blockListener(
|
|
594
|
+
element: TQuarkElement,
|
|
595
|
+
options: QuarkOptions
|
|
596
|
+
): (e: Event) => void {
|
|
597
|
+
let fn = this.blockFns.get(element);
|
|
598
|
+
if (!fn) {
|
|
599
|
+
const block = this.block!;
|
|
600
|
+
fn = (e: Event) => block.runEvent(element, e, options);
|
|
601
|
+
this.blockFns.set(element, fn);
|
|
602
|
+
}
|
|
603
|
+
return fn;
|
|
604
|
+
}
|
|
605
|
+
private warnOnce(element: Element, topic: string, message: string) {
|
|
606
|
+
let topics = this.warned.get(element);
|
|
607
|
+
if (!topics) this.warned.set(element, (topics = new Set()));
|
|
608
|
+
if (topics.has(topic)) return;
|
|
609
|
+
topics.add(topic);
|
|
610
|
+
QuarkLogger.warn({
|
|
611
|
+
method: "listener",
|
|
612
|
+
message: `Quark: ${this.key} — ${message}`,
|
|
613
|
+
element,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Evaluate one per-event option in the block's scope: the event, the
|
|
618
|
+
* delegate (once known) and the element's current bindings.
|
|
619
|
+
*/
|
|
620
|
+
private evaluateOption(
|
|
621
|
+
element: TQuarkElement,
|
|
622
|
+
state: ListenerState,
|
|
623
|
+
name: DynamicListenerOption,
|
|
624
|
+
e: Event,
|
|
625
|
+
delegate: Element | undefined
|
|
626
|
+
): unknown {
|
|
627
|
+
const source = state.dynamic.find((o) => o.name === name);
|
|
628
|
+
if (!source) return undefined;
|
|
629
|
+
if (source.text === null) {
|
|
630
|
+
this.warnOnce(element, name, `option "${name}" needs a value`);
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
return resolveExpression({
|
|
634
|
+
element,
|
|
635
|
+
key: `${this.key} option`,
|
|
636
|
+
value: source.text,
|
|
637
|
+
options: {
|
|
638
|
+
...state.options,
|
|
639
|
+
rule: this.parent,
|
|
640
|
+
property: this,
|
|
641
|
+
event: e,
|
|
642
|
+
eventTarget: delegate ?? (e.target as Element | null) ?? null,
|
|
643
|
+
},
|
|
644
|
+
hash: state.hash,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
/** `debounce` / `throttle` as milliseconds, or `undefined` (warned once). */
|
|
648
|
+
private evaluateMs(
|
|
649
|
+
element: TQuarkElement,
|
|
650
|
+
state: ListenerState,
|
|
651
|
+
name: "debounce" | "throttle",
|
|
652
|
+
e: Event,
|
|
653
|
+
delegate: Element | undefined
|
|
654
|
+
): number | undefined {
|
|
655
|
+
if (!state.dynamic.some((o) => o.name === name)) return undefined;
|
|
656
|
+
const value = this.evaluateOption(element, state, name, e, delegate);
|
|
657
|
+
if (isNoop(value)) return undefined;
|
|
658
|
+
const ms = typeof value === "string" ? parseFloat(value) : value;
|
|
659
|
+
if (typeof ms !== "number" || !(ms > 0)) {
|
|
660
|
+
this.warnOnce(
|
|
661
|
+
element,
|
|
662
|
+
name,
|
|
663
|
+
`option "${name}" needs a positive number of milliseconds`
|
|
664
|
+
);
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
return ms;
|
|
668
|
+
}
|
|
669
|
+
/** Call the `handle:` result(s) with the event; `this` is the element. */
|
|
670
|
+
private callHandlers(element: TQuarkElement, value: unknown, e: Event) {
|
|
671
|
+
if (value === undefined || value === null || isNoop(value)) return;
|
|
672
|
+
const handlers = Array.isArray(value) ? value.flat(Infinity) : [value];
|
|
673
|
+
for (const handler of handlers) {
|
|
674
|
+
if (typeof handler === "function") {
|
|
675
|
+
handler.call(element, e);
|
|
676
|
+
} else if (typeof handler === "string") {
|
|
677
|
+
this.warnOnce(
|
|
678
|
+
element,
|
|
679
|
+
"handle",
|
|
680
|
+
`handle: "${handler}" is a string — write the bare name of a function`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* The one function registered for an `@on` with options. Created once
|
|
687
|
+
* per element; `state` is replaced each run so the wrapper always
|
|
688
|
+
* sees the latest flags, option sources and run context.
|
|
689
|
+
*/
|
|
690
|
+
wrapper(element: TQuarkElement, state: ListenerState): (e: Event) => void {
|
|
691
|
+
const existing = this.wrappers.get(element);
|
|
692
|
+
if (existing) {
|
|
693
|
+
// keep timing bookkeeping across runs
|
|
694
|
+
state.timer = existing.state.timer;
|
|
695
|
+
state.last = existing.state.last;
|
|
696
|
+
existing.state = state;
|
|
697
|
+
return existing.fn;
|
|
698
|
+
}
|
|
699
|
+
const entry = { state, fn: (_e: Event) => {} };
|
|
700
|
+
const block = this.block;
|
|
701
|
+
const slot = this.key;
|
|
702
|
+
const run = (e: Event, delegate: Element | undefined) => {
|
|
703
|
+
const { options } = entry.state;
|
|
704
|
+
if (entry.state.dynamic.some((o) => o.name === "handle")) {
|
|
705
|
+
this.callHandlers(
|
|
706
|
+
element,
|
|
707
|
+
this.evaluateOption(element, entry.state, "handle", e, delegate),
|
|
708
|
+
e
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
if (block) block.runEvent(element, e, options, delegate ?? null);
|
|
712
|
+
};
|
|
713
|
+
entry.fn = (e: Event) => {
|
|
714
|
+
const { opts, internal, hash, ruleId } = entry.state;
|
|
715
|
+
const onHost = !!opts.host;
|
|
716
|
+
if (onHost && !element.isConnected) {
|
|
717
|
+
// the element left the document: drop the window / document listener
|
|
718
|
+
internal.removeAllListeners(hash, ruleId, slot);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (opts.self && e.target !== e.currentTarget) return;
|
|
722
|
+
let delegate: Element | undefined;
|
|
723
|
+
if (entry.state.dynamic.some((o) => o.name === "target")) {
|
|
724
|
+
const selector = this.evaluateOption(
|
|
725
|
+
element,
|
|
726
|
+
entry.state,
|
|
727
|
+
"target",
|
|
728
|
+
e,
|
|
729
|
+
undefined
|
|
730
|
+
);
|
|
731
|
+
if (isNoop(selector)) return;
|
|
732
|
+
if (typeof selector !== "string" || !selector.trim()) {
|
|
733
|
+
this.warnOnce(
|
|
734
|
+
element,
|
|
735
|
+
"target",
|
|
736
|
+
`option "target" needs a string value`
|
|
737
|
+
);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const origin = e.target as Element | null;
|
|
741
|
+
const found =
|
|
742
|
+
origin && typeof origin.closest === "function"
|
|
743
|
+
? tc(() => origin.closest(selector))
|
|
744
|
+
: null;
|
|
745
|
+
if (!found) return;
|
|
746
|
+
if (!onHost && !element.contains(found)) return;
|
|
747
|
+
delegate = found;
|
|
748
|
+
}
|
|
749
|
+
if (entry.state.dynamic.some((o) => o.name === "key")) {
|
|
750
|
+
const filter = this.evaluateOption(
|
|
751
|
+
element,
|
|
752
|
+
entry.state,
|
|
753
|
+
"key",
|
|
754
|
+
e,
|
|
755
|
+
delegate
|
|
756
|
+
);
|
|
757
|
+
if (isNoop(filter)) return;
|
|
758
|
+
if (typeof filter !== "string" || !filter.trim()) {
|
|
759
|
+
this.warnOnce(element, "key", `option "key" needs a string value`);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (!matchesKey(filter, e)) return;
|
|
763
|
+
}
|
|
764
|
+
if (opts.once) internal.removeAllListeners(hash, ruleId, slot);
|
|
765
|
+
// event flags act now, before any timing, so cancelation is synchronous
|
|
766
|
+
if (opts.preventDefault) e.preventDefault();
|
|
767
|
+
if (opts.stopPropagation) e.stopPropagation();
|
|
768
|
+
if (opts.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
769
|
+
const debounce = this.evaluateMs(
|
|
770
|
+
element,
|
|
771
|
+
entry.state,
|
|
772
|
+
"debounce",
|
|
773
|
+
e,
|
|
774
|
+
delegate
|
|
775
|
+
);
|
|
776
|
+
if (debounce) {
|
|
777
|
+
clearTimeout(entry.state.timer);
|
|
778
|
+
entry.state.timer = setTimeout(() => run(e, delegate), debounce);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const throttle = this.evaluateMs(
|
|
782
|
+
element,
|
|
783
|
+
entry.state,
|
|
784
|
+
"throttle",
|
|
785
|
+
e,
|
|
786
|
+
delegate
|
|
787
|
+
);
|
|
788
|
+
if (throttle) {
|
|
789
|
+
const now = Date.now();
|
|
790
|
+
const last = entry.state.last;
|
|
791
|
+
if (last !== undefined && now - last < throttle) return;
|
|
792
|
+
entry.state.last = now;
|
|
793
|
+
}
|
|
794
|
+
run(e, delegate);
|
|
795
|
+
};
|
|
796
|
+
// give the wrapper a readable name for DevTools / stack traces
|
|
797
|
+
Object.defineProperty(entry.fn, "name", {
|
|
798
|
+
value: `on:${this.eventTypes.join(",")}`,
|
|
799
|
+
});
|
|
800
|
+
this.wrappers.set(element, entry);
|
|
801
|
+
return entry.fn;
|
|
802
|
+
}
|
|
803
|
+
_run(element: TQuarkElement, options: QuarkOptions) {
|
|
804
|
+
return resolveField({
|
|
805
|
+
element,
|
|
806
|
+
key: this.key,
|
|
807
|
+
value: this.value,
|
|
808
|
+
options: { ...options, rule: this.parent, property: this },
|
|
809
|
+
hash: this.parent.quarkInstance.hash,
|
|
810
|
+
listener: {
|
|
811
|
+
eventTypes: this.eventTypes,
|
|
812
|
+
slot: this.key,
|
|
813
|
+
optionSources: this.optionSources,
|
|
814
|
+
block: this.block ?? undefined,
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|