framer-motion 12.40.0 → 12.42.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.md +1 -1
- package/dist/cjs/client.js +1 -1
- package/dist/cjs/{feature-bundle-frRcWALD.js → feature-bundle-riC1cckV.js} +6 -2
- package/dist/cjs/feature-bundle-riC1cckV.js.map +1 -0
- package/dist/cjs/index.js +15 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/dom.js +1 -1
- package/dist/es/components/AnimatePresence/PresenceChild.mjs +16 -2
- package/dist/es/components/AnimatePresence/PresenceChild.mjs.map +1 -1
- package/dist/es/gestures/pan/PanSession.mjs +5 -1
- package/dist/es/gestures/pan/PanSession.mjs.map +1 -1
- package/dist/framer-motion.dev.js +1150 -477
- package/dist/framer-motion.js +1 -1
- package/dist/size-rollup-animate.js +1 -1
- package/dist/size-rollup-animate.js.map +1 -1
- package/dist/size-rollup-dom-animation-assets.js +1 -1
- package/dist/size-rollup-dom-animation.js +1 -1
- package/dist/size-rollup-dom-max-assets.js +1 -1
- package/dist/size-rollup-dom-max.js +1 -1
- package/dist/size-rollup-motion.js +1 -1
- package/dist/size-rollup-motion.js.map +1 -1
- package/dist/size-rollup-scroll.js.map +1 -1
- package/dist/size-rollup-waapi-animate.js.map +1 -1
- package/package.json +3 -3
- package/dist/cjs/feature-bundle-frRcWALD.js.map +0 -1
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/*#__NO_SIDE_EFFECTS__*/
|
|
161
|
-
const noop
|
|
161
|
+
const noop = (any) => any;
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* Pipe
|
|
@@ -295,7 +295,7 @@
|
|
|
295
295
|
function cubicBezier(mX1, mY1, mX2, mY2) {
|
|
296
296
|
// If this is a linear gradient, return linear easing
|
|
297
297
|
if (mX1 === mY1 && mX2 === mY2)
|
|
298
|
-
return noop
|
|
298
|
+
return noop;
|
|
299
299
|
const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
|
|
300
300
|
// If animation is at start/end, return t without easing
|
|
301
301
|
return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
|
|
@@ -356,7 +356,7 @@
|
|
|
356
356
|
const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
|
|
357
357
|
|
|
358
358
|
const easingLookup = {
|
|
359
|
-
linear: noop
|
|
359
|
+
linear: noop,
|
|
360
360
|
easeIn,
|
|
361
361
|
easeInOut,
|
|
362
362
|
easeOut,
|
|
@@ -397,12 +397,7 @@
|
|
|
397
397
|
"postRender", // Compute
|
|
398
398
|
];
|
|
399
399
|
|
|
400
|
-
|
|
401
|
-
value: null,
|
|
402
|
-
addProjectionMetrics: null,
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
function createRenderStep(runNextFrame, stepName) {
|
|
400
|
+
function createRenderStep(runNextFrame) {
|
|
406
401
|
/**
|
|
407
402
|
* We create and reuse two queues, one to queue jobs for the current frame
|
|
408
403
|
* and one for the next. We reuse to avoid triggering GC after x frames.
|
|
@@ -424,13 +419,11 @@
|
|
|
424
419
|
timestamp: 0.0,
|
|
425
420
|
isProcessing: false,
|
|
426
421
|
};
|
|
427
|
-
let numCalls = 0;
|
|
428
422
|
function triggerCallback(callback) {
|
|
429
423
|
if (toKeepAlive.has(callback)) {
|
|
430
424
|
step.schedule(callback);
|
|
431
425
|
runNextFrame();
|
|
432
426
|
}
|
|
433
|
-
numCalls++;
|
|
434
427
|
callback(latestFrameData);
|
|
435
428
|
}
|
|
436
429
|
const step = {
|
|
@@ -473,13 +466,6 @@
|
|
|
473
466
|
nextFrame = prevFrame;
|
|
474
467
|
// Execute this frame
|
|
475
468
|
thisFrame.forEach(triggerCallback);
|
|
476
|
-
/**
|
|
477
|
-
* If we're recording stats then
|
|
478
|
-
*/
|
|
479
|
-
if (stepName && statsBuffer.value) {
|
|
480
|
-
statsBuffer.value.frameloop[stepName].push(numCalls);
|
|
481
|
-
}
|
|
482
|
-
numCalls = 0;
|
|
483
469
|
// Clear the frame so no callbacks remain. This is to avoid
|
|
484
470
|
// memory leaks should this render step not run for a while.
|
|
485
471
|
thisFrame.clear();
|
|
@@ -504,7 +490,7 @@
|
|
|
504
490
|
};
|
|
505
491
|
const flagRunNextFrame = () => (runNextFrame = true);
|
|
506
492
|
const steps = stepsOrder.reduce((acc, key) => {
|
|
507
|
-
acc[key] = createRenderStep(flagRunNextFrame
|
|
493
|
+
acc[key] = createRenderStep(flagRunNextFrame);
|
|
508
494
|
return acc;
|
|
509
495
|
}, {});
|
|
510
496
|
const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
|
|
@@ -560,7 +546,7 @@
|
|
|
560
546
|
return { schedule, cancel, state, steps };
|
|
561
547
|
}
|
|
562
548
|
|
|
563
|
-
const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop
|
|
549
|
+
const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
|
|
564
550
|
|
|
565
551
|
let now;
|
|
566
552
|
function clearTime() {
|
|
@@ -589,12 +575,6 @@
|
|
|
589
575
|
},
|
|
590
576
|
};
|
|
591
577
|
|
|
592
|
-
const activeAnimations = {
|
|
593
|
-
layout: 0,
|
|
594
|
-
mainThread: 0,
|
|
595
|
-
waapi: 0,
|
|
596
|
-
};
|
|
597
|
-
|
|
598
578
|
const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
|
|
599
579
|
const isCSSVariableName =
|
|
600
580
|
/*@__PURE__*/ checkStringStartsWith("--");
|
|
@@ -1582,7 +1562,7 @@
|
|
|
1582
1562
|
for (let i = 0; i < numMixers; i++) {
|
|
1583
1563
|
let mixer = mixerFactory(output[i], output[i + 1]);
|
|
1584
1564
|
if (ease) {
|
|
1585
|
-
const easingFunction = Array.isArray(ease) ? ease[i] || noop
|
|
1565
|
+
const easingFunction = Array.isArray(ease) ? ease[i] || noop : ease;
|
|
1586
1566
|
mixer = pipe(easingFunction, mixer);
|
|
1587
1567
|
}
|
|
1588
1568
|
mixers.push(mixer);
|
|
@@ -1796,7 +1776,6 @@
|
|
|
1796
1776
|
this.teardown();
|
|
1797
1777
|
this.options.onStop?.();
|
|
1798
1778
|
};
|
|
1799
|
-
activeAnimations.mainThread++;
|
|
1800
1779
|
this.options = options;
|
|
1801
1780
|
this.initAnimation();
|
|
1802
1781
|
this.play();
|
|
@@ -2101,7 +2080,6 @@
|
|
|
2101
2080
|
this.state = "idle";
|
|
2102
2081
|
this.stopDriver();
|
|
2103
2082
|
this.startTime = this.holdTime = null;
|
|
2104
|
-
activeAnimations.mainThread--;
|
|
2105
2083
|
}
|
|
2106
2084
|
stopDriver() {
|
|
2107
2085
|
if (!this.driver)
|
|
@@ -2513,9 +2491,6 @@
|
|
|
2513
2491
|
*/
|
|
2514
2492
|
if (Array.isArray(easing))
|
|
2515
2493
|
keyframeOptions.easing = easing;
|
|
2516
|
-
if (statsBuffer.value) {
|
|
2517
|
-
activeAnimations.waapi++;
|
|
2518
|
-
}
|
|
2519
2494
|
const options = {
|
|
2520
2495
|
delay,
|
|
2521
2496
|
duration,
|
|
@@ -2526,13 +2501,7 @@
|
|
|
2526
2501
|
};
|
|
2527
2502
|
if (pseudoElement)
|
|
2528
2503
|
options.pseudoElement = pseudoElement;
|
|
2529
|
-
|
|
2530
|
-
if (statsBuffer.value) {
|
|
2531
|
-
animation.finished.finally(() => {
|
|
2532
|
-
activeAnimations.waapi--;
|
|
2533
|
-
});
|
|
2534
|
-
}
|
|
2535
|
-
return animation;
|
|
2504
|
+
return element.animate(keyframeOptions, options);
|
|
2536
2505
|
}
|
|
2537
2506
|
|
|
2538
2507
|
function isGenerator(type) {
|
|
@@ -2711,7 +2680,7 @@
|
|
|
2711
2680
|
this.animation.rangeStart = rangeStart;
|
|
2712
2681
|
if (rangeEnd)
|
|
2713
2682
|
this.animation.rangeEnd = rangeEnd;
|
|
2714
|
-
return noop
|
|
2683
|
+
return noop;
|
|
2715
2684
|
}
|
|
2716
2685
|
else {
|
|
2717
2686
|
return observe(this);
|
|
@@ -3064,7 +3033,7 @@
|
|
|
3064
3033
|
}
|
|
3065
3034
|
animation.finished.then(() => {
|
|
3066
3035
|
this.notifyFinished();
|
|
3067
|
-
}).catch(noop
|
|
3036
|
+
}).catch(noop);
|
|
3068
3037
|
if (this.pendingTimeline) {
|
|
3069
3038
|
this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
|
|
3070
3039
|
this.pendingTimeline = undefined;
|
|
@@ -5242,9 +5211,16 @@
|
|
|
5242
5211
|
claimedPointerDownEvents.add(startEvent);
|
|
5243
5212
|
}
|
|
5244
5213
|
const onPressEnd = onPressStart(target, startEvent);
|
|
5214
|
+
/**
|
|
5215
|
+
* End listeners run in the capture phase so a descendant calling
|
|
5216
|
+
* stopPropagation() in its own pointerup handler can't prevent the
|
|
5217
|
+
* press gesture from ending. This also keeps the gesture-end
|
|
5218
|
+
* ordering consistent with the drag gesture. See #2794.
|
|
5219
|
+
*/
|
|
5220
|
+
const endEventOptions = { ...eventOptions, capture: true };
|
|
5245
5221
|
const onPointerEnd = (endEvent, success) => {
|
|
5246
|
-
window.removeEventListener("pointerup", onPointerUp);
|
|
5247
|
-
window.removeEventListener("pointercancel", onPointerCancel);
|
|
5222
|
+
window.removeEventListener("pointerup", onPointerUp, endEventOptions);
|
|
5223
|
+
window.removeEventListener("pointercancel", onPointerCancel, endEventOptions);
|
|
5248
5224
|
if (isPressing.has(target)) {
|
|
5249
5225
|
isPressing.delete(target);
|
|
5250
5226
|
}
|
|
@@ -5264,8 +5240,8 @@
|
|
|
5264
5240
|
const onPointerCancel = (cancelEvent) => {
|
|
5265
5241
|
onPointerEnd(cancelEvent, false);
|
|
5266
5242
|
};
|
|
5267
|
-
window.addEventListener("pointerup", onPointerUp,
|
|
5268
|
-
window.addEventListener("pointercancel", onPointerCancel,
|
|
5243
|
+
window.addEventListener("pointerup", onPointerUp, endEventOptions);
|
|
5244
|
+
window.addEventListener("pointercancel", onPointerCancel, endEventOptions);
|
|
5269
5245
|
};
|
|
5270
5246
|
targets.forEach((target) => {
|
|
5271
5247
|
const pointerDownTarget = options.useGlobalTarget ? window : target;
|
|
@@ -5404,116 +5380,34 @@
|
|
|
5404
5380
|
return () => cancelFrame(onFrame);
|
|
5405
5381
|
}
|
|
5406
5382
|
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
}
|
|
5413
|
-
value.frameloop.rate.push(frameData.delta);
|
|
5414
|
-
value.animations.mainThread.push(activeAnimations.mainThread);
|
|
5415
|
-
value.animations.waapi.push(activeAnimations.waapi);
|
|
5416
|
-
value.animations.layout.push(activeAnimations.layout);
|
|
5417
|
-
}
|
|
5418
|
-
function mean(values) {
|
|
5419
|
-
return values.reduce((acc, value) => acc + value, 0) / values.length;
|
|
5420
|
-
}
|
|
5421
|
-
function summarise(values, calcAverage = mean) {
|
|
5422
|
-
if (values.length === 0) {
|
|
5423
|
-
return {
|
|
5424
|
-
min: 0,
|
|
5425
|
-
max: 0,
|
|
5426
|
-
avg: 0,
|
|
5427
|
-
};
|
|
5428
|
-
}
|
|
5429
|
-
return {
|
|
5430
|
-
min: Math.min(...values),
|
|
5431
|
-
max: Math.max(...values),
|
|
5432
|
-
avg: calcAverage(values),
|
|
5433
|
-
};
|
|
5434
|
-
}
|
|
5435
|
-
const msToFps = (ms) => Math.round(1000 / ms);
|
|
5383
|
+
const statsBuffer = {
|
|
5384
|
+
value: null,
|
|
5385
|
+
addProjectionMetrics: null,
|
|
5386
|
+
};
|
|
5387
|
+
|
|
5436
5388
|
function clearStatsBuffer() {
|
|
5437
5389
|
statsBuffer.value = null;
|
|
5438
5390
|
statsBuffer.addProjectionMetrics = null;
|
|
5439
5391
|
}
|
|
5440
|
-
function reportStats() {
|
|
5441
|
-
const { value } = statsBuffer;
|
|
5442
|
-
if (!value) {
|
|
5443
|
-
throw new Error("Stats are not being measured");
|
|
5444
|
-
}
|
|
5445
|
-
clearStatsBuffer();
|
|
5446
|
-
cancelFrame(record);
|
|
5447
|
-
const summary = {
|
|
5448
|
-
frameloop: {
|
|
5449
|
-
setup: summarise(value.frameloop.setup),
|
|
5450
|
-
rate: summarise(value.frameloop.rate),
|
|
5451
|
-
read: summarise(value.frameloop.read),
|
|
5452
|
-
resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
|
|
5453
|
-
preUpdate: summarise(value.frameloop.preUpdate),
|
|
5454
|
-
update: summarise(value.frameloop.update),
|
|
5455
|
-
preRender: summarise(value.frameloop.preRender),
|
|
5456
|
-
render: summarise(value.frameloop.render),
|
|
5457
|
-
postRender: summarise(value.frameloop.postRender),
|
|
5458
|
-
},
|
|
5459
|
-
animations: {
|
|
5460
|
-
mainThread: summarise(value.animations.mainThread),
|
|
5461
|
-
waapi: summarise(value.animations.waapi),
|
|
5462
|
-
layout: summarise(value.animations.layout),
|
|
5463
|
-
},
|
|
5464
|
-
layoutProjection: {
|
|
5465
|
-
nodes: summarise(value.layoutProjection.nodes),
|
|
5466
|
-
calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
|
|
5467
|
-
calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
|
|
5468
|
-
},
|
|
5469
|
-
};
|
|
5470
|
-
/**
|
|
5471
|
-
* Convert the rate to FPS
|
|
5472
|
-
*/
|
|
5473
|
-
const { rate } = summary.frameloop;
|
|
5474
|
-
rate.min = msToFps(rate.min);
|
|
5475
|
-
rate.max = msToFps(rate.max);
|
|
5476
|
-
rate.avg = msToFps(rate.avg);
|
|
5477
|
-
[rate.min, rate.max] = [rate.max, rate.min];
|
|
5478
|
-
return summary;
|
|
5479
|
-
}
|
|
5480
5392
|
function recordStats() {
|
|
5481
5393
|
if (statsBuffer.value) {
|
|
5482
5394
|
clearStatsBuffer();
|
|
5483
5395
|
throw new Error("Stats are already being measured");
|
|
5484
5396
|
}
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5487
|
-
frameloop: {
|
|
5488
|
-
setup: [],
|
|
5489
|
-
rate: [],
|
|
5490
|
-
read: [],
|
|
5491
|
-
resolveKeyframes: [],
|
|
5492
|
-
preUpdate: [],
|
|
5493
|
-
update: [],
|
|
5494
|
-
preRender: [],
|
|
5495
|
-
render: [],
|
|
5496
|
-
postRender: [],
|
|
5497
|
-
},
|
|
5498
|
-
animations: {
|
|
5499
|
-
mainThread: [],
|
|
5500
|
-
waapi: [],
|
|
5501
|
-
layout: [],
|
|
5502
|
-
},
|
|
5397
|
+
const buffer = statsBuffer;
|
|
5398
|
+
buffer.value = {
|
|
5503
5399
|
layoutProjection: {
|
|
5504
5400
|
nodes: [],
|
|
5505
5401
|
calculatedTargetDeltas: [],
|
|
5506
5402
|
calculatedProjections: [],
|
|
5507
5403
|
},
|
|
5508
5404
|
};
|
|
5509
|
-
|
|
5510
|
-
const { layoutProjection } =
|
|
5405
|
+
buffer.addProjectionMetrics = (metrics) => {
|
|
5406
|
+
const { layoutProjection } = buffer.value;
|
|
5511
5407
|
layoutProjection.nodes.push(metrics.nodes);
|
|
5512
5408
|
layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
|
|
5513
5409
|
layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
|
|
5514
5410
|
};
|
|
5515
|
-
frame.postRender(record, true);
|
|
5516
|
-
return reportStats;
|
|
5517
5411
|
}
|
|
5518
5412
|
|
|
5519
5413
|
/**
|
|
@@ -5799,14 +5693,154 @@
|
|
|
5799
5693
|
*/
|
|
5800
5694
|
const findValueType = (v) => valueTypes.find(testValueType(v));
|
|
5801
5695
|
|
|
5696
|
+
let nameCount = 0;
|
|
5697
|
+
/**
|
|
5698
|
+
* Generated names live in their own namespace so we can tell a name we own
|
|
5699
|
+
* (and must clean up) from an author-defined one - and so a stale generated
|
|
5700
|
+
* name left behind by an interrupted transition is re-owned, not mistaken for
|
|
5701
|
+
* the author's and leaked.
|
|
5702
|
+
*/
|
|
5703
|
+
const generatedName = () => `motion-view-${nameCount++}`;
|
|
5704
|
+
const isGeneratedName = (name) => name.startsWith("motion-view-");
|
|
5705
|
+
/**
|
|
5706
|
+
* Tag a captured element with a `view-transition-class` so authors can target
|
|
5707
|
+
* its generated layer from CSS (e.g. `::view-transition-group(.hero)`) without
|
|
5708
|
+
* the opaque generated name. Tracked in `classed` - separate from the generated
|
|
5709
|
+
* names in `assigned` - so cleanup removes the class without ever stripping an
|
|
5710
|
+
* author's own inline `view-transition-name`.
|
|
5711
|
+
*/
|
|
5712
|
+
function tagClass(element, className, classed) {
|
|
5713
|
+
if (!className)
|
|
5714
|
+
return;
|
|
5715
|
+
element.style?.setProperty("view-transition-class", className);
|
|
5716
|
+
classed.push(element);
|
|
5717
|
+
}
|
|
5718
|
+
/**
|
|
5719
|
+
* Set the element's `view-transition-group` so its layer reconstructs the DOM
|
|
5720
|
+
* hierarchy in the pseudo-tree (`contain`) - or stays flat (`none`). Tracked in
|
|
5721
|
+
* `grouped` for cleanup. When the element clips (any non-`visible` overflow) its
|
|
5722
|
+
* name is recorded in `clipChildren` so the caller can clip the nested children
|
|
5723
|
+
* (`::view-transition-group-children(name)`), mirroring the live clip through
|
|
5724
|
+
* the whole transition rather than only at the live-DOM handoff.
|
|
5725
|
+
*
|
|
5726
|
+
* Ignored by browsers without nested view-transition groups, where it harmlessly
|
|
5727
|
+
* degrades to the flat default.
|
|
5728
|
+
*/
|
|
5729
|
+
function applyGroup(element, name, group, grouped, clipChildren) {
|
|
5730
|
+
if (!group)
|
|
5731
|
+
return;
|
|
5732
|
+
element.style?.setProperty("view-transition-group", group);
|
|
5733
|
+
grouped.push(element);
|
|
5734
|
+
if (group !== "none" && clipChildren) {
|
|
5735
|
+
const style = getComputedStyle(element);
|
|
5736
|
+
if (style.overflowX !== "visible" || style.overflowY !== "visible") {
|
|
5737
|
+
clipChildren.add(name);
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
/**
|
|
5742
|
+
* Resolve a selector/Element to elements and ensure each one carries a
|
|
5743
|
+
* `view-transition-name` we can target from script.
|
|
5744
|
+
*
|
|
5745
|
+
* Author-defined names are reused as-is. Elements that are unnamed (or use
|
|
5746
|
+
* the browser's `auto`/`match-element`, whose generated name is not exposed
|
|
5747
|
+
* to script) are given a unique generated name, set inline so it's captured,
|
|
5748
|
+
* and tracked in `assigned` for later cleanup.
|
|
5749
|
+
*
|
|
5750
|
+
* `registry` maps each Element to its name so the same element keeps its name
|
|
5751
|
+
* across both captures (before and after the update), which is what allows a
|
|
5752
|
+
* persistent element to animate as a single `group` layer.
|
|
5753
|
+
*/
|
|
5754
|
+
function assignViewTransitionNames(definition, registry, assigned, forcedNames, className, classed = [], group, grouped = [], clipChildren) {
|
|
5755
|
+
const elements = resolveElements(definition);
|
|
5756
|
+
/**
|
|
5757
|
+
* The new end of a paired morph: give each element the matching name from
|
|
5758
|
+
* the old end (by index) so the two share one layer and morph. If the new
|
|
5759
|
+
* end resolves to *more* elements than the old end named, the extras have no
|
|
5760
|
+
* counterpart - give them a fresh name so they animate as newcomers rather
|
|
5761
|
+
* than being silently left unnamed. We return the names actually assigned
|
|
5762
|
+
* (sized to the resolved elements), not the raw `forcedNames`, so stagger
|
|
5763
|
+
* totals and the layer set stay in step with what's on the page.
|
|
5764
|
+
*/
|
|
5765
|
+
if (forcedNames) {
|
|
5766
|
+
return elements.map((element, i) => {
|
|
5767
|
+
const existing = registry.get(element);
|
|
5768
|
+
if (existing)
|
|
5769
|
+
return existing;
|
|
5770
|
+
const name = forcedNames[i] ?? generatedName();
|
|
5771
|
+
element.style?.setProperty("view-transition-name", name);
|
|
5772
|
+
assigned.push(element);
|
|
5773
|
+
registry.set(element, name);
|
|
5774
|
+
tagClass(element, className, classed);
|
|
5775
|
+
applyGroup(element, name, group, grouped, clipChildren);
|
|
5776
|
+
return name;
|
|
5777
|
+
});
|
|
5778
|
+
}
|
|
5779
|
+
/**
|
|
5780
|
+
* Read every current name up front, before assigning any. Interleaving the
|
|
5781
|
+
* reads with the inline `setProperty` writes below would dirty styles
|
|
5782
|
+
* between reads and force a style recalc per element; batching the reads
|
|
5783
|
+
* keeps it to one. Elements already in the registry keep their name and
|
|
5784
|
+
* need no read.
|
|
5785
|
+
*/
|
|
5786
|
+
const currentNames = elements.map((element) => registry.has(element)
|
|
5787
|
+
? undefined
|
|
5788
|
+
: getComputedStyle(element).getPropertyValue("view-transition-name"));
|
|
5789
|
+
return elements.map((element, i) => {
|
|
5790
|
+
const existing = registry.get(element);
|
|
5791
|
+
if (existing)
|
|
5792
|
+
return existing;
|
|
5793
|
+
const current = currentNames[i];
|
|
5794
|
+
let name;
|
|
5795
|
+
if (current &&
|
|
5796
|
+
current !== "none" &&
|
|
5797
|
+
current !== "auto" &&
|
|
5798
|
+
current !== "match-element" &&
|
|
5799
|
+
!isGeneratedName(current)) {
|
|
5800
|
+
/**
|
|
5801
|
+
* The author already named this layer - target it as-is and leave
|
|
5802
|
+
* it to them to clean up. `auto`/`match-element` are overridden
|
|
5803
|
+
* because their generated name is not exposed to script, and a
|
|
5804
|
+
* stale `motion-view-*` (e.g. left by an interrupted transition) is
|
|
5805
|
+
* re-owned rather than adopted as an author name and leaked.
|
|
5806
|
+
*/
|
|
5807
|
+
name = current;
|
|
5808
|
+
}
|
|
5809
|
+
else {
|
|
5810
|
+
name = generatedName();
|
|
5811
|
+
element.style?.setProperty("view-transition-name", name);
|
|
5812
|
+
assigned.push(element);
|
|
5813
|
+
}
|
|
5814
|
+
registry.set(element, name);
|
|
5815
|
+
tagClass(element, className, classed);
|
|
5816
|
+
applyGroup(element, name, group, grouped, clipChildren);
|
|
5817
|
+
return name;
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5820
|
+
/**
|
|
5821
|
+
* Remove the `view-transition-name`s we generated and the
|
|
5822
|
+
* `view-transition-class`es we applied. Author-defined names are never touched
|
|
5823
|
+
* (they're not in `assigned`). Safe to call more than once (e.g. on both a
|
|
5824
|
+
* finished and an interrupted transition).
|
|
5825
|
+
*/
|
|
5826
|
+
function releaseViewTransitionNames(assigned, classed = [], grouped = []) {
|
|
5827
|
+
for (const element of assigned) {
|
|
5828
|
+
element.style?.removeProperty("view-transition-name");
|
|
5829
|
+
}
|
|
5830
|
+
for (const element of classed) {
|
|
5831
|
+
element.style?.removeProperty("view-transition-class");
|
|
5832
|
+
}
|
|
5833
|
+
for (const element of grouped) {
|
|
5834
|
+
element.style?.removeProperty("view-transition-group");
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
|
|
5802
5838
|
function chooseLayerType(valueName) {
|
|
5803
5839
|
if (valueName === "layout")
|
|
5804
5840
|
return "group";
|
|
5805
5841
|
if (valueName === "enter" || valueName === "new")
|
|
5806
5842
|
return "new";
|
|
5807
|
-
|
|
5808
|
-
return "old";
|
|
5809
|
-
return "group";
|
|
5843
|
+
return "old";
|
|
5810
5844
|
}
|
|
5811
5845
|
|
|
5812
5846
|
let pendingRules = {};
|
|
@@ -5841,21 +5875,21 @@
|
|
|
5841
5875
|
};
|
|
5842
5876
|
|
|
5843
5877
|
function getViewAnimationLayerInfo(pseudoElement) {
|
|
5844
|
-
const match = pseudoElement.match(
|
|
5878
|
+
const match = pseudoElement.match(
|
|
5879
|
+
// `group-children` (nested transitions) before `group` so it wins.
|
|
5880
|
+
/::view-transition-(old|new|group-children|group|image-pair)\((.*?)\)/);
|
|
5845
5881
|
if (!match)
|
|
5846
5882
|
return null;
|
|
5847
5883
|
return { layer: match[2], type: match[1] };
|
|
5848
5884
|
}
|
|
5849
5885
|
|
|
5850
|
-
function filterViewAnimations(animation) {
|
|
5851
|
-
const { effect } = animation;
|
|
5852
|
-
if (!effect)
|
|
5853
|
-
return false;
|
|
5854
|
-
return (effect.target === document.documentElement &&
|
|
5855
|
-
effect.pseudoElement?.startsWith("::view-transition"));
|
|
5856
|
-
}
|
|
5857
5886
|
function getViewAnimations() {
|
|
5858
|
-
return document.getAnimations().filter(
|
|
5887
|
+
return document.getAnimations().filter((animation) => {
|
|
5888
|
+
const { effect } = animation;
|
|
5889
|
+
return (!!effect &&
|
|
5890
|
+
effect.target === document.documentElement &&
|
|
5891
|
+
effect.pseudoElement?.startsWith("::view-transition"));
|
|
5892
|
+
});
|
|
5859
5893
|
}
|
|
5860
5894
|
|
|
5861
5895
|
function hasTarget(target, targets) {
|
|
@@ -5863,87 +5897,432 @@
|
|
|
5863
5897
|
}
|
|
5864
5898
|
|
|
5865
5899
|
const definitionNames = ["layout", "enter", "exit", "new", "old"];
|
|
5900
|
+
/**
|
|
5901
|
+
* The `ViewTransitionTarget` buckets driving each generated layer type, in
|
|
5902
|
+
* priority order - the inverse of `chooseLayerType`. The new view is driven by
|
|
5903
|
+
* `new`/`enter`, the old by `old`/`exit`. `group-children`/`image-pair` have no
|
|
5904
|
+
* bucket; they follow the default layout timing.
|
|
5905
|
+
*/
|
|
5906
|
+
const typeBuckets = {
|
|
5907
|
+
group: ["layout"],
|
|
5908
|
+
new: ["new", "enter"],
|
|
5909
|
+
old: ["old", "exit"],
|
|
5910
|
+
};
|
|
5911
|
+
/**
|
|
5912
|
+
* Default "absent" origin for a single-value keyframe, by pseudo type, so e.g.
|
|
5913
|
+
* `enter({ scale: 1 })` grows in from 0.85 and `exit({ opacity: 0 })` fades
|
|
5914
|
+
* from 1. `enter` prefers the matching `exit` value over these (see below).
|
|
5915
|
+
*/
|
|
5916
|
+
const ORIGIN_DEFAULTS = {
|
|
5917
|
+
new: { opacity: 0, scale: 0.85 },
|
|
5918
|
+
old: { opacity: 1, scale: 1 },
|
|
5919
|
+
};
|
|
5920
|
+
/**
|
|
5921
|
+
* How much two box aspect ratios must differ before a morph is treated as
|
|
5922
|
+
* aspect-changing (and so worth cropping). Matches the projection engine's
|
|
5923
|
+
* `preserve-aspect` threshold, so small layout jitter doesn't trigger a crop.
|
|
5924
|
+
*/
|
|
5925
|
+
const ASPECT_TOLERANCE = 0.2;
|
|
5866
5926
|
function startViewAnimation(builder) {
|
|
5867
|
-
const { update, targets, options: defaultOptions } = builder;
|
|
5927
|
+
const { update, targets, resolveDefs, cropOverride, pairs, classNames, flatGroups, options: defaultOptions, } = builder;
|
|
5868
5928
|
if (!document.startViewTransition) {
|
|
5869
|
-
|
|
5929
|
+
// An async IIFE (not `new Promise(async …)`) so a throwing/rejecting
|
|
5930
|
+
// update rejects this promise rather than leaving it unsettled.
|
|
5931
|
+
return (async () => {
|
|
5870
5932
|
await update();
|
|
5871
|
-
|
|
5872
|
-
});
|
|
5933
|
+
return new GroupAnimation([]);
|
|
5934
|
+
})();
|
|
5873
5935
|
}
|
|
5874
|
-
// TODO: Go over existing targets and ensure they all have ids
|
|
5875
5936
|
/**
|
|
5876
|
-
*
|
|
5877
|
-
*
|
|
5937
|
+
* Resolve any selector/Element targets to layer names, assigning a
|
|
5938
|
+
* `view-transition-name` to each element as we go. We run this before the
|
|
5939
|
+
* update (so the elements are captured in the old snapshot) and again
|
|
5940
|
+
* after it (for the new snapshot). An element present in both keeps the
|
|
5941
|
+
* same name and animates as a single `group` layer.
|
|
5942
|
+
*/
|
|
5943
|
+
const nameRegistry = new Map();
|
|
5944
|
+
const assigned = [];
|
|
5945
|
+
/**
|
|
5946
|
+
* Elements we tagged with a `view-transition-class` (via `.class()`),
|
|
5947
|
+
* tracked separately from `assigned` so cleanup removes the class without
|
|
5948
|
+
* ever stripping an author's own inline `view-transition-name`.
|
|
5949
|
+
*/
|
|
5950
|
+
const classed = [];
|
|
5951
|
+
/**
|
|
5952
|
+
* Elements we set a `view-transition-group` on (for nesting), tracked for
|
|
5953
|
+
* cleanup. `clipChildren` collects the names of nested parents that clip in
|
|
5954
|
+
* the live layout, so their `::view-transition-group-children` is clipped
|
|
5955
|
+
* through the transition - not just once the live DOM takes back over.
|
|
5956
|
+
*/
|
|
5957
|
+
const grouped = [];
|
|
5958
|
+
const clipChildren = new Set();
|
|
5959
|
+
const layerTargets = new Map();
|
|
5960
|
+
const croppedNames = new Set();
|
|
5961
|
+
/**
|
|
5962
|
+
* Each layer's explicit `.crop(true | false)` override (by resolved name),
|
|
5963
|
+
* so `finalizeCrop` can let an author's choice win over the morph default.
|
|
5878
5964
|
*/
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5965
|
+
const cropForName = new Map();
|
|
5966
|
+
/**
|
|
5967
|
+
* Each layer's stagger position (index + total) within its subject, per
|
|
5968
|
+
* snapshot. Resolving against the snapshot the layer belongs to keeps
|
|
5969
|
+
* stagger correct when `update()` replaces the matched elements, and lets
|
|
5970
|
+
* us skip a layer that's absent from a snapshot (e.g. an exited element
|
|
5971
|
+
* has no `new` pseudo-element).
|
|
5972
|
+
*/
|
|
5973
|
+
const layerStagger = new Map();
|
|
5974
|
+
/**
|
|
5975
|
+
* Names allocated for a paired subject in the old snapshot, replayed onto
|
|
5976
|
+
* its new-snapshot target so both ends share a layer and morph.
|
|
5977
|
+
*/
|
|
5978
|
+
const pairNames = new Map();
|
|
5979
|
+
/**
|
|
5980
|
+
* The old (`from`) elements of each paired subject, so their names can be
|
|
5981
|
+
* transferred off before the new (`to`) elements inherit them.
|
|
5982
|
+
*/
|
|
5983
|
+
const pairFrom = new Map();
|
|
5984
|
+
const resolveLayers = (phase) => {
|
|
5985
|
+
targets.forEach((target, definition) => {
|
|
5986
|
+
const className = classNames.get(definition);
|
|
5987
|
+
/**
|
|
5988
|
+
* Nest each resolved layer under its DOM-ancestor layer by default
|
|
5989
|
+
* (`contain`), so an ancestor's clip/transform/opacity reach it
|
|
5990
|
+
* through the transition; `.group(false)` opts a subject out (`none`)
|
|
5991
|
+
* to stay flat and escape. Skipped for root / pre-named layers, which
|
|
5992
|
+
* aren't elements we resolve and style.
|
|
5993
|
+
*/
|
|
5994
|
+
const group = definition === "root" || !resolveDefs.has(definition)
|
|
5995
|
+
? undefined
|
|
5996
|
+
: flatGroups.has(definition)
|
|
5997
|
+
? "none"
|
|
5998
|
+
: "contain";
|
|
5999
|
+
let names;
|
|
6000
|
+
if (definition === "root" || !resolveDefs.has(definition)) {
|
|
6001
|
+
names = [definition];
|
|
6002
|
+
}
|
|
6003
|
+
else if (pairs.has(definition)) {
|
|
6004
|
+
/**
|
|
6005
|
+
* Paired morph: name the old target in the old snapshot, then
|
|
6006
|
+
* force the same name(s) onto the new target in the new one, so
|
|
6007
|
+
* two different elements morph as a single layer.
|
|
6008
|
+
*/
|
|
6009
|
+
if (phase === "old") {
|
|
6010
|
+
pairFrom.set(definition, resolveElements(definition));
|
|
6011
|
+
names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed, group, grouped, clipChildren);
|
|
6012
|
+
pairNames.set(definition, names);
|
|
6013
|
+
}
|
|
6014
|
+
else {
|
|
6015
|
+
/**
|
|
6016
|
+
* Transfer the name(s) off the `from` elements before the
|
|
6017
|
+
* `to` elements inherit them. A `from` that survives into
|
|
6018
|
+
* the new snapshot (e.g. hidden with `visibility: hidden`
|
|
6019
|
+
* rather than removed) would otherwise keep the name and
|
|
6020
|
+
* collide - "duplicate view-transition-name".
|
|
6021
|
+
*/
|
|
6022
|
+
for (const el of pairFrom.get(definition) ?? []) {
|
|
6023
|
+
el.style?.removeProperty("view-transition-name");
|
|
6024
|
+
/**
|
|
6025
|
+
* Drop the old end from the registry too, so the new
|
|
6026
|
+
* end alone supplies this name's `new` crop radii - we
|
|
6027
|
+
* neither re-measure nor get ordered by a stale element.
|
|
6028
|
+
*/
|
|
6029
|
+
nameRegistry.delete(el);
|
|
6030
|
+
}
|
|
6031
|
+
names = assignViewTransitionNames(pairs.get(definition), nameRegistry, assigned, pairNames.get(definition), className, classed, group, grouped, clipChildren);
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
else {
|
|
6035
|
+
names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed, group, grouped, clipChildren);
|
|
6036
|
+
}
|
|
6037
|
+
// Record any explicit `.crop(true | false)` per resolved name; the
|
|
6038
|
+
// crop set itself is computed later by `finalizeCrop` (it needs both
|
|
6039
|
+
// snapshots to know which morphs change aspect ratio).
|
|
6040
|
+
const override = cropOverride.get(definition);
|
|
6041
|
+
names.forEach((name, index) => {
|
|
6042
|
+
/**
|
|
6043
|
+
* If two subjects resolve to the same element, merge their
|
|
6044
|
+
* definitions so neither subject's animations are dropped.
|
|
6045
|
+
*/
|
|
6046
|
+
const existing = layerTargets.get(name);
|
|
6047
|
+
layerTargets.set(name, existing && existing !== target
|
|
6048
|
+
? { ...existing, ...target }
|
|
6049
|
+
: target);
|
|
6050
|
+
if (override !== undefined)
|
|
6051
|
+
cropForName.set(name, override);
|
|
6052
|
+
const stagger = layerStagger.get(name) ?? {};
|
|
6053
|
+
stagger[phase] = [index, names.length];
|
|
6054
|
+
layerStagger.set(name, stagger);
|
|
6055
|
+
});
|
|
5882
6056
|
});
|
|
5883
|
-
}
|
|
6057
|
+
};
|
|
5884
6058
|
/**
|
|
5885
|
-
*
|
|
5886
|
-
*
|
|
5887
|
-
*
|
|
5888
|
-
*
|
|
5889
|
-
* This allows us to set easing via updateTiming - which can be changed.
|
|
6059
|
+
* The stagger index/total for a layer, resolved against the snapshot it
|
|
6060
|
+
* belongs to. Returns index -1 when the layer is absent from that snapshot
|
|
6061
|
+
* so the caller can skip a pseudo-element that doesn't exist.
|
|
5890
6062
|
*/
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
6063
|
+
const staggerPosition = (name, type) => {
|
|
6064
|
+
const stagger = layerStagger.get(name);
|
|
6065
|
+
const position = type === "old"
|
|
6066
|
+
? stagger?.old
|
|
6067
|
+
: type === "new"
|
|
6068
|
+
? stagger?.new
|
|
6069
|
+
: // group / group-children / image-pair persist across both.
|
|
6070
|
+
stagger?.new ?? stagger?.old;
|
|
6071
|
+
return position ?? [-1, 1];
|
|
6072
|
+
};
|
|
6073
|
+
/**
|
|
6074
|
+
* Merge default + per-layer transition options for a generated layer and
|
|
6075
|
+
* resolve any stagger/delay function against this element's position. Used
|
|
6076
|
+
* by both the morph-retiming and crop corner-radius passes.
|
|
6077
|
+
*/
|
|
6078
|
+
const resolveLayerTransition = (target, type, transitionName, index, total) => {
|
|
6079
|
+
const transition = mergeTransition(getValueTransition$1(defaultOptions, transitionName), getValueTransition$1((layerOptions(target, type) ?? {}), transitionName));
|
|
6080
|
+
if (typeof transition.delay === "function") {
|
|
6081
|
+
transition.delay = transition.delay(index, total);
|
|
6082
|
+
}
|
|
6083
|
+
return transition;
|
|
6084
|
+
};
|
|
6085
|
+
/**
|
|
6086
|
+
* Each layer's box size per snapshot, so `finalizeCrop` can tell whether a
|
|
6087
|
+
* morph's aspect ratio changed (the only case worth cropping).
|
|
6088
|
+
*/
|
|
6089
|
+
const cropBox = new Map();
|
|
6090
|
+
const measureLayers = (phase) => nameRegistry.forEach((name, element) => {
|
|
6091
|
+
const rect = element.getBoundingClientRect?.();
|
|
6092
|
+
if (rect && rect.height) {
|
|
6093
|
+
const entry = cropBox.get(name) ?? {};
|
|
6094
|
+
entry[phase] = { width: rect.width, height: rect.height };
|
|
6095
|
+
cropBox.set(name, entry);
|
|
6096
|
+
}
|
|
5896
6097
|
});
|
|
5897
|
-
|
|
6098
|
+
/**
|
|
6099
|
+
* With both snapshots measured, settle which layers crop. The default crops
|
|
6100
|
+
* only a morph whose aspect ratio *changes* between snapshots - the one case
|
|
6101
|
+
* where `object-fit: cover` does real work. A same-aspect morph or a
|
|
6102
|
+
* fade-only layer is left uncropped: its corners scale naturally (no flash
|
|
6103
|
+
* from squaring, no `overflow: clip` eating its shadow) and a backdrop can't
|
|
6104
|
+
* be clipped to nothing. An explicit `.crop(true | false)` overrides either
|
|
6105
|
+
* way. Runs after both snapshots are measured, since aspect needs both.
|
|
6106
|
+
*/
|
|
6107
|
+
const finalizeCrop = () => {
|
|
6108
|
+
croppedNames.clear();
|
|
6109
|
+
for (const name of layerStagger.keys()) {
|
|
6110
|
+
if (name === "root")
|
|
6111
|
+
continue;
|
|
6112
|
+
// An explicit `.crop(true | false)` wins; otherwise crop a morph
|
|
6113
|
+
// whose aspect ratio changed.
|
|
6114
|
+
if (cropForName.get(name) ?? aspectChanged(name)) {
|
|
6115
|
+
croppedNames.add(name);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
};
|
|
6119
|
+
/**
|
|
6120
|
+
* Whether a layer is a morph whose box aspect ratio changed between
|
|
6121
|
+
* snapshots (beyond a small tolerance). Fade-only layers (one snapshot) are
|
|
6122
|
+
* never "changed".
|
|
6123
|
+
*/
|
|
6124
|
+
const aspectChanged = (name) => {
|
|
6125
|
+
const box = cropBox.get(name);
|
|
6126
|
+
if (!box?.old || !box?.new || !box.old.height || !box.new.height) {
|
|
6127
|
+
return false;
|
|
6128
|
+
}
|
|
6129
|
+
return (Math.abs(box.old.width / box.old.height -
|
|
6130
|
+
box.new.width / box.new.height) > ASPECT_TOLERANCE);
|
|
6131
|
+
};
|
|
6132
|
+
/**
|
|
6133
|
+
* Write the persistent view-transition CSS: suppress root capture when the
|
|
6134
|
+
* root has no animations of its own; force linear timing (baked into the
|
|
6135
|
+
* keyframes, so we can retime later via updateTiming); and clip +
|
|
6136
|
+
* object-fit: cover every cropped morph (the UA default overflows on
|
|
6137
|
+
* aspect-ratio change).
|
|
6138
|
+
*
|
|
6139
|
+
* `css.commit` replaces rather than appends, so we re-set the full rule set
|
|
6140
|
+
* each call - the crop rules are only known after `finalizeCrop` runs in the
|
|
6141
|
+
* update callback, so the second call writes them.
|
|
6142
|
+
*/
|
|
6143
|
+
const commitViewCSS = () => {
|
|
6144
|
+
if (!hasTarget("root", targets)) {
|
|
6145
|
+
css.set(":root", { "view-transition-name": "none" });
|
|
6146
|
+
}
|
|
6147
|
+
css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
|
|
6148
|
+
croppedNames.forEach((name) => {
|
|
6149
|
+
css.set(`::view-transition-group(${name})`, { overflow: "clip" });
|
|
6150
|
+
css.set(`::view-transition-old(${name}), ::view-transition-new(${name})`, { width: "100%", height: "100%", "object-fit": "cover" });
|
|
6151
|
+
});
|
|
6152
|
+
/**
|
|
6153
|
+
* Clip the nested children of any layer that clips in the live layout,
|
|
6154
|
+
* so a wrapper crops its child for the whole morph (mirroring the DOM)
|
|
6155
|
+
* rather than only at the live-DOM handoff. No-op on browsers without
|
|
6156
|
+
* nested view-transition groups.
|
|
6157
|
+
*/
|
|
6158
|
+
clipChildren.forEach((name) => {
|
|
6159
|
+
css.set(`::view-transition-group-children(${name})`, {
|
|
6160
|
+
overflow: "clip",
|
|
6161
|
+
});
|
|
6162
|
+
});
|
|
6163
|
+
css.commit(); // Write
|
|
6164
|
+
};
|
|
6165
|
+
const cleanup = () => {
|
|
6166
|
+
releaseViewTransitionNames(assigned, classed, grouped);
|
|
5898
6167
|
css.remove(); // Write
|
|
5899
|
-
}
|
|
5900
|
-
|
|
5901
|
-
|
|
6168
|
+
};
|
|
6169
|
+
const callback = async () => {
|
|
6170
|
+
await update();
|
|
6171
|
+
/**
|
|
6172
|
+
* Re-resolve so elements created by the update are named for the new
|
|
6173
|
+
* snapshot, then measure them. With both snapshots measured we can
|
|
6174
|
+
* settle the crop set (aspect-changing morphs + forced).
|
|
6175
|
+
*/
|
|
6176
|
+
resolveLayers("new");
|
|
6177
|
+
measureLayers("new");
|
|
6178
|
+
finalizeCrop();
|
|
6179
|
+
/**
|
|
6180
|
+
* Re-commit the crop CSS unconditionally: `finalizeCrop` is computed
|
|
6181
|
+
* here (after both snapshots are measured), so the clip rules must be
|
|
6182
|
+
* (re)written to match the settled set.
|
|
6183
|
+
*/
|
|
6184
|
+
commitViewCSS();
|
|
6185
|
+
};
|
|
6186
|
+
let transition;
|
|
6187
|
+
try {
|
|
6188
|
+
resolveLayers("old");
|
|
6189
|
+
/**
|
|
6190
|
+
* Measure the old snapshot against the optimistic crop set (the new
|
|
6191
|
+
* snapshot doesn't exist yet, so aspect change can't be known here;
|
|
6192
|
+
* `finalizeCrop` settles it post-update).
|
|
6193
|
+
*/
|
|
6194
|
+
measureLayers("old");
|
|
6195
|
+
commitViewCSS();
|
|
6196
|
+
transition = document.startViewTransition(callback);
|
|
6197
|
+
}
|
|
6198
|
+
catch (error) {
|
|
6199
|
+
/**
|
|
6200
|
+
* The prelude writes inline names before the transition exists. If it
|
|
6201
|
+
* throws (e.g. startViewTransition rejects in a bad UA state), release
|
|
6202
|
+
* them so we neither leak DOM state nor stall the queue on a promise
|
|
6203
|
+
* that never settles - hand back a rejection it can advance past.
|
|
6204
|
+
*/
|
|
6205
|
+
cleanup();
|
|
6206
|
+
return Promise.reject(error);
|
|
6207
|
+
}
|
|
6208
|
+
transition.finished.finally(cleanup);
|
|
6209
|
+
return new Promise((resolve, reject) => {
|
|
6210
|
+
transition.ready
|
|
6211
|
+
.then(() => {
|
|
5902
6212
|
const generatedViewAnimations = getViewAnimations();
|
|
5903
6213
|
const animations = [];
|
|
5904
6214
|
/**
|
|
5905
6215
|
* Create animations for each of our explicitly-defined subjects.
|
|
6216
|
+
* `opacityAnimated` additionally tracks which `${name}:${type}`
|
|
6217
|
+
* we faded, so we can keep the UA `plus-lighter` blend only for a
|
|
6218
|
+
* genuine opacity crossfade (both sides fading) and drop it for a
|
|
6219
|
+
* slide/transform, where additive compositing would flash bright.
|
|
5906
6220
|
*/
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
6221
|
+
const explicitlyAnimated = new Set();
|
|
6222
|
+
const opacityAnimated = new Set();
|
|
6223
|
+
layerTargets.forEach((target, name) => {
|
|
6224
|
+
const stagger = layerStagger.get(name);
|
|
6225
|
+
/**
|
|
6226
|
+
* Presence: `enter` only fires for a pure newcomer (a new
|
|
6227
|
+
* view with no old), `exit` only for a pure leaver. A
|
|
6228
|
+
* survivor (both) gets neither - it just morphs.
|
|
6229
|
+
*/
|
|
6230
|
+
const enterApplies = !!stagger?.new && !stagger?.old;
|
|
6231
|
+
const exitApplies = !!stagger?.old && !stagger?.new;
|
|
5910
6232
|
for (const key of definitionNames) {
|
|
5911
|
-
if (!
|
|
6233
|
+
if (!target[key])
|
|
5912
6234
|
continue;
|
|
5913
|
-
|
|
6235
|
+
if (key === "enter" && !enterApplies)
|
|
6236
|
+
continue;
|
|
6237
|
+
if (key === "exit" && !exitApplies)
|
|
6238
|
+
continue;
|
|
6239
|
+
const type = chooseLayerType(key);
|
|
6240
|
+
const [index, total] = staggerPosition(name, type);
|
|
6241
|
+
// Skip a layer absent from its snapshot.
|
|
6242
|
+
if (index === -1)
|
|
6243
|
+
continue;
|
|
6244
|
+
const { keyframes, options } = target[key];
|
|
5914
6245
|
for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
|
|
5915
|
-
|
|
6246
|
+
// Skip only missing values - `0` (e.g. opacity: 0)
|
|
6247
|
+
// is valid and must reach the from-value inference.
|
|
6248
|
+
if (valueKeyframes == null)
|
|
5916
6249
|
continue;
|
|
5917
|
-
const valueOptions = {
|
|
5918
|
-
...getValueTransition$1(defaultOptions, valueName),
|
|
5919
|
-
...getValueTransition$1(options, valueName),
|
|
5920
|
-
};
|
|
5921
|
-
const type = chooseLayerType(key);
|
|
5922
6250
|
/**
|
|
5923
|
-
*
|
|
5924
|
-
*
|
|
6251
|
+
* The view path hands keyframes straight to WAAPI,
|
|
6252
|
+
* so Motion's `x`/`y` shorthands (compiled to
|
|
6253
|
+
* `transform` only via the value pipeline) have no
|
|
6254
|
+
* effect. Warn and skip - use `transform`/`translate`.
|
|
6255
|
+
*/
|
|
6256
|
+
if (valueName === "x" || valueName === "y") {
|
|
6257
|
+
warnOnce(false, `animateView() animates view-transition layers with CSS properties; the "${valueName}" shorthand has no effect - use transform, e.g. { transform: "translateX(40px)" }.`);
|
|
6258
|
+
continue;
|
|
6259
|
+
}
|
|
6260
|
+
/**
|
|
6261
|
+
* enter/exit win over new/old on a shared property -
|
|
6262
|
+
* skip it here when the gated bucket also defines it.
|
|
6263
|
+
*/
|
|
6264
|
+
if (key === "new" &&
|
|
6265
|
+
enterApplies &&
|
|
6266
|
+
target.enter?.keyframes[valueName] != null) {
|
|
6267
|
+
continue;
|
|
6268
|
+
}
|
|
6269
|
+
if (key === "old" &&
|
|
6270
|
+
exitApplies &&
|
|
6271
|
+
target.exit?.keyframes[valueName] != null) {
|
|
6272
|
+
continue;
|
|
6273
|
+
}
|
|
6274
|
+
const valueOptions = mergeTransition(getValueTransition$1(defaultOptions, valueName), getValueTransition$1(options, valueName));
|
|
6275
|
+
/**
|
|
6276
|
+
* Infer an origin for a single-value keyframe. An
|
|
6277
|
+
* `enter` mirrors the matching `exit` value (a
|
|
6278
|
+
* defined exit reverses into the enter for free);
|
|
6279
|
+
* otherwise the per-type default (opacity 0/1, scale
|
|
6280
|
+
* 0.85). No default -> left as-is (animates from the
|
|
6281
|
+
* live value).
|
|
6282
|
+
*
|
|
6283
|
+
* `new`/`old` fire for survivors too, where only the
|
|
6284
|
+
* opacity crossfade default applies - a transform
|
|
6285
|
+
* default like scale 0.85 would pop a persisting
|
|
6286
|
+
* element, so gate it on the layer actually
|
|
6287
|
+
* entering/leaving.
|
|
5925
6288
|
*/
|
|
5926
|
-
if (
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
6289
|
+
if (!Array.isArray(valueKeyframes)) {
|
|
6290
|
+
const exitValue = key === "enter"
|
|
6291
|
+
? target.exit?.keyframes[valueName]
|
|
6292
|
+
: undefined;
|
|
6293
|
+
const allowDefault = valueName === "opacity" ||
|
|
6294
|
+
(type === "new" ? enterApplies : exitApplies);
|
|
6295
|
+
const from = exitValue != null
|
|
6296
|
+
? Array.isArray(exitValue)
|
|
6297
|
+
? exitValue[exitValue.length - 1]
|
|
6298
|
+
: exitValue
|
|
6299
|
+
: allowDefault
|
|
6300
|
+
? ORIGIN_DEFAULTS[type]?.[valueName]
|
|
6301
|
+
: undefined;
|
|
6302
|
+
if (from !== undefined) {
|
|
6303
|
+
valueKeyframes = [from, valueKeyframes];
|
|
6304
|
+
}
|
|
5930
6305
|
}
|
|
5931
6306
|
/**
|
|
5932
|
-
* Resolve stagger function if provided
|
|
6307
|
+
* Resolve stagger function if provided, per element
|
|
6308
|
+
* across this subject's resolved layers.
|
|
5933
6309
|
*/
|
|
5934
6310
|
if (typeof valueOptions.delay === "function") {
|
|
5935
|
-
valueOptions.delay = valueOptions.delay(
|
|
6311
|
+
valueOptions.delay = valueOptions.delay(index, total);
|
|
5936
6312
|
}
|
|
5937
6313
|
valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
|
|
5938
6314
|
valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
|
|
5939
|
-
|
|
6315
|
+
animations.push(new NativeAnimation({
|
|
5940
6316
|
...valueOptions,
|
|
5941
6317
|
element: document.documentElement,
|
|
5942
6318
|
name: valueName,
|
|
5943
|
-
pseudoElement: `::view-transition-${type}(${
|
|
6319
|
+
pseudoElement: `::view-transition-${type}(${name})`,
|
|
5944
6320
|
keyframes: valueKeyframes,
|
|
5945
|
-
});
|
|
5946
|
-
|
|
6321
|
+
}));
|
|
6322
|
+
explicitlyAnimated.add(`${name}:${type}`);
|
|
6323
|
+
if (valueName === "opacity") {
|
|
6324
|
+
opacityAnimated.add(`${name}:${type}`);
|
|
6325
|
+
}
|
|
5947
6326
|
}
|
|
5948
6327
|
}
|
|
5949
6328
|
});
|
|
@@ -5962,45 +6341,129 @@
|
|
|
5962
6341
|
const name = getViewAnimationLayerInfo(pseudoElement);
|
|
5963
6342
|
if (!name)
|
|
5964
6343
|
continue;
|
|
5965
|
-
const targetDefinition =
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
}
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
effect
|
|
5990
|
-
.getKeyframes()
|
|
5991
|
-
.some((keyframe) => keyframe.mixBlendMode)) {
|
|
5992
|
-
animations.push(new NativeAnimationWrapper(animation));
|
|
5993
|
-
}
|
|
5994
|
-
else {
|
|
5995
|
-
animation.cancel();
|
|
6344
|
+
const targetDefinition = layerTargets.get(name.layer);
|
|
6345
|
+
/**
|
|
6346
|
+
* We built our own animation for this layer, so drop the
|
|
6347
|
+
* browser-generated fade we're replacing. The UA
|
|
6348
|
+
* `plus-lighter` blend is a *separate* generated animation on
|
|
6349
|
+
* the same pseudo (it sets `mix-blend-mode` in its keyframes):
|
|
6350
|
+
* keep it *only* for a true opacity crossfade - both sides
|
|
6351
|
+
* fading - so a symmetric crossfade composites without
|
|
6352
|
+
* darkening, but a slide/transform (where both layers stay
|
|
6353
|
+
* opaque and overlap) doesn't flash bright from the addition.
|
|
6354
|
+
*/
|
|
6355
|
+
if (explicitlyAnimated.has(`${name.layer}:${name.type}`)) {
|
|
6356
|
+
const isCrossfade = opacityAnimated.has(`${name.layer}:new`) &&
|
|
6357
|
+
opacityAnimated.has(`${name.layer}:old`);
|
|
6358
|
+
if (isCrossfade &&
|
|
6359
|
+
effect
|
|
6360
|
+
.getKeyframes()
|
|
6361
|
+
.some((keyframe) => keyframe.mixBlendMode)) {
|
|
6362
|
+
animations.push(new NativeAnimationWrapper(animation));
|
|
6363
|
+
}
|
|
6364
|
+
else {
|
|
6365
|
+
animation.cancel();
|
|
6366
|
+
}
|
|
6367
|
+
continue;
|
|
5996
6368
|
}
|
|
6369
|
+
/**
|
|
6370
|
+
* Otherwise retime the browser-generated animation to
|
|
6371
|
+
* Motion's timing. This auto-enables the layout (group)
|
|
6372
|
+
* morph for any resolved/named target, and applies the
|
|
6373
|
+
* default timing to old/new layers we haven't explicitly
|
|
6374
|
+
* overridden.
|
|
6375
|
+
*
|
|
6376
|
+
* group + group-children both follow the layout timing so
|
|
6377
|
+
* the nesting container stays in sync with the morph.
|
|
6378
|
+
*/
|
|
6379
|
+
/**
|
|
6380
|
+
* A survivor's old + new are the two halves of one
|
|
6381
|
+
* `plus-lighter` crossfade. They must share identical timing
|
|
6382
|
+
* (so their opacities stay mirrored and sum to 1 - else the
|
|
6383
|
+
* additive blend flashes bright wherever both are partly
|
|
6384
|
+
* visible) and fade linearly (the bounce belongs on the
|
|
6385
|
+
* group's geometry, not the opacity). So time them as the
|
|
6386
|
+
* group, rather than via their own - potentially staggered,
|
|
6387
|
+
* or enter/exit-derived - old/new options.
|
|
6388
|
+
*/
|
|
6389
|
+
const stagger = layerStagger.get(name.layer);
|
|
6390
|
+
const isMorphCrossfade = (name.type === "old" || name.type === "new") &&
|
|
6391
|
+
!!stagger?.old &&
|
|
6392
|
+
!!stagger?.new;
|
|
6393
|
+
const timingType = name.type.startsWith("group") || isMorphCrossfade
|
|
6394
|
+
? "group"
|
|
6395
|
+
: name.type;
|
|
6396
|
+
const [index, total] = staggerPosition(name.layer, timingType);
|
|
6397
|
+
const transitionName = timingType === "group" ? "layout" : "";
|
|
6398
|
+
let animationTransition = resolveLayerTransition(targetDefinition, timingType, transitionName, index === -1 ? 0 : index, total);
|
|
6399
|
+
/**
|
|
6400
|
+
* The crossfade should resolve at the spring's *perceptual*
|
|
6401
|
+
* (visual) duration - the geometry can keep bouncing, but the
|
|
6402
|
+
* opacity shouldn't drag through the settle. So capture
|
|
6403
|
+
* `visualDuration` before `applyGeneratorOptions` replaces it
|
|
6404
|
+
* with the full overshoot duration, and use it for the fade.
|
|
6405
|
+
*/
|
|
6406
|
+
const visualDuration = animationTransition.visualDuration;
|
|
6407
|
+
animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
|
|
6408
|
+
animationTransition =
|
|
6409
|
+
applyGeneratorOptions(animationTransition);
|
|
6410
|
+
const duration = isMorphCrossfade && visualDuration !== undefined
|
|
6411
|
+
? secondsToMilliseconds(visualDuration)
|
|
6412
|
+
: animationTransition.duration;
|
|
6413
|
+
const easing = isMorphCrossfade
|
|
6414
|
+
? "linear"
|
|
6415
|
+
: mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
|
|
6416
|
+
effect.updateTiming({
|
|
6417
|
+
delay: secondsToMilliseconds(animationTransition.delay ?? 0),
|
|
6418
|
+
duration,
|
|
6419
|
+
easing,
|
|
6420
|
+
});
|
|
6421
|
+
animations.push(new NativeAnimationWrapper(animation));
|
|
5997
6422
|
}
|
|
5998
6423
|
resolve(new GroupAnimation(animations));
|
|
5999
|
-
})
|
|
6424
|
+
})
|
|
6425
|
+
.catch(() =>
|
|
6426
|
+
/**
|
|
6427
|
+
* `ready` rejects when the transition is skipped - no visual
|
|
6428
|
+
* change, or superseded by an interrupting transition. The DOM
|
|
6429
|
+
* update still applied, so settle with no animations rather than
|
|
6430
|
+
* surfacing it as an error to an awaiting caller. A genuine
|
|
6431
|
+
* failure in `update()` rejects `updateCallbackDone` (already
|
|
6432
|
+
* settled by now), so propagate that instead.
|
|
6433
|
+
*/
|
|
6434
|
+
transition.updateCallbackDone.then(() => resolve(new GroupAnimation([])), reject));
|
|
6000
6435
|
});
|
|
6001
6436
|
}
|
|
6002
|
-
|
|
6003
|
-
|
|
6437
|
+
/**
|
|
6438
|
+
* The options that should time a given generated layer type, so a retimed
|
|
6439
|
+
* group/old/new picks up any per-target transition the user provided. Checks
|
|
6440
|
+
* the type's buckets in priority order (e.g. `new` before `enter`).
|
|
6441
|
+
*/
|
|
6442
|
+
function layerOptions(target, type) {
|
|
6443
|
+
for (const bucket of typeBuckets[type] ?? []) {
|
|
6444
|
+
const options = target?.[bucket]?.options;
|
|
6445
|
+
if (options)
|
|
6446
|
+
return options;
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
/**
|
|
6450
|
+
* Merge a base transition (e.g. the default `options`) with a per-layer/value
|
|
6451
|
+
* override. An explicit `duration` on the override must win over an inherited
|
|
6452
|
+
* generator's own timing: a spring prefers `visualDuration`, and
|
|
6453
|
+
* `spring.applyToOptions` overwrites `duration` with the computed settle time -
|
|
6454
|
+
* so without this the override is silently discarded. Dropping the inherited
|
|
6455
|
+
* `type`/`visualDuration` makes the layer a plain tween of that duration, unless
|
|
6456
|
+
* it asked for its own generator `type`/`visualDuration`.
|
|
6457
|
+
*/
|
|
6458
|
+
function mergeTransition(base, override) {
|
|
6459
|
+
const merged = { ...base, ...override };
|
|
6460
|
+
if (override.duration !== undefined) {
|
|
6461
|
+
if (override.visualDuration === undefined)
|
|
6462
|
+
delete merged.visualDuration;
|
|
6463
|
+
if (override.type === undefined)
|
|
6464
|
+
delete merged.type;
|
|
6465
|
+
}
|
|
6466
|
+
return merged;
|
|
6004
6467
|
}
|
|
6005
6468
|
|
|
6006
6469
|
let builders = [];
|
|
@@ -6014,10 +6477,16 @@
|
|
|
6014
6477
|
function start(builder) {
|
|
6015
6478
|
removeItem(builders, builder);
|
|
6016
6479
|
current = builder;
|
|
6017
|
-
startViewAnimation(builder)
|
|
6480
|
+
startViewAnimation(builder)
|
|
6481
|
+
.then((animation) => {
|
|
6018
6482
|
builder.notifyReady(animation);
|
|
6019
|
-
animation.finished
|
|
6020
|
-
})
|
|
6483
|
+
return animation.finished;
|
|
6484
|
+
})
|
|
6485
|
+
// A genuinely failed transition (a throwing update) rejects the
|
|
6486
|
+
// builder; a skipped/interrupted one resolves with no animations (see
|
|
6487
|
+
// start.ts). Either way, advance the queue - else later transitions hang.
|
|
6488
|
+
.catch((error) => builder.notifyReject(error))
|
|
6489
|
+
.finally(next);
|
|
6021
6490
|
}
|
|
6022
6491
|
function processQueue() {
|
|
6023
6492
|
/**
|
|
@@ -6054,31 +6523,129 @@
|
|
|
6054
6523
|
constructor(update, options = {}) {
|
|
6055
6524
|
this.currentSubject = "root";
|
|
6056
6525
|
this.targets = new Map();
|
|
6057
|
-
|
|
6058
|
-
|
|
6526
|
+
/**
|
|
6527
|
+
* Definitions that must be resolved to elements (and assigned a
|
|
6528
|
+
* `view-transition-name`) rather than treated as pre-named layers.
|
|
6529
|
+
*/
|
|
6530
|
+
this.resolveDefs = new Set();
|
|
6531
|
+
/**
|
|
6532
|
+
* Per-subject crop override: `true` forces the crop (clip + object-fit:
|
|
6533
|
+
* cover + animated corner radii) on, `false` forces it off. A subject with
|
|
6534
|
+
* no entry uses the default - crop only a genuine morph (a layer present in
|
|
6535
|
+
* both snapshots), so a fade-only enter/exit isn't clipped to nothing.
|
|
6536
|
+
*/
|
|
6537
|
+
this.cropOverride = new Map();
|
|
6538
|
+
/**
|
|
6539
|
+
* Subjects paired with a different new-snapshot target (the second `.add()`
|
|
6540
|
+
* argument), so two distinct elements share one name and morph into each
|
|
6541
|
+
* other - a shared-element transition.
|
|
6542
|
+
*/
|
|
6543
|
+
this.pairs = new Map();
|
|
6544
|
+
/**
|
|
6545
|
+
* A `view-transition-class` to apply to each subject's resolved elements,
|
|
6546
|
+
* so authors can target the generated layers from CSS by class rather than
|
|
6547
|
+
* the opaque generated name.
|
|
6548
|
+
*/
|
|
6549
|
+
this.classNames = new Map();
|
|
6550
|
+
/**
|
|
6551
|
+
* Subjects opted out of automatic group nesting via `.group(false)`. Their
|
|
6552
|
+
* layer stays a flat top-level group (`view-transition-group: none`) instead
|
|
6553
|
+
* of nesting under its DOM-ancestor layer - so it animates independently and
|
|
6554
|
+
* escapes an ancestor's clip/transform (e.g. an element that lifts out of a
|
|
6555
|
+
* card and flies across, which nesting would clip to the card).
|
|
6556
|
+
*/
|
|
6557
|
+
this.flatGroups = new Set();
|
|
6558
|
+
this.notifyReady = noop;
|
|
6559
|
+
this.notifyReject = noop;
|
|
6560
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
6059
6561
|
this.notifyReady = resolve;
|
|
6562
|
+
this.notifyReject = reject;
|
|
6060
6563
|
});
|
|
6061
6564
|
this.update = update;
|
|
6062
6565
|
this.options = {
|
|
6063
6566
|
interrupt: "wait",
|
|
6064
6567
|
...options,
|
|
6065
6568
|
};
|
|
6569
|
+
// Avoid an unhandled rejection when a failed transition has no
|
|
6570
|
+
// `.then(_, reject)` handler attached (e.g. fire-and-forget).
|
|
6571
|
+
this.readyPromise.catch(noop);
|
|
6066
6572
|
addToQueue(this);
|
|
6067
6573
|
}
|
|
6068
|
-
|
|
6574
|
+
/**
|
|
6575
|
+
* Target elements resolved from a selector or Element, each assigned a
|
|
6576
|
+
* `view-transition-name` automatically.
|
|
6577
|
+
*
|
|
6578
|
+
* Passing a second target pairs them: the first is resolved in the old
|
|
6579
|
+
* snapshot and the second in the new, sharing one name so two *different*
|
|
6580
|
+
* elements morph into each other (e.g. `.add(card, ".modal")`). Symmetric -
|
|
6581
|
+
* pass them the other way round to morph back.
|
|
6582
|
+
*/
|
|
6583
|
+
add(subject, newSubject) {
|
|
6069
6584
|
this.currentSubject = subject;
|
|
6585
|
+
this.resolveDefs.add(subject);
|
|
6586
|
+
if (newSubject !== undefined)
|
|
6587
|
+
this.pairs.set(subject, newSubject);
|
|
6588
|
+
// Register the subject so it participates (and gets an automatic
|
|
6589
|
+
// layout/morph animation) even without an explicit enter/exit/layout.
|
|
6590
|
+
if (!this.targets.has(subject))
|
|
6591
|
+
this.targets.set(subject, {});
|
|
6070
6592
|
return this;
|
|
6071
6593
|
}
|
|
6072
|
-
|
|
6073
|
-
|
|
6594
|
+
/**
|
|
6595
|
+
* Control this subject's crop (clip + `object-fit: cover` + animated
|
|
6596
|
+
* corners). By default a subject auto-crops only when it actually morphs -
|
|
6597
|
+
* present in both snapshots (a survivor, or an `.add(a, b)` pair). A
|
|
6598
|
+
* fade-only enter/exit has no second box to crop against, so it's left to
|
|
6599
|
+
* the browser default; in particular the `overflow: clip` a crop adds would
|
|
6600
|
+
* otherwise clip a mis-sized enter/exit layer to nothing.
|
|
6601
|
+
*
|
|
6602
|
+
* `.crop(false)` forces the crop off (e.g. a text morph, where
|
|
6603
|
+
* `object-fit: cover` clips glyphs as the box grows); `.crop(true)` forces
|
|
6604
|
+
* it on for a non-morph the default wouldn't otherwise crop.
|
|
6605
|
+
*/
|
|
6606
|
+
crop(enabled = true) {
|
|
6607
|
+
this.cropOverride.set(this.currentSubject, enabled);
|
|
6074
6608
|
return this;
|
|
6075
6609
|
}
|
|
6076
|
-
|
|
6077
|
-
|
|
6610
|
+
/**
|
|
6611
|
+
* By default a subject's layer nests under its nearest DOM-ancestor layer
|
|
6612
|
+
* (`view-transition-group: contain`), so the ancestor's clip/transform/opacity
|
|
6613
|
+
* apply to it through the transition - mirroring how the DOM actually paints,
|
|
6614
|
+
* and letting a wrapper crop its child for the whole morph rather than only
|
|
6615
|
+
* once the live DOM takes back over. (Needs a browser that supports nested
|
|
6616
|
+
* view-transition groups; elsewhere it degrades to the flat default.)
|
|
6617
|
+
*
|
|
6618
|
+
* Call `.group(false)` to opt out: the layer stays flat and top-level, so it
|
|
6619
|
+
* animates independently and escapes an ancestor's clip - e.g. an element
|
|
6620
|
+
* that should lift out of a card and fly across, which nesting would clip.
|
|
6621
|
+
*/
|
|
6622
|
+
group(enabled = true) {
|
|
6623
|
+
enabled
|
|
6624
|
+
? this.flatGroups.delete(this.currentSubject)
|
|
6625
|
+
: this.flatGroups.add(this.currentSubject);
|
|
6078
6626
|
return this;
|
|
6079
6627
|
}
|
|
6080
|
-
|
|
6081
|
-
|
|
6628
|
+
/**
|
|
6629
|
+
* Tag this subject's generated layers with a `view-transition-class`, so
|
|
6630
|
+
* they can be targeted from CSS - `::view-transition-group(.name)`,
|
|
6631
|
+
* `::view-transition-old/new(.name)`, `::view-transition-image-pair(.name)`
|
|
6632
|
+
* - without the opaque generated `view-transition-name`. Because `.add()`
|
|
6633
|
+
* can match many elements, a shared class targets them all at once (and,
|
|
6634
|
+
* for a pair, both ends). The escape hatch for z-index / custom keyframes
|
|
6635
|
+
* on a morph layer.
|
|
6636
|
+
*/
|
|
6637
|
+
class(name) {
|
|
6638
|
+
this.classNames.set(this.currentSubject, name);
|
|
6639
|
+
return this;
|
|
6640
|
+
}
|
|
6641
|
+
/**
|
|
6642
|
+
* Set the transition for this subject's morph. The morph is enabled
|
|
6643
|
+
* automatically by `.add()`; this just customises its timing (duration,
|
|
6644
|
+
* easing, a `delay`/`stagger`, …). On the implicit `root` subject it also
|
|
6645
|
+
* opts the page into the transition (the root crossfade).
|
|
6646
|
+
*/
|
|
6647
|
+
layout(options = {}) {
|
|
6648
|
+
this.updateTarget("layout", {}, options);
|
|
6082
6649
|
return this;
|
|
6083
6650
|
}
|
|
6084
6651
|
enter(keyframes, options) {
|
|
@@ -6089,9 +6656,21 @@
|
|
|
6089
6656
|
this.updateTarget("exit", keyframes, options);
|
|
6090
6657
|
return this;
|
|
6091
6658
|
}
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6659
|
+
/**
|
|
6660
|
+
* Animate the new view directly, whether the element is appearing or
|
|
6661
|
+
* persisting (unlike `.enter()`, which only fires for a pure newcomer).
|
|
6662
|
+
* Pair with `.old()` for a crossfade or slide-through.
|
|
6663
|
+
*/
|
|
6664
|
+
new(keyframes, options) {
|
|
6665
|
+
this.updateTarget("new", keyframes, options);
|
|
6666
|
+
return this;
|
|
6667
|
+
}
|
|
6668
|
+
/**
|
|
6669
|
+
* Animate the old view directly, whether the element is leaving or
|
|
6670
|
+
* persisting (unlike `.exit()`, which only fires for a pure leaver).
|
|
6671
|
+
*/
|
|
6672
|
+
old(keyframes, options) {
|
|
6673
|
+
this.updateTarget("old", keyframes, options);
|
|
6095
6674
|
return this;
|
|
6096
6675
|
}
|
|
6097
6676
|
updateTarget(target, keyframes, options = {}) {
|
|
@@ -6106,8 +6685,8 @@
|
|
|
6106
6685
|
return this.readyPromise.then(resolve, reject);
|
|
6107
6686
|
}
|
|
6108
6687
|
}
|
|
6109
|
-
function animateView(update,
|
|
6110
|
-
return new ViewTransitionBuilder(update,
|
|
6688
|
+
function animateView(update, options = {}) {
|
|
6689
|
+
return new ViewTransitionBuilder(update, options);
|
|
6111
6690
|
}
|
|
6112
6691
|
|
|
6113
6692
|
const createAxisDelta = () => ({
|
|
@@ -8207,7 +8786,7 @@
|
|
|
8207
8786
|
: values.borderRadius;
|
|
8208
8787
|
}
|
|
8209
8788
|
const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut);
|
|
8210
|
-
const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop
|
|
8789
|
+
const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop);
|
|
8211
8790
|
function compress(min, max, easing) {
|
|
8212
8791
|
return (p) => {
|
|
8213
8792
|
// Could replace ifs with clamp
|
|
@@ -8227,7 +8806,7 @@
|
|
|
8227
8806
|
|
|
8228
8807
|
function addDomEvent(target, eventName, handler, options = { passive: true }) {
|
|
8229
8808
|
target.addEventListener(eventName, handler, options);
|
|
8230
|
-
return () => target.removeEventListener(eventName, handler);
|
|
8809
|
+
return () => target.removeEventListener(eventName, handler, options);
|
|
8231
8810
|
}
|
|
8232
8811
|
|
|
8233
8812
|
const compareByDepth = (a, b) => a.depth - b.depth;
|
|
@@ -9497,7 +10076,6 @@
|
|
|
9497
10076
|
*/
|
|
9498
10077
|
this.pendingAnimation = frame.update(() => {
|
|
9499
10078
|
globalProjectionState.hasAnimatedSinceResize = true;
|
|
9500
|
-
activeAnimations.layout++;
|
|
9501
10079
|
this.motionValue || (this.motionValue = motionValue(0));
|
|
9502
10080
|
this.motionValue.jump(0, false);
|
|
9503
10081
|
this.currentAnimation = animateSingleValue(this.motionValue, [0, 1000], {
|
|
@@ -9508,11 +10086,7 @@
|
|
|
9508
10086
|
this.mixTargetDelta(latest);
|
|
9509
10087
|
options.onUpdate && options.onUpdate(latest);
|
|
9510
10088
|
},
|
|
9511
|
-
onStop: () => {
|
|
9512
|
-
activeAnimations.layout--;
|
|
9513
|
-
},
|
|
9514
10089
|
onComplete: () => {
|
|
9515
|
-
activeAnimations.layout--;
|
|
9516
10090
|
options.onComplete && options.onComplete();
|
|
9517
10091
|
this.completeAnimation();
|
|
9518
10092
|
},
|
|
@@ -10027,7 +10601,7 @@
|
|
|
10027
10601
|
*/
|
|
10028
10602
|
const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
|
|
10029
10603
|
? Math.round
|
|
10030
|
-
: noop
|
|
10604
|
+
: noop;
|
|
10031
10605
|
function roundAxis(axis) {
|
|
10032
10606
|
// Round to the nearest .5 pixels to support subpixel layouts
|
|
10033
10607
|
axis.min = roundPoint(axis.min);
|
|
@@ -10101,39 +10675,221 @@
|
|
|
10101
10675
|
checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
|
|
10102
10676
|
});
|
|
10103
10677
|
|
|
10104
|
-
const layoutSelector = "[data-layout],
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10678
|
+
const layoutSelector = "[data-layout],[data-layout-id]";
|
|
10679
|
+
/**
|
|
10680
|
+
* All imperatively-created projection nodes live in one persistent tree,
|
|
10681
|
+
* shared across animateLayout() calls (and with any React-created nodes,
|
|
10682
|
+
* via the singleton document root). Keyed by element for reuse.
|
|
10683
|
+
*/
|
|
10684
|
+
const layoutNodes = new WeakMap();
|
|
10685
|
+
/**
|
|
10686
|
+
* Builders created within the same synchronous tick are flushed together
|
|
10687
|
+
* as a single "commit": every node is snapshotted before any updateDom
|
|
10688
|
+
* runs, mirroring React batching renders from different parts of the tree.
|
|
10689
|
+
*/
|
|
10690
|
+
let pendingBuilders;
|
|
10691
|
+
function collectLayoutElements(scope) {
|
|
10692
|
+
const elements = [];
|
|
10693
|
+
if (scope instanceof HTMLElement && scope.matches(layoutSelector)) {
|
|
10694
|
+
elements.push(scope);
|
|
10695
|
+
}
|
|
10696
|
+
scope.querySelectorAll(layoutSelector).forEach((element) => {
|
|
10697
|
+
if (element instanceof HTMLElement)
|
|
10698
|
+
elements.push(element);
|
|
10699
|
+
});
|
|
10700
|
+
return elements;
|
|
10701
|
+
}
|
|
10702
|
+
/**
|
|
10703
|
+
* Process any work scheduled on the frameloop now. A previous animation
|
|
10704
|
+
* may have been seeked while paused (controls.time = x) without a frame
|
|
10705
|
+
* having rendered it - we must materialise that state into the DOM
|
|
10706
|
+
* before taking snapshots.
|
|
10707
|
+
*/
|
|
10708
|
+
function flushPendingFrame() {
|
|
10709
|
+
if (frameData.isProcessing)
|
|
10710
|
+
return;
|
|
10711
|
+
const now = time.now();
|
|
10712
|
+
frameData.delta = clamp(0, 1000 / 60, now - frameData.timestamp);
|
|
10713
|
+
frameData.timestamp = now;
|
|
10714
|
+
frameData.isProcessing = true;
|
|
10715
|
+
frameSteps.update.process(frameData);
|
|
10716
|
+
frameSteps.preRender.process(frameData);
|
|
10717
|
+
frameSteps.render.process(frameData);
|
|
10718
|
+
frameData.isProcessing = false;
|
|
10719
|
+
}
|
|
10720
|
+
function getProjectionParent(element) {
|
|
10721
|
+
let ancestor = element.parentElement;
|
|
10722
|
+
while (ancestor) {
|
|
10723
|
+
const node = layoutNodes.get(ancestor);
|
|
10724
|
+
if (node && node.instance)
|
|
10725
|
+
return node;
|
|
10726
|
+
ancestor = ancestor.parentElement;
|
|
10727
|
+
}
|
|
10728
|
+
return undefined;
|
|
10729
|
+
}
|
|
10730
|
+
function createVisualElement() {
|
|
10731
|
+
return new HTMLVisualElement({
|
|
10732
|
+
props: {},
|
|
10733
|
+
presenceContext: null,
|
|
10734
|
+
visualState: {
|
|
10735
|
+
latestValues: {},
|
|
10736
|
+
renderState: {
|
|
10737
|
+
transform: {},
|
|
10738
|
+
transformOrigin: {},
|
|
10739
|
+
style: {},
|
|
10740
|
+
vars: {},
|
|
10741
|
+
},
|
|
10742
|
+
},
|
|
10743
|
+
}, { allowProjection: true });
|
|
10744
|
+
}
|
|
10745
|
+
function readNodeOptions(element, transition) {
|
|
10746
|
+
const layoutAttr = element.getAttribute("data-layout");
|
|
10747
|
+
const layoutId = element.getAttribute("data-layout-id") ?? undefined;
|
|
10114
10748
|
return {
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10749
|
+
layoutId,
|
|
10750
|
+
layout: layoutAttr !== null ? true : undefined,
|
|
10751
|
+
animationType: (!layoutAttr || layoutAttr === "true"
|
|
10752
|
+
? "both"
|
|
10753
|
+
: layoutAttr),
|
|
10754
|
+
transition,
|
|
10120
10755
|
};
|
|
10121
10756
|
}
|
|
10757
|
+
function prepareNode(element, transition) {
|
|
10758
|
+
let node = layoutNodes.get(element);
|
|
10759
|
+
if (!node) {
|
|
10760
|
+
let visualElement = visualElementStore.get(element);
|
|
10761
|
+
if (!visualElement)
|
|
10762
|
+
visualElement = createVisualElement();
|
|
10763
|
+
/**
|
|
10764
|
+
* A first-time element may carry a projection transform in its
|
|
10765
|
+
* inline style (e.g. it was cloned from an element mid-animation).
|
|
10766
|
+
* That transform isn't tracked in latestValues so the engine can't
|
|
10767
|
+
* reset it before measuring - clear it now so the first layout
|
|
10768
|
+
* measurement isn't inflated.
|
|
10769
|
+
*/
|
|
10770
|
+
if (element.style.transform &&
|
|
10771
|
+
!hasTransform(visualElement.latestValues)) {
|
|
10772
|
+
element.style.transform = "";
|
|
10773
|
+
}
|
|
10774
|
+
node = new HTMLProjectionNode(visualElement.latestValues, getProjectionParent(element));
|
|
10775
|
+
visualElement.projection = node;
|
|
10776
|
+
node.setOptions({
|
|
10777
|
+
...readNodeOptions(element, transition),
|
|
10778
|
+
visualElement,
|
|
10779
|
+
});
|
|
10780
|
+
node.mount(element);
|
|
10781
|
+
layoutNodes.set(element, node);
|
|
10782
|
+
}
|
|
10783
|
+
else {
|
|
10784
|
+
node.setOptions(readNodeOptions(element, transition));
|
|
10785
|
+
}
|
|
10786
|
+
node.isPresent = true;
|
|
10787
|
+
if (node.options.onExitComplete) {
|
|
10788
|
+
node.setOptions({ onExitComplete: undefined });
|
|
10789
|
+
}
|
|
10790
|
+
return node;
|
|
10791
|
+
}
|
|
10792
|
+
function sortDocumentOrder(elements) {
|
|
10793
|
+
return [...elements].sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
10794
|
+
}
|
|
10795
|
+
function dropNode(element, node) {
|
|
10796
|
+
node.setOptions({ onExitComplete: undefined });
|
|
10797
|
+
/**
|
|
10798
|
+
* Stop any lingering animation so it can't leak into future updates.
|
|
10799
|
+
* A follow node can share its currentAnimation with a surviving lead
|
|
10800
|
+
* (via resumingFrom), in which case it isn't ours to stop.
|
|
10801
|
+
*/
|
|
10802
|
+
const stack = node.getStack();
|
|
10803
|
+
if (!stack || node.isLead())
|
|
10804
|
+
node.currentAnimation?.stop();
|
|
10805
|
+
node.unmount();
|
|
10806
|
+
layoutNodes.delete(element);
|
|
10807
|
+
}
|
|
10808
|
+
function flushPendingBuilders() {
|
|
10809
|
+
const builders = pendingBuilders;
|
|
10810
|
+
pendingBuilders = undefined;
|
|
10811
|
+
flushPendingFrame();
|
|
10812
|
+
/**
|
|
10813
|
+
* Discover and mount every node across all builders before snapshotting
|
|
10814
|
+
* any of them. Mounting during an active update flags isLayoutDirty,
|
|
10815
|
+
* which would make that node's own willUpdate skip its snapshot.
|
|
10816
|
+
* Document order guarantees ancestors mount before descendants, even
|
|
10817
|
+
* when they're discovered by different builders.
|
|
10818
|
+
*/
|
|
10819
|
+
const targets = new Map();
|
|
10820
|
+
for (const builder of builders) {
|
|
10821
|
+
for (const element of builder.collectTargets()) {
|
|
10822
|
+
const owners = targets.get(element);
|
|
10823
|
+
owners ? owners.push(builder) : targets.set(element, [builder]);
|
|
10824
|
+
}
|
|
10825
|
+
}
|
|
10826
|
+
const union = new Map();
|
|
10827
|
+
for (const element of sortDocumentOrder(targets.keys())) {
|
|
10828
|
+
const owners = targets.get(element);
|
|
10829
|
+
const node = prepareNode(element, owners[owners.length - 1].transitionFor(element));
|
|
10830
|
+
for (const owner of owners)
|
|
10831
|
+
owner.adopt(element, node);
|
|
10832
|
+
union.set(element, node);
|
|
10833
|
+
}
|
|
10834
|
+
union.forEach((node) => {
|
|
10835
|
+
node.isLayoutDirty = false;
|
|
10836
|
+
node.willUpdate();
|
|
10837
|
+
});
|
|
10838
|
+
const updatePromises = [];
|
|
10839
|
+
for (const builder of builders) {
|
|
10840
|
+
const result = builder.runUpdate();
|
|
10841
|
+
if (result)
|
|
10842
|
+
updatePromises.push(result);
|
|
10843
|
+
}
|
|
10844
|
+
const commit = () => {
|
|
10845
|
+
/**
|
|
10846
|
+
* Process all additions before any removals so that, even across
|
|
10847
|
+
* builders, a removed member knows whether a replacement with the
|
|
10848
|
+
* same layoutId was added in this commit.
|
|
10849
|
+
*/
|
|
10850
|
+
const newMemberIds = new Set();
|
|
10851
|
+
for (const builder of builders) {
|
|
10852
|
+
builder.reconcileAdditions(newMemberIds);
|
|
10853
|
+
}
|
|
10854
|
+
for (const builder of builders) {
|
|
10855
|
+
builder.reconcileRemovals(newMemberIds);
|
|
10856
|
+
}
|
|
10857
|
+
let root;
|
|
10858
|
+
union.forEach((node) => (root || (root = node.root)));
|
|
10859
|
+
for (const builder of builders)
|
|
10860
|
+
root || (root = builder.getRoot());
|
|
10861
|
+
root?.didUpdate();
|
|
10862
|
+
/**
|
|
10863
|
+
* The root flushes the update on a microtask, synchronously
|
|
10864
|
+
* processing the frame that creates the layout animations. Collect
|
|
10865
|
+
* them in a later microtask step of the same pass.
|
|
10866
|
+
*/
|
|
10867
|
+
microtask.render(() => {
|
|
10868
|
+
for (const builder of builders)
|
|
10869
|
+
builder.finalize();
|
|
10870
|
+
});
|
|
10871
|
+
};
|
|
10872
|
+
updatePromises.length ? Promise.all(updatePromises).then(commit) : commit();
|
|
10873
|
+
}
|
|
10122
10874
|
class LayoutAnimationBuilder {
|
|
10123
10875
|
constructor(scope, updateDom, defaultOptions) {
|
|
10124
|
-
this.sharedTransitions = new Map();
|
|
10125
|
-
this.notifyReady = noop;
|
|
10126
|
-
this.rejectReady = noop;
|
|
10127
10876
|
this.scope = scope;
|
|
10128
10877
|
this.updateDom = updateDom;
|
|
10129
10878
|
this.defaultOptions = defaultOptions;
|
|
10879
|
+
this.sharedTransitions = new Map();
|
|
10880
|
+
this.notifyReady = () => { };
|
|
10881
|
+
this.rejectReady = () => { };
|
|
10882
|
+
this.tracked = new Map();
|
|
10883
|
+
this.restorePoints = new Map();
|
|
10130
10884
|
this.readyPromise = new Promise((resolve, reject) => {
|
|
10131
10885
|
this.notifyReady = resolve;
|
|
10132
10886
|
this.rejectReady = reject;
|
|
10133
10887
|
});
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10888
|
+
if (!pendingBuilders) {
|
|
10889
|
+
pendingBuilders = [];
|
|
10890
|
+
queueMicrotask(flushPendingBuilders);
|
|
10891
|
+
}
|
|
10892
|
+
pendingBuilders.push(this);
|
|
10137
10893
|
}
|
|
10138
10894
|
shared(id, transition) {
|
|
10139
10895
|
this.sharedTransitions.set(id, transition);
|
|
@@ -10142,114 +10898,107 @@
|
|
|
10142
10898
|
then(resolve, reject) {
|
|
10143
10899
|
return this.readyPromise.then(resolve, reject);
|
|
10144
10900
|
}
|
|
10145
|
-
|
|
10146
|
-
const
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
}
|
|
10156
|
-
else if (projection.snapshot) {
|
|
10157
|
-
projection.snapshot = undefined;
|
|
10158
|
-
}
|
|
10159
|
-
}
|
|
10160
|
-
else if (projection.snapshot &&
|
|
10161
|
-
(projection.currentAnimation || projection.isProjecting())) {
|
|
10162
|
-
projection.snapshot = undefined;
|
|
10163
|
-
}
|
|
10164
|
-
projection.isPresent = true;
|
|
10165
|
-
projection.willUpdate();
|
|
10901
|
+
transitionFor(element) {
|
|
10902
|
+
const layoutId = element.getAttribute("data-layout-id");
|
|
10903
|
+
return ((layoutId && this.sharedTransitions.get(layoutId)) ||
|
|
10904
|
+
this.defaultOptions);
|
|
10905
|
+
}
|
|
10906
|
+
adopt(element, node) {
|
|
10907
|
+
this.tracked.set(element, node);
|
|
10908
|
+
this.restorePoints.set(element, {
|
|
10909
|
+
parent: element.parentElement,
|
|
10910
|
+
next: element.nextSibling,
|
|
10166
10911
|
});
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
const
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
return;
|
|
10179
|
-
const currentTransform = instance.style.transform;
|
|
10180
|
-
const resumeFromTransform = resumeFromInstance.style.transform;
|
|
10181
|
-
if (currentTransform &&
|
|
10182
|
-
resumeFromTransform &&
|
|
10183
|
-
currentTransform === resumeFromTransform) {
|
|
10184
|
-
instance.style.transform = "";
|
|
10185
|
-
instance.style.transformOrigin = "";
|
|
10912
|
+
}
|
|
10913
|
+
collectTargets() {
|
|
10914
|
+
return collectLayoutElements(this.scope);
|
|
10915
|
+
}
|
|
10916
|
+
runUpdate() {
|
|
10917
|
+
try {
|
|
10918
|
+
const result = this.updateDom();
|
|
10919
|
+
if (result && typeof result.then === "function") {
|
|
10920
|
+
return result.then(undefined, (error) => {
|
|
10921
|
+
this.updateError = error;
|
|
10922
|
+
});
|
|
10186
10923
|
}
|
|
10187
|
-
}
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
}
|
|
10191
|
-
|
|
10192
|
-
root?.didUpdate();
|
|
10193
|
-
await new Promise((resolve) => {
|
|
10194
|
-
frame.postRender(() => resolve());
|
|
10195
|
-
});
|
|
10196
|
-
const animations = collectAnimations(afterRecords);
|
|
10197
|
-
const animation = new GroupAnimation(animations);
|
|
10198
|
-
return animation;
|
|
10924
|
+
}
|
|
10925
|
+
catch (error) {
|
|
10926
|
+
this.updateError = error;
|
|
10927
|
+
}
|
|
10928
|
+
return undefined;
|
|
10199
10929
|
}
|
|
10200
|
-
|
|
10201
|
-
const
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
const
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
? this.sharedTransitions.get(layoutId)
|
|
10208
|
-
: undefined;
|
|
10209
|
-
const transition = override || this.defaultOptions;
|
|
10210
|
-
const record = getOrCreateRecord(element, parentRecord?.projection, {
|
|
10211
|
-
layout,
|
|
10212
|
-
layoutId,
|
|
10213
|
-
animationType: typeof layout === "string" ? layout : "both",
|
|
10214
|
-
transition: transition,
|
|
10215
|
-
});
|
|
10216
|
-
recordMap.set(element, record);
|
|
10217
|
-
records.push(record);
|
|
10930
|
+
reconcileAdditions(newMemberIds) {
|
|
10931
|
+
for (const element of collectLayoutElements(this.scope)) {
|
|
10932
|
+
if (this.tracked.has(element))
|
|
10933
|
+
continue;
|
|
10934
|
+
const node = prepareNode(element, this.transitionFor(element));
|
|
10935
|
+
this.adopt(element, node);
|
|
10936
|
+
node.options.layoutId && newMemberIds.add(node.options.layoutId);
|
|
10218
10937
|
}
|
|
10219
|
-
return records;
|
|
10220
10938
|
}
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
if (afterElementsSet.has(record.element))
|
|
10939
|
+
reconcileRemovals(newMemberIds) {
|
|
10940
|
+
this.tracked.forEach((node, element) => {
|
|
10941
|
+
if (element.isConnected)
|
|
10225
10942
|
return;
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10943
|
+
const restore = this.restorePoints.get(element);
|
|
10944
|
+
this.restorePoints.delete(element);
|
|
10945
|
+
const { layoutId } = node.options;
|
|
10946
|
+
const stack = node.getStack();
|
|
10947
|
+
const hasSurvivor = stack &&
|
|
10948
|
+
stack.members.some((member) => member !== node &&
|
|
10949
|
+
member.instance
|
|
10950
|
+
?.isConnected);
|
|
10951
|
+
/**
|
|
10952
|
+
* A removed lead with a surviving stack member - and no
|
|
10953
|
+
* replacement member added this commit - runs an exit
|
|
10954
|
+
* crossfade: restore the element to its old position in the
|
|
10955
|
+
* DOM, relegate it and let the survivor take over. It's
|
|
10956
|
+
* removed again once the animation completes.
|
|
10957
|
+
*/
|
|
10958
|
+
if (layoutId &&
|
|
10959
|
+
node.isLead() &&
|
|
10960
|
+
hasSurvivor &&
|
|
10961
|
+
!newMemberIds.has(layoutId)) {
|
|
10962
|
+
if (restore && restore.parent.isConnected) {
|
|
10963
|
+
restore.parent.insertBefore(element, restore.next && restore.next.parentNode === restore.parent
|
|
10964
|
+
? restore.next
|
|
10965
|
+
: null);
|
|
10966
|
+
node.isPresent = false;
|
|
10967
|
+
node.setOptions({
|
|
10968
|
+
onExitComplete: () => {
|
|
10969
|
+
element.remove();
|
|
10970
|
+
dropNode(element, node);
|
|
10971
|
+
},
|
|
10972
|
+
});
|
|
10973
|
+
if (node.relegate())
|
|
10974
|
+
return;
|
|
10975
|
+
element.remove();
|
|
10976
|
+
}
|
|
10231
10977
|
}
|
|
10232
|
-
|
|
10233
|
-
|
|
10978
|
+
dropNode(element, node);
|
|
10979
|
+
this.tracked.delete(element);
|
|
10234
10980
|
});
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10981
|
+
}
|
|
10982
|
+
getRoot() {
|
|
10983
|
+
let root;
|
|
10984
|
+
this.tracked.forEach((node) => (root || (root = node.root)));
|
|
10985
|
+
return root;
|
|
10986
|
+
}
|
|
10987
|
+
finalize() {
|
|
10988
|
+
if (this.updateError) {
|
|
10989
|
+
this.rejectReady(this.updateError);
|
|
10990
|
+
return;
|
|
10991
|
+
}
|
|
10992
|
+
const animations = new Set();
|
|
10993
|
+
this.tracked.forEach((node) => {
|
|
10994
|
+
if (node.instance && node.currentAnimation) {
|
|
10995
|
+
animations.add(node.currentAnimation);
|
|
10247
10996
|
}
|
|
10248
10997
|
});
|
|
10998
|
+
this.notifyReady(new GroupAnimation([...animations]));
|
|
10249
10999
|
}
|
|
10250
11000
|
}
|
|
10251
11001
|
function parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options) {
|
|
10252
|
-
// animateLayout(updateDom)
|
|
10253
11002
|
if (typeof scopeOrUpdateDom === "function") {
|
|
10254
11003
|
return {
|
|
10255
11004
|
scope: document,
|
|
@@ -10257,107 +11006,15 @@
|
|
|
10257
11006
|
defaultOptions: updateDomOrOptions,
|
|
10258
11007
|
};
|
|
10259
11008
|
}
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
11009
|
+
const scope = scopeOrUpdateDom instanceof Document
|
|
11010
|
+
? scopeOrUpdateDom
|
|
11011
|
+
: resolveElements(scopeOrUpdateDom)[0] ?? document;
|
|
10263
11012
|
return {
|
|
10264
11013
|
scope,
|
|
10265
11014
|
updateDom: updateDomOrOptions,
|
|
10266
11015
|
defaultOptions: options,
|
|
10267
11016
|
};
|
|
10268
11017
|
}
|
|
10269
|
-
function collectLayoutElements(scope) {
|
|
10270
|
-
const elements = Array.from(scope.querySelectorAll(layoutSelector));
|
|
10271
|
-
if (scope instanceof Element && scope.matches(layoutSelector)) {
|
|
10272
|
-
if (!elements.includes(scope)) {
|
|
10273
|
-
elements.unshift(scope);
|
|
10274
|
-
}
|
|
10275
|
-
}
|
|
10276
|
-
return elements;
|
|
10277
|
-
}
|
|
10278
|
-
function readLayoutAttributes(element) {
|
|
10279
|
-
const layoutId = element.getAttribute("data-layout-id") || undefined;
|
|
10280
|
-
const rawLayout = element.getAttribute("data-layout");
|
|
10281
|
-
let layout;
|
|
10282
|
-
if (rawLayout === "" || rawLayout === "true") {
|
|
10283
|
-
layout = true;
|
|
10284
|
-
}
|
|
10285
|
-
else if (rawLayout) {
|
|
10286
|
-
layout = rawLayout;
|
|
10287
|
-
}
|
|
10288
|
-
return {
|
|
10289
|
-
layout,
|
|
10290
|
-
layoutId,
|
|
10291
|
-
};
|
|
10292
|
-
}
|
|
10293
|
-
function createVisualState() {
|
|
10294
|
-
return {
|
|
10295
|
-
latestValues: {},
|
|
10296
|
-
renderState: {
|
|
10297
|
-
transform: {},
|
|
10298
|
-
transformOrigin: {},
|
|
10299
|
-
style: {},
|
|
10300
|
-
vars: {},
|
|
10301
|
-
},
|
|
10302
|
-
};
|
|
10303
|
-
}
|
|
10304
|
-
function getOrCreateRecord(element, parentProjection, projectionOptions) {
|
|
10305
|
-
const existing = visualElementStore.get(element);
|
|
10306
|
-
const visualElement = existing ??
|
|
10307
|
-
new HTMLVisualElement({
|
|
10308
|
-
props: {},
|
|
10309
|
-
presenceContext: null,
|
|
10310
|
-
visualState: createVisualState(),
|
|
10311
|
-
}, { allowProjection: true });
|
|
10312
|
-
if (!existing || !visualElement.projection) {
|
|
10313
|
-
visualElement.projection = new HTMLProjectionNode(visualElement.latestValues, parentProjection);
|
|
10314
|
-
}
|
|
10315
|
-
visualElement.projection.setOptions({
|
|
10316
|
-
...projectionOptions,
|
|
10317
|
-
visualElement,
|
|
10318
|
-
});
|
|
10319
|
-
if (!visualElement.current) {
|
|
10320
|
-
visualElement.mount(element);
|
|
10321
|
-
}
|
|
10322
|
-
else if (!visualElement.projection.instance) {
|
|
10323
|
-
// Mount projection if VisualElement is already mounted but projection isn't
|
|
10324
|
-
// This happens when animate() was called before animateLayout()
|
|
10325
|
-
visualElement.projection.mount(element);
|
|
10326
|
-
}
|
|
10327
|
-
if (!existing) {
|
|
10328
|
-
visualElementStore.set(element, visualElement);
|
|
10329
|
-
}
|
|
10330
|
-
return {
|
|
10331
|
-
element,
|
|
10332
|
-
visualElement,
|
|
10333
|
-
projection: visualElement.projection,
|
|
10334
|
-
};
|
|
10335
|
-
}
|
|
10336
|
-
function findParentRecord(element, recordMap, scope) {
|
|
10337
|
-
let parent = element.parentElement;
|
|
10338
|
-
while (parent) {
|
|
10339
|
-
const record = recordMap.get(parent);
|
|
10340
|
-
if (record)
|
|
10341
|
-
return record;
|
|
10342
|
-
if (parent === scope)
|
|
10343
|
-
break;
|
|
10344
|
-
parent = parent.parentElement;
|
|
10345
|
-
}
|
|
10346
|
-
return undefined;
|
|
10347
|
-
}
|
|
10348
|
-
function getProjectionRoot(afterRecords, beforeRecords) {
|
|
10349
|
-
const record = afterRecords[0] || beforeRecords[0];
|
|
10350
|
-
return record?.projection.root;
|
|
10351
|
-
}
|
|
10352
|
-
function collectAnimations(afterRecords) {
|
|
10353
|
-
const animations = new Set();
|
|
10354
|
-
afterRecords.forEach((record) => {
|
|
10355
|
-
const animation = record.projection.currentAnimation;
|
|
10356
|
-
if (animation)
|
|
10357
|
-
animations.add(animation);
|
|
10358
|
-
});
|
|
10359
|
-
return Array.from(animations);
|
|
10360
|
-
}
|
|
10361
11018
|
|
|
10362
11019
|
/**
|
|
10363
11020
|
* @deprecated
|
|
@@ -10546,6 +11203,14 @@
|
|
|
10546
11203
|
const PresenceChild = ({ children, initial, isPresent, onExitComplete, custom, presenceAffectsLayout, mode, anchorX, anchorY, root }) => {
|
|
10547
11204
|
const presenceChildren = useConstant(newChildrenMap);
|
|
10548
11205
|
const id = React$1.useId();
|
|
11206
|
+
// Written in a layout effect (not render) so discarded concurrent
|
|
11207
|
+
// renders can't leave the refs pointing at uncommitted state.
|
|
11208
|
+
const isPresentRef = React$1.useRef(isPresent);
|
|
11209
|
+
const onExitCompleteRef = React$1.useRef(onExitComplete);
|
|
11210
|
+
useIsomorphicLayoutEffect(() => {
|
|
11211
|
+
isPresentRef.current = isPresent;
|
|
11212
|
+
onExitCompleteRef.current = onExitComplete;
|
|
11213
|
+
});
|
|
10549
11214
|
let isReusedContext = true;
|
|
10550
11215
|
let context = React$1.useMemo(() => {
|
|
10551
11216
|
isReusedContext = false;
|
|
@@ -10564,7 +11229,12 @@
|
|
|
10564
11229
|
},
|
|
10565
11230
|
register: (childId) => {
|
|
10566
11231
|
presenceChildren.set(childId, false);
|
|
10567
|
-
return () =>
|
|
11232
|
+
return () => {
|
|
11233
|
+
presenceChildren.delete(childId);
|
|
11234
|
+
!isPresentRef.current &&
|
|
11235
|
+
!presenceChildren.size &&
|
|
11236
|
+
onExitCompleteRef.current?.();
|
|
11237
|
+
};
|
|
10568
11238
|
},
|
|
10569
11239
|
};
|
|
10570
11240
|
}, [isPresent, presenceChildren, onExitComplete]);
|
|
@@ -12041,7 +12711,11 @@
|
|
|
12041
12711
|
const { onSessionStart } = handlers;
|
|
12042
12712
|
onSessionStart &&
|
|
12043
12713
|
onSessionStart(event, getPanInfo(initialInfo, this.history));
|
|
12044
|
-
|
|
12714
|
+
// Listen in the capture phase so a descendant calling
|
|
12715
|
+
// stopPropagation() (e.g. in its own pointerup handler) can't
|
|
12716
|
+
// prevent the gesture from ending. See #2794.
|
|
12717
|
+
const eventOptions = { passive: true, capture: true };
|
|
12718
|
+
this.removeListeners = pipe(addPointerEvent(this.contextWindow, "pointermove", this.handlePointerMove, eventOptions), addPointerEvent(this.contextWindow, "pointerup", this.handlePointerUp, eventOptions), addPointerEvent(this.contextWindow, "pointercancel", this.handlePointerUp, eventOptions));
|
|
12045
12719
|
// Start scroll tracking if element provided
|
|
12046
12720
|
if (element) {
|
|
12047
12721
|
this.startScrollTracking(element);
|
|
@@ -12885,8 +13559,8 @@
|
|
|
12885
13559
|
class DragGesture extends Feature {
|
|
12886
13560
|
constructor(node) {
|
|
12887
13561
|
super(node);
|
|
12888
|
-
this.removeGroupControls = noop
|
|
12889
|
-
this.removeListeners = noop
|
|
13562
|
+
this.removeGroupControls = noop;
|
|
13563
|
+
this.removeListeners = noop;
|
|
12890
13564
|
this.controls = new VisualElementDragControls(node);
|
|
12891
13565
|
}
|
|
12892
13566
|
mount() {
|
|
@@ -12896,7 +13570,7 @@
|
|
|
12896
13570
|
if (dragControls) {
|
|
12897
13571
|
this.removeGroupControls = dragControls.subscribe(this.controls);
|
|
12898
13572
|
}
|
|
12899
|
-
this.removeListeners = this.controls.addListeners() || noop
|
|
13573
|
+
this.removeListeners = this.controls.addListeners() || noop;
|
|
12900
13574
|
}
|
|
12901
13575
|
update() {
|
|
12902
13576
|
const { dragControls } = this.node.getProps();
|
|
@@ -12935,7 +13609,7 @@
|
|
|
12935
13609
|
class PanGesture extends Feature {
|
|
12936
13610
|
constructor() {
|
|
12937
13611
|
super(...arguments);
|
|
12938
|
-
this.removePointerDownListener = noop
|
|
13612
|
+
this.removePointerDownListener = noop;
|
|
12939
13613
|
}
|
|
12940
13614
|
onPointerDown(pointerDownEvent) {
|
|
12941
13615
|
this.session = new PanSession(pointerDownEvent, this.createPanHandlers(), {
|
|
@@ -14722,7 +15396,7 @@
|
|
|
14722
15396
|
const getEventTarget = (element) => element === document.scrollingElement ? window : element;
|
|
14723
15397
|
function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
|
|
14724
15398
|
if (!container)
|
|
14725
|
-
return noop
|
|
15399
|
+
return noop;
|
|
14726
15400
|
let containerHandlers = onScrollHandlers.get(container);
|
|
14727
15401
|
/**
|
|
14728
15402
|
* Get the onScroll handlers for this container.
|
|
@@ -15006,7 +15680,7 @@
|
|
|
15006
15680
|
|
|
15007
15681
|
function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) {
|
|
15008
15682
|
if (!container)
|
|
15009
|
-
return noop
|
|
15683
|
+
return noop;
|
|
15010
15684
|
const optionsWithDefaults = { axis, container, ...options };
|
|
15011
15685
|
return typeof onScroll === "function"
|
|
15012
15686
|
? attachToFunction(onScroll, optionsWithDefaults)
|
|
@@ -16177,7 +16851,7 @@
|
|
|
16177
16851
|
};
|
|
16178
16852
|
appearComplete.set(id, false);
|
|
16179
16853
|
if (readyAnimation.ready) {
|
|
16180
|
-
readyAnimation.ready.then(startAnimation).catch(noop
|
|
16854
|
+
readyAnimation.ready.then(startAnimation).catch(noop);
|
|
16181
16855
|
}
|
|
16182
16856
|
else {
|
|
16183
16857
|
startAnimation();
|
|
@@ -16334,7 +17008,6 @@
|
|
|
16334
17008
|
exports.VisualElement = VisualElement;
|
|
16335
17009
|
exports.WillChangeMotionValue = WillChangeMotionValue;
|
|
16336
17010
|
exports.acceleratedValues = acceleratedValues;
|
|
16337
|
-
exports.activeAnimations = activeAnimations;
|
|
16338
17011
|
exports.addAttrValue = addAttrValue;
|
|
16339
17012
|
exports.addDomEvent = addDomEvent;
|
|
16340
17013
|
exports.addPointerEvent = addPointerEvent;
|
|
@@ -16549,7 +17222,7 @@
|
|
|
16549
17222
|
exports.motionValue = motionValue;
|
|
16550
17223
|
exports.moveItem = moveItem;
|
|
16551
17224
|
exports.nodeGroup = nodeGroup;
|
|
16552
|
-
exports.noop = noop
|
|
17225
|
+
exports.noop = noop;
|
|
16553
17226
|
exports.number = number;
|
|
16554
17227
|
exports.numberValueTypes = numberValueTypes;
|
|
16555
17228
|
exports.observeTimeline = observeTimeline;
|