diff --git a/src/runtime/internal/transitions.ts b/src/runtime/internal/transitions.ts index 12bfc5d33c..c303e819a8 100644 --- a/src/runtime/internal/transitions.ts +++ b/src/runtime/internal/transitions.ts @@ -1,416 +1,419 @@ -import { identity as linear, is_function, noop, run_all } from './utils'; +import { identity as linear, is_function, noop, run_all } from './utils.js'; import { now } from './environment'; import { loop } from './loop'; import { create_rule, delete_rule } from './style_manager'; import { custom_event } from './dom'; import { add_render_callback } from './scheduler'; -import { TransitionConfig } from '../transition'; -import type { Fragment } from './public'; -let promise: Promise | null; -type INTRO = 1; -type OUTRO = 0; -interface Outro { - /** - * remaining outros - */ - r: number; - /** - * callbacks - */ - c: Function[]; - /** - * parent outro - */ - p: Outro; -} +/** + * @type {Promise | null} */ +let promise; +/** + * @returns {Promise} */ function wait() { - if (!promise) { - promise = Promise.resolve(); - promise.then(() => { - promise = null; - }); - } - - return promise; + if (!promise) { + promise = Promise.resolve(); + promise.then(() => { + promise = null; + }); + } + return promise; } -function dispatch(node: Element, direction: INTRO | OUTRO | boolean, kind: 'start' | 'end') { - node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`)); +/** + * @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(); -let outros: Outro; +/** + * @type {Outro} */ +let outros; + +/** + * @returns {void} */ export function group_outros() { - outros = { - r: 0, // remaining outros - c: [], // callbacks - p: outros // parent group - }; + outros = { + r: 0, + c: [], + p: outros // parent group + }; } +/** + * @returns {void} */ export function check_outros() { - if (!outros.r) { - run_all(outros.c); - } - outros = outros.p; + if (!outros.r) { + run_all(outros.c); + } + outros = outros.p; } -export function transition_in(block: Fragment, local?: 0 | 1) { - if (block && block.i) { - outroing.delete(block); - block.i(local); - } +/** + * @param {Fragment} block + * @param {0 | 1} local + * @returns {void} + */ +export function transition_in(block, local) { + if (block && block.i) { + outroing.delete(block); + block.i(local); + } } -export function transition_out(block: Fragment, local: 0 | 1, detach?: 0 | 1, 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(); - } +/** + * @param {Fragment} block + * @param {0 | 1} local + * @param {0 | 1} detach + * @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(); + } } -const null_transition: TransitionConfig = { duration: 0 }; - -type TransitionOptions = { direction: 'in' | 'out' | 'both' }; -type TransitionFn = (node: Element, params: any, options: TransitionOptions) => TransitionConfig; - -export function create_in_transition( - node: Element & ElementCSSInlineStyle, - fn: TransitionFn, - params: any -) { - const options: TransitionOptions = { direction: 'in' }; - let config = fn(node, params, options); - let running = false; - let animation_name; - let task; - let uid = 0; - - function cleanup() { - if (animation_name) delete_rule(node, animation_name); - } - - 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; - } - } - }; +/** + * @type {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; + } + } + }; } -export function create_out_transition( - node: Element & ElementCSSInlineStyle, - fn: TransitionFn, - params: any -) { - const options: TransitionOptions = { direction: 'out' }; - let config = fn(node, params, options); - let running = true; - let animation_name; - - const group = outros; - - group.r += 1; - - 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')); - - 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 && 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 + * @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; + + /** + * @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')); + 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 && config.tick) { + config.tick(1, 0); + } + if (running) { + if (animation_name) + delete_rule(node, animation_name); + running = false; + } + } + }; } -interface PendingProgram { - start: number; - b: INTRO | OUTRO; - group?: Outro; -} -interface Program { - a: number; - b: INTRO | OUTRO; - /** - * direction - */ - d: 1 | -1; - duration: number; - start: number; - end: number; - group?: Outro; +/** + * @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; + + /** + * @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 = (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 (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; + } + }; } -export function create_bidirectional_transition( - node: Element & ElementCSSInlineStyle, - fn: TransitionFn, - params: any, - intro: boolean -) { - const options: TransitionOptions = { direction: 'both' }; - let config = fn(node, params, options); - - let t = intro ? 0 : 1; - - let running_program: Program | null = null; - let pending_program: PendingProgram | null = null; - let animation_name = null; - - function clear_animation() { - if (animation_name) delete_rule(node, animation_name); - } - - function init(program: PendingProgram, duration: number): Program { - const d = (program.b - t) as Program['d']; - duration *= Math.abs(d); - - return { - a: t, - b: program.b, - d, - duration, - start: program.start, - end: program.start + duration, - group: program.group - }; - } - - function go(b: INTRO | OUTRO) { - const { - delay = 0, - duration = 300, - easing = linear, - tick = noop, - css - } = config || null_transition; - const program: PendingProgram = { - start: now() + delay, - b - }; - - if (!b) { - // @ts-ignore todo: improve typings - program.group = outros; - outros.r += 1; - } - - 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: INTRO | OUTRO) { - if (is_function(config)) { - wait().then(() => { - const opts: TransitionOptions = { 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) => TransitionConfig} TransitionFn */ + +/** @typedef {Object} Outro + * @property {number} r + * remaining outros + * @property {Function[]} c + * callbacks + * @property {Object} p + * parent outro + */ +/** @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 + * direction + * @property {number} duration + * @property {number} start + * @property {number} end + * @property {Outro} [group] + */ \ No newline at end of file