this repo has no description
at main 461 lines 9.9 kB view raw
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 */