this repo has no description
1import { identity as linear, is_function, noop, run_all } from './utils.js';
2import { now } from './environment.js';
3import { loop } from './loop.js';
4import { create_rule, delete_rule } from './style_manager.js';
5import { custom_event } from './dom.js';
6import { add_render_callback } from './scheduler.js';
7
8/**
9 * @type {Promise<void> | null}
10 */
11let promise;
12
13/**
14 * @returns {Promise<void>}
15 */
16function wait() {
17 if (!promise) {
18 promise = Promise.resolve();
19 promise.then(() => {
20 promise = null;
21 });
22 }
23 return promise;
24}
25
26/**
27 * @param {Element} node
28 * @param {INTRO | OUTRO | boolean} direction
29 * @param {'start' | 'end'} kind
30 * @returns {void}
31 */
32function dispatch(node, direction, kind) {
33 node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`));
34}
35
36const outroing = new Set();
37
38/**
39 * @type {Outro}
40 */
41let outros;
42
43/**
44 * @returns {void} */
45export function group_outros() {
46 outros = {
47 r: 0,
48 c: [],
49 p: outros // parent group
50 };
51}
52
53/**
54 * @returns {void} */
55export function check_outros() {
56 if (!outros.r) {
57 run_all(outros.c);
58 }
59 outros = outros.p;
60}
61
62/**
63 * @param {import('./private.js').Fragment} block
64 * @param {0 | 1} [local]
65 * @returns {void}
66 */
67export function transition_in(block, local) {
68 if (block && block.i) {
69 outroing.delete(block);
70 block.i(local);
71 }
72}
73
74/**
75 * @param {import('./private.js').Fragment} block
76 * @param {0 | 1} local
77 * @param {0 | 1} [detach]
78 * @param {() => void} [callback]
79 * @returns {void}
80 */
81export function transition_out(block, local, detach, callback) {
82 if (block && block.o) {
83 if (outroing.has(block)) return;
84 outroing.add(block);
85 outros.c.push(() => {
86 outroing.delete(block);
87 if (callback) {
88 if (detach) block.d(1);
89 callback();
90 }
91 });
92 block.o(local);
93 } else if (callback) {
94 callback();
95 }
96}
97
98/**
99 * @type {import('../transition/public.js').TransitionConfig}
100 */
101const null_transition = { duration: 0 };
102
103/**
104 * @param {Element & ElementCSSInlineStyle} node
105 * @param {TransitionFn} fn
106 * @param {any} params
107 * @returns {{ start(): void; invalidate(): void; end(): void; }}
108 */
109export function create_in_transition(node, fn, params) {
110 /**
111 * @type {TransitionOptions} */
112 const options = { direction: 'in' };
113 let config = fn(node, params, options);
114 let running = false;
115 let animation_name;
116 let task;
117 let uid = 0;
118
119 /**
120 * @returns {void} */
121 function cleanup() {
122 if (animation_name) delete_rule(node, animation_name);
123 }
124
125 /**
126 * @returns {void} */
127 function go() {
128 const {
129 delay = 0,
130 duration = 300,
131 easing = linear,
132 tick = noop,
133 css
134 } = config || null_transition;
135 if (css) animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++);
136 tick(0, 1);
137 const start_time = now() + delay;
138 const end_time = start_time + duration;
139 if (task) task.abort();
140 running = true;
141 add_render_callback(() => dispatch(node, true, 'start'));
142 task = loop((now) => {
143 if (running) {
144 if (now >= end_time) {
145 tick(1, 0);
146 dispatch(node, true, 'end');
147 cleanup();
148 return (running = false);
149 }
150 if (now >= start_time) {
151 const t = easing((now - start_time) / duration);
152 tick(t, 1 - t);
153 }
154 }
155 return running;
156 });
157 }
158 let started = false;
159 return {
160 start() {
161 if (started) return;
162 started = true;
163 delete_rule(node);
164 if (is_function(config)) {
165 config = config(options);
166 wait().then(go);
167 } else {
168 go();
169 }
170 },
171 invalidate() {
172 started = false;
173 },
174 end() {
175 if (running) {
176 cleanup();
177 running = false;
178 }
179 }
180 };
181}
182
183/**
184 * @param {Element & ElementCSSInlineStyle} node
185 * @param {TransitionFn} fn
186 * @param {any} params
187 * @returns {{ end(reset: any): void; }}
188 */
189export function create_out_transition(node, fn, params) {
190 /** @type {TransitionOptions} */
191 const options = { direction: 'out' };
192 let config = fn(node, params, options);
193 let running = true;
194 let animation_name;
195 const group = outros;
196 group.r += 1;
197 /** @type {boolean} */
198 let original_inert_value;
199
200 /**
201 * @returns {void} */
202 function go() {
203 const {
204 delay = 0,
205 duration = 300,
206 easing = linear,
207 tick = noop,
208 css
209 } = config || null_transition;
210
211 if (css) animation_name = create_rule(node, 1, 0, duration, delay, easing, css);
212
213 const start_time = now() + delay;
214 const end_time = start_time + duration;
215 add_render_callback(() => dispatch(node, false, 'start'));
216
217 if ('inert' in node) {
218 original_inert_value = /** @type {HTMLElement} */ (node).inert;
219 node.inert = true;
220 }
221
222 loop((now) => {
223 if (running) {
224 if (now >= end_time) {
225 tick(0, 1);
226 dispatch(node, false, 'end');
227 if (!--group.r) {
228 // this will result in `end()` being called,
229 // so we don't need to clean up here
230 run_all(group.c);
231 }
232 return false;
233 }
234 if (now >= start_time) {
235 const t = easing((now - start_time) / duration);
236 tick(1 - t, t);
237 }
238 }
239 return running;
240 });
241 }
242
243 if (is_function(config)) {
244 wait().then(() => {
245 // @ts-ignore
246 config = config(options);
247 go();
248 });
249 } else {
250 go();
251 }
252
253 return {
254 end(reset) {
255 if (reset && 'inert' in node) {
256 node.inert = original_inert_value;
257 }
258 if (reset && config.tick) {
259 config.tick(1, 0);
260 }
261 if (running) {
262 if (animation_name) delete_rule(node, animation_name);
263 running = false;
264 }
265 }
266 };
267}
268
269/**
270 * @param {Element & ElementCSSInlineStyle} node
271 * @param {TransitionFn} fn
272 * @param {any} params
273 * @param {boolean} intro
274 * @returns {{ run(b: 0 | 1): void; end(): void; }}
275 */
276export function create_bidirectional_transition(node, fn, params, intro) {
277 /**
278 * @type {TransitionOptions} */
279 const options = { direction: 'both' };
280 let config = fn(node, params, options);
281 let t = intro ? 0 : 1;
282
283 /**
284 * @type {Program | null} */
285 let running_program = null;
286
287 /**
288 * @type {PendingProgram | null} */
289 let pending_program = null;
290 let animation_name = null;
291
292 /** @type {boolean} */
293 let original_inert_value;
294
295 /**
296 * @returns {void} */
297 function clear_animation() {
298 if (animation_name) delete_rule(node, animation_name);
299 }
300
301 /**
302 * @param {PendingProgram} program
303 * @param {number} duration
304 * @returns {Program}
305 */
306 function init(program, duration) {
307 const d = /** @type {Program['d']} */ (program.b - t);
308 duration *= Math.abs(d);
309 return {
310 a: t,
311 b: program.b,
312 d,
313 duration,
314 start: program.start,
315 end: program.start + duration,
316 group: program.group
317 };
318 }
319
320 /**
321 * @param {INTRO | OUTRO} b
322 * @returns {void}
323 */
324 function go(b) {
325 const {
326 delay = 0,
327 duration = 300,
328 easing = linear,
329 tick = noop,
330 css
331 } = config || null_transition;
332
333 /**
334 * @type {PendingProgram} */
335 const program = {
336 start: now() + delay,
337 b
338 };
339
340 if (!b) {
341 // @ts-ignore todo: improve typings
342 program.group = outros;
343 outros.r += 1;
344 }
345
346 if ('inert' in node) {
347 if (b) {
348 if (original_inert_value !== undefined) {
349 // aborted/reversed outro — restore previous inert value
350 node.inert = original_inert_value;
351 }
352 } else {
353 original_inert_value = /** @type {HTMLElement} */ (node).inert;
354 node.inert = true;
355 }
356 }
357
358 if (running_program || pending_program) {
359 pending_program = program;
360 } else {
361 // if this is an intro, and there's a delay, we need to do
362 // an initial tick and/or apply CSS animation immediately
363 if (css) {
364 clear_animation();
365 animation_name = create_rule(node, t, b, duration, delay, easing, css);
366 }
367 if (b) tick(0, 1);
368 running_program = init(program, duration);
369 add_render_callback(() => dispatch(node, b, 'start'));
370 loop((now) => {
371 if (pending_program && now > pending_program.start) {
372 running_program = init(pending_program, duration);
373 pending_program = null;
374 dispatch(node, running_program.b, 'start');
375 if (css) {
376 clear_animation();
377 animation_name = create_rule(
378 node,
379 t,
380 running_program.b,
381 running_program.duration,
382 0,
383 easing,
384 config.css
385 );
386 }
387 }
388 if (running_program) {
389 if (now >= running_program.end) {
390 tick((t = running_program.b), 1 - t);
391 dispatch(node, running_program.b, 'end');
392 if (!pending_program) {
393 // we're done
394 if (running_program.b) {
395 // intro — we can tidy up immediately
396 clear_animation();
397 } else {
398 // outro — needs to be coordinated
399 if (!--running_program.group.r) run_all(running_program.group.c);
400 }
401 }
402 running_program = null;
403 } else if (now >= running_program.start) {
404 const p = now - running_program.start;
405 t = running_program.a + running_program.d * easing(p / running_program.duration);
406 tick(t, 1 - t);
407 }
408 }
409 return !!(running_program || pending_program);
410 });
411 }
412 }
413 return {
414 run(b) {
415 if (is_function(config)) {
416 wait().then(() => {
417 const opts = { direction: b ? 'in' : 'out' };
418 // @ts-ignore
419 config = config(opts);
420 go(b);
421 });
422 } else {
423 go(b);
424 }
425 },
426 end() {
427 clear_animation();
428 running_program = pending_program = null;
429 }
430 };
431}
432
433/** @typedef {1} INTRO */
434/** @typedef {0} OUTRO */
435/** @typedef {{ direction: 'in' | 'out' | 'both' }} TransitionOptions */
436/** @typedef {(node: Element, params: any, options: TransitionOptions) => import('../transition/public.js').TransitionConfig} TransitionFn */
437
438/**
439 * @typedef {Object} Outro
440 * @property {number} r
441 * @property {Function[]} c
442 * @property {Object} p
443 */
444
445/**
446 * @typedef {Object} PendingProgram
447 * @property {number} start
448 * @property {INTRO|OUTRO} b
449 * @property {Outro} [group]
450 */
451
452/**
453 * @typedef {Object} Program
454 * @property {number} a
455 * @property {INTRO|OUTRO} b
456 * @property {1|-1} d
457 * @property {number} duration
458 * @property {number} start
459 * @property {number} end
460 * @property {Outro} [group]
461 */