import { identity as linear, is_function, noop, run_all } from './utils.js'; import { now } from './environment.js'; import { loop } from './loop.js'; import { create_rule, delete_rule } from './style_manager.js'; import { custom_event } from './dom.js'; import { add_render_callback } from './scheduler.js'; /** * @type {Promise | null} */ let promise; /** * @returns {Promise} */ function wait() { if (!promise) { promise = Promise.resolve(); promise.then(() => { promise = null; }); } return promise; } /** * @param {Element} node * @param {INTRO | OUTRO | boolean} direction * @param {'start' | 'end'} kind * @returns {void} */ function dispatch(node, direction, kind) { node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`)); } const outroing = new Set(); /** * @type {Outro} */ let outros; /** * @returns {void} */ export function group_outros() { outros = { r: 0, c: [], p: outros // parent group }; } /** * @returns {void} */ export function check_outros() { if (!outros.r) { run_all(outros.c); } outros = outros.p; } /** * @param {import('./private.js').Fragment} block * @param {0 | 1} [local] * @returns {void} */ export function transition_in(block, local) { if (block && block.i) { outroing.delete(block); block.i(local); } } /** * @param {import('./private.js').Fragment} block * @param {0 | 1} local * @param {0 | 1} [detach] * @param {() => void} [callback] * @returns {void} */ export function transition_out(block, local, detach, callback) { if (block && block.o) { if (outroing.has(block)) return; outroing.add(block); outros.c.push(() => { outroing.delete(block); if (callback) { if (detach) block.d(1); callback(); } }); block.o(local); } else if (callback) { callback(); } } /** * @type {import('../transition/public.js').TransitionConfig} */ const null_transition = { duration: 0 }; /** * @param {Element & ElementCSSInlineStyle} node * @param {TransitionFn} fn * @param {any} params * @returns {{ start(): void; invalidate(): void; end(): void; }} */ export function create_in_transition(node, fn, params) { /** * @type {TransitionOptions} */ const options = { direction: 'in' }; let config = fn(node, params, options); let running = false; let animation_name; let task; let uid = 0; /** * @returns {void} */ function cleanup() { if (animation_name) delete_rule(node, animation_name); } /** * @returns {void} */ function go() { const { delay = 0, duration = 300, easing = linear, tick = noop, css } = config || null_transition; if (css) animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++); tick(0, 1); const start_time = now() + delay; const end_time = start_time + duration; if (task) task.abort(); running = true; add_render_callback(() => dispatch(node, true, 'start')); task = loop((now) => { if (running) { if (now >= end_time) { tick(1, 0); dispatch(node, true, 'end'); cleanup(); return (running = false); } if (now >= start_time) { const t = easing((now - start_time) / duration); tick(t, 1 - t); } } return running; }); } let started = false; return { start() { if (started) return; started = true; delete_rule(node); if (is_function(config)) { config = config(options); wait().then(go); } else { go(); } }, invalidate() { started = false; }, end() { if (running) { cleanup(); running = false; } } }; } /** * @param {Element & ElementCSSInlineStyle} node * @param {TransitionFn} fn * @param {any} params * @returns {{ end(reset: any): void; }} */ export function create_out_transition(node, fn, params) { /** @type {TransitionOptions} */ const options = { direction: 'out' }; let config = fn(node, params, options); let running = true; let animation_name; const group = outros; group.r += 1; /** @type {boolean} */ let original_inert_value; /** * @returns {void} */ function go() { const { delay = 0, duration = 300, easing = linear, tick = noop, css } = config || null_transition; if (css) animation_name = create_rule(node, 1, 0, duration, delay, easing, css); const start_time = now() + delay; const end_time = start_time + duration; add_render_callback(() => dispatch(node, false, 'start')); if ('inert' in node) { original_inert_value = /** @type {HTMLElement} */ (node).inert; node.inert = true; } loop((now) => { if (running) { if (now >= end_time) { tick(0, 1); dispatch(node, false, 'end'); if (!--group.r) { // this will result in `end()` being called, // so we don't need to clean up here run_all(group.c); } return false; } if (now >= start_time) { const t = easing((now - start_time) / duration); tick(1 - t, t); } } return running; }); } if (is_function(config)) { wait().then(() => { // @ts-ignore config = config(options); go(); }); } else { go(); } return { end(reset) { if (reset && 'inert' in node) { node.inert = original_inert_value; } if (reset && config.tick) { config.tick(1, 0); } if (running) { if (animation_name) delete_rule(node, animation_name); running = false; } } }; } /** * @param {Element & ElementCSSInlineStyle} node * @param {TransitionFn} fn * @param {any} params * @param {boolean} intro * @returns {{ run(b: 0 | 1): void; end(): void; }} */ export function create_bidirectional_transition(node, fn, params, intro) { /** * @type {TransitionOptions} */ const options = { direction: 'both' }; let config = fn(node, params, options); let t = intro ? 0 : 1; /** * @type {Program | null} */ let running_program = null; /** * @type {PendingProgram | null} */ let pending_program = null; let animation_name = null; /** @type {boolean} */ let original_inert_value; /** * @returns {void} */ function clear_animation() { if (animation_name) delete_rule(node, animation_name); } /** * @param {PendingProgram} program * @param {number} duration * @returns {Program} */ function init(program, duration) { const d = /** @type {Program['d']} */ (program.b - t); duration *= Math.abs(d); return { a: t, b: program.b, d, duration, start: program.start, end: program.start + duration, group: program.group }; } /** * @param {INTRO | OUTRO} b * @returns {void} */ function go(b) { const { delay = 0, duration = 300, easing = linear, tick = noop, css } = config || null_transition; /** * @type {PendingProgram} */ const program = { start: now() + delay, b }; if (!b) { // @ts-ignore todo: improve typings program.group = outros; outros.r += 1; } if ('inert' in node) { if (b) { if (original_inert_value !== undefined) { // aborted/reversed outro — restore previous inert value node.inert = original_inert_value; } } else { original_inert_value = /** @type {HTMLElement} */ (node).inert; node.inert = true; } } if (running_program || pending_program) { pending_program = program; } else { // if this is an intro, and there's a delay, we need to do // an initial tick and/or apply CSS animation immediately if (css) { clear_animation(); animation_name = create_rule(node, t, b, duration, delay, easing, css); } if (b) tick(0, 1); running_program = init(program, duration); add_render_callback(() => dispatch(node, b, 'start')); loop((now) => { if (pending_program && now > pending_program.start) { running_program = init(pending_program, duration); pending_program = null; dispatch(node, running_program.b, 'start'); if (css) { clear_animation(); animation_name = create_rule( node, t, running_program.b, running_program.duration, 0, easing, config.css ); } } if (running_program) { if (now >= running_program.end) { tick((t = running_program.b), 1 - t); dispatch(node, running_program.b, 'end'); if (!pending_program) { // we're done if (running_program.b) { // intro — we can tidy up immediately clear_animation(); } else { // outro — needs to be coordinated if (!--running_program.group.r) run_all(running_program.group.c); } } running_program = null; } else if (now >= running_program.start) { const p = now - running_program.start; t = running_program.a + running_program.d * easing(p / running_program.duration); tick(t, 1 - t); } } return !!(running_program || pending_program); }); } } return { run(b) { if (is_function(config)) { wait().then(() => { const opts = { direction: b ? 'in' : 'out' }; // @ts-ignore config = config(opts); go(b); }); } else { go(b); } }, end() { clear_animation(); running_program = pending_program = null; } }; } /** @typedef {1} INTRO */ /** @typedef {0} OUTRO */ /** @typedef {{ direction: 'in' | 'out' | 'both' }} TransitionOptions */ /** @typedef {(node: Element, params: any, options: TransitionOptions) => import('../transition/public.js').TransitionConfig} TransitionFn */ /** * @typedef {Object} Outro * @property {number} r * @property {Function[]} c * @property {Object} p */ /** * @typedef {Object} PendingProgram * @property {number} start * @property {INTRO|OUTRO} b * @property {Outro} [group] */ /** * @typedef {Object} Program * @property {number} a * @property {INTRO|OUTRO} b * @property {1|-1} d * @property {number} duration * @property {number} start * @property {number} end * @property {Outro} [group] */