@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/observer.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { CHILD_REMOVED } from "./constants";
|
|
2
|
+
import type { QuarkListenerConfig } from "./rule";
|
|
3
|
+
import { deref, QuarkLogger } from "./utils";
|
|
4
|
+
type CB = (arg: { element: HTMLElement; attribute: string }) => any;
|
|
5
|
+
|
|
6
|
+
/** Attach the sheet's root event listeners (prop changes, binding changes). */
|
|
7
|
+
export const listen = (listenerConfig: QuarkListenerConfig) => {
|
|
8
|
+
listenerConfig.forEach(([element, eventName, listener]) => {
|
|
9
|
+
deref(element)?.addEventListener(eventName, listener);
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** True when `nodes` holds at least one element. */
|
|
14
|
+
const hasElement = (nodes: NodeList) => {
|
|
15
|
+
for (const node of nodes) {
|
|
16
|
+
if (node.nodeType === Node.ELEMENT_NODE) return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const classTokens = (value: string | null) =>
|
|
22
|
+
new Set(value ? value.split(/\s+/) : []);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* True when a token in `names` is in one class value but not the other.
|
|
26
|
+
* Comparing each record's old value with the current one is enough: a
|
|
27
|
+
* net change shows in the batch's first record for that element.
|
|
28
|
+
*/
|
|
29
|
+
const classNamesFlipped = (
|
|
30
|
+
names: Set<string>,
|
|
31
|
+
before: string | null,
|
|
32
|
+
after: string | null
|
|
33
|
+
) => {
|
|
34
|
+
if (before === after) return false;
|
|
35
|
+
const a = classTokens(before);
|
|
36
|
+
const b = classTokens(after);
|
|
37
|
+
for (const token of a) if (!b.has(token) && names.has(token)) return true;
|
|
38
|
+
for (const token of b) if (!a.has(token) && names.has(token)) return true;
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Watch the host subtree. Attr records are filtered to names the sheet's
|
|
44
|
+
* rules reference; with `classNames`, a `class` record counts only when
|
|
45
|
+
* one of those tokens was added or removed (styling churn on other
|
|
46
|
+
* classes is dropped here). childList records queue on the *parent*:
|
|
47
|
+
* insertions as `content` (matching rules run; `content` rules below
|
|
48
|
+
* the insert re-run); when `childRemovals` is set (a rule depends on
|
|
49
|
+
* children or sibling position: `:has()`, `:nth-child()`, `a + b`),
|
|
50
|
+
* element-only removals as `CHILD_REMOVED`. Text-only records have
|
|
51
|
+
* nothing to match. Who changed the nodes (Quark, an element, app JS)
|
|
52
|
+
* does not matter; no render-event contract.
|
|
53
|
+
*/
|
|
54
|
+
export const observe = (
|
|
55
|
+
host: HTMLElement | WeakRef<HTMLElement>,
|
|
56
|
+
attributeFilter: string[],
|
|
57
|
+
cb: CB,
|
|
58
|
+
{
|
|
59
|
+
childRemovals = false,
|
|
60
|
+
classNames = null,
|
|
61
|
+
}: { childRemovals?: boolean; classNames?: Set<string> | null } = {}
|
|
62
|
+
) => {
|
|
63
|
+
const observer = new MutationObserver((mutationRecords: MutationRecord[]) => {
|
|
64
|
+
mutationRecords.forEach((record) => {
|
|
65
|
+
const { type, attributeName, target } = record;
|
|
66
|
+
if (type === "attributes" && attributeName) {
|
|
67
|
+
if (
|
|
68
|
+
classNames &&
|
|
69
|
+
attributeName === "class" &&
|
|
70
|
+
!classNamesFlipped(
|
|
71
|
+
classNames,
|
|
72
|
+
record.oldValue,
|
|
73
|
+
(target as Element).getAttribute("class")
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
cb({ element: target as HTMLElement, attribute: attributeName });
|
|
79
|
+
} else if (type === "childList") {
|
|
80
|
+
if (hasElement(record.addedNodes)) {
|
|
81
|
+
cb({ element: target as HTMLElement, attribute: "content" });
|
|
82
|
+
} else if (childRemovals && hasElement(record.removedNodes)) {
|
|
83
|
+
cb({ element: target as HTMLElement, attribute: CHILD_REMOVED });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
const h = deref(host);
|
|
89
|
+
if (!h) return QuarkLogger.error("Quark: Host not found");
|
|
90
|
+
observer.observe(h, {
|
|
91
|
+
childList: true,
|
|
92
|
+
subtree: true,
|
|
93
|
+
// `attributeFilter` with `attributes: false` is a TypeError
|
|
94
|
+
...(attributeFilter.length > 0
|
|
95
|
+
? { attributes: true, attributeFilter, attributeOldValue: !!classNames }
|
|
96
|
+
: { attributes: false }),
|
|
97
|
+
characterData: false,
|
|
98
|
+
});
|
|
99
|
+
return observer;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const unobserve = (
|
|
103
|
+
observer: MutationObserver | null,
|
|
104
|
+
listenerConfig: QuarkListenerConfig
|
|
105
|
+
) => {
|
|
106
|
+
listenerConfig.forEach(([element, eventName, listener]) => {
|
|
107
|
+
deref(element)?.removeEventListener(eventName, listener);
|
|
108
|
+
});
|
|
109
|
+
observer?.disconnect?.();
|
|
110
|
+
};
|
package/src/paint.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paint queues and the commit that flushes them.
|
|
3
|
+
*
|
|
4
|
+
* Resolvers never write the document directly: they schedule a paint.
|
|
5
|
+
* Paints batch into two priority queues and commit together in a later
|
|
6
|
+
* task, avoiding extra reflows and observer trips. Priority 0 (high): add
|
|
7
|
+
* DOM nodes. Priority 1 (low): set attrs. Needed because setting an attr
|
|
8
|
+
* on a custom element can trigger a query; if that runs before children
|
|
9
|
+
* are inserted, the query misses them.
|
|
10
|
+
*
|
|
11
|
+
* View transitions (`@view-transition`): a paint may carry a
|
|
12
|
+
* `PaintTransition`. When a commit holds one that changes the document,
|
|
13
|
+
* the commit runs inside `document.startViewTransition()`:
|
|
14
|
+
*
|
|
15
|
+
* - pending: the browser captures the old state at its next rendering
|
|
16
|
+
* opportunity, so nothing commits until it calls `update`;
|
|
17
|
+
* - updating: `update` commits every queued paint (flagged or not: one
|
|
18
|
+
* tick is one cut), then waits for Quark to settle (`whenSettled`,
|
|
19
|
+
* capped by `timeout`, optionally `until` a fact); commits meanwhile
|
|
20
|
+
* run inline, inside the same cut;
|
|
21
|
+
* - then the browser captures the new state and animates.
|
|
22
|
+
*
|
|
23
|
+
* Without a flagged paint, or when no transition can run (no API, reduced
|
|
24
|
+
* motion, hidden document, nothing would change, another transition
|
|
25
|
+
* active under `if-active: skip`), the commit is plain.
|
|
26
|
+
*/
|
|
27
|
+
import type { SettleUntil } from "./settle";
|
|
28
|
+
import { addBusyCheck, whenSettled } from "./settle";
|
|
29
|
+
import type { PaintFn } from "./types";
|
|
30
|
+
import { QuarkLogger } from "./utils";
|
|
31
|
+
import { getDevtoolsHook, publicize } from "@excom/kit-devtools";
|
|
32
|
+
import { LoopGuard, tc } from "@excom/kit-utils";
|
|
33
|
+
|
|
34
|
+
let callbacksToRun: Array<() => void> = [];
|
|
35
|
+
let commitTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
|
|
37
|
+
export const reqCommit = (cb: () => void) => {
|
|
38
|
+
if (!callbacksToRun.includes(cb)) {
|
|
39
|
+
callbacksToRun.push(cb);
|
|
40
|
+
if (!commitTimeout) {
|
|
41
|
+
commitTimeout = setTimeout(() => {
|
|
42
|
+
const cbs = [...callbacksToRun];
|
|
43
|
+
callbacksToRun = [];
|
|
44
|
+
commitTimeout = null;
|
|
45
|
+
cbs.forEach((cb) => cb());
|
|
46
|
+
}, 0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** How a flagged paint is committed (resolved `@view-transition` options). */
|
|
52
|
+
export interface PaintTransition {
|
|
53
|
+
/** `startViewTransition({ types })`, unioned across one commit. */
|
|
54
|
+
types: string[];
|
|
55
|
+
/** Cap on how long `update` waits for Quark to settle, in ms. */
|
|
56
|
+
timeout: number;
|
|
57
|
+
/** Hold the paint back this long before queuing it, in ms. */
|
|
58
|
+
delay?: number;
|
|
59
|
+
/** Another transition is active: commit unanimated, or start anyway. */
|
|
60
|
+
ifActive: "skip" | "replace";
|
|
61
|
+
/** Keep `update` open until this is met (capped by `timeout`). */
|
|
62
|
+
until?: SettleUntil;
|
|
63
|
+
/** The block's display text (`@view-transition (types: "t")`). */
|
|
64
|
+
source: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One queued paint. */
|
|
68
|
+
export interface PaintEntry {
|
|
69
|
+
/** The paint, inside the loop-guard depth of the run that scheduled it. */
|
|
70
|
+
run: () => void;
|
|
71
|
+
transition?: PaintTransition;
|
|
72
|
+
/**
|
|
73
|
+
* Commit-time check for a flagged paint: `false` when committing it
|
|
74
|
+
* would leave the document as it is. Absent means it changes something.
|
|
75
|
+
*/
|
|
76
|
+
willChange?: () => boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const PAINT_QUEUES: [Set<PaintEntry>, Set<PaintEntry>] = [
|
|
80
|
+
new Set(),
|
|
81
|
+
new Set(),
|
|
82
|
+
];
|
|
83
|
+
// TODO consider making each queue a Map with unique attr keys to prevent duplicate paints
|
|
84
|
+
|
|
85
|
+
/** Queued paints that carry a transition, in queue order. */
|
|
86
|
+
const flaggedPaints = new Set<PaintEntry>();
|
|
87
|
+
|
|
88
|
+
/** Where the current commit cycle is in a transition (see module comment). */
|
|
89
|
+
let phase: "pending" | "updating" | null = null;
|
|
90
|
+
/** Id of the latest transition cycle; a stale `update` / fallback is ignored. */
|
|
91
|
+
let cycle = 0;
|
|
92
|
+
/** The transition Quark started, until its `finished` settles. */
|
|
93
|
+
let ownTransition: ViewTransition | null = null;
|
|
94
|
+
/** Once-per-block warnings (`source` + kind). */
|
|
95
|
+
const warned = new Set<string>();
|
|
96
|
+
|
|
97
|
+
/** A pending transition whose `update` never arrives commits anyway. */
|
|
98
|
+
const PENDING_FALLBACK_MS = 1000;
|
|
99
|
+
|
|
100
|
+
addBusyCheck(
|
|
101
|
+
() => commitTimeout !== null || PAINT_QUEUES.some((queue) => queue.size > 0)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const warnOnce = (key: string, message: string) => {
|
|
105
|
+
if (warned.has(key)) return;
|
|
106
|
+
warned.add(key);
|
|
107
|
+
QuarkLogger.warn({ method: "viewTransition", message });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const enqueue = (entry: PaintEntry, priority: number) => {
|
|
111
|
+
PAINT_QUEUES[priority].add(entry);
|
|
112
|
+
if (entry.transition) flaggedPaints.add(entry);
|
|
113
|
+
reqCommit(paint);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const runEntry = (entry: PaintEntry) => {
|
|
117
|
+
try {
|
|
118
|
+
entry.run();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
QuarkLogger.error({
|
|
121
|
+
method: "paint",
|
|
122
|
+
message: "Quark: a paint threw; the other paints still commit",
|
|
123
|
+
error: [error],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Commit every queued paint, high priority first. */
|
|
129
|
+
const drain = () =>
|
|
130
|
+
PAINT_QUEUES.forEach((queue, priority) => {
|
|
131
|
+
const entries = Array.from(queue);
|
|
132
|
+
if (entries.length) {
|
|
133
|
+
QuarkLogger.info({
|
|
134
|
+
method: `triggerPaint(${priority})`,
|
|
135
|
+
numberOfPaints: entries.length,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
entries.forEach((entry) => {
|
|
139
|
+
queue.delete(entry);
|
|
140
|
+
if (entry.transition) flaggedPaints.delete(entry);
|
|
141
|
+
runEntry(entry);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const paint = () => {
|
|
146
|
+
// the pending transition's `update` commits everything
|
|
147
|
+
if (phase === "pending") return;
|
|
148
|
+
if (phase === null && flaggedPaints.size && startTransition()) return;
|
|
149
|
+
drain();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const schedulePaint = (
|
|
153
|
+
action: PaintFn,
|
|
154
|
+
priority: number = 1,
|
|
155
|
+
extras?: Pick<PaintEntry, "transition" | "willChange">
|
|
156
|
+
) => {
|
|
157
|
+
// paints commit in a later task: carry the causal depth of the run that
|
|
158
|
+
// scheduled them so the loop guard sees one continuous chain
|
|
159
|
+
const depth = LoopGuard.current();
|
|
160
|
+
const entry: PaintEntry = {
|
|
161
|
+
run: () => LoopGuard.run(depth, action),
|
|
162
|
+
transition: extras?.transition,
|
|
163
|
+
willChange: extras?.willChange,
|
|
164
|
+
};
|
|
165
|
+
const delay = entry.transition?.delay;
|
|
166
|
+
if (delay) {
|
|
167
|
+
setTimeout(
|
|
168
|
+
() =>
|
|
169
|
+
enqueue(
|
|
170
|
+
{ ...entry, transition: { ...entry.transition!, delay: 0 } },
|
|
171
|
+
priority
|
|
172
|
+
),
|
|
173
|
+
delay
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
enqueue(entry, priority);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const isTransitionActive = () =>
|
|
181
|
+
!!ownTransition ||
|
|
182
|
+
!!(document as Document & { activeViewTransition?: unknown })
|
|
183
|
+
.activeViewTransition ||
|
|
184
|
+
tc(() => document.documentElement.matches(":active-view-transition")) ===
|
|
185
|
+
true;
|
|
186
|
+
|
|
187
|
+
/** The flagged paints that would change the document (a throwing check counts). */
|
|
188
|
+
const changing = (entries: Iterable<PaintEntry>) =>
|
|
189
|
+
[...entries].filter((e) => !e.willChange || tc(e.willChange) !== false);
|
|
190
|
+
|
|
191
|
+
/** Why a commit with flagged paints commits plainly; `null` when it can animate. */
|
|
192
|
+
const skipReason = (changed: PaintEntry[]): string | null => {
|
|
193
|
+
if (typeof document.startViewTransition !== "function") return "unsupported";
|
|
194
|
+
if (
|
|
195
|
+
tc(() => window.matchMedia("(prefers-reduced-motion: reduce)").matches) ===
|
|
196
|
+
true
|
|
197
|
+
) {
|
|
198
|
+
return "reduced-motion";
|
|
199
|
+
}
|
|
200
|
+
if (document.visibilityState === "hidden") return "hidden";
|
|
201
|
+
if (!changed.length) return "unchanged";
|
|
202
|
+
if (changed[0].transition!.ifActive !== "replace" && isTransitionActive()) {
|
|
203
|
+
return "active";
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const typesOf = (entries: PaintEntry[]) => [
|
|
209
|
+
...new Set(entries.flatMap((entry) => entry.transition!.types)),
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
/** DevTools: `quark/transition` for a commit's flagged paints. */
|
|
213
|
+
const publish = (
|
|
214
|
+
phase: string,
|
|
215
|
+
entries: PaintEntry[],
|
|
216
|
+
meta?: Record<string, unknown>
|
|
217
|
+
) => {
|
|
218
|
+
if (getDevtoolsHook()?.publicize) {
|
|
219
|
+
publicize(["quark", "transition"], {
|
|
220
|
+
phase,
|
|
221
|
+
...meta,
|
|
222
|
+
types: typesOf(entries),
|
|
223
|
+
paints: entries.length,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Commit the queued paints inside a view transition. `false` when none
|
|
230
|
+
* can run (an engine without transition types throws on the options
|
|
231
|
+
* object and lands here too); the caller then commits plainly.
|
|
232
|
+
*/
|
|
233
|
+
const startTransition = (): boolean => {
|
|
234
|
+
// paints that change nothing neither start a transition nor name it
|
|
235
|
+
const flagged = changing(flaggedPaints);
|
|
236
|
+
const reason = skipReason(flagged);
|
|
237
|
+
if (reason) {
|
|
238
|
+
publish("skip", [...flaggedPaints], { reason });
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const types = typesOf(flagged);
|
|
242
|
+
let transition: ViewTransition | undefined;
|
|
243
|
+
const id = ++cycle;
|
|
244
|
+
const update = async () => {
|
|
245
|
+
clearTimeout(fallback);
|
|
246
|
+
// the fallback already committed (maybe another transition is pending
|
|
247
|
+
// now): capture whatever is there
|
|
248
|
+
if (id !== cycle || phase !== "pending") return;
|
|
249
|
+
phase = "updating";
|
|
250
|
+
try {
|
|
251
|
+
// flagged paints queued while pending join this cut
|
|
252
|
+
const cut = changing(flaggedPaints);
|
|
253
|
+
for (const type of typesOf(cut)) {
|
|
254
|
+
if (!types.includes(type)) transition?.types?.add(type);
|
|
255
|
+
}
|
|
256
|
+
const withUntil = cut.filter((entry) => entry.transition!.until);
|
|
257
|
+
for (const { transition: extra } of withUntil.slice(1)) {
|
|
258
|
+
warnOnce(
|
|
259
|
+
`${extra!.source}:until`,
|
|
260
|
+
`Quark: ${extra!.source} — one \`until\` per view transition; this one is ignored`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const wait = withUntil[0]?.transition;
|
|
264
|
+
const timeout = Math.max(0, ...cut.map((e) => e.transition!.timeout));
|
|
265
|
+
drain();
|
|
266
|
+
const result = await whenSettled({ timeout, until: wait?.until });
|
|
267
|
+
publish("settled", cut, { result });
|
|
268
|
+
if (result === "timeout") {
|
|
269
|
+
const source = wait?.source ?? cut[0]?.transition!.source;
|
|
270
|
+
warnOnce(
|
|
271
|
+
`${source}:timeout`,
|
|
272
|
+
`Quark: ${source} — ${wait ? "`until` was not met" : "Quark did not settle"} within ${timeout}ms; the new state was captured anyway`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
} finally {
|
|
276
|
+
phase = null;
|
|
277
|
+
if (PAINT_QUEUES.some((queue) => queue.size > 0)) reqCommit(paint);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
phase = "pending";
|
|
281
|
+
const fallback = setTimeout(() => {
|
|
282
|
+
if (id !== cycle || phase !== "pending") return;
|
|
283
|
+
phase = null;
|
|
284
|
+
drain();
|
|
285
|
+
}, PENDING_FALLBACK_MS);
|
|
286
|
+
try {
|
|
287
|
+
transition = document.startViewTransition({ update, types });
|
|
288
|
+
} catch {
|
|
289
|
+
clearTimeout(fallback);
|
|
290
|
+
phase = null;
|
|
291
|
+
publish("skip", flagged, { reason: "error" });
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const started = transition;
|
|
295
|
+
ownTransition = started;
|
|
296
|
+
const release = () => {
|
|
297
|
+
if (ownTransition === started) ownTransition = null;
|
|
298
|
+
};
|
|
299
|
+
const ignore = () => {};
|
|
300
|
+
started.finished?.then(release, release);
|
|
301
|
+
started.ready?.catch(ignore);
|
|
302
|
+
started.updateCallbackDone?.catch(ignore);
|
|
303
|
+
publish("start", flagged);
|
|
304
|
+
return true;
|
|
305
|
+
};
|