'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;