replace transition code

pull/10798/head
Rich Harris 2 years ago
parent ffb27f667a
commit edbf57ab36

@ -6,9 +6,13 @@ import {
set_current_hydration_fragment
} from '../hydration.js';
import { remove } from '../reconciler.js';
import { current_block, execute_effect } from '../../runtime.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../elements/transitions.js';
import { current_block } from '../../runtime.js';
import {
destroy_effect,
pause_effect,
render_effect,
resume_effect
} from '../../reactivity/effects.js';
/** @returns {import('#client').IfBlock} */
function create_if_block() {
@ -64,12 +68,60 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
*/
let current_branch_effect = null;
/** @type {import('#client').Effect} */
/** @type {import('#client').Effect | null} */
let consequent_effect;
/** @type {import('#client').Effect} */
/** @type {import('#client').Effect | null} */
let alternate_effect;
function create_consequent_effect() {
return render_effect(
() => {
consequent_fn(anchor_node);
if (mismatch && current_branch_effect === null) {
set_current_hydration_fragment([]);
}
consequent_dom = block.d;
return () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (consequent_dom !== null) {
remove(consequent_dom);
consequent_dom = null;
}
};
},
block,
true
);
}
function create_alternate_effect() {
return render_effect(
() => {
/** @type {((anchor: Node) => void)} */ (alternate_fn)(anchor_node);
if (mismatch && current_branch_effect === null) {
set_current_hydration_fragment([]);
}
alternate_dom = block.d;
return () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (alternate_dom !== null) {
remove(alternate_dom);
alternate_dom = null;
}
};
},
block,
true
);
}
const if_effect = render_effect(() => {
const result = !!condition_fn();
@ -77,114 +129,67 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
block.v = result;
if (has_mounted) {
const consequent_transitions = block.c;
const alternate_transitions = block.a;
if (result) {
if (alternate_transitions === null || alternate_transitions.size === 0) {
execute_effect(alternate_effect);
if (consequent_effect) {
resume_effect(consequent_effect);
} else {
trigger_transitions(alternate_transitions, 'out');
consequent_effect = create_consequent_effect();
}
if (consequent_transitions === null || consequent_transitions.size === 0) {
execute_effect(consequent_effect);
} else {
trigger_transitions(consequent_transitions, 'in');
if (alternate_effect) {
pause_effect(alternate_effect, () => {
alternate_effect = null;
if (alternate_dom) remove(alternate_dom);
});
}
} else {
if (consequent_transitions === null || consequent_transitions.size === 0) {
execute_effect(consequent_effect);
} else {
trigger_transitions(consequent_transitions, 'out');
if (alternate_effect) {
resume_effect(alternate_effect);
} else if (alternate_fn) {
alternate_effect = create_alternate_effect();
}
if (alternate_transitions === null || alternate_transitions.size === 0) {
execute_effect(alternate_effect);
if (consequent_effect) {
pause_effect(consequent_effect, () => {
consequent_effect = null;
if (consequent_dom) remove(consequent_dom);
});
}
}
} else {
if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
if (
!comment_text ||
(comment_text === 'ssr:if:true' && !result) ||
(comment_text === 'ssr:if:false' && result)
) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
mismatch = true;
} else {
trigger_transitions(alternate_transitions, 'in');
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
current_hydration_fragment.shift();
}
}
} else if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
if (
!comment_text ||
(comment_text === 'ssr:if:true' && !result) ||
(comment_text === 'ssr:if:false' && result)
) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
mismatch = true;
} else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
current_hydration_fragment.shift();
if (result) {
consequent_effect ??= create_consequent_effect();
} else if (alternate_fn) {
alternate_effect ??= create_alternate_effect();
}
}
has_mounted = true;
}
// create these here so they have the correct parent/child relationship
consequent_effect ??= render_effect(
(/** @type {any} */ _, /** @type {import('#client').Effect | null} */ consequent_effect) => {
const result = block.v;
if (!result && consequent_dom !== null) {
remove(consequent_dom);
consequent_dom = null;
}
if (result && current_branch_effect !== consequent_effect) {
consequent_fn(anchor_node);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = consequent_effect;
consequent_dom = block.d;
}
block.d = null;
},
block,
true
);
block.ce = consequent_effect;
alternate_effect ??= render_effect(
(/** @type {any} */ _, /** @type {import('#client').Effect | null} */ alternate_effect) => {
const result = block.v;
if (result && alternate_dom !== null) {
remove(alternate_dom);
alternate_dom = null;
}
if (!result && current_branch_effect !== alternate_effect) {
if (alternate_fn !== null) {
alternate_fn(anchor_node);
}
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = alternate_effect;
alternate_dom = block.d;
}
block.d = null;
},
block,
true
);
block.ae = alternate_effect;
}, block);
if_effect.ondestroy = () => {
// TODO make this unnecessary by linking the dom to the effect,
// and removing automatically on teardown
if (consequent_dom !== null) {
remove(consequent_dom);
}
@ -193,8 +198,12 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
remove(alternate_dom);
}
destroy_effect(consequent_effect);
destroy_effect(alternate_effect);
if (consequent_effect) {
destroy_effect(consequent_effect);
}
if (alternate_effect) {
destroy_effect(alternate_effect);
}
};
block.e = if_effect;

