@wcstack/view-transition 1.31.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/README.ja.md +169 -0
- package/README.md +169 -0
- package/dist/auto.min.js +2 -0
- package/dist/auto.min.js.map +1 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.esm.js +753 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
const _config = {
|
|
2
|
+
tagNames: {
|
|
3
|
+
viewTransition: "wcs-view-transition",
|
|
4
|
+
},
|
|
5
|
+
};
|
|
6
|
+
function deepFreeze(obj) {
|
|
7
|
+
if (obj === null || typeof obj !== "object")
|
|
8
|
+
return obj;
|
|
9
|
+
Object.freeze(obj);
|
|
10
|
+
for (const key of Object.keys(obj)) {
|
|
11
|
+
deepFreeze(obj[key]);
|
|
12
|
+
}
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
15
|
+
function deepClone(obj) {
|
|
16
|
+
if (obj === null || typeof obj !== "object")
|
|
17
|
+
return obj;
|
|
18
|
+
const clone = {};
|
|
19
|
+
for (const key of Object.keys(obj)) {
|
|
20
|
+
clone[key] = deepClone(obj[key]);
|
|
21
|
+
}
|
|
22
|
+
return clone;
|
|
23
|
+
}
|
|
24
|
+
let frozenConfig = null;
|
|
25
|
+
// Note: this is the live, mutable internal config. It is not part of the public
|
|
26
|
+
// package exports (see exports.ts) — only `getConfig()` (a frozen snapshot) is
|
|
27
|
+
// surfaced. `setConfig()` is applied internally via `bootstrapViewTransition()` and
|
|
28
|
+
// is not re-exported from the package root, though a deep path import
|
|
29
|
+
// (`.../src/config.js`) can still reach and mutate it. Accepted as-is for
|
|
30
|
+
// cross-package consistency: every @wcstack package follows this same shape.
|
|
31
|
+
// Use `getConfig()` for a frozen, safe read.
|
|
32
|
+
const config = _config;
|
|
33
|
+
function getConfig() {
|
|
34
|
+
if (!frozenConfig) {
|
|
35
|
+
frozenConfig = deepFreeze(deepClone(_config));
|
|
36
|
+
}
|
|
37
|
+
return frozenConfig;
|
|
38
|
+
}
|
|
39
|
+
function setConfig(partialConfig) {
|
|
40
|
+
if (partialConfig.tagNames) {
|
|
41
|
+
Object.assign(_config.tagNames, partialConfig.tagNames);
|
|
42
|
+
}
|
|
43
|
+
frozenConfig = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
48
|
+
// Generated from /protocol/transition-runner.ts by scripts/sync-protocol-types.mjs.
|
|
49
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
50
|
+
// ===========================================================================
|
|
51
|
+
// transition-runner protocol — how a package that mutates the DOM hands that
|
|
52
|
+
// mutation to whoever is arbitrating view transitions on the page.
|
|
53
|
+
//
|
|
54
|
+
// @wcstack/state and @wcstack/router must not depend on @wcstack/view-transition
|
|
55
|
+
// (zero runtime dependencies, independently publishable), so the arbiter installs
|
|
56
|
+
// itself on a well-known global symbol and the participants look it up lazily.
|
|
57
|
+
// No arbiter installed means the mutation is invoked directly, synchronously —
|
|
58
|
+
// byte-for-byte the behavior these packages had before the protocol existed.
|
|
59
|
+
//
|
|
60
|
+
// docs/view-transition-design.md §4 is the normative description.
|
|
61
|
+
//
|
|
62
|
+
// SINGLE SOURCE OF TRUTH: edit only this file (/protocol/transition-runner.ts), then run
|
|
63
|
+
// `node scripts/sync-protocol-types.mjs` to regenerate the per-package copies
|
|
64
|
+
// (packages/<pkg>/src/protocol/transitionRunner.ts). Those copies are generated — do not edit them.
|
|
65
|
+
/**
|
|
66
|
+
* Global key the arbiter installs itself under. `Symbol.for` so independently
|
|
67
|
+
* loaded copies of this file (two CDN bundles on one page) still agree.
|
|
68
|
+
*/
|
|
69
|
+
const TRANSITION_RUNNER_KEY = Symbol.for("wcstack.transition-runner");
|
|
70
|
+
/**
|
|
71
|
+
* The installed arbiter, or null when there is none, it speaks a version this
|
|
72
|
+
* reader does not, or it does not accept this participant.
|
|
73
|
+
*
|
|
74
|
+
* Looked up on every call rather than cached: the tag can be added, removed, or
|
|
75
|
+
* reconfigured at any point in a page's life, and a stale cache would either
|
|
76
|
+
* animate what the author just switched off or miss what they switched on.
|
|
77
|
+
*/
|
|
78
|
+
function getTransitionRunner(source) {
|
|
79
|
+
const candidate = globalThis[TRANSITION_RUNNER_KEY];
|
|
80
|
+
if (candidate === undefined || candidate === null)
|
|
81
|
+
return null;
|
|
82
|
+
if (candidate.protocol !== "wcs-transition-runner")
|
|
83
|
+
return null;
|
|
84
|
+
if (typeof candidate.version !== "number" || candidate.version < 1)
|
|
85
|
+
return null;
|
|
86
|
+
if (typeof candidate.run !== "function")
|
|
87
|
+
return null;
|
|
88
|
+
if (typeof candidate.accepts !== "function" || !candidate.accepts(source))
|
|
89
|
+
return null;
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Run `mutate` under the installed arbiter, or directly when there is none.
|
|
94
|
+
*
|
|
95
|
+
* Returns `undefined` in the no-arbiter case instead of a resolved promise: the
|
|
96
|
+
* state drain calls this on every batch, and awaiting is a caller's choice, not
|
|
97
|
+
* an allocation the common path should pay for. `await` accepts both.
|
|
98
|
+
*/
|
|
99
|
+
function runTransition(source, mutate, types) {
|
|
100
|
+
const runner = getTransitionRunner(source);
|
|
101
|
+
if (runner === null) {
|
|
102
|
+
mutate();
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return runner.run(mutate, { source, types });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const DEFAULT_NAMING_LIMIT = 200;
|
|
109
|
+
const DEFAULT_PARTICIPANTS = ["router", "state"];
|
|
110
|
+
function toError(value) {
|
|
111
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Accept both the array form and the space-separated string an attribute (or a
|
|
115
|
+
* `data-wcs` binding) produces. The Core is a public export, so `core.types = "a b"`
|
|
116
|
+
* is a call an adopter can make — and without normalizing it here that string
|
|
117
|
+
* would degrade into single characters (`new Set("router")` contains no
|
|
118
|
+
* `"router"`, so `accepts()` would answer false for every participant).
|
|
119
|
+
*/
|
|
120
|
+
function toStringList(value) {
|
|
121
|
+
if (typeof value === "string") {
|
|
122
|
+
return value.split(/\s+/).filter((token) => token !== "");
|
|
123
|
+
}
|
|
124
|
+
return [...value];
|
|
125
|
+
}
|
|
126
|
+
function prefersReducedMotion() {
|
|
127
|
+
// never-throw: matchMedia is absent in happy-dom and in non-browser hosts.
|
|
128
|
+
try {
|
|
129
|
+
const mm = globalThis.matchMedia;
|
|
130
|
+
if (typeof mm !== "function")
|
|
131
|
+
return false;
|
|
132
|
+
return mm.call(globalThis, "(prefers-reduced-motion: reduce)").matches === true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether `startViewTransition({ update, types })` is understood. Detected on
|
|
140
|
+
* `ViewTransition.prototype`, because passing the object form to an
|
|
141
|
+
* implementation that only accepts a callback would throw at the call site —
|
|
142
|
+
* after the browser has already decided it has no update callback to run.
|
|
143
|
+
*/
|
|
144
|
+
function supportsTypes() {
|
|
145
|
+
try {
|
|
146
|
+
const ctor = globalThis.ViewTransition;
|
|
147
|
+
return ctor !== undefined && "types" in ctor.prototype;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Headless view-transition arbiter — the single place on a page that decides
|
|
155
|
+
* whether a DOM mutation animates, and what happens when two of them collide.
|
|
156
|
+
*
|
|
157
|
+
* It is not an I/O node: nothing is read from a device and there is no data to
|
|
158
|
+
* bind. It is a *policy* node. Participants (`@wcstack/router`, `@wcstack/state`)
|
|
159
|
+
* never import it; they find it through the transition-runner protocol on a
|
|
160
|
+
* well-known global symbol and hand it a mutation to run
|
|
161
|
+
* (docs/view-transition-design.md §4).
|
|
162
|
+
*
|
|
163
|
+
* The one invariant everything else is subordinate to: **a mutation handed to
|
|
164
|
+
* `run()` is applied exactly once**, whatever is decided about animating it. An
|
|
165
|
+
* unsupported browser, a hidden tab, reduced motion, a colliding transition and a
|
|
166
|
+
* `startViewTransition` that throws all end in the mutation running — the page
|
|
167
|
+
* must never be left showing stale DOM because an animation could not be played.
|
|
168
|
+
*/
|
|
169
|
+
class ViewTransitionCore extends EventTarget {
|
|
170
|
+
static wcBindable = {
|
|
171
|
+
protocol: "wc-bindable",
|
|
172
|
+
version: 1,
|
|
173
|
+
properties: [
|
|
174
|
+
{ name: "active", event: "wcs-view-transition:active-changed", semantics: "state" },
|
|
175
|
+
{ name: "error", event: "wcs-view-transition:error", semantics: "state" },
|
|
176
|
+
],
|
|
177
|
+
commands: [
|
|
178
|
+
{ name: "skip" },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
_target;
|
|
182
|
+
_mode = "latest";
|
|
183
|
+
_naming = "manual";
|
|
184
|
+
_namingLimit = DEFAULT_NAMING_LIMIT;
|
|
185
|
+
_reducedMotion = "skip";
|
|
186
|
+
_types = [];
|
|
187
|
+
_disabled = false;
|
|
188
|
+
_participants = new Set(DEFAULT_PARTICIPANTS);
|
|
189
|
+
_active = false;
|
|
190
|
+
_error = null;
|
|
191
|
+
/** Requests waiting for the microtask flush that starts a transition. */
|
|
192
|
+
_pending = null;
|
|
193
|
+
_flushScheduled = false;
|
|
194
|
+
/**
|
|
195
|
+
* The batch handed to the running transition while its update callback has not
|
|
196
|
+
* fired yet. Non-null means "capturing": a request arriving now still joins this
|
|
197
|
+
* batch, which is both the coalescing window and the only ordering guarantee
|
|
198
|
+
* that keeps a later `exhaust`/`latest` request from applying ahead of it.
|
|
199
|
+
*/
|
|
200
|
+
_batch = null;
|
|
201
|
+
_transition = null;
|
|
202
|
+
_queue = [];
|
|
203
|
+
constructor(target) {
|
|
204
|
+
super();
|
|
205
|
+
this._target = target ?? this;
|
|
206
|
+
}
|
|
207
|
+
// --- transition-runner protocol surface ---
|
|
208
|
+
get protocol() {
|
|
209
|
+
return "wcs-transition-runner";
|
|
210
|
+
}
|
|
211
|
+
get version() {
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
get naming() {
|
|
215
|
+
return this._naming;
|
|
216
|
+
}
|
|
217
|
+
set naming(value) {
|
|
218
|
+
this._naming = value === "auto" ? "auto" : "manual";
|
|
219
|
+
}
|
|
220
|
+
get namingLimit() {
|
|
221
|
+
return this._namingLimit;
|
|
222
|
+
}
|
|
223
|
+
set namingLimit(value) {
|
|
224
|
+
this._namingLimit = Number.isFinite(value) && value >= 0 ? Math.floor(value) : DEFAULT_NAMING_LIMIT;
|
|
225
|
+
}
|
|
226
|
+
accepts(source) {
|
|
227
|
+
return this._participants.has(source);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Install this core as the page's arbiter. Returns false (and warns) when
|
|
231
|
+
* another one already holds the slot — two arbiters would each think they own
|
|
232
|
+
* the exclusion, which is precisely the thing an arbiter exists to prevent.
|
|
233
|
+
*/
|
|
234
|
+
install() {
|
|
235
|
+
const slot = globalThis;
|
|
236
|
+
const current = slot[TRANSITION_RUNNER_KEY];
|
|
237
|
+
if (current !== undefined && current !== null && current !== this) {
|
|
238
|
+
console.warn("[@wcstack/view-transition] a transition runner is already installed; " +
|
|
239
|
+
"this element is inert. Use one <wcs-view-transition> per document.");
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
slot[TRANSITION_RUNNER_KEY] = this;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
/** Release the arbiter slot, but only if it is still ours. */
|
|
246
|
+
uninstall() {
|
|
247
|
+
const slot = globalThis;
|
|
248
|
+
if (slot[TRANSITION_RUNNER_KEY] === this) {
|
|
249
|
+
delete slot[TRANSITION_RUNNER_KEY];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// --- configuration ---
|
|
253
|
+
get mode() {
|
|
254
|
+
return this._mode;
|
|
255
|
+
}
|
|
256
|
+
set mode(value) {
|
|
257
|
+
this._mode = value === "queue" || value === "exhaust" ? value : "latest";
|
|
258
|
+
}
|
|
259
|
+
get reducedMotion() {
|
|
260
|
+
return this._reducedMotion;
|
|
261
|
+
}
|
|
262
|
+
set reducedMotion(value) {
|
|
263
|
+
this._reducedMotion = value === "animate" ? "animate" : "skip";
|
|
264
|
+
}
|
|
265
|
+
get types() {
|
|
266
|
+
return this._types;
|
|
267
|
+
}
|
|
268
|
+
set types(value) {
|
|
269
|
+
this._types = toStringList(value);
|
|
270
|
+
}
|
|
271
|
+
get disabled() {
|
|
272
|
+
return this._disabled;
|
|
273
|
+
}
|
|
274
|
+
set disabled(value) {
|
|
275
|
+
this._disabled = value === true;
|
|
276
|
+
}
|
|
277
|
+
get participants() {
|
|
278
|
+
return [...this._participants];
|
|
279
|
+
}
|
|
280
|
+
set participants(value) {
|
|
281
|
+
const list = toStringList(value);
|
|
282
|
+
this._participants = new Set(list.length > 0 ? list : DEFAULT_PARTICIPANTS);
|
|
283
|
+
}
|
|
284
|
+
// --- observable outputs ---
|
|
285
|
+
get active() {
|
|
286
|
+
return this._active;
|
|
287
|
+
}
|
|
288
|
+
get error() {
|
|
289
|
+
return this._error;
|
|
290
|
+
}
|
|
291
|
+
// --- commands ---
|
|
292
|
+
/**
|
|
293
|
+
* Finish the running transition now. Per spec the update callback still runs if
|
|
294
|
+
* it has not yet, so skipping loses the animation and never the DOM update.
|
|
295
|
+
*/
|
|
296
|
+
skip() {
|
|
297
|
+
this._transition?.skipTransition();
|
|
298
|
+
}
|
|
299
|
+
// --- the protocol entry point ---
|
|
300
|
+
run(mutate, _options) {
|
|
301
|
+
if (!this._canTransition()) {
|
|
302
|
+
return this._applyNow(mutate);
|
|
303
|
+
}
|
|
304
|
+
return new Promise((resolve, reject) => {
|
|
305
|
+
const entry = { mutate, resolve, reject };
|
|
306
|
+
// Capturing: the running transition has not called its update callback yet,
|
|
307
|
+
// so this mutation can still ride along — and must, or it would be applied
|
|
308
|
+
// before mutations that were requested earlier.
|
|
309
|
+
if (this._batch !== null) {
|
|
310
|
+
this._batch.push(entry);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (this._transition !== null && this._mode === "exhaust") {
|
|
314
|
+
this._settle(entry);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
(this._pending ??= []).push(entry);
|
|
318
|
+
this._schedule();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
dispose() {
|
|
322
|
+
this.uninstall();
|
|
323
|
+
// Anything still unapplied belongs to a page that is going away; apply it so
|
|
324
|
+
// the DOM does not stay behind the state that asked for the change.
|
|
325
|
+
//
|
|
326
|
+
// Order is request order, and that is why the capturing batch has to be taken
|
|
327
|
+
// first: its mutations were requested *before* everything in _pending and
|
|
328
|
+
// _queue, but they are the ones still waiting on a frame. Settling only the
|
|
329
|
+
// later two would apply them out of order. Clearing _batch also turns the
|
|
330
|
+
// running transition's update callback into a no-op, which is what keeps
|
|
331
|
+
// "applied exactly once" true across a dispose.
|
|
332
|
+
const capturing = this._batch;
|
|
333
|
+
this._batch = null;
|
|
334
|
+
const abandoned = [...(capturing ?? []), ...(this._pending ?? []), ...this._queue.flat()];
|
|
335
|
+
this._pending = null;
|
|
336
|
+
this._queue = [];
|
|
337
|
+
for (const entry of abandoned) {
|
|
338
|
+
this._settle(entry);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// --- internals ---
|
|
342
|
+
_canTransition() {
|
|
343
|
+
if (this._disabled)
|
|
344
|
+
return false;
|
|
345
|
+
const doc = globalThis.document;
|
|
346
|
+
if (doc === undefined || typeof doc.startViewTransition !== "function") {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
// SSR: no transition is started while rendering on the server (G5). The gate
|
|
350
|
+
// lives here rather than in each participant because the protocol is public —
|
|
351
|
+
// a third-party participant has no reason to know wcstack's SSR marker, and
|
|
352
|
+
// the arbiter is the one place that owns the policy. `@wcstack/server` sets
|
|
353
|
+
// the attribute on its own document and it never reaches the client HTML.
|
|
354
|
+
if (doc.documentElement?.hasAttribute("data-wcs-server") === true)
|
|
355
|
+
return false;
|
|
356
|
+
// A hidden tab gets no rendering opportunities, so the update callback would
|
|
357
|
+
// not run until the page is looked at again — the DOM would silently freeze
|
|
358
|
+
// for as long as the tab stays in the background. Apply straight through.
|
|
359
|
+
if (doc.hidden === true)
|
|
360
|
+
return false;
|
|
361
|
+
if (this._reducedMotion === "skip" && prefersReducedMotion())
|
|
362
|
+
return false;
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
_applyNow(mutate) {
|
|
366
|
+
try {
|
|
367
|
+
mutate();
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
return Promise.reject(error);
|
|
371
|
+
}
|
|
372
|
+
return Promise.resolve();
|
|
373
|
+
}
|
|
374
|
+
_settle(entry) {
|
|
375
|
+
try {
|
|
376
|
+
entry.mutate();
|
|
377
|
+
entry.resolve();
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
entry.reject(error);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
_schedule() {
|
|
384
|
+
if (this._flushScheduled)
|
|
385
|
+
return;
|
|
386
|
+
this._flushScheduled = true;
|
|
387
|
+
queueMicrotask(() => this._flush());
|
|
388
|
+
}
|
|
389
|
+
_flush() {
|
|
390
|
+
this._flushScheduled = false;
|
|
391
|
+
const batch = this._pending;
|
|
392
|
+
this._pending = null;
|
|
393
|
+
if (batch === null || batch.length === 0)
|
|
394
|
+
return;
|
|
395
|
+
if (this._transition !== null) {
|
|
396
|
+
if (this._mode === "queue") {
|
|
397
|
+
this._queue.push(batch);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (this._mode === "exhaust") {
|
|
401
|
+
for (const entry of batch) {
|
|
402
|
+
this._settle(entry);
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// "latest": starting a new transition skips the running one, and the
|
|
407
|
+
// running one is past its update callback (a capturing batch is joined in
|
|
408
|
+
// run(), never reaching here), so ordering holds.
|
|
409
|
+
}
|
|
410
|
+
this._start(batch);
|
|
411
|
+
}
|
|
412
|
+
_start(batch) {
|
|
413
|
+
const doc = globalThis.document;
|
|
414
|
+
const start = doc.startViewTransition;
|
|
415
|
+
this._batch = batch;
|
|
416
|
+
const update = () => {
|
|
417
|
+
const running = this._batch;
|
|
418
|
+
this._batch = null;
|
|
419
|
+
if (running === null)
|
|
420
|
+
return;
|
|
421
|
+
for (const entry of running) {
|
|
422
|
+
this._settle(entry);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
let transition;
|
|
426
|
+
try {
|
|
427
|
+
transition = this._types.length > 0 && supportsTypes()
|
|
428
|
+
? start.call(doc, { update, types: [...this._types] })
|
|
429
|
+
: start.call(doc, update);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
// Could not even start: apply the mutations rather than lose them.
|
|
433
|
+
this._batch = null;
|
|
434
|
+
this._setError(toError(error));
|
|
435
|
+
for (const entry of batch) {
|
|
436
|
+
this._settle(entry);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
this._transition = transition;
|
|
441
|
+
this._setError(null);
|
|
442
|
+
this._setActive(true);
|
|
443
|
+
// `finished` rejects when the update callback throws — it cannot here, since
|
|
444
|
+
// _settle catches per entry — and `ready` rejects whenever the transition is
|
|
445
|
+
// skipped, which is routine. Both are attached defensively so a routine skip
|
|
446
|
+
// never surfaces as an unhandled rejection.
|
|
447
|
+
const done = () => this._onFinished(transition);
|
|
448
|
+
transition.finished.then(done, done);
|
|
449
|
+
transition.ready.then(undefined, () => { });
|
|
450
|
+
transition.updateCallbackDone.then(undefined, () => { });
|
|
451
|
+
}
|
|
452
|
+
_onFinished(transition) {
|
|
453
|
+
// A superseded transition ("latest") still settles; only the current one owns
|
|
454
|
+
// the active flag and the queue.
|
|
455
|
+
if (this._transition !== transition)
|
|
456
|
+
return;
|
|
457
|
+
this._transition = null;
|
|
458
|
+
this._setActive(false);
|
|
459
|
+
const next = this._queue.shift();
|
|
460
|
+
if (next !== undefined) {
|
|
461
|
+
this._start(next);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
_setActive(value) {
|
|
465
|
+
if (this._active === value)
|
|
466
|
+
return;
|
|
467
|
+
this._active = value;
|
|
468
|
+
this._dispatch("wcs-view-transition:active-changed", value);
|
|
469
|
+
}
|
|
470
|
+
_setError(error) {
|
|
471
|
+
if (this._error === error)
|
|
472
|
+
return;
|
|
473
|
+
this._error = error;
|
|
474
|
+
this._dispatch("wcs-view-transition:error", error);
|
|
475
|
+
}
|
|
476
|
+
_dispatch(type, detail) {
|
|
477
|
+
this._target.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ===========================================================================
|
|
482
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
483
|
+
// Generated from /protocol/upgrade-properties.ts by scripts/sync-protocol-types.mjs.
|
|
484
|
+
// Run `node scripts/sync-protocol-types.mjs` after editing the source.
|
|
485
|
+
// ===========================================================================
|
|
486
|
+
function hasAccessorOnPrototype(target, name) {
|
|
487
|
+
let proto = Object.getPrototypeOf(target);
|
|
488
|
+
while (proto !== null) {
|
|
489
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
|
|
490
|
+
if (descriptor !== undefined) {
|
|
491
|
+
return typeof descriptor.get === "function" || typeof descriptor.set === "function";
|
|
492
|
+
}
|
|
493
|
+
proto = Object.getPrototypeOf(proto);
|
|
494
|
+
}
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* `connectedCallback` の先頭で呼ぶ。宣言済み input のうち upgrade 前の代入で
|
|
499
|
+
* accessor をシャドウしている own プロパティを、delete → 再代入で setter に通し直す。
|
|
500
|
+
*
|
|
501
|
+
* - 冪等: 再代入は accessor を通るので own プロパティは残らず、2 回目以降は no-op。
|
|
502
|
+
* - 宣言に `inputs` が無い要素、`wcBindable` を持たない要素では何もしない。
|
|
503
|
+
* - 値の意味は変えない。今まで捨てられていた代入が届くようになる一方向の変化。
|
|
504
|
+
*/
|
|
505
|
+
function upgradeProperties(element) {
|
|
506
|
+
const declaration = element.constructor?.wcBindable;
|
|
507
|
+
const inputs = declaration?.inputs;
|
|
508
|
+
if (inputs === undefined)
|
|
509
|
+
return;
|
|
510
|
+
for (const input of inputs) {
|
|
511
|
+
const name = input.name;
|
|
512
|
+
if (!Object.prototype.hasOwnProperty.call(element, name))
|
|
513
|
+
continue;
|
|
514
|
+
if (!hasAccessorOnPrototype(element, name))
|
|
515
|
+
continue;
|
|
516
|
+
const record = element;
|
|
517
|
+
const value = record[name];
|
|
518
|
+
delete record[name];
|
|
519
|
+
record[name] = value;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* `<wcs-view-transition>` — the page's view-transition policy node.
|
|
525
|
+
*
|
|
526
|
+
* It renders nothing and binds no data. It declares *how* the DOM changes that
|
|
527
|
+
* `@wcstack/router` and `@wcstack/state` make should animate, and it is the single
|
|
528
|
+
* arbiter that decides what happens when two of those changes collide. Dropping
|
|
529
|
+
* the tag on a page is the opt-in; removing it restores the framework's original
|
|
530
|
+
* synchronous behavior exactly (docs/view-transition-design.md §3, G1/G2).
|
|
531
|
+
*
|
|
532
|
+
* ```html
|
|
533
|
+
* <wcs-view-transition for="router" mode="latest"></wcs-view-transition>
|
|
534
|
+
* ```
|
|
535
|
+
*
|
|
536
|
+
* The animation itself is written in CSS against `::view-transition-*`. This tag
|
|
537
|
+
* starts and arbitrates transitions; it never describes one.
|
|
538
|
+
*/
|
|
539
|
+
class WcsViewTransition extends HTMLElement {
|
|
540
|
+
static observedAttributes = [
|
|
541
|
+
"mode", "naming", "naming-limit", "reduced-motion", "types", "disabled", "for",
|
|
542
|
+
];
|
|
543
|
+
// `properties` and `commands` come from the Core through the spread, so a
|
|
544
|
+
// member added there cannot be missed here. Only `inputs` — the attribute
|
|
545
|
+
// surface, which exists on the element and not on the Core — is declared.
|
|
546
|
+
static wcBindable = {
|
|
547
|
+
...ViewTransitionCore.wcBindable,
|
|
548
|
+
inputs: [
|
|
549
|
+
{ name: "disabled", attribute: "disabled" },
|
|
550
|
+
{ name: "mode", attribute: "mode" },
|
|
551
|
+
{ name: "naming", attribute: "naming" },
|
|
552
|
+
{ name: "namingLimit", attribute: "naming-limit" },
|
|
553
|
+
{ name: "reducedMotion", attribute: "reduced-motion" },
|
|
554
|
+
{ name: "types", attribute: "types" },
|
|
555
|
+
{ name: "participants", attribute: "for" },
|
|
556
|
+
],
|
|
557
|
+
};
|
|
558
|
+
_core;
|
|
559
|
+
_internals = null;
|
|
560
|
+
_installed = false;
|
|
561
|
+
constructor() {
|
|
562
|
+
super();
|
|
563
|
+
this._core = new ViewTransitionCore(this);
|
|
564
|
+
this._internals = this._initInternals();
|
|
565
|
+
this._wireStates({
|
|
566
|
+
"wcs-view-transition:active-changed": (d) => ({ active: d === true }),
|
|
567
|
+
"wcs-view-transition:error": (d) => ({ error: d != null }),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/** The headless arbiter, for direct (non-DOM) use. */
|
|
571
|
+
get core() {
|
|
572
|
+
return this._core;
|
|
573
|
+
}
|
|
574
|
+
// CSS state reflection (:state()) — debug-only snapshot getter. NOT part of
|
|
575
|
+
// wc-bindable. MUST NOT return the live CustomStateSet.
|
|
576
|
+
get debugStates() {
|
|
577
|
+
return this._internals ? [...this._internals.states] : [];
|
|
578
|
+
}
|
|
579
|
+
_initInternals() {
|
|
580
|
+
// never-throw: attachInternals is absent in happy-dom / older environments,
|
|
581
|
+
// and pre-125 Chromium rejects non-dashed state names (probed and discarded).
|
|
582
|
+
try {
|
|
583
|
+
if (typeof this.attachInternals !== "function")
|
|
584
|
+
return null;
|
|
585
|
+
const internals = this.attachInternals();
|
|
586
|
+
internals.states.add("wcs-probe");
|
|
587
|
+
internals.states.delete("wcs-probe");
|
|
588
|
+
return internals;
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
_wireStates(map) {
|
|
595
|
+
if (this._internals === null)
|
|
596
|
+
return;
|
|
597
|
+
const states = this._internals.states;
|
|
598
|
+
for (const [event, toStates] of Object.entries(map)) {
|
|
599
|
+
this.addEventListener(event, (e) => {
|
|
600
|
+
const debug = this.hasAttribute("debug-states");
|
|
601
|
+
for (const [name, on] of Object.entries(toStates(e.detail))) {
|
|
602
|
+
try {
|
|
603
|
+
if (on) {
|
|
604
|
+
states.add(name);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
states.delete(name);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch { /* never-throw */ }
|
|
611
|
+
if (debug)
|
|
612
|
+
this.toggleAttribute(`data-wcs-state-${name}`, on);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// --- inputs ---
|
|
618
|
+
get disabled() {
|
|
619
|
+
return this._core.disabled;
|
|
620
|
+
}
|
|
621
|
+
set disabled(value) {
|
|
622
|
+
this._core.disabled = value === true;
|
|
623
|
+
this.toggleAttribute("disabled", value === true);
|
|
624
|
+
}
|
|
625
|
+
get mode() {
|
|
626
|
+
return this._core.mode;
|
|
627
|
+
}
|
|
628
|
+
set mode(value) {
|
|
629
|
+
this._core.mode = value;
|
|
630
|
+
}
|
|
631
|
+
get naming() {
|
|
632
|
+
return this._core.naming;
|
|
633
|
+
}
|
|
634
|
+
set naming(value) {
|
|
635
|
+
this._core.naming = value;
|
|
636
|
+
}
|
|
637
|
+
get namingLimit() {
|
|
638
|
+
return this._core.namingLimit;
|
|
639
|
+
}
|
|
640
|
+
set namingLimit(value) {
|
|
641
|
+
this._core.namingLimit = Number(value);
|
|
642
|
+
}
|
|
643
|
+
get reducedMotion() {
|
|
644
|
+
return this._core.reducedMotion;
|
|
645
|
+
}
|
|
646
|
+
set reducedMotion(value) {
|
|
647
|
+
this._core.reducedMotion = value;
|
|
648
|
+
}
|
|
649
|
+
get types() {
|
|
650
|
+
return this._core.types;
|
|
651
|
+
}
|
|
652
|
+
set types(value) {
|
|
653
|
+
this._core.types = value;
|
|
654
|
+
}
|
|
655
|
+
get participants() {
|
|
656
|
+
return this._core.participants;
|
|
657
|
+
}
|
|
658
|
+
set participants(value) {
|
|
659
|
+
this._core.participants = value;
|
|
660
|
+
}
|
|
661
|
+
// --- observable outputs ---
|
|
662
|
+
get active() {
|
|
663
|
+
return this._core.active;
|
|
664
|
+
}
|
|
665
|
+
get error() {
|
|
666
|
+
return this._core.error;
|
|
667
|
+
}
|
|
668
|
+
// --- commands ---
|
|
669
|
+
skip() {
|
|
670
|
+
this._core.skip();
|
|
671
|
+
}
|
|
672
|
+
// --- lifecycle ---
|
|
673
|
+
connectedCallback() {
|
|
674
|
+
upgradeProperties(this);
|
|
675
|
+
this._syncAllAttributes();
|
|
676
|
+
this._installed = this._core.install();
|
|
677
|
+
}
|
|
678
|
+
disconnectedCallback() {
|
|
679
|
+
if (this._installed) {
|
|
680
|
+
// dispose(), not uninstall(): a mutation already handed to this arbiter must
|
|
681
|
+
// still be applied even though the page just removed its policy tag.
|
|
682
|
+
this._core.dispose();
|
|
683
|
+
this._installed = false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
687
|
+
if (oldValue === newValue)
|
|
688
|
+
return;
|
|
689
|
+
this._applyAttribute(name, newValue);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Apply the attributes present at connect time. Absent ones are deliberately
|
|
693
|
+
* skipped rather than applied as null: a property assigned before upgrade
|
|
694
|
+
* (Angular's `[prop]`, Lit's `.prop=`, or plain `el.mode = ...`) has just been
|
|
695
|
+
* replayed through the setter by `upgradeProperties`, and re-applying a missing
|
|
696
|
+
* attribute would immediately reset it to the default. Removing an attribute
|
|
697
|
+
* still resets, via `attributeChangedCallback`.
|
|
698
|
+
*/
|
|
699
|
+
_syncAllAttributes() {
|
|
700
|
+
for (const name of WcsViewTransition.observedAttributes) {
|
|
701
|
+
const value = this.getAttribute(name);
|
|
702
|
+
if (value === null)
|
|
703
|
+
continue;
|
|
704
|
+
this._applyAttribute(name, value);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
_applyAttribute(name, value) {
|
|
708
|
+
switch (name) {
|
|
709
|
+
case "mode":
|
|
710
|
+
this._core.mode = (value ?? "latest");
|
|
711
|
+
break;
|
|
712
|
+
case "naming":
|
|
713
|
+
this._core.naming = (value ?? "manual");
|
|
714
|
+
break;
|
|
715
|
+
case "naming-limit":
|
|
716
|
+
this._core.namingLimit = value === null ? Number.NaN : Number(value);
|
|
717
|
+
break;
|
|
718
|
+
case "reduced-motion":
|
|
719
|
+
this._core.reducedMotion = (value ?? "skip");
|
|
720
|
+
break;
|
|
721
|
+
case "types":
|
|
722
|
+
this._core.types = value ?? "";
|
|
723
|
+
break;
|
|
724
|
+
case "disabled":
|
|
725
|
+
this._core.disabled = value !== null;
|
|
726
|
+
break;
|
|
727
|
+
case "for":
|
|
728
|
+
this._core.participants = value ?? "";
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Register this package's tags. Pass a scoped `CustomElementRegistry` to define
|
|
736
|
+
* them for a single shadow tree -- scoped registries do not inherit the global
|
|
737
|
+
* one, so a tree using one needs its own definitions.
|
|
738
|
+
*/
|
|
739
|
+
function registerComponents(registry = customElements) {
|
|
740
|
+
if (!registry.get(config.tagNames.viewTransition)) {
|
|
741
|
+
registry.define(config.tagNames.viewTransition, WcsViewTransition);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function bootstrapViewTransition(userConfig, registry) {
|
|
746
|
+
if (userConfig) {
|
|
747
|
+
setConfig(userConfig);
|
|
748
|
+
}
|
|
749
|
+
registerComponents(registry);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export { TRANSITION_RUNNER_KEY, ViewTransitionCore, WcsViewTransition, bootstrapViewTransition, getConfig, getTransitionRunner, runTransition };
|
|
753
|
+
//# sourceMappingURL=index.esm.js.map
|