@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.
Files changed (130) hide show
  1. package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +6 -0
  11. package/index.ts +4 -0
  12. package/language.ts +7 -0
  13. package/package.json +45 -0
  14. package/rush-logs/quark.apply-exports.cache.log +1 -0
  15. package/rush-logs/quark.apply-exports.log +1 -0
  16. package/rush-logs/quark.build_package-metas.cache.log +1 -0
  17. package/rush-logs/quark.build_package-metas.log +1 -0
  18. package/src/actions.ts +445 -0
  19. package/src/ast.ts +187 -0
  20. package/src/bindings.ts +115 -0
  21. package/src/builtin-modules.ts +401 -0
  22. package/src/constants.ts +47 -0
  23. package/src/cycle-check.ts +121 -0
  24. package/src/devtools-hook.ts +303 -0
  25. package/src/element-api.ts +109 -0
  26. package/src/evaluator.ts +317 -0
  27. package/src/form-controls.ts +69 -0
  28. package/src/language-tables.ts +137 -0
  29. package/src/language.ts +1225 -0
  30. package/src/observer.ts +110 -0
  31. package/src/paint.ts +305 -0
  32. package/src/properties.ts +818 -0
  33. package/src/props.ts +94 -0
  34. package/src/quark-element.ts +7 -0
  35. package/src/quark-internal.ts +365 -0
  36. package/src/quark.ts +817 -0
  37. package/src/resolvers.ts +867 -0
  38. package/src/rule.ts +800 -0
  39. package/src/scope-id.ts +47 -0
  40. package/src/selector-utils.ts +430 -0
  41. package/src/settle.ts +117 -0
  42. package/src/types.ts +155 -0
  43. package/src/utils.ts +79 -0
  44. package/src/variables.ts +359 -0
  45. package/support/demos/css-variables.html +18 -0
  46. package/support/demos/dispatch.html +20 -0
  47. package/support/demos/events.html +9 -0
  48. package/support/demos/iterate.html +13 -0
  49. package/support/demos/js-api.html +17 -0
  50. package/support/demos/provider-list.html +16 -0
  51. package/support/demos/toggle-content.html +14 -0
  52. package/support/demos/unset.html +12 -0
  53. package/support/demos/view-transition.html +36 -0
  54. package/support/docs/ATTRIBUTES.md +37 -0
  55. package/support/docs/AT_RULES.md +45 -0
  56. package/support/docs/BUILTINS.md +57 -0
  57. package/support/docs/CONTENT.md +40 -0
  58. package/support/docs/CSS_VARIABLES.md +16 -0
  59. package/support/docs/DECLARATIONS.md +26 -0
  60. package/support/docs/DELAY.md +37 -0
  61. package/support/docs/DIAGNOSTICS.md +32 -0
  62. package/support/docs/DISPATCH.md +81 -0
  63. package/support/docs/ELEMENT_PROPERTIES.md +34 -0
  64. package/support/docs/EXPRESSIONS.md +31 -0
  65. package/support/docs/INTERNAL.md +8 -0
  66. package/support/docs/JS_API.md +53 -0
  67. package/support/docs/JS_WRITES.md +36 -0
  68. package/support/docs/LIMITATIONS.md +26 -0
  69. package/support/docs/LOOP_GUARD.md +30 -0
  70. package/support/docs/METHODS.md +43 -0
  71. package/support/docs/MODULES.md +111 -0
  72. package/support/docs/NO_REVERSION.md +18 -0
  73. package/support/docs/ON.md +87 -0
  74. package/support/docs/REACTIVITY.md +24 -0
  75. package/support/docs/README.md +143 -0
  76. package/support/docs/SELECTORS.md +87 -0
  77. package/support/docs/SHEETS.md +67 -0
  78. package/support/docs/SYNTAX.md +36 -0
  79. package/support/docs/USE.md +24 -0
  80. package/support/docs/VALUES.md +27 -0
  81. package/support/docs/VARIABLES.md +47 -0
  82. package/support/docs/VIEW_TRANSITION.md +64 -0
  83. package/support/docs-sections.json +65 -0
  84. package/support/package-meta.json +208 -0
  85. package/support/scripts/build-language-docs.mjs +196 -0
  86. package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
  87. package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
  88. package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
  89. package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
  90. package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
  91. package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
  92. package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
  93. package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
  94. package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
  95. package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
  96. package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
  97. package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
  98. package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
  99. package/support/tests/actions.test.ts +437 -0
  100. package/support/tests/builtin-modules.test.ts +337 -0
  101. package/support/tests/builtins.test.ts +189 -0
  102. package/support/tests/css-variables.view.test.ts +35 -0
  103. package/support/tests/delay.test.ts +244 -0
  104. package/support/tests/devtools-hook.test.ts +496 -0
  105. package/support/tests/diagnostics.test.ts +224 -0
  106. package/support/tests/dispatch.view.test.ts +43 -0
  107. package/support/tests/element-api.test.ts +202 -0
  108. package/support/tests/evaluator.test.ts +237 -0
  109. package/support/tests/events.view.test.ts +40 -0
  110. package/support/tests/form-controls.test.ts +452 -0
  111. package/support/tests/helpers.ts +354 -0
  112. package/support/tests/internals.test.ts +568 -0
  113. package/support/tests/iterate.view.test.ts +40 -0
  114. package/support/tests/js-api.view.test.ts +45 -0
  115. package/support/tests/language-docs.test.ts +147 -0
  116. package/support/tests/lifecycle.test.ts +440 -0
  117. package/support/tests/loop-guard.test.ts +323 -0
  118. package/support/tests/provider-list.view.test.ts +47 -0
  119. package/support/tests/quark-features.test.ts +2887 -0
  120. package/support/tests/quark-with-providers.test.ts +169 -0
  121. package/support/tests/quark.test.ts +359 -0
  122. package/support/tests/resolvers.test.ts +346 -0
  123. package/support/tests/selector-analysis.test.ts +281 -0
  124. package/support/tests/selectors.test.ts +670 -0
  125. package/support/tests/toggle-content.view.test.ts +40 -0
  126. package/support/tests/unset.view.test.ts +42 -0
  127. package/support/tests/view-helpers.ts +81 -0
  128. package/support/tests/view-transition.test.ts +1069 -0
  129. package/support/tests/view-transition.view.test.ts +76 -0
  130. 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,7 @@
1
+ export type TQuarkElement =
2
+ | HTMLElement
3
+ | Element
4
+ | Document
5
+ | DocumentFragment
6
+ | Node
7
+ | any;
@@ -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
+ }