902 lines
30 KiB
JavaScript
902 lines
30 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
var motionUtils = require('motion-utils');
|
|
|
|
const supportsScrollTimeline = motionUtils.memo(() => window.ScrollTimeline !== undefined);
|
|
|
|
class BaseGroupPlaybackControls {
|
|
constructor(animations) {
|
|
// Bound to accomodate common `return animation.stop` pattern
|
|
this.stop = () => this.runAll("stop");
|
|
this.animations = animations.filter(Boolean);
|
|
}
|
|
get finished() {
|
|
// Support for new finished Promise and legacy thennable API
|
|
return Promise.all(this.animations.map((animation) => "finished" in animation ? animation.finished : animation));
|
|
}
|
|
/**
|
|
* TODO: Filter out cancelled or stopped animations before returning
|
|
*/
|
|
getAll(propName) {
|
|
return this.animations[0][propName];
|
|
}
|
|
setAll(propName, newValue) {
|
|
for (let i = 0; i < this.animations.length; i++) {
|
|
this.animations[i][propName] = newValue;
|
|
}
|
|
}
|
|
attachTimeline(timeline, fallback) {
|
|
const subscriptions = this.animations.map((animation) => {
|
|
if (supportsScrollTimeline() && animation.attachTimeline) {
|
|
return animation.attachTimeline(timeline);
|
|
}
|
|
else if (typeof fallback === "function") {
|
|
return fallback(animation);
|
|
}
|
|
});
|
|
return () => {
|
|
subscriptions.forEach((cancel, i) => {
|
|
cancel && cancel();
|
|
this.animations[i].stop();
|
|
});
|
|
};
|
|
}
|
|
get time() {
|
|
return this.getAll("time");
|
|
}
|
|
set time(time) {
|
|
this.setAll("time", time);
|
|
}
|
|
get speed() {
|
|
return this.getAll("speed");
|
|
}
|
|
set speed(speed) {
|
|
this.setAll("speed", speed);
|
|
}
|
|
get startTime() {
|
|
return this.getAll("startTime");
|
|
}
|
|
get duration() {
|
|
let max = 0;
|
|
for (let i = 0; i < this.animations.length; i++) {
|
|
max = Math.max(max, this.animations[i].duration);
|
|
}
|
|
return max;
|
|
}
|
|
runAll(methodName) {
|
|
this.animations.forEach((controls) => controls[methodName]());
|
|
}
|
|
flatten() {
|
|
this.runAll("flatten");
|
|
}
|
|
play() {
|
|
this.runAll("play");
|
|
}
|
|
pause() {
|
|
this.runAll("pause");
|
|
}
|
|
cancel() {
|
|
this.runAll("cancel");
|
|
}
|
|
complete() {
|
|
this.runAll("complete");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TODO: This is a temporary class to support the legacy
|
|
* thennable API
|
|
*/
|
|
class GroupPlaybackControls extends BaseGroupPlaybackControls {
|
|
then(onResolve, onReject) {
|
|
return Promise.all(this.animations).then(onResolve).catch(onReject);
|
|
}
|
|
}
|
|
|
|
function getValueTransition(transition, key) {
|
|
return transition
|
|
? transition[key] ||
|
|
transition["default"] ||
|
|
transition
|
|
: undefined;
|
|
}
|
|
|
|
/**
|
|
* Implement a practical max duration for keyframe generation
|
|
* to prevent infinite loops
|
|
*/
|
|
const maxGeneratorDuration = 20000;
|
|
function calcGeneratorDuration(generator) {
|
|
let duration = 0;
|
|
const timeStep = 50;
|
|
let state = generator.next(duration);
|
|
while (!state.done && duration < maxGeneratorDuration) {
|
|
duration += timeStep;
|
|
state = generator.next(duration);
|
|
}
|
|
return duration >= maxGeneratorDuration ? Infinity : duration;
|
|
}
|
|
|
|
/**
|
|
* Create a progress => progress easing function from a generator.
|
|
*/
|
|
function createGeneratorEasing(options, scale = 100, createGenerator) {
|
|
const generator = createGenerator({ ...options, keyframes: [0, scale] });
|
|
const duration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
|
|
return {
|
|
type: "keyframes",
|
|
ease: (progress) => {
|
|
return generator.next(duration * progress).value / scale;
|
|
},
|
|
duration: motionUtils.millisecondsToSeconds(duration),
|
|
};
|
|
}
|
|
|
|
function isGenerator(type) {
|
|
return typeof type === "function";
|
|
}
|
|
|
|
function attachTimeline(animation, timeline) {
|
|
animation.timeline = timeline;
|
|
animation.onfinish = null;
|
|
}
|
|
|
|
class NativeAnimationControls {
|
|
constructor(animation) {
|
|
this.animation = animation;
|
|
}
|
|
get duration() {
|
|
var _a, _b, _c;
|
|
const durationInMs = ((_b = (_a = this.animation) === null || _a === void 0 ? void 0 : _a.effect) === null || _b === void 0 ? void 0 : _b.getComputedTiming().duration) ||
|
|
((_c = this.options) === null || _c === void 0 ? void 0 : _c.duration) ||
|
|
300;
|
|
return motionUtils.millisecondsToSeconds(Number(durationInMs));
|
|
}
|
|
get time() {
|
|
var _a;
|
|
if (this.animation) {
|
|
return motionUtils.millisecondsToSeconds(((_a = this.animation) === null || _a === void 0 ? void 0 : _a.currentTime) || 0);
|
|
}
|
|
return 0;
|
|
}
|
|
set time(newTime) {
|
|
if (this.animation) {
|
|
this.animation.currentTime = motionUtils.secondsToMilliseconds(newTime);
|
|
}
|
|
}
|
|
get speed() {
|
|
return this.animation ? this.animation.playbackRate : 1;
|
|
}
|
|
set speed(newSpeed) {
|
|
if (this.animation) {
|
|
this.animation.playbackRate = newSpeed;
|
|
}
|
|
}
|
|
get state() {
|
|
return this.animation ? this.animation.playState : "finished";
|
|
}
|
|
get startTime() {
|
|
return this.animation ? this.animation.startTime : null;
|
|
}
|
|
get finished() {
|
|
return this.animation ? this.animation.finished : Promise.resolve();
|
|
}
|
|
play() {
|
|
this.animation && this.animation.play();
|
|
}
|
|
pause() {
|
|
this.animation && this.animation.pause();
|
|
}
|
|
stop() {
|
|
if (!this.animation ||
|
|
this.state === "idle" ||
|
|
this.state === "finished") {
|
|
return;
|
|
}
|
|
if (this.animation.commitStyles) {
|
|
this.animation.commitStyles();
|
|
}
|
|
this.cancel();
|
|
}
|
|
flatten() {
|
|
var _a;
|
|
if (!this.animation)
|
|
return;
|
|
(_a = this.animation.effect) === null || _a === void 0 ? void 0 : _a.updateTiming({ easing: "linear" });
|
|
}
|
|
attachTimeline(timeline) {
|
|
if (this.animation)
|
|
attachTimeline(this.animation, timeline);
|
|
return motionUtils.noop;
|
|
}
|
|
complete() {
|
|
this.animation && this.animation.finish();
|
|
}
|
|
cancel() {
|
|
try {
|
|
this.animation && this.animation.cancel();
|
|
}
|
|
catch (e) { }
|
|
}
|
|
}
|
|
|
|
const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
|
|
|
|
/**
|
|
* Add the ability for test suites to manually set support flags
|
|
* to better test more environments.
|
|
*/
|
|
const supportsFlags = {
|
|
linearEasing: undefined,
|
|
};
|
|
|
|
function memoSupports(callback, supportsFlag) {
|
|
const memoized = motionUtils.memo(callback);
|
|
return () => { var _a; return (_a = supportsFlags[supportsFlag]) !== null && _a !== void 0 ? _a : memoized(); };
|
|
}
|
|
|
|
const supportsLinearEasing = /*@__PURE__*/ memoSupports(() => {
|
|
try {
|
|
document
|
|
.createElement("div")
|
|
.animate({ opacity: 0 }, { easing: "linear(0, 1)" });
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}, "linearEasing");
|
|
|
|
const generateLinearEasing = (easing, duration, // as milliseconds
|
|
resolution = 10 // as milliseconds
|
|
) => {
|
|
let points = "";
|
|
const numPoints = Math.max(Math.round(duration / resolution), 2);
|
|
for (let i = 0; i < numPoints; i++) {
|
|
points += easing(motionUtils.progress(0, numPoints - 1, i)) + ", ";
|
|
}
|
|
return `linear(${points.substring(0, points.length - 2)})`;
|
|
};
|
|
|
|
function isWaapiSupportedEasing(easing) {
|
|
return Boolean((typeof easing === "function" && supportsLinearEasing()) ||
|
|
!easing ||
|
|
(typeof easing === "string" &&
|
|
(easing in supportedWaapiEasing || supportsLinearEasing())) ||
|
|
isBezierDefinition(easing) ||
|
|
(Array.isArray(easing) && easing.every(isWaapiSupportedEasing)));
|
|
}
|
|
const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
|
|
const supportedWaapiEasing = {
|
|
linear: "linear",
|
|
ease: "ease",
|
|
easeIn: "ease-in",
|
|
easeOut: "ease-out",
|
|
easeInOut: "ease-in-out",
|
|
circIn: /*@__PURE__*/ cubicBezierAsString([0, 0.65, 0.55, 1]),
|
|
circOut: /*@__PURE__*/ cubicBezierAsString([0.55, 0, 1, 0.45]),
|
|
backIn: /*@__PURE__*/ cubicBezierAsString([0.31, 0.01, 0.66, -0.59]),
|
|
backOut: /*@__PURE__*/ cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
|
|
};
|
|
function mapEasingToNativeEasing(easing, duration) {
|
|
if (!easing) {
|
|
return undefined;
|
|
}
|
|
else if (typeof easing === "function" && supportsLinearEasing()) {
|
|
return generateLinearEasing(easing, duration);
|
|
}
|
|
else if (isBezierDefinition(easing)) {
|
|
return cubicBezierAsString(easing);
|
|
}
|
|
else if (Array.isArray(easing)) {
|
|
return easing.map((segmentEasing) => mapEasingToNativeEasing(segmentEasing, duration) ||
|
|
supportedWaapiEasing.easeOut);
|
|
}
|
|
else {
|
|
return supportedWaapiEasing[easing];
|
|
}
|
|
}
|
|
|
|
const isDragging = {
|
|
x: false,
|
|
y: false,
|
|
};
|
|
function isDragActive() {
|
|
return isDragging.x || isDragging.y;
|
|
}
|
|
|
|
function resolveElements(elementOrSelector, scope, selectorCache) {
|
|
var _a;
|
|
if (elementOrSelector instanceof Element) {
|
|
return [elementOrSelector];
|
|
}
|
|
else if (typeof elementOrSelector === "string") {
|
|
let root = document;
|
|
if (scope) {
|
|
// TODO: Refactor to utils package
|
|
// invariant(
|
|
// Boolean(scope.current),
|
|
// "Scope provided, but no element detected."
|
|
// )
|
|
root = scope.current;
|
|
}
|
|
const elements = (_a = selectorCache === null || selectorCache === void 0 ? void 0 : selectorCache[elementOrSelector]) !== null && _a !== void 0 ? _a : root.querySelectorAll(elementOrSelector);
|
|
return elements ? Array.from(elements) : [];
|
|
}
|
|
return Array.from(elementOrSelector);
|
|
}
|
|
|
|
function setupGesture(elementOrSelector, options) {
|
|
const elements = resolveElements(elementOrSelector);
|
|
const gestureAbortController = new AbortController();
|
|
const eventOptions = {
|
|
passive: true,
|
|
...options,
|
|
signal: gestureAbortController.signal,
|
|
};
|
|
const cancel = () => gestureAbortController.abort();
|
|
return [elements, eventOptions, cancel];
|
|
}
|
|
|
|
/**
|
|
* Filter out events that are not pointer events, or are triggering
|
|
* while a Motion gesture is active.
|
|
*/
|
|
function filterEvents$1(callback) {
|
|
return (event) => {
|
|
if (event.pointerType === "touch" || isDragActive())
|
|
return;
|
|
callback(event);
|
|
};
|
|
}
|
|
/**
|
|
* Create a hover gesture. hover() is different to .addEventListener("pointerenter")
|
|
* in that it has an easier syntax, filters out polyfilled touch events, interoperates
|
|
* with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
|
|
*
|
|
* @public
|
|
*/
|
|
function hover(elementOrSelector, onHoverStart, options = {}) {
|
|
const [elements, eventOptions, cancel] = setupGesture(elementOrSelector, options);
|
|
const onPointerEnter = filterEvents$1((enterEvent) => {
|
|
const { target } = enterEvent;
|
|
const onHoverEnd = onHoverStart(enterEvent);
|
|
if (typeof onHoverEnd !== "function" || !target)
|
|
return;
|
|
const onPointerLeave = filterEvents$1((leaveEvent) => {
|
|
onHoverEnd(leaveEvent);
|
|
target.removeEventListener("pointerleave", onPointerLeave);
|
|
});
|
|
target.addEventListener("pointerleave", onPointerLeave, eventOptions);
|
|
});
|
|
elements.forEach((element) => {
|
|
element.addEventListener("pointerenter", onPointerEnter, eventOptions);
|
|
});
|
|
return cancel;
|
|
}
|
|
|
|
/**
|
|
* Recursively traverse up the tree to check whether the provided child node
|
|
* is the parent or a descendant of it.
|
|
*
|
|
* @param parent - Element to find
|
|
* @param child - Element to test against parent
|
|
*/
|
|
const isNodeOrChild = (parent, child) => {
|
|
if (!child) {
|
|
return false;
|
|
}
|
|
else if (parent === child) {
|
|
return true;
|
|
}
|
|
else {
|
|
return isNodeOrChild(parent, child.parentElement);
|
|
}
|
|
};
|
|
|
|
const isPrimaryPointer = (event) => {
|
|
if (event.pointerType === "mouse") {
|
|
return typeof event.button !== "number" || event.button <= 0;
|
|
}
|
|
else {
|
|
/**
|
|
* isPrimary is true for all mice buttons, whereas every touch point
|
|
* is regarded as its own input. So subsequent concurrent touch points
|
|
* will be false.
|
|
*
|
|
* Specifically match against false here as incomplete versions of
|
|
* PointerEvents in very old browser might have it set as undefined.
|
|
*/
|
|
return event.isPrimary !== false;
|
|
}
|
|
};
|
|
|
|
const focusableElements = new Set([
|
|
"BUTTON",
|
|
"INPUT",
|
|
"SELECT",
|
|
"TEXTAREA",
|
|
"A",
|
|
]);
|
|
function isElementKeyboardAccessible(element) {
|
|
return (focusableElements.has(element.tagName) ||
|
|
element.tabIndex !== -1);
|
|
}
|
|
|
|
const isPressing = new WeakSet();
|
|
|
|
/**
|
|
* Filter out events that are not "Enter" keys.
|
|
*/
|
|
function filterEvents(callback) {
|
|
return (event) => {
|
|
if (event.key !== "Enter")
|
|
return;
|
|
callback(event);
|
|
};
|
|
}
|
|
function firePointerEvent(target, type) {
|
|
target.dispatchEvent(new PointerEvent("pointer" + type, { isPrimary: true, bubbles: true }));
|
|
}
|
|
const enableKeyboardPress = (focusEvent, eventOptions) => {
|
|
const element = focusEvent.currentTarget;
|
|
if (!element)
|
|
return;
|
|
const handleKeydown = filterEvents(() => {
|
|
if (isPressing.has(element))
|
|
return;
|
|
firePointerEvent(element, "down");
|
|
const handleKeyup = filterEvents(() => {
|
|
firePointerEvent(element, "up");
|
|
});
|
|
const handleBlur = () => firePointerEvent(element, "cancel");
|
|
element.addEventListener("keyup", handleKeyup, eventOptions);
|
|
element.addEventListener("blur", handleBlur, eventOptions);
|
|
});
|
|
element.addEventListener("keydown", handleKeydown, eventOptions);
|
|
/**
|
|
* Add an event listener that fires on blur to remove the keydown events.
|
|
*/
|
|
element.addEventListener("blur", () => element.removeEventListener("keydown", handleKeydown), eventOptions);
|
|
};
|
|
|
|
/**
|
|
* Filter out events that are not primary pointer events, or are triggering
|
|
* while a Motion gesture is active.
|
|
*/
|
|
function isValidPressEvent(event) {
|
|
return isPrimaryPointer(event) && !isDragActive();
|
|
}
|
|
/**
|
|
* Create a press gesture.
|
|
*
|
|
* Press is different to `"pointerdown"`, `"pointerup"` in that it
|
|
* automatically filters out secondary pointer events like right
|
|
* click and multitouch.
|
|
*
|
|
* It also adds accessibility support for keyboards, where
|
|
* an element with a press gesture will receive focus and
|
|
* trigger on Enter `"keydown"` and `"keyup"` events.
|
|
*
|
|
* This is different to a browser's `"click"` event, which does
|
|
* respond to keyboards but only for the `"click"` itself, rather
|
|
* than the press start and end/cancel. The element also needs
|
|
* to be focusable for this to work, whereas a press gesture will
|
|
* make an element focusable by default.
|
|
*
|
|
* @public
|
|
*/
|
|
function press(elementOrSelector, onPressStart, options = {}) {
|
|
const [elements, eventOptions, cancelEvents] = setupGesture(elementOrSelector, options);
|
|
const startPress = (startEvent) => {
|
|
const element = startEvent.currentTarget;
|
|
if (!isValidPressEvent(startEvent) || isPressing.has(element))
|
|
return;
|
|
isPressing.add(element);
|
|
const onPressEnd = onPressStart(startEvent);
|
|
const onPointerEnd = (endEvent, success) => {
|
|
window.removeEventListener("pointerup", onPointerUp);
|
|
window.removeEventListener("pointercancel", onPointerCancel);
|
|
if (!isValidPressEvent(endEvent) || !isPressing.has(element)) {
|
|
return;
|
|
}
|
|
isPressing.delete(element);
|
|
if (typeof onPressEnd === "function") {
|
|
onPressEnd(endEvent, { success });
|
|
}
|
|
};
|
|
const onPointerUp = (upEvent) => {
|
|
onPointerEnd(upEvent, options.useGlobalTarget ||
|
|
isNodeOrChild(element, upEvent.target));
|
|
};
|
|
const onPointerCancel = (cancelEvent) => {
|
|
onPointerEnd(cancelEvent, false);
|
|
};
|
|
window.addEventListener("pointerup", onPointerUp, eventOptions);
|
|
window.addEventListener("pointercancel", onPointerCancel, eventOptions);
|
|
};
|
|
elements.forEach((element) => {
|
|
if (!isElementKeyboardAccessible(element) &&
|
|
element.getAttribute("tabindex") === null) {
|
|
element.tabIndex = 0;
|
|
}
|
|
const target = options.useGlobalTarget ? window : element;
|
|
target.addEventListener("pointerdown", startPress, eventOptions);
|
|
element.addEventListener("focus", (event) => enableKeyboardPress(event, eventOptions), eventOptions);
|
|
});
|
|
return cancelEvents;
|
|
}
|
|
|
|
const defaultEasing = "easeOut";
|
|
function applyGeneratorOptions(options) {
|
|
var _a;
|
|
if (isGenerator(options.type)) {
|
|
const generatorOptions = createGeneratorEasing(options, 100, options.type);
|
|
options.ease = supportsLinearEasing()
|
|
? generatorOptions.ease
|
|
: defaultEasing;
|
|
options.duration = motionUtils.secondsToMilliseconds(generatorOptions.duration);
|
|
options.type = "keyframes";
|
|
}
|
|
else {
|
|
options.duration = motionUtils.secondsToMilliseconds((_a = options.duration) !== null && _a !== void 0 ? _a : 0.3);
|
|
options.ease = options.ease || defaultEasing;
|
|
}
|
|
}
|
|
// TODO: Reuse for NativeAnimation
|
|
function convertMotionOptionsToNative(valueName, keyframes, options) {
|
|
var _a;
|
|
const nativeKeyframes = {};
|
|
const nativeOptions = {
|
|
fill: "both",
|
|
easing: "linear",
|
|
composite: "replace",
|
|
};
|
|
nativeOptions.delay = motionUtils.secondsToMilliseconds((_a = options.delay) !== null && _a !== void 0 ? _a : 0);
|
|
applyGeneratorOptions(options);
|
|
nativeOptions.duration = options.duration;
|
|
const { ease, times } = options;
|
|
if (times)
|
|
nativeKeyframes.offset = times;
|
|
nativeKeyframes[valueName] = keyframes;
|
|
const easing = mapEasingToNativeEasing(ease, options.duration);
|
|
/**
|
|
* If this is an easing array, apply to keyframes, not animation as a whole
|
|
*/
|
|
if (Array.isArray(easing)) {
|
|
nativeKeyframes.easing = easing;
|
|
}
|
|
else {
|
|
nativeOptions.easing = easing;
|
|
}
|
|
return {
|
|
keyframes: nativeKeyframes,
|
|
options: nativeOptions,
|
|
};
|
|
}
|
|
|
|
class PseudoAnimation extends NativeAnimationControls {
|
|
constructor(target, pseudoElement, valueName, keyframes, options) {
|
|
const animationOptions = convertMotionOptionsToNative(valueName, keyframes, options);
|
|
const animation = target.animate(animationOptions.keyframes, {
|
|
pseudoElement,
|
|
...animationOptions.options,
|
|
});
|
|
super(animation);
|
|
}
|
|
}
|
|
|
|
function chooseLayerType(valueName) {
|
|
if (valueName === "layout")
|
|
return "group";
|
|
if (valueName === "enter" || valueName === "new")
|
|
return "new";
|
|
if (valueName === "exit" || valueName === "old")
|
|
return "old";
|
|
return "group";
|
|
}
|
|
|
|
let pendingRules = {};
|
|
let style = null;
|
|
const css = {
|
|
set: (selector, values) => {
|
|
pendingRules[selector] = values;
|
|
},
|
|
commit: () => {
|
|
if (!style) {
|
|
style = document.createElement("style");
|
|
style.id = "motion-view";
|
|
}
|
|
let cssText = "";
|
|
for (const selector in pendingRules) {
|
|
const rule = pendingRules[selector];
|
|
cssText += `${selector} {\n`;
|
|
for (const [property, value] of Object.entries(rule)) {
|
|
cssText += ` ${property}: ${value};\n`;
|
|
}
|
|
cssText += "}\n";
|
|
}
|
|
style.textContent = cssText;
|
|
document.head.appendChild(style);
|
|
pendingRules = {};
|
|
},
|
|
remove: () => {
|
|
if (style && style.parentElement) {
|
|
style.parentElement.removeChild(style);
|
|
}
|
|
},
|
|
};
|
|
|
|
function getLayerName(pseudoElement) {
|
|
const match = pseudoElement.match(/::view-transition-(old|new|group|image-pair)\((.*?)\)/);
|
|
if (!match)
|
|
return null;
|
|
return { layer: match[2], type: match[1] };
|
|
}
|
|
|
|
function filterViewAnimations(animation) {
|
|
var _a;
|
|
const { effect } = animation;
|
|
if (!effect)
|
|
return false;
|
|
return (effect.target === document.documentElement &&
|
|
((_a = effect.pseudoElement) === null || _a === void 0 ? void 0 : _a.startsWith("::view-transition")));
|
|
}
|
|
function getViewAnimations() {
|
|
return document.getAnimations().filter(filterViewAnimations);
|
|
}
|
|
|
|
function hasTarget(target, targets) {
|
|
return targets.has(target) && Object.keys(targets.get(target)).length > 0;
|
|
}
|
|
|
|
const definitionNames = ["layout", "enter", "exit", "new", "old"];
|
|
function startViewAnimation(update, defaultOptions, targets) {
|
|
if (!document.startViewTransition) {
|
|
return new Promise(async (resolve) => {
|
|
await update();
|
|
resolve(new BaseGroupPlaybackControls([]));
|
|
});
|
|
}
|
|
// TODO: Go over existing targets and ensure they all have ids
|
|
/**
|
|
* If we don't have any animations defined for the root target,
|
|
* remove it from being captured.
|
|
*/
|
|
if (!hasTarget("root", targets)) {
|
|
css.set(":root", {
|
|
"view-transition-name": "none",
|
|
});
|
|
}
|
|
/**
|
|
* Set the timing curve to linear for all view transition layers.
|
|
* This gets baked into the keyframes, which can't be changed
|
|
* without breaking the generated animation.
|
|
*
|
|
* This allows us to set easing via updateTiming - which can be changed.
|
|
*/
|
|
css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
|
|
css.commit(); // Write
|
|
const transition = document.startViewTransition(async () => {
|
|
await update();
|
|
// TODO: Go over new targets and ensure they all have ids
|
|
});
|
|
transition.finished.finally(() => {
|
|
css.remove(); // Write
|
|
});
|
|
return new Promise((resolve) => {
|
|
transition.ready.then(() => {
|
|
var _a;
|
|
const generatedViewAnimations = getViewAnimations();
|
|
const animations = [];
|
|
/**
|
|
* Create animations for our definitions
|
|
*/
|
|
targets.forEach((definition, target) => {
|
|
// TODO: If target is not "root", resolve elements
|
|
// and iterate over each
|
|
for (const key of definitionNames) {
|
|
if (!definition[key])
|
|
continue;
|
|
const { keyframes, options } = definition[key];
|
|
for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
|
|
if (!valueKeyframes)
|
|
continue;
|
|
const valueOptions = {
|
|
...getValueTransition(defaultOptions, valueName),
|
|
...getValueTransition(options, valueName),
|
|
};
|
|
const type = chooseLayerType(key);
|
|
/**
|
|
* If this is an opacity animation, and keyframes are not an array,
|
|
* we need to convert them into an array and set an initial value.
|
|
*/
|
|
if (valueName === "opacity" &&
|
|
!Array.isArray(valueKeyframes)) {
|
|
const initialValue = type === "new" ? 0 : 1;
|
|
valueKeyframes = [initialValue, valueKeyframes];
|
|
}
|
|
/**
|
|
* Resolve stagger function if provided.
|
|
*/
|
|
if (typeof valueOptions.delay === "function") {
|
|
valueOptions.delay = valueOptions.delay(0, 1);
|
|
}
|
|
const animation = new PseudoAnimation(document.documentElement, `::view-transition-${type}(${target})`, valueName, valueKeyframes, valueOptions);
|
|
animations.push(animation);
|
|
}
|
|
}
|
|
});
|
|
/**
|
|
* Handle browser generated animations
|
|
*/
|
|
for (const animation of generatedViewAnimations) {
|
|
if (animation.playState === "finished")
|
|
continue;
|
|
const { effect } = animation;
|
|
if (!effect || !(effect instanceof KeyframeEffect))
|
|
continue;
|
|
const { pseudoElement } = effect;
|
|
if (!pseudoElement)
|
|
continue;
|
|
const name = getLayerName(pseudoElement);
|
|
if (!name)
|
|
continue;
|
|
const targetDefinition = targets.get(name.layer);
|
|
if (!targetDefinition) {
|
|
/**
|
|
* If transition name is group then update the timing of the animation
|
|
* whereas if it's old or new then we could possibly replace it using
|
|
* the above method.
|
|
*/
|
|
const transitionName = name.type === "group" ? "layout" : "";
|
|
const animationTransition = {
|
|
...getValueTransition(defaultOptions, transitionName),
|
|
};
|
|
applyGeneratorOptions(animationTransition);
|
|
const easing = mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
|
|
effect.updateTiming({
|
|
delay: motionUtils.secondsToMilliseconds((_a = animationTransition.delay) !== null && _a !== void 0 ? _a : 0),
|
|
duration: animationTransition.duration,
|
|
easing,
|
|
});
|
|
animations.push(new NativeAnimationControls(animation));
|
|
}
|
|
else if (hasOpacity(targetDefinition, "enter") &&
|
|
hasOpacity(targetDefinition, "exit") &&
|
|
effect
|
|
.getKeyframes()
|
|
.some((keyframe) => keyframe.mixBlendMode)) {
|
|
animations.push(new NativeAnimationControls(animation));
|
|
}
|
|
else {
|
|
animation.cancel();
|
|
}
|
|
}
|
|
resolve(new BaseGroupPlaybackControls(animations));
|
|
});
|
|
});
|
|
}
|
|
function hasOpacity(target, key) {
|
|
var _a;
|
|
return (_a = target === null || target === void 0 ? void 0 : target[key]) === null || _a === void 0 ? void 0 : _a.keyframes.opacity;
|
|
}
|
|
|
|
/**
|
|
* TODO:
|
|
* - Create view transition on next tick
|
|
* - Replace animations with Motion animations
|
|
* - Return GroupAnimation on next tick
|
|
*/
|
|
class ViewTransitionBuilder {
|
|
constructor(update, options = {}) {
|
|
this.currentTarget = "root";
|
|
this.targets = new Map();
|
|
this.notifyReady = motionUtils.noop;
|
|
this.readyPromise = new Promise((resolve) => {
|
|
this.notifyReady = resolve;
|
|
});
|
|
queueMicrotask(() => {
|
|
startViewAnimation(update, options, this.targets).then((animation) => this.notifyReady(animation));
|
|
});
|
|
}
|
|
get(selector) {
|
|
this.currentTarget = selector;
|
|
return this;
|
|
}
|
|
layout(keyframes, options) {
|
|
this.updateTarget("layout", keyframes, options);
|
|
return this;
|
|
}
|
|
new(keyframes, options) {
|
|
this.updateTarget("new", keyframes, options);
|
|
return this;
|
|
}
|
|
old(keyframes, options) {
|
|
this.updateTarget("old", keyframes, options);
|
|
return this;
|
|
}
|
|
enter(keyframes, options) {
|
|
this.updateTarget("enter", keyframes, options);
|
|
return this;
|
|
}
|
|
exit(keyframes, options) {
|
|
this.updateTarget("exit", keyframes, options);
|
|
return this;
|
|
}
|
|
crossfade(options) {
|
|
this.updateTarget("enter", { opacity: 1 }, options);
|
|
this.updateTarget("exit", { opacity: 0 }, options);
|
|
return this;
|
|
}
|
|
updateTarget(target, keyframes, options = {}) {
|
|
const { currentTarget, targets } = this;
|
|
if (!targets.has(currentTarget)) {
|
|
targets.set(currentTarget, {});
|
|
}
|
|
const targetData = targets.get(currentTarget);
|
|
targetData[target] = { keyframes, options };
|
|
}
|
|
then(resolve, reject) {
|
|
return this.readyPromise.then(resolve, reject);
|
|
}
|
|
}
|
|
function view(update, defaultOptions = {}) {
|
|
return new ViewTransitionBuilder(update, defaultOptions);
|
|
}
|
|
|
|
function setDragLock(axis) {
|
|
if (axis === "x" || axis === "y") {
|
|
if (isDragging[axis]) {
|
|
return null;
|
|
}
|
|
else {
|
|
isDragging[axis] = true;
|
|
return () => {
|
|
isDragging[axis] = false;
|
|
};
|
|
}
|
|
}
|
|
else {
|
|
if (isDragging.x || isDragging.y) {
|
|
return null;
|
|
}
|
|
else {
|
|
isDragging.x = isDragging.y = true;
|
|
return () => {
|
|
isDragging.x = isDragging.y = false;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
exports.GroupPlaybackControls = GroupPlaybackControls;
|
|
exports.NativeAnimationControls = NativeAnimationControls;
|
|
exports.ViewTransitionBuilder = ViewTransitionBuilder;
|
|
exports.attachTimeline = attachTimeline;
|
|
exports.calcGeneratorDuration = calcGeneratorDuration;
|
|
exports.createGeneratorEasing = createGeneratorEasing;
|
|
exports.cubicBezierAsString = cubicBezierAsString;
|
|
exports.generateLinearEasing = generateLinearEasing;
|
|
exports.getValueTransition = getValueTransition;
|
|
exports.hover = hover;
|
|
exports.isBezierDefinition = isBezierDefinition;
|
|
exports.isDragActive = isDragActive;
|
|
exports.isDragging = isDragging;
|
|
exports.isGenerator = isGenerator;
|
|
exports.isNodeOrChild = isNodeOrChild;
|
|
exports.isPrimaryPointer = isPrimaryPointer;
|
|
exports.isWaapiSupportedEasing = isWaapiSupportedEasing;
|
|
exports.mapEasingToNativeEasing = mapEasingToNativeEasing;
|
|
exports.maxGeneratorDuration = maxGeneratorDuration;
|
|
exports.press = press;
|
|
exports.resolveElements = resolveElements;
|
|
exports.setDragLock = setDragLock;
|
|
exports.supportedWaapiEasing = supportedWaapiEasing;
|
|
exports.supportsFlags = supportsFlags;
|
|
exports.supportsLinearEasing = supportsLinearEasing;
|
|
exports.supportsScrollTimeline = supportsScrollTimeline;
|
|
exports.view = view;
|