@ -1,31 +1,13 @@
import { EACH_IS_ANIMATED, EACH_IS_CONTROLLED } from '../../../../constants.js';
import { run_all } from '../../../common.js';
import {
AWAIT_BLOCK,
DYNAMIC_COMPONENT_BLOCK,
EACH_BLOCK,
EACH_ITEM_BLOCK,
IF_BLOCK,
KEY_BLOCK,
ROOT_BLOCK
} from '../../constants.js';
import { noop } from '../../../common.js';
import { destroy_each_item_block, get_first_element } from '../blocks/each.js';
import { schedule_raf_task } from '../task.js';
import { append_child, empty } from '../operations.js';
import {
destroy_effect,
effect,
managed_effect,
managed_pre_effect
} from '../../reactivity/effects.js';
import {
current_block,
current_effect,
execute_effect,
mark_subtree_inert,
untrack
} from '../../runtime.js';
import { user_effect } from '../../reactivity/effects.js';
import { current_effect, execute_effect, untrack } from '../../runtime.js';
import { raf } from '../../timing.js';
import { loop } from '../../loop.js';
import { run_transitions } from '../../render.js';
const active_tick_animations = new Set();
const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER;
@ -53,7 +35,8 @@ export function transition(dom, get_transition_fn, props, global = false) {
* @returns {void}
*/
export function animate(dom, get_transition_fn, props) {
bind_transition(dom, get_transition_fn, props, 'key', false);
// TODO
// bind_transition(dom, get_transition_fn, props, 'key', false);
}
/**
@ -305,517 +288,152 @@ function handle_raf(time) {
}
}
/**
* @param {{(t: number): number;(t: number): number;(arg0: number): any;}} easing_fn
* @param {((t: number, u: number) => string)} css_fn
* @param {number} duration
* @param {string} direction
* @param {boolean} reverse
*/
function create_keyframes(easing_fn, css_fn, duration, direction, reverse) {
/** @type {Keyframe[]} */
const keyframes = [];
// We need at least two frames
const frame_time = 16.666;
const max_duration = Math.max(duration, frame_time);
// Have a keyframe every fame for 60 FPS
for (let i = 0; i <= max_duration; i += frame_time) {
let time;
if (i + frame_time > max_duration) {
time = 1;
} else if (i === 0) {
time = 0;
} else {
time = i / max_duration;
}
let t = easing_fn(time);
if (reverse) {
t = 1 - t;
}
keyframes.push(css_to_keyframe(css_fn(t, 1 - t)));
}
if (direction === 'out' || reverse) {
keyframes.reverse();
}
return keyframes;
}
/** @param {number} t */
const linear = (t) => t;
/**
* @param {HTMLElement} dom
* @param {() => import('../../types.js').TransitionPayload} init
* @param {'in' | 'out' | 'both' | 'key'} direction
* @param {import('../../types.js').Effect} effect
* @returns {import('../../types.js').Transition}
*/
function create_transition(dom, init, direction, effect) {
let curr_direction = 'in';
/** @type {Array<() => void>} */
let subs = [];
/** @type {null | Animation | TickAnimation} */
let animation = null;
let cancelled = false;
const create_animation = () => {
let payload = /** @type {import('../../types.js').TransitionPayload} */ (transition.p);
if (typeof payload === 'function') {
// @ts-ignore
payload = payload({ direction: curr_direction });
}
if (payload == null) {
return;
}
const duration = payload.duration ?? 300;
const delay = payload.delay ?? 0;
const css_fn = payload.css;
const tick_fn = payload.tick;
const easing_fn = payload.easing || linear;
if (typeof tick_fn === 'function') {
animation = new TickAnimation(tick_fn, duration, delay, direction === 'out');
} else {
const keyframes =
typeof css_fn === 'function'
? create_keyframes(easing_fn, css_fn, duration, direction, false)
: [];
animation = dom.animate(keyframes, {
duration,
endDelay: delay,
delay,
fill: 'both'
});
}
animation.pause();
animation.onfinish = () => {
const is_outro = curr_direction === 'out';
/** @type {Animation | TickAnimation} */ (animation).cancel();
if (is_outro) {
run_all(subs);
subs = [];
}
dispatch_event(dom, is_outro ? 'outroend' : 'introend');
};
};
/** @type {import('../../types.js').Transition} */
const transition = {
e: effect,
i: init,
// payload
p: null,
// finished
/** @param {() => void} fn */
f(fn) {
subs.push(fn);
},
in() {
const needs_reverse = curr_direction !== 'in';
curr_direction = 'in';
if (animation === null || cancelled) {
cancelled = false;
create_animation();
}
if (animation === null) {
transition.x();
} else {
dispatch_event(dom, 'introstart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
}
/** @type {Animation | TickAnimation} */ (animation).play();
}
},
// out
o() {
// @ts-ignore
const has_keyed_transition = dom.__animate;
// If we're outroing an element that has an animation, then we need to fix
// its position to ensure it behaves nicely without causing layout shift.
if (has_keyed_transition) {
const style = getComputedStyle(dom);
const position = style.position;
if (position !== 'absolute' && position !== 'fixed') {
const { width, height } = style;
const a = dom.getBoundingClientRect();
dom.style.position = 'absolute';
dom.style.width = width;
dom.style.height = height;
const b = dom.getBoundingClientRect();
if (a.left !== b.left || a.top !== b.top) {
const translate = `translate(${a.left - b.left}px, ${a.top - b.top}px)`;
const existing_transform = style.transform;
if (existing_transform === 'none') {
dom.style.transform = translate;
} else {
// Previously, in the Svelte 4, we'd just apply the transform the the DOM element. However,
// because we're now using Web Animations, we can't do that as it won't work properly if the
// animation is also making use of the same transformations. So instead, we apply an
// instantaneous animation and pause it on the first frame, just applying the same behavior.
// We also need to take into consideration matrix transforms and how they might combine with
// an existing behavior that is already in progress (such as scale).
// > Follow the white rabbit.
const transform = existing_transform.startsWith('matrix(1,')
? translate
: `matrix(1,0,0,1,0,0)`;
const frame = {
transform
};
const animation = dom.animate([frame, frame], { duration: 1 });
animation.pause();
}
}
}
}
const needs_reverse = direction === 'both' && curr_direction !== 'out';
curr_direction = 'out';
if (animation === null || cancelled) {
cancelled = false;
create_animation();
}
if (animation === null) {
transition.x();
} else {
dispatch_event(dom, 'outrostart');
if (needs_reverse) {
const payload = transition.p;
const current_animation = /** @type {Animation} */ (animation);
// If we are working with CSS animations, then before we call reverse, we also need to ensure
// that we reverse the easing logic. To do this we need to re-create the keyframes so they're
// in reverse with easing properly reversed too.
if (
payload !== null &&
payload.css !== undefined &&
current_animation.playState === 'idle'
) {
const duration = payload.duration ?? 300;
const css_fn = payload.css;
const easing_fn = payload.easing || linear;
const keyframes = create_keyframes(easing_fn, css_fn, duration, direction, true);
const effect = current_animation.effect;
if (effect !== null) {
// @ts-ignore
effect.setKeyframes(keyframes);
}
}
/** @type {Animation | TickAnimation} */ (animation).reverse();
} else {
/** @type {Animation | TickAnimation} */ (animation).play();
}
}
},
// cancel
c() {
if (animation !== null) {
/** @type {Animation | TickAnimation} */ (animation).cancel();
}
cancelled = true;
},
// cleanup
x() {
run_all(subs);
subs = [];
},
r: direction,
d: dom
};
return transition;
}
/**
* @param {import('../../types.js').Block} block
* @returns {boolean}
* @param {Set<import('../../types.js').Transition>} transitions
* @param {'in' | 'out' | 'key'} target_direction
* @param {DOMRect} [from]
* @returns {void}
*/
function is_transition_block(block) {
const type = block.t;
return (
type === IF_BLOCK ||
type === EACH_ITEM_BLOCK ||
type === KEY_BLOCK ||
type === AWAIT_BLOCK ||
type === DYNAMIC_COMPONENT_BLOCK ||
(type === EACH_BLOCK && block.v.length === 0)
);
export function trigger_transitions(transitions, target_direction, from) {
// noop, until we excise it from the codebase
}
/**
* @template P
* @param {HTMLElement} dom
* @param {() => import('../../types.js').TransitionFn<P | undefined> | import('../../types.js').AnimateFn<P | undefined>} get_transition_fn
* @param {(() => P) | null} props_fn
* @param {'in' | 'out' | 'both' | 'key'} direction
* @param {HTMLElement} element
* @param {() => import('#client').TransitionFn<P | undefined>} get_fn
* @param {(() => P) | null} get_params
* @param {'in' | 'out' | 'both'} direction
* @param {boolean} global
* @returns {void}
*/
function bind_transition(dom, get_transition_fn, props_fn, direction, global) {
const transition_effect = /** @type {import('../../types.js').Effect} */ (current_effect);
const block = current_block;
const is_keyed_transition = direction === 'key';
export function bind_transition(element, get_fn, get_params, direction, global) {
const effect = /** @type {import('#client').Effect} */ (current_effect);
let can_show_intro_on_mount = true;
let can_apply_lazy_transitions = false;
let p = direction === 'out' ? 1 : 0;
if (is_keyed_transition) {
// @ts-ignore
dom.__animate = true;
}
/** @type {import('../../types.js').Block | null} */
let transition_block = block;
main: while (transition_block !== null) {
if (is_transition_block(transition_block)) {
if (transition_block.t === EACH_ITEM_BLOCK) {
// Lazily apply the each block transition
transition_block.r = each_item_transition;
transition_block.a = each_item_animate;
transition_block = transition_block.p;
} else if (transition_block.t === AWAIT_BLOCK && transition_block.n /* pending */) {
can_show_intro_on_mount = true;
} else if (transition_block.t === IF_BLOCK) {
transition_block.r = if_block_transition;
if (can_show_intro_on_mount) {
/** @type {import('../../types.js').Block | null} */
let if_block = transition_block;
while (if_block.t === IF_BLOCK) {
// If we have an if block parent that is currently falsy then
// we can show the intro on mount as long as that block is mounted
if (if_block.e !== null && !if_block.v) {
can_show_intro_on_mount = true;
break main;
}
if_block = if_block.p;
}
}
}
if (!can_apply_lazy_transitions && can_show_intro_on_mount) {
can_show_intro_on_mount = transition_block.e !== null;
/** @type {Animation | null} */
let current_animation;
/** @type {import('#client').Task | null} */
let current_task;
/** @type {import('#client').TransitionPayload | null} */
let current_options;
let current_delta = 0;
/** @type {import('#client').Transition2} */
const transition = {
global,
to(target, callback) {
if (current_task) {
current_task.abort();
current_task = null;
}
if (can_show_intro_on_mount || !global) {
can_apply_lazy_transitions = true;
if (current_animation && current_options) {
const time = /** @type {number} */ (current_animation.currentTime);
const duration = /** @type {number} */ (current_options.duration);
p = (Math.abs(current_delta) * time) / duration;
current_animation.cancel();
}
} else if (transition_block.t === ROOT_BLOCK && !can_apply_lazy_transitions) {
can_show_intro_on_mount = transition_block.e !== null || transition_block.i;
}
transition_block = transition_block.p;
}
/** @type {import('../../types.js').Transition} */
let transition;
current_options ??= get_fn()(element, get_params?.(), { direction });
effect(() => {
let already_mounted = false;
if (transition !== undefined) {
already_mounted = true;
// Destroy any existing transitions first
transition.x();
}
const transition_fn = get_transition_fn();
/** @param {DOMRect} [from] */
const init = (from) =>
untrack(() => {
const props = props_fn === null ? {} : props_fn();
return is_keyed_transition
? /** @type {import('../../types.js').AnimateFn<any>} */ (transition_fn)(
dom,
{ from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() },
props,
{}
)
: /** @type {import('../../types.js').TransitionFn<any>} */ (transition_fn)(dom, props, {
direction
});
});
if (!current_options?.duration) {
current_options = null;
callback?.();
return;
}
transition = create_transition(dom, init, direction, transition_effect);
const is_intro = direction === 'in';
const show_intro = can_show_intro_on_mount && (is_intro || direction === 'both');
const { delay = 0, duration, css, tick, easing = linear } = current_options;
if (show_intro && !already_mounted) {
transition.p = transition.i();
}
const n = current_options.duration / (1000 / 60);
current_delta = target - p;
const effect = managed_pre_effect(() => {
destroy_effect(effect);
dom.inert = false;
const adjusted_duration = duration * Math.abs(current_delta);
if (show_intro && !already_mounted) {
transition.in();
}
if (css) {
// WAAPI
const keyframes = [];
/** @type {import('../../types.js').Block | null} */
let transition_block = block;
while (!is_intro && transition_block !== null) {
const parent = transition_block.p;
if (is_transition_block(transition_block)) {
if (transition_block.r !== null) {
transition_block.r(transition);
}
if (
parent === null ||
(!global && (transition_block.t !== IF_BLOCK || parent.t !== IF_BLOCK || parent.v))
) {
break;
}
for (let i = 0; i <= n; i += 1) {
const eased = easing(i / n);
const t = p + current_delta * eased;
const styles = css(t, 1 - t);
keyframes.push(css_to_keyframe(styles));
}
transition_block = parent;
}
});
});
if (direction === 'key') {
effect(() => {
return () => {
transition.x();
};
});
}
}
/**
* @param {Set<import('../../types.js').Transition>} transitions
* @param {'in' | 'out' | 'key'} target_direction
* @param {DOMRect} [from]
* @returns {void}
*/
export function trigger_transitions(transitions, target_direction, from) {
/** @type {Array<() => void>} */
const outros = [];
for (const transition of transitions) {
const direction = transition.r;
const effect = transition.e;
if (target_direction === 'in') {
if (direction === 'in' || direction === 'both') {
transition.in();
} else {
transition.c();
}
transition.d.inert = false;
mark_subtree_inert(effect, false);
} else if (target_direction === 'key') {
if (direction === 'key') {
if (!transition.p) {
transition.p = transition.i(/** @type {DOMRect} */ (from));
}
transition.in();
}
} else {
if (direction === 'out' || direction === 'both') {
if (!transition.p) {
transition.p = transition.i();
}
outros.push(transition.o);
current_animation = element.animate(keyframes, {
delay,
duration: adjusted_duration,
easing: 'linear',
fill: 'forwards'
});
current_animation.finished
.then(() => {
p = target;
current_animation = current_options = null;
callback?.();
})
.catch(noop);
} else if (tick) {
// Timer
let running = true;
const start_time = raf.now() + delay;
const start_p = p;
const end_time = start_time + adjusted_duration;
tick(p, 1 - p); // TODO put in nested effect, to avoid interleaved reads/writes?
current_task = loop((now) => {
if (running) {
if (now >= end_time) {
p = target;
tick(target, 1 - target);
// dispatch(node, true, 'end'); TODO
current_task = null;
callback?.();
return (running = false);
}
if (now >= start_time) {
p = start_p + current_delta * easing((now - start_time) / adjusted_duration);
tick(p, 1 - p);
}
}
return running;
});
current_options = null;
}
transition.d.inert = true;
mark_subtree_inert(effect, true);
}
}
if (outros.length > 0) {
// Defer the outros to a microtask
const e = managed_pre_effect(() => {
destroy_effect(e);
const e2 = managed_effect(() => {
destroy_effect(e2);
run_all(outros);
});
});
}
}
};
/**
* @this {import('../../types.js').IfBlock}
* @param {import('../../types.js').Transition} transition
* @returns {void}
*/
function if_block_transition(transition) {
const block = this;
// block.value === true
if (block.v) {
const consequent_transitions = (block.c ??= new Set());
consequent_transitions.add(transition);
transition.f(() => {
const c = /** @type {Set<import('../../types.js').Transition>} */ (consequent_transitions);
c.delete(transition);
// If the block has changed to falsy and has transitions
if (!block.v && c.size === 0) {
const consequent_effect = block.ce;
execute_effect(/** @type {import('../../types.js').Effect} */ (consequent_effect));
}
});
} else {
const alternate_transitions = (block.a ??= new Set());
alternate_transitions.add(transition);
transition.f(() => {
const a = /** @type {Set<import('../../types.js').Transition>} */ (alternate_transitions);
a.delete(transition);
// If the block has changed to truthy and has transitions
if (block.v && a.size === 0) {
const alternate_effect = block.ae;
execute_effect(/** @type {import('../../types.js').Effect} */ (alternate_effect));
}
});
}
}
// TODO don't pass strings around like this, it's silly
if (direction === 'in' || direction === 'both') {
(effect.in ??= []).push(transition);
/**
* @this {import('../../types.js').EachItemBlock}
* @param {import('../../types.js').Transition} transition
* @returns {void}
*/
function each_item_transition(transition) {
const block = this;
const each_block = block.p;
const is_controlled = (each_block.f & EACH_IS_CONTROLLED) !== 0;
// Disable optimization
if (is_controlled) {
const anchor = empty();
each_block.f ^= EACH_IS_CONTROLLED;
append_child(/** @type {Element} */ (each_block.a), anchor);
each_block.a = anchor;
}
if (transition.r === 'key' && (each_block.f & EACH_IS_ANIMATED) === 0) {
each_block.f |= EACH_IS_ANIMATED;
}
const transitions = (block.s ??= new Set());
transition.f(() => {
transitions.delete(transition);
if (transition.r !== 'key') {
for (let other of transitions) {
const type = other.r;
if (type === 'key' || type === 'in') {
transitions.delete(other);
}
}
if (transitions.size === 0) {
block.s = null;
destroy_each_item_block(block, null, true);
}
}
});
transitions.add(transition);
}
// if this is a local transition, we only want to run it if the parent (block) effect's
// parent (branch) effect is where the state change happened. we can determine that by
// looking at whether the branch effect is currently initializing
const should_run =
run_transitions && (global || /** @type {import('#client').Effect} */ (effect.parent).ran);
/**
*
* @param {import('../../types.js').EachItemBlock} block
* @param {Set<import('../../types.js').Transition>} transitions
*/
function each_item_animate(block, transitions) {
const from_dom = /** @type {Element} */ (get_first_element(block));
const from = from_dom.getBoundingClientRect();
// Cancel any existing key transitions
for (const transition of transitions) {
const type = transition.r;
if (type === 'key') {
transition.c();
if (should_run) {
user_effect(() => {
untrack(() => transition.to(1));
});
} else {
p = 1;
}
}
schedule_raf_task(() => {
trigger_transitions(transitions, 'key', from);
});
if (direction === 'out' || direction === 'both') {
(effect.out ??= []).push(transition);
}
}

@ -1,18 +1,29 @@
import { DEV } from 'esm-env';
import {
check_dirtiness,
current_block,
current_component_context,
current_effect,
current_reaction,
destroy_children,
execute_effect,
get,
remove_reactions,
schedule_effect,
set_signal_status,
untrack
} from '../runtime.js';
import { DIRTY, MANAGED, RENDER_EFFECT, EFFECT, PRE_EFFECT, DESTROYED } from '../constants.js';
import {
DIRTY,
MANAGED,
RENDER_EFFECT,
EFFECT,
PRE_EFFECT,
DESTROYED,
INERT
} from '../constants.js';
import { set } from './sources.js';
import { noop } from '../../common.js';
/**
* @param {import('./types.js').EffectType} type
@ -25,6 +36,7 @@ import { set } from './sources.js';
function create_effect(type, fn, sync, block = current_block, init = true) {
/** @type {import('#client').Effect} */
const signal = {
parent: current_effect,
block,
deps: null,
f: type | DIRTY,
@ -34,7 +46,10 @@ function create_effect(type, fn, sync, block = current_block, init = true) {
deriveds: null,
teardown: null,
ctx: current_component_context,
ondestroy: null
ondestroy: null,
in: null,
out: null,
ran: false
};
if (current_effect !== null) {
@ -236,3 +251,89 @@ export function destroy_effect(signal) {
signal.deps =
null;
}
/**
* @param {import('#client').Effect} effect
* @param {() => void} callback
*/
export function pause_effect(effect, callback = noop) {
/** @type {import('#client').Transition2[]} */
const transitions = [];
pause_children(effect, transitions, true);
let remaining = transitions.length;
if (remaining > 0) {
const check = () => {
if (!--remaining) {
destroy_effect(effect);
callback();
}
};
for (const transition of transitions) {
transition.to(0, check);
}
} else {
destroy_effect(effect);
callback();
}
}
/**
* @param {import('#client').Effect} effect
* @param {import('#client').Transition2[]} transitions
* @param {boolean} local
*/
function pause_children(effect, transitions, local) {
if ((effect.f & INERT) !== 0) return;
effect.f |= INERT;
if (effect.out) {
for (const transition of effect.out) {
if (transition.global || local) {
transitions.push(transition);
}
}
}
if (effect.effects) {
for (const child of effect.effects) {
pause_children(child, transitions, false);
}
}
}
/**
* @param {import('#client').Effect} effect
*/
export function resume_effect(effect) {
resume_children(effect, true);
}
/**
* @param {import('#client').Effect} effect
* @param {boolean} local
*/
function resume_children(effect, local) {
if (check_dirtiness(effect)) {
execute_effect(effect);
}
if (effect.effects) {
for (const child of effect.effects) {
resume_children(child, false);
}
}
effect.f ^= INERT;
if (effect.in) {
for (const transition of effect.in) {
if (transition.global || local) {
transition.to(1);
}
}
}
}

@ -36,6 +36,7 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {
}
export interface Effect extends Reaction {
parent: Effect | null;
/** The block associated with this effect */
block: null | Block;
/** The associated component context */
@ -48,6 +49,11 @@ export interface Effect extends Reaction {
teardown: null | (() => void);
/** The depth from the root signal, used for ordering render/pre-effects topologically **/
l: number;
// transitions TODO
in: null | any[];
out: null | any[];
ran: boolean; // TODO fold this into the bitmask
}
export interface ValueDebug<V = unknown> extends Value<V> {

@ -21,6 +21,8 @@ export const all_registered_events = new Set();
/** @type {Set<(events: Array<string>) => void>} */
export const root_event_handles = new Set();
export let run_transitions = true;
/**
* @param {Element} dom
* @param {() => string} value
@ -198,6 +200,8 @@ function _mount(Component, options) {
const registered_events = new Set();
const container = options.target;
run_transitions = options.intro ?? false;
/** @type {import('#client').RootBlock} */
const block = {
// dom
@ -246,6 +250,8 @@ function _mount(Component, options) {
const bound_event_listener = handle_event_propagation.bind(null, container);
const bound_document_event_listener = handle_event_propagation.bind(null, document);
run_transitions = true;
/** @param {Array<string>} events */
const event_handle = (events) => {
for (let i = 0; i < events.length; i++) {

@ -147,7 +147,7 @@ export function batch_inspect(target, prop, receiver) {
* @param {import('./types.js').Reaction} reaction
* @returns {boolean}
*/
function check_dirtiness(reaction) {
export function check_dirtiness(reaction) {
var flags = reaction.f;
if ((flags & DIRTY) !== 0) {
@ -548,6 +548,8 @@ export function schedule_effect(signal, sync) {
}
}
}
signal.ran = true;
}
/**

@ -348,4 +348,11 @@ export type ProxyStateObject<T = Record<string | symbol, any>> = T & {
[STATE_SYMBOL]: ProxyMetadata;
};
// TODO remove the other transition types once we're
// happy we don't need them, and rename this
export interface Transition2 {
global: boolean;
to(target: number, callback?: () => void): void;
}
export * from './reactivity/types';

@ -30,6 +30,7 @@ function tick(time) {
}
}
// TODO tidy this up
class Animation {
#keyframes;
#duration;
@ -38,6 +39,12 @@ class Animation {
#target;
#paused;
#offset = 0;
#finished = () => {};
#cancelled = () => {};
currentTime = 0;
/**
* @param {HTMLElement} target
* @param {Keyframe[]} keyframes
@ -47,6 +54,7 @@ class Animation {
this.#target = target;
this.#keyframes = keyframes;
this.#duration = options.duration || 0;
this.#timeline_offset = 0;
this.#reversed = false;
this.#paused = false;
@ -59,6 +67,23 @@ class Animation {
this.#keyframes = keyframes;
}
};
this.#offset = raf.time;
// Promise-like semantics, but call callbacks immediately on raf.tick
this.finished = {
/** @param {() => void} callback */
then: (callback) => {
this.#finished = callback;
return {
/** @param {() => void} callback */
catch: (callback) => {
this.#cancelled = callback;
}
};
}
};
}
play() {
@ -80,6 +105,11 @@ class Animation {
}
const target_frame = this.currentTime / this.#duration;
this._applyKeyFrame(target_frame);
if (this.currentTime >= this.#duration) {
this.#finished();
raf.animations.delete(this);
}
}
/**
@ -126,6 +156,9 @@ class Animation {
if (this.currentTime > 0 && this.currentTime < this.#duration) {
this._applyKeyFrame(this.#reversed ? this.#keyframes.length - 1 : 0);
}
this.#cancelled();
raf.animations.delete(this);
}
pause() {
@ -150,6 +183,7 @@ class Animation {
*/
HTMLElement.prototype.animate = function (keyframes, options) {
const animation = new Animation(this, keyframes, options);
raf.animations.add(animation);
// @ts-ignore
return animation;
};

@ -11,6 +11,6 @@ export default test({
btn1.click();
});
assert.htmlEqual(target.innerHTML, `<button>hide</button><div style="opacity: 0;">hello</div>`);
assert.htmlEqual(target.innerHTML, `<button>hide</button><div>hello</div>`);
}
});

@ -34,7 +34,7 @@ export default test({
assert.htmlEqual(
target.innerHTML,
`<button>show</button><button>animate</button><h1 style="opacity: 0;">Hello\n!</h1>`
`<button>show</button><button>animate</button><h1>Hello\n!</h1>`
);
}
});

Loading…
Cancel
Save