UNPKG

@vtbag/utensil-drawer

Version:

Pull out just what you need to craft seamless transitions. The Utensil Drawer holds reusable functions to help you build websites with view transitions. It is a bit sparse right now, but like the one in your kitchen, it is bound to fill up over time.

128 lines (125 loc) 6.26 kB
import { getTypeAttributes, polyfilledTypes, root } from './polyfilled-types.js'; import { createViewTransitionProxy, } from './switchable-view-transition.js'; import { createViewTransitionSurrogate } from './view-transition-surrogate.js'; const collisionBehaviors = ['skipOld', 'chaining', 'chaining-only', 'skipNew', 'never']; const scopes = new WeakMap(); export function getCurrentViewTransition(scope = document) { return getScopeData(scope).currentViewTransition; } function getScopeData(scope = document) { let data = scopes.get(scope); if (!data) { data = { currentViewTransition: undefined, open: undefined, keepLast: false, chained: [], updates: [], }; scopes.set(scope, data); } return data; } const close = (scopeData) => { cancelAnimationFrame(scopeData.open); scopeData.open = undefined; }; let nativeSupport = 'none'; if (document.startViewTransition) { nativeSupport = 'ViewTransitionTypeSet' in window ? 'full' : 'partial'; if (Element.prototype.startViewTransition) { nativeSupport = 'scoped'; } } export function nativeViewTransitionSupport() { return nativeSupport; } /* One version of startViewTransition() for all browsers with or without native support. Without native support or with "respectReducedMotion" and reduced motion the function behaves as if every transition were skipped. Collision behavior: By default ("skipOld") behaves like the API function where new invocations skip active ones. This can be set to "skipNew", where new invocations are skipped if another one is still active. 'never' will behave as if view transitions are not supported at all. With "chaining" the update functions of concurrent calls will be combined to one view transition (all calls that arrive before the old state is captured) or in followup transitions for all new calls arriving later. You can suppress combining calls that arrive before the old state is captured by setting "collisionBehavior" to "chaining-only". You can crank up the speed of running animations by setting "speedUpWhenChained" to > 1. */ export function mayStartViewTransition(param, ext = {}) { let { scope = document, collisionBehavior = 'skipOld', speedUpWhenChained = 1, respectReducedMotion = true, useTypesPolyfill = 'never', catchErrors = true, } = ext; if (!('documentElement' in scope) && nativeSupport !== 'scoped') { console.warn(`Using a scope other than document is only supported in browsers with scoped view transitions`); scope = document; collisionBehavior = 'never'; } const scopeData = getScopeData(scope); collisionBehaviors.includes(collisionBehavior) || (console.warn(`Invalid collisionBehavior "${collisionBehavior}" specified, using "skipOld" instead`), (collisionBehavior = 'skipOld')); const extensions = { collisionBehavior, speedUpWhenChained, respectReducedMotion, useTypesPolyfill, }; const update = (param instanceof Function ? param : param?.update) ?? (() => { }); const types = new Set(param instanceof Function ? [] : (param?.types ?? [])); const reduceMotion = respectReducedMotion && window.matchMedia('(prefers-reduced-motion: reduce)').matches; let surrogate = false; if ((collisionBehavior === 'skipNew' && scopeData.currentViewTransition) || collisionBehavior === 'never' || nativeSupport === 'none' || reduceMotion) { surrogate = true; } if (!scopeData.currentViewTransition || collisionBehavior === 'skipOld') { scopeData.keepLast = !!scopeData.currentViewTransition && !!scopeData.open; scopeData.open = requestAnimationFrame(() => close(scopeData)); scopeData.currentViewTransition = polyfilledTypes(scope, (scopeData.currentViewTransition = surrogate ? createViewTransitionSurrogate(unchainUpdates) : scope.startViewTransition(unchainUpdates)), useTypesPolyfill === 'always' || (useTypesPolyfill !== 'never' && nativeSupport === 'partial')); if (catchErrors) { const error = (e) => catchErrors !== 'suppress' && (console.error(e), undefined); scopeData.currentViewTransition.updateCallbackDone.catch(error); scopeData.currentViewTransition.ready.catch(error); } scopeData.currentViewTransition.finished.finally(() => { getTypeAttributes()?.forEach((t) => root(scope).classList.remove(t)); scopeData.currentViewTransition = undefined; scopeData.chained .splice(0, scopeData.chained.length) .forEach(({ update, extensions, proxy }) => { mayStartViewTransition({ update, types: [...proxy.types] }, extensions); proxy.switch(); }); }); } if (scopeData.open && (collisionBehavior !== 'chaining-only' || scopeData.updates.length === 0)) { types.forEach((t) => scopeData.currentViewTransition.types?.add(t)); scopeData.updates.push(update); (scope.ownerDocument ? scope : scope.documentElement).vtbagFlushUpdates = flushUpdates; return scopeData.currentViewTransition; } const proxy = createViewTransitionProxy(types); scopeData.chained.push({ update, extensions, proxy }); if (extensions.speedUpWhenChained !== 1) { root(scope) .getAnimations() .forEach((a) => { a.effect?.pseudoElement?.startsWith('::view-transition') && (a.playbackRate *= extensions.speedUpWhenChained); }); } (scope.ownerDocument ? scope : scope.documentElement).vtbagFlushUpdates = flushUpdates; return proxy; async function unchainUpdates() { const current = scopeData.updates.splice(0, scopeData.updates.length - (scopeData.keepLast ? 1 : 0)); scopeData.keepLast = false; const rejected = (await Promise.allSettled(current.map((u) => u()))).find((r) => r.status === 'rejected'); if (rejected) throw new Error(rejected.reason); } function flushUpdates() { scopeData.updates.length = 0; } }