@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/props.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `prop("name")` reactivity: observe a JS property on the matched
|
|
3
|
+
* element, whatever kind it is.
|
|
4
|
+
*
|
|
5
|
+
* Subscriptions go through `observeProperty` (kit-utils), the same
|
|
6
|
+
* helper Neutron defines its props with, so one own accessor per
|
|
7
|
+
* element + property intercepts writes from any source. Writes coalesce
|
|
8
|
+
* per element into one bubbling `quark-prop-change` per microtask
|
|
9
|
+
* (`detail.names`). Every sheet hears it at its root and matches
|
|
10
|
+
* `propIndex`. Same bus as `quark-binding-change`.
|
|
11
|
+
*
|
|
12
|
+
* Subscriptions are refcounted per (element, name) across sheets; each
|
|
13
|
+
* sheet releases the ones it took on `unregister()`.
|
|
14
|
+
*/
|
|
15
|
+
import { observeProperty } from "@excom/kit-utils";
|
|
16
|
+
|
|
17
|
+
export const PROP_CHANGE_EVENT = "quark-prop-change";
|
|
18
|
+
|
|
19
|
+
export interface PropChangeDetail {
|
|
20
|
+
names: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Subscription {
|
|
24
|
+
count: number;
|
|
25
|
+
unsubscribe: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SUBSCRIPTIONS = new WeakMap<Element, Map<string, Subscription>>();
|
|
29
|
+
const PENDING = new Map<Element, Set<string>>();
|
|
30
|
+
let flushQueued = false;
|
|
31
|
+
|
|
32
|
+
const flush = () => {
|
|
33
|
+
flushQueued = false;
|
|
34
|
+
const batch = [...PENDING];
|
|
35
|
+
PENDING.clear();
|
|
36
|
+
batch.forEach(([element, names]) => {
|
|
37
|
+
if (!element.isConnected) return;
|
|
38
|
+
element.dispatchEvent(
|
|
39
|
+
new CustomEvent<PropChangeDetail>(PROP_CHANGE_EVENT, {
|
|
40
|
+
bubbles: true,
|
|
41
|
+
detail: { names: [...names] },
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const queuePropChange = (element: Element, name: string) => {
|
|
48
|
+
let names = PENDING.get(element);
|
|
49
|
+
if (!names) PENDING.set(element, (names = new Set()));
|
|
50
|
+
names.add(name);
|
|
51
|
+
if (!flushQueued) {
|
|
52
|
+
flushQueued = true;
|
|
53
|
+
queueMicrotask(flush);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Take a (refcounted) subscription to `element[name]`. A custom element
|
|
59
|
+
* that is not defined yet is observed now and re-observed once it
|
|
60
|
+
* upgrades, in case upgrade replaced the property (the vanilla "upgrade
|
|
61
|
+
* property" idiom deletes own descriptors). Returns a release function.
|
|
62
|
+
*/
|
|
63
|
+
export const subscribeProp = (element: Element, name: string): (() => void) => {
|
|
64
|
+
let byName = SUBSCRIPTIONS.get(element);
|
|
65
|
+
if (!byName) SUBSCRIPTIONS.set(element, (byName = new Map()));
|
|
66
|
+
let subscription = byName.get(name);
|
|
67
|
+
if (!subscription) {
|
|
68
|
+
const observer = () => queuePropChange(element, name);
|
|
69
|
+
let unsubscribe = observeProperty(element, name, observer);
|
|
70
|
+
const tag = element.localName;
|
|
71
|
+
if (tag.includes("-") && !customElements.get(tag)) {
|
|
72
|
+
customElements.whenDefined(tag).then(() => {
|
|
73
|
+
// still wanted? re-observe: a no-op when the wrapper survived
|
|
74
|
+
if (byName?.get(name) === subscription) {
|
|
75
|
+
unsubscribe();
|
|
76
|
+
unsubscribe = observeProperty(element, name, observer);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
subscription = { count: 0, unsubscribe: () => unsubscribe() };
|
|
81
|
+
byName.set(name, subscription);
|
|
82
|
+
}
|
|
83
|
+
subscription.count++;
|
|
84
|
+
let released = false;
|
|
85
|
+
return () => {
|
|
86
|
+
if (released) return;
|
|
87
|
+
released = true;
|
|
88
|
+
subscription.count--;
|
|
89
|
+
if (subscription.count === 0) {
|
|
90
|
+
subscription.unsubscribe();
|
|
91
|
+
byName.delete(name);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
};
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { syncFormControlAttribute } from "./form-controls";
|
|
2
|
+
import type { UtilFn } from "./types";
|
|
3
|
+
|
|
4
|
+
/** How an `@on` slot's functions are registered on the DOM (once per event type). */
|
|
5
|
+
export interface ListenerMeta {
|
|
6
|
+
eventTypes: string[];
|
|
7
|
+
target: EventTarget;
|
|
8
|
+
options: AddEventListenerOptions;
|
|
9
|
+
}
|
|
10
|
+
import { deref, QuarkLogger } from "./utils";
|
|
11
|
+
import { hashObject } from "@excom/hash-object";
|
|
12
|
+
import { LoopGuard } from "@excom/kit-utils";
|
|
13
|
+
export interface TQuarkElement extends HTMLElement {
|
|
14
|
+
_q_: QuarkInternal;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getQuarkInternal = (_el): InstanceType<typeof QuarkInternal> => {
|
|
18
|
+
let element = deref(_el);
|
|
19
|
+
if (element) {
|
|
20
|
+
if (!element._q_) {
|
|
21
|
+
new QuarkInternal(element);
|
|
22
|
+
}
|
|
23
|
+
return element._q_;
|
|
24
|
+
}
|
|
25
|
+
// lie to everything else...
|
|
26
|
+
return QuarkLogger.error(
|
|
27
|
+
"Quark: Element not found"
|
|
28
|
+
) as unknown as InstanceType<typeof QuarkInternal>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class QuarkInternal {
|
|
32
|
+
element: HTMLElement;
|
|
33
|
+
loop: {
|
|
34
|
+
item?: any;
|
|
35
|
+
index?: any;
|
|
36
|
+
hash?: any;
|
|
37
|
+
key?: any;
|
|
38
|
+
} = {};
|
|
39
|
+
/**
|
|
40
|
+
* $variable bindings. Shared across sheets (CSS custom-property
|
|
41
|
+
* semantics: no per-sheet isolation; last writer wins per element).
|
|
42
|
+
* Non-primitives are stored as `[hash, value]` for change detection.
|
|
43
|
+
*/
|
|
44
|
+
vars: Record<string, [string, any] | any> = {};
|
|
45
|
+
/**
|
|
46
|
+
* Nearest binding owner seen per $name on this element's last reads.
|
|
47
|
+
* Lets change-event fan-out skip re-runs for shadowed (farther) owners.
|
|
48
|
+
*/
|
|
49
|
+
varOwners: Record<string, WeakRef<HTMLElement>> = {};
|
|
50
|
+
instances: {
|
|
51
|
+
[hash: string]: {
|
|
52
|
+
modules: Record<string, any>;
|
|
53
|
+
attributes: Record<string, boolean>;
|
|
54
|
+
rules: {
|
|
55
|
+
[id: number]: {
|
|
56
|
+
/** Attached listener functions per `@on` slot (`@on click (once)`). */
|
|
57
|
+
listeners: Record<string, Array<UtilFn | undefined>>;
|
|
58
|
+
/** Where and how each slot's functions were registered. */
|
|
59
|
+
listenerMeta: Record<string, ListenerMeta>;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
} = {};
|
|
64
|
+
constructor(_element: HTMLElement) {
|
|
65
|
+
const element = _element as unknown as TQuarkElement;
|
|
66
|
+
if (!element._q_) {
|
|
67
|
+
this.element = element;
|
|
68
|
+
element._q_ = this;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
getLoopItem() {
|
|
72
|
+
return this.loop.item;
|
|
73
|
+
}
|
|
74
|
+
setLoopItem(value) {
|
|
75
|
+
this.loop.item = value;
|
|
76
|
+
}
|
|
77
|
+
getLoopIndex() {
|
|
78
|
+
return this.loop.index;
|
|
79
|
+
}
|
|
80
|
+
setLoopIndex(value) {
|
|
81
|
+
this.loop.index = value;
|
|
82
|
+
}
|
|
83
|
+
getLoopHash() {
|
|
84
|
+
return this.loop.hash;
|
|
85
|
+
}
|
|
86
|
+
setLoopHash(value) {
|
|
87
|
+
this.loop.hash = value;
|
|
88
|
+
}
|
|
89
|
+
getLoopKey() {
|
|
90
|
+
return this.loop.key;
|
|
91
|
+
}
|
|
92
|
+
setLoopKey(value) {
|
|
93
|
+
this.loop.key = value;
|
|
94
|
+
}
|
|
95
|
+
/*
|
|
96
|
+
Modules and listeners are scoped to the quark instance (sheet).
|
|
97
|
+
Variables are not: like CSS custom properties, they live on the
|
|
98
|
+
element and cascade to descendants, whichever sheet set them.
|
|
99
|
+
Attributes are shared and compared across instances.
|
|
100
|
+
TBD: item/index iterations
|
|
101
|
+
*/
|
|
102
|
+
getInstance(quarkHash) {
|
|
103
|
+
if (!this.instances[quarkHash]) {
|
|
104
|
+
this.instances[quarkHash] = {
|
|
105
|
+
modules: {
|
|
106
|
+
dfault: {},
|
|
107
|
+
},
|
|
108
|
+
attributes: {},
|
|
109
|
+
rules: {},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return this.instances[quarkHash];
|
|
113
|
+
}
|
|
114
|
+
getRule(instance, ruleId) {
|
|
115
|
+
if (!instance.rules[ruleId]) {
|
|
116
|
+
instance.rules[ruleId] = {
|
|
117
|
+
listeners: {},
|
|
118
|
+
listenerMeta: {},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return instance.rules[ruleId];
|
|
122
|
+
}
|
|
123
|
+
/** Write a binding. Returns `true` when the stored value actually changed. */
|
|
124
|
+
setVar(varName: string, value: unknown): boolean {
|
|
125
|
+
const isComparable =
|
|
126
|
+
value === null ||
|
|
127
|
+
value === undefined ||
|
|
128
|
+
["string", "number", "boolean", "symbol"].includes(typeof value);
|
|
129
|
+
// This will skip hashing for primitives,
|
|
130
|
+
// which can be compared directly
|
|
131
|
+
if (isComparable) {
|
|
132
|
+
const isDefined = varName in this.vars;
|
|
133
|
+
if (!isDefined || this.vars[varName] !== value) {
|
|
134
|
+
this.vars[varName] = value;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const existingHash = this.vars[varName]?.[0];
|
|
140
|
+
const newHash = hashObject(value);
|
|
141
|
+
if (existingHash === newHash) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
this.vars[varName] = [newHash, value];
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
getVar(varName: string) {
|
|
148
|
+
return Array.isArray(this.vars[varName])
|
|
149
|
+
? this.vars[varName]?.[1]
|
|
150
|
+
: this.vars[varName];
|
|
151
|
+
}
|
|
152
|
+
hasVar(varName: string): boolean {
|
|
153
|
+
return varName in this.vars;
|
|
154
|
+
}
|
|
155
|
+
/** `unset`: remove the binding so descendants fall through to an ancestor. */
|
|
156
|
+
deleteVar(varName: string): boolean {
|
|
157
|
+
if (!(varName in this.vars)) return false;
|
|
158
|
+
delete this.vars[varName];
|
|
159
|
+
delete this.varOwners[varName];
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
propertyHasBeenSet(
|
|
163
|
+
quarkHash: string,
|
|
164
|
+
ruleId: number,
|
|
165
|
+
type: "variable" | "listener" | "attribute" | "diagnostic" | "delay",
|
|
166
|
+
key: string,
|
|
167
|
+
// unused
|
|
168
|
+
_value?: string
|
|
169
|
+
) {
|
|
170
|
+
const instance = this.getInstance(quarkHash);
|
|
171
|
+
if (type === "variable") {
|
|
172
|
+
// a def is "set" when the binding exists on this element
|
|
173
|
+
return key in this.vars;
|
|
174
|
+
} else if (type === "listener") {
|
|
175
|
+
const rule = this.getRule(instance, ruleId);
|
|
176
|
+
return !!rule.listeners[key]; // ?.has?.(value);
|
|
177
|
+
} else if (type === "attribute") {
|
|
178
|
+
return !!instance.attributes[key];
|
|
179
|
+
/*
|
|
180
|
+
* if (element) {
|
|
181
|
+
* const instance = QuarkElement.getInstance(element, quarkHash);
|
|
182
|
+
* return !!instance.listeners[key];
|
|
183
|
+
* } else {
|
|
184
|
+
* return false;
|
|
185
|
+
* }
|
|
186
|
+
*/
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setModule(quarkHash, moduleName = "dfault", value) {
|
|
191
|
+
const instance = this.getInstance(quarkHash);
|
|
192
|
+
// replace, never mutate: the expression scope caches per modules record
|
|
193
|
+
instance.modules = { ...instance.modules, [moduleName]: value };
|
|
194
|
+
}
|
|
195
|
+
addModules(quarkHash, modules) {
|
|
196
|
+
const instance = this.getInstance(quarkHash);
|
|
197
|
+
instance.modules = {
|
|
198
|
+
...instance.modules,
|
|
199
|
+
...modules,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
getAllAttrs(quarkHash: string): Record<string, boolean> {
|
|
203
|
+
const instance = this.getInstance(quarkHash);
|
|
204
|
+
return instance.attributes;
|
|
205
|
+
}
|
|
206
|
+
setAttr(
|
|
207
|
+
quarkHash: string,
|
|
208
|
+
name: string,
|
|
209
|
+
value: string | null | undefined | unknown | boolean
|
|
210
|
+
) {
|
|
211
|
+
// `undefined` = no-op (leave the attribute alone). `null` / false = wipe.
|
|
212
|
+
if (value === undefined) return;
|
|
213
|
+
const instance = this.getInstance(quarkHash);
|
|
214
|
+
const _val = typeof value === "boolean" ? (value ? "" : null) : value;
|
|
215
|
+
const shouldRemove = _val === null;
|
|
216
|
+
const oldVal = this.element.getAttribute(name);
|
|
217
|
+
if (_val !== oldVal) {
|
|
218
|
+
// one causal hop for the loop guard; past its limit the write is
|
|
219
|
+
// dropped and the runaway chain ends here
|
|
220
|
+
const written = LoopGuard.write(this.element, name, () => {
|
|
221
|
+
this.element[shouldRemove ? "removeAttribute" : "setAttribute"](
|
|
222
|
+
name,
|
|
223
|
+
_val as string
|
|
224
|
+
);
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
if (written === false) return;
|
|
228
|
+
instance.attributes[name] = true;
|
|
229
|
+
}
|
|
230
|
+
/*
|
|
231
|
+
* Form controls: the attr is authoritative. Keep the live
|
|
232
|
+
* (dirty-flag) property in step even when the attr itself did not
|
|
233
|
+
* change, since the user may have edited the control away from it.
|
|
234
|
+
*/
|
|
235
|
+
syncFormControlAttribute(
|
|
236
|
+
this.element,
|
|
237
|
+
name,
|
|
238
|
+
shouldRemove ? null : String(_val)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
setContentAttr(quarkHash: string) {
|
|
242
|
+
const instance = this.getInstance(quarkHash);
|
|
243
|
+
instance.attributes.content = true;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Write a CSS custom property (`--x:`) on the inline style. `undefined`
|
|
247
|
+
* = no-op; `null` = remove. A trailing `!important` in the value maps
|
|
248
|
+
* to the priority arg (`setProperty` rejects it inline). Recorded in
|
|
249
|
+
* `instance.attributes` so `propertyIsNew` gating works. `--` names
|
|
250
|
+
* cannot clash with real attrs.
|
|
251
|
+
*/
|
|
252
|
+
setStyleProperty(
|
|
253
|
+
quarkHash: string,
|
|
254
|
+
name: string,
|
|
255
|
+
value: string | null | undefined
|
|
256
|
+
) {
|
|
257
|
+
if (value === undefined) return;
|
|
258
|
+
const instance = this.getInstance(quarkHash);
|
|
259
|
+
const style = (this.element as HTMLElement).style;
|
|
260
|
+
if (value === null) {
|
|
261
|
+
style.removeProperty(name);
|
|
262
|
+
} else {
|
|
263
|
+
const important = /\s*!important\s*$/i.test(value);
|
|
264
|
+
style.setProperty(
|
|
265
|
+
name,
|
|
266
|
+
important ? value.replace(/\s*!important\s*$/i, "") : value,
|
|
267
|
+
important ? "important" : ""
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
instance.attributes[name] = true;
|
|
271
|
+
}
|
|
272
|
+
getModule(quarkHash, moduleName = "dfault") {
|
|
273
|
+
const instance = this.getInstance(quarkHash);
|
|
274
|
+
return instance.modules[moduleName];
|
|
275
|
+
}
|
|
276
|
+
getModules(quarkHash) {
|
|
277
|
+
const instance = this.getInstance(quarkHash);
|
|
278
|
+
return instance.modules;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getListeners(quarkHash, ruleId, slot) {
|
|
282
|
+
const instance = this.getInstance(quarkHash);
|
|
283
|
+
const rule = this.getRule(instance, ruleId);
|
|
284
|
+
if (!rule.listeners[slot]) {
|
|
285
|
+
rule.listeners[slot] = [];
|
|
286
|
+
}
|
|
287
|
+
return rule.listeners[slot];
|
|
288
|
+
}
|
|
289
|
+
setListeners(quarkHash, ruleId, slot, value) {
|
|
290
|
+
const instance = this.getInstance(quarkHash);
|
|
291
|
+
const rule = this.getRule(instance, ruleId);
|
|
292
|
+
rule.listeners[slot] = value;
|
|
293
|
+
}
|
|
294
|
+
/** Registration target / options recorded for a slot, if any. */
|
|
295
|
+
getListenerMeta(quarkHash, ruleId, slot): ListenerMeta | undefined {
|
|
296
|
+
const instance = this.getInstance(quarkHash);
|
|
297
|
+
return this.getRule(instance, ruleId).listenerMeta[slot];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Detach every function registered under `slot` and forget the slot. */
|
|
301
|
+
removeAllListeners(quarkHash, ruleId, slot) {
|
|
302
|
+
const instance = this.getInstance(quarkHash);
|
|
303
|
+
const rule = this.getRule(instance, ruleId);
|
|
304
|
+
const meta = rule.listenerMeta[slot];
|
|
305
|
+
const listenerArray = rule.listeners[slot] ?? [];
|
|
306
|
+
if (meta) {
|
|
307
|
+
listenerArray.forEach((fn) => {
|
|
308
|
+
if (fn) {
|
|
309
|
+
meta.eventTypes.forEach((eventType) =>
|
|
310
|
+
meta.target.removeEventListener(eventType, fn, meta.options)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
rule.listeners[slot] = [];
|
|
316
|
+
delete rule.listenerMeta[slot];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Make the registered functions for `slot` equal `fnList`, in order,
|
|
321
|
+
* touching only positions whose function identity changed. A change of
|
|
322
|
+
* target or registration options re-registers everything.
|
|
323
|
+
*/
|
|
324
|
+
setOrderedListeners(quarkHash, ruleId, slot, fnList, meta: ListenerMeta) {
|
|
325
|
+
const instance = this.getInstance(quarkHash);
|
|
326
|
+
const rule = this.getRule(instance, ruleId);
|
|
327
|
+
const previous = rule.listenerMeta[slot];
|
|
328
|
+
let listenerArray = this.getListeners(quarkHash, ruleId, slot);
|
|
329
|
+
if (
|
|
330
|
+
fnList.length !== listenerArray.length ||
|
|
331
|
+
(previous &&
|
|
332
|
+
(previous.target !== meta.target ||
|
|
333
|
+
previous.eventTypes.join("\0") !== meta.eventTypes.join("\0") ||
|
|
334
|
+
!!previous.options.capture !== !!meta.options.capture ||
|
|
335
|
+
!!previous.options.passive !== !!meta.options.passive))
|
|
336
|
+
) {
|
|
337
|
+
this.removeAllListeners(quarkHash, ruleId, slot);
|
|
338
|
+
listenerArray = this.getListeners(quarkHash, ruleId, slot);
|
|
339
|
+
}
|
|
340
|
+
rule.listenerMeta[slot] = meta;
|
|
341
|
+
const { target, eventTypes, options } = meta;
|
|
342
|
+
const add = (fn: UtilFn | undefined) =>
|
|
343
|
+
fn &&
|
|
344
|
+
eventTypes.forEach((type) => target.addEventListener(type, fn, options));
|
|
345
|
+
const remove = (fn: UtilFn | undefined) =>
|
|
346
|
+
fn &&
|
|
347
|
+
eventTypes.forEach((type) =>
|
|
348
|
+
target.removeEventListener(type, fn, options)
|
|
349
|
+
);
|
|
350
|
+
if (listenerArray.length === 0) {
|
|
351
|
+
fnList.forEach((fn) => {
|
|
352
|
+
add(fn);
|
|
353
|
+
listenerArray.push(fn);
|
|
354
|
+
});
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
fnList.forEach((fn, index) => {
|
|
358
|
+
const existingListener = listenerArray[index];
|
|
359
|
+
if (fn === existingListener) return;
|
|
360
|
+
remove(existingListener);
|
|
361
|
+
add(fn);
|
|
362
|
+
listenerArray[index] = fn;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|