Convert ./src/runtime/internal/transitions.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent 00912f2e01
commit ab50e6042e

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