}
+ */
+ let effects = new Set();
- // Managed effect
- render.e = render_effect(
- () => {
- const current = block.d;
- if (current !== null) {
- remove(current);
- block.d = null;
- }
- if (component) {
- render_fn(component);
- }
- render.d = block.d;
- block.d = null;
- },
- block,
- true
- );
+ const component_effect = render_effect(
+ () => {
+ if (component === (component = get_component())) return;
- current_render = render;
- };
+ if (effect) {
+ var e = effect;
+ pause_effect(e, () => {
+ effects.delete(e);
+ });
+ }
- const render = () => {
- const render = current_render;
+ if (component) {
+ effect = render_effect(
+ () => {
+ render_fn(component);
- if (render === null) {
- create_render_effect();
- return;
- }
+ // @ts-expect-error TODO this should be unnecessary
+ const dom = block.d;
- const transitions = render.s;
+ return () => {
+ if (dom !== null) {
+ remove(dom);
+ }
+ };
+ },
+ block,
+ true,
+ true
+ );
- if (transitions.size === 0) {
- if (render.d !== null) {
- remove(render.d);
- render.d = null;
- }
- if (render.e) {
- execute_effect(render.e);
- } else {
- create_render_effect();
- }
- } else {
- create_render_effect();
- trigger_transitions(transitions, 'out');
- }
- };
+ // @ts-expect-error TODO tidy up
+ effect.d = block.d;
- const component_effect = render_effect(
- () => {
- const next_component = component_fn();
- if (component !== next_component) {
- component = next_component;
- render();
+ effects.add(effect);
}
},
block,
@@ -129,19 +71,9 @@ export function component(anchor_node, component_fn, render_fn) {
);
component_effect.ondestroy = () => {
- let render = current_render;
- while (render !== null) {
- const dom = render.d;
- if (dom !== null) {
- remove(dom);
- }
- const effect = render.e;
- if (effect !== null) {
- destroy_effect(effect);
- }
- render = render.p;
+ for (const e of effects) {
+ // @ts-expect-error TODO tidy up. ondestroy should be totally unnecessary
+ if (e.d) remove(e.d);
}
};
-
- block.e = component_effect;
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
index 98950fd317..8e63fcc61b 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
@@ -1,11 +1,17 @@
import { namespace_svg } from '../../../../constants.js';
-import { DYNAMIC_ELEMENT_BLOCK } from '../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
-import { destroy_effect, render_effect } from '../../reactivity/effects.js';
-import { insert, remove } from '../reconciler.js';
-import { current_block, execute_effect } from '../../runtime.js';
+import {
+ destroy_effect,
+ pause_effect,
+ render_effect,
+ resume_effect
+} from '../../reactivity/effects.js';
+import { remove } from '../reconciler.js';
+import { current_block } from '../../runtime.js';
import { is_array } from '../../utils.js';
+import { set_should_intro } from '../../render.js';
+import { current_each_item_block, set_current_each_item_block } from './each.js';
/**
* @param {import('#client').Block} block
@@ -28,13 +34,13 @@ function swap_block_dom(block, from, to) {
}
/**
- * @param {Comment} anchor_node
- * @param {() => string} tag_fn
+ * @param {Comment} anchor
+ * @param {() => string} get_tag
* @param {boolean | null} is_svg `null` == not statically known
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
* @returns {void}
*/
-export function element(anchor_node, tag_fn, is_svg, render_fn) {
+export function element(anchor, get_tag, is_svg, render_fn) {
/** @type {import('#client').DynamicElementBlock} */
const block = {
// dom
@@ -42,98 +48,119 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) {
// effect
e: null,
// parent
- p: /** @type {import('#client').Block} */ (current_block),
- // transition
- r: null,
- // type
- t: DYNAMIC_ELEMENT_BLOCK
+ p: /** @type {import('#client').Block} */ (current_block)
};
- hydrate_block_anchor(anchor_node);
- let has_mounted = false;
+ hydrate_block_anchor(anchor);
- /** @type {string} */
+ /** @type {string | null} */
let tag;
+ /** @type {string | null} */
+ let current_tag;
+
/** @type {null | Element} */
let element = null;
- const element_effect = render_effect(
- () => {
- tag = tag_fn();
- if (has_mounted) {
- execute_effect(render_effect_signal);
- }
- has_mounted = true;
- },
- block,
- false
- );
-
- // Managed effect
- const render_effect_signal = render_effect(
- () => {
- // We try our best infering the namespace in case it's not possible to determine statically,
- // but on the first render on the client (without hydration) the parent will be undefined,
- // since the anchor is not attached to its parent / the dom yet.
- const ns =
- is_svg || tag === 'svg'
- ? namespace_svg
- : is_svg === false || anchor_node.parentElement?.tagName === 'foreignObject'
- ? null
- : anchor_node.parentElement?.namespaceURI ?? null;
-
- const next_element = tag
- ? hydrating
- ? /** @type {Element} */ (current_hydration_fragment[0])
- : ns
- ? document.createElementNS(ns, tag)
- : document.createElement(tag)
- : null;
-
- const prev_element = element;
- if (prev_element !== null) {
- block.d = null;
- }
+ /** @type {import('#client').Effect | null} */
+ let effect;
- element = next_element;
- if (element !== null && render_fn !== undefined) {
- let anchor;
- if (hydrating) {
- // Use the existing ssr comment as the anchor so that the inner open and close
- // methods can pick up the existing nodes correctly
- anchor = /** @type {Comment} */ (element.firstChild);
- } else {
- anchor = empty();
- element.appendChild(anchor);
- }
- render_fn(element, anchor);
- }
+ /**
+ * The keyed `{#each ...}` item block, if any, that this element is inside.
+ * We track this so we can set it when changing the element, allowing any
+ * `animate:` directive to bind itself to the correct block
+ */
+ let each_item_block = current_each_item_block;
- const has_prev_element = prev_element !== null;
- if (has_prev_element) {
- remove(prev_element);
- }
- if (element !== null) {
- insert(element, null, anchor_node);
- if (has_prev_element) {
- const parent_block = block.p;
- swap_block_dom(parent_block, prev_element, element);
- }
+ const wrapper = render_effect(() => {
+ const next_tag = get_tag() || null;
+ if (next_tag === tag) return;
+
+ // See explanation of `each_item_block` above
+ var previous_each_item_block = current_each_item_block;
+ set_current_each_item_block(each_item_block);
+
+ // We try our best infering the namespace in case it's not possible to determine statically,
+ // but on the first render on the client (without hydration) the parent will be undefined,
+ // since the anchor is not attached to its parent / the dom yet.
+ const ns =
+ is_svg || next_tag === 'svg'
+ ? namespace_svg
+ : is_svg === false || anchor.parentElement?.tagName === 'foreignObject'
+ ? null
+ : anchor.parentElement?.namespaceURI ?? null;
+
+ if (effect) {
+ if (next_tag === null) {
+ // start outro
+ pause_effect(effect, () => {
+ effect = null;
+ current_tag = null;
+ element?.remove(); // TODO this should be unnecessary
+ });
+ } else if (next_tag === current_tag) {
+ // same tag as is currently rendered — abort outro
+ resume_effect(effect);
+ } else {
+ // tag is changing — destroy immediately, render contents without intro transitions
+ destroy_effect(effect);
+ set_should_intro(false);
}
- },
- block,
- true
- );
+ }
- element_effect.ondestroy = () => {
+ if (next_tag && next_tag !== current_tag) {
+ effect = render_effect(
+ () => {
+ const prev_element = element;
+ element = hydrating
+ ? /** @type {Element} */ (current_hydration_fragment[0])
+ : ns
+ ? document.createElementNS(ns, next_tag)
+ : document.createElement(next_tag);
+
+ if (render_fn) {
+ let anchor;
+ if (hydrating) {
+ // Use the existing ssr comment as the anchor so that the inner open and close
+ // methods can pick up the existing nodes correctly
+ anchor = /** @type {Comment} */ (element.firstChild);
+ } else {
+ anchor = empty();
+ element.appendChild(anchor);
+ }
+ render_fn(element, anchor);
+ }
+
+ anchor.before(element);
+
+ if (prev_element) {
+ swap_block_dom(block.p, prev_element, element);
+ prev_element.remove();
+ }
+ },
+ block,
+ true
+ );
+ }
+
+ tag = next_tag;
+ if (tag) current_tag = tag;
+ set_should_intro(true);
+
+ set_current_each_item_block(previous_each_item_block);
+ }, block);
+
+ wrapper.ondestroy = () => {
if (element !== null) {
remove(element);
block.d = null;
element = null;
}
- destroy_effect(render_effect_signal);
+
+ if (effect) {
+ destroy_effect(effect);
+ }
};
- block.e = element_effect;
+ block.e = wrapper;
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
index 0147a5fe88..c0916e7a29 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -1,4 +1,3 @@
-import { HEAD_BLOCK } from '../../constants.js';
import {
current_hydration_fragment,
get_hydration_fragment,
@@ -22,11 +21,7 @@ export function head(render_fn) {
// effect
e: null,
// parent
- p: /** @type {import('#client').Block} */ (current_block),
- // transition
- r: null,
- // type
- t: HEAD_BLOCK
+ p: /** @type {import('#client').Block} */ (current_block)
};
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
index fcf56b7e89..86d75948ee 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
@@ -1,5 +1,5 @@
import { DEV } from 'esm-env';
-import { render_effect } from '../../../reactivity/effects.js';
+import { render_effect, user_effect } from '../../../reactivity/effects.js';
import { stringify } from '../../../render.js';
/**
@@ -96,6 +96,11 @@ export function bind_group(inputs, group_index, input, get_value, update) {
}
});
+ user_effect(() => {
+ // necessary to maintain binding group order in all insertion scenarios. TODO optimise
+ binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
+ });
+
render_effect(() => {
return () => {
var index = binding_group.indexOf(input);
diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js
index 4a9615d387..387089f262 100644
--- a/packages/svelte/src/internal/client/dom/elements/transitions.js
+++ b/packages/svelte/src/internal/client/dom/elements/transitions.js
@@ -1,85 +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 { 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 { noop } from '../../../common.js';
+import { user_effect } from '../../reactivity/effects.js';
+import { current_effect, untrack } from '../../runtime.js';
import { raf } from '../../timing.js';
-
-const active_tick_animations = new Set();
-const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER;
-
-/** @type {undefined | number} */
-let active_tick_ref = undefined;
-
-/**
- * @template P
- * @param {HTMLElement} dom
- * @param {() => import('#client').TransitionFn} get_transition_fn
- * @param {(() => P) | null} props
- * @param {any} global
- * @returns {void}
- */
-export function transition(dom, get_transition_fn, props, global = false) {
- bind_transition(dom, get_transition_fn, props, 'both', global);
-}
-
-/**
- * @template P
- * @param {HTMLElement} dom
- * @param {() => import('#client').TransitionFn
} get_transition_fn
- * @param {(() => P) | null} props
- * @returns {void}
- */
-export function animate(dom, get_transition_fn, props) {
- bind_transition(dom, get_transition_fn, props, 'key', false);
-}
-
-/**
- * @template P
- * @param {HTMLElement} dom
- * @param {() => import('#client').TransitionFn
} get_transition_fn
- * @param {(() => P) | null} props
- * @param {any} global
- * @returns {void}
- */
-function in_fn(dom, get_transition_fn, props, global = false) {
- bind_transition(dom, get_transition_fn, props, 'in', global);
-}
-export { in_fn as in };
-
-/**
- * @template P
- * @param {HTMLElement} dom
- * @param {() => import('#client').TransitionFn
} get_transition_fn
- * @param {(() => P) | null} props
- * @param {any} global
- * @returns {void}
- */
-export function out(dom, get_transition_fn, props, global = false) {
- bind_transition(dom, get_transition_fn, props, 'out', global);
-}
+import { loop } from '../../loop.js';
+import { should_intro } from '../../render.js';
+import { is_function } from '../../utils.js';
+import { current_each_item_block } from '../blocks/each.js';
+import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
+import { EFFECT_RAN } from '../../constants.js';
/**
* @template T
@@ -95,7 +23,7 @@ function custom_event(type, detail, { bubbles = false, cancelable = false } = {}
}
/**
- * @param {HTMLElement} dom
+ * @param {Element} dom
* @param {'introstart' | 'introend' | 'outrostart' | 'outroend'} type
* @returns {void}
*/
@@ -137,685 +65,281 @@ function css_to_keyframe(css) {
return keyframe;
}
-class TickAnimation {
- /** @type {null | (() => void)} */
- onfinish;
-
- /** @type {(t: number, u: number) => string} */
- #tick_fn;
-
- /** @type {number} */
- #duration;
-
- /** @type {number} */
- #current;
-
- /** @type {number} */
- #delay;
-
- /** @type {number} */
- #previous;
-
- /** @type {boolean} */
- paused;
-
- /** @type {boolean} */
- #reversed;
-
- /** @type {number} */
- #delay_current;
-
- /** @type {boolean} */
- #delayed_reverse;
-
- /**
- * @param {(t: number, u: number) => string} tick_fn
- * @param {number} duration
- * @param {number} delay
- * @param {boolean} out
- */
- constructor(tick_fn, duration, delay, out) {
- this.#duration = duration;
- this.#delay = delay;
- this.paused = false;
- this.#tick_fn = tick_fn;
- this.#reversed = out;
- this.#delay_current = delay;
- this.#current = out ? duration : 0;
- this.#previous = 0;
- this.#delayed_reverse = false;
- this.onfinish = null;
- if (this.#delay) {
- if (!out) {
- this.#tick_fn(0, 1);
- }
- }
- }
-
- pause() {
- this.paused = true;
- }
-
- play() {
- this.paused = false;
- if (!active_tick_animations.has(this)) {
- this.#previous = raf.now();
- if (active_tick_ref === undefined) {
- active_tick_ref = raf.tick(handle_raf);
- }
- active_tick_animations.add(this);
- }
- }
-
- #reverse() {
- this.#reversed = !this.#reversed;
- if (this.paused) {
- if (this.#current === 0) {
- this.#current = this.#duration;
- }
- this.play();
- }
- }
-
- reverse() {
- if (this.#delay === 0) {
- this.#reverse();
- } else {
- this.#delay_current = this.#delay;
- this.#delayed_reverse = true;
- }
- }
+/** @param {number} t */
+const linear = (t) => t;
- cancel() {
- active_tick_animations.delete(this);
- const current = this.#current / this.#duration;
- if (current > 0 && current < 1) {
- const t = this.#reversed ? 1 : 0;
- this.#tick_fn(t, 1 - t);
- }
- }
+/**
+ * Called inside keyed `{#each ...}` blocks (as `$.animation(...)`). This creates an animation manager
+ * and attaches it to the block, so that moves can be animated following reconciliation.
+ * @template P
+ * @param {Element} element
+ * @param {() => import('#client').AnimateFn
} get_fn
+ * @param {(() => P) | null} get_params
+ */
+export function animation(element, get_fn, get_params) {
+ var block = /** @type {import('#client').EachItemBlock} */ (current_each_item_block);
- finish() {
- active_tick_animations.delete(this);
- if (this.onfinish) {
- this.onfinish();
- }
- }
+ /** @type {DOMRect} */
+ var from;
- /** @param {number} time */
- _update(time) {
- let diff = time - this.#previous;
- this.#previous = time;
- if (this.#delay_current !== 0) {
- const is_delayed = this.#delay_current === DELAY_NEXT_TICK;
- let cancel = !this.#delayed_reverse;
- this.#delay_current -= diff;
- if (this.#delay_current < 0 || is_delayed || (this.#delay_current === 0 && this.#reversed)) {
- const delay_diff = is_delayed ? 0 : -this.#delay_current;
- this.#delay_current = 0;
-
- if (this.#delayed_reverse) {
- this.#delayed_reverse = false;
- this.#reverse();
- } else if (delay_diff !== 0 || this.#reversed) {
- diff = delay_diff;
- }
- cancel = false;
- } else if (this.#delay_current === 0) {
- this.#delay_current = DELAY_NEXT_TICK;
- }
- if (cancel) {
- return;
- }
- }
- this.#current += this.#reversed ? -diff : diff;
- let t = this.#current / this.#duration;
+ /** @type {DOMRect} */
+ var to;
- if (t < 0) {
- t = 0;
- } else if (t > 1) {
- t = 1;
- }
+ /** @type {import('#client').Animation | undefined} */
+ var animation;
- if ((this.#reversed && t <= 0) || (!this.#reversed && t >= 1)) {
- t = this.#reversed ? 0 : 1;
- if (this.#delay_current === 0) {
- active_tick_animations.delete(this);
- if (this.onfinish) {
- this.paused = true;
- this.onfinish();
- }
+ block.a ??= {
+ element,
+ measure() {
+ from = this.element.getBoundingClientRect();
+ },
+ apply() {
+ animation?.abort();
+
+ to = this.element.getBoundingClientRect();
+
+ const options = get_fn()(this.element, { from, to }, get_params?.());
+
+ if (
+ from.left !== to.left ||
+ from.right !== to.right ||
+ from.top !== to.top ||
+ from.bottom !== to.bottom
+ ) {
+ animation = animate(this.element, options, undefined, 1, () => {
+ animation?.abort();
+ animation = undefined;
+ });
}
}
- this.#tick_fn(t, 1 - t);
- }
-}
+ };
-/** @param {number} time */
-function handle_raf(time) {
- for (const animation of active_tick_animations) {
- if (!animation.paused) {
- animation._update(time);
- }
- }
- if (active_tick_animations.size !== 0) {
- active_tick_ref = raf.tick(handle_raf);
- } else {
- active_tick_ref = undefined;
- }
+ // in the case of a ``, it's possible for `$.animation(...)` to be called
+ // when an animation manager already exists, if the tag changes. in that case, we need to
+ // swap out the element rather than creating a new manager, in case it happened at the same
+ // moment as a reconciliation
+ block.a.element = element;
}
/**
- * @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
+ * Called inside block effects as `$.transition(...)`. This creates a transition manager and
+ * attaches it to the current effect — later, inside `pause_effect` and `resume_effect`, we
+ * use this to create `intro` and `outro` transitions.
+ * @template P
+ * @param {number} flags
+ * @param {HTMLElement} element
+ * @param {() => import('#client').TransitionFn} get_fn
+ * @param {(() => P) | null} get_params
+ * @returns {void}
*/
-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;
-}
+export function transition(flags, element, get_fn, get_params) {
+ var is_intro = (flags & TRANSITION_IN) !== 0;
+ var is_outro = (flags & TRANSITION_OUT) !== 0;
+ var is_global = (flags & TRANSITION_GLOBAL) !== 0;
-/** @param {number} t */
-const linear = (t) => t;
+ /** @type {'in' | 'out' | 'both'} */
+ var direction = is_intro && is_outro ? 'both' : is_intro ? 'in' : 'out';
-/**
- * @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 {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig) | undefined} */
+ var current_options;
- /** @type {Array<() => void>} */
- let subs = [];
+ var inert = element.inert;
- /** @type {null | Animation | TickAnimation} */
- let animation = null;
- let cancelled = false;
+ /** @type {import('#client').Animation | undefined} */
+ var intro;
- 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();
+ /** @type {import('#client').Animation | undefined} */
+ var outro;
- 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 {(() => void) | undefined} */
+ var reset;
- /** @type {import('../../types.js').Transition} */
- const transition = {
- e: effect,
- i: init,
- // payload
- p: null,
-
- // finished
- /** @param {() => void} fn */
- f(fn) {
- subs.push(fn);
- },
+ function get_options() {
+ // If a transition is still ongoing, we use the existing options rather than generating
+ // new ones. This ensures that reversible transitions reverse smoothly, rather than
+ // jumping to a new spot because (for example) a different `duration` was used
+ return (current_options ??= get_fn()(element, get_params?.(), { direction }));
+ }
+
+ /** @type {import('#client').TransitionManager} */
+ var transition = {
+ is_global,
in() {
- const needs_reverse = curr_direction !== 'in';
- curr_direction = 'in';
- if (animation === null || cancelled) {
- cancelled = false;
- create_animation();
- }
- if (animation === null) {
- transition.x();
+ element.inert = inert;
+
+ if (is_intro) {
+ dispatch_event(element, 'introstart');
+ intro = animate(element, get_options(), outro, 1, () => {
+ dispatch_event(element, 'introend');
+ intro = current_options = undefined;
+ });
} else {
- dispatch_event(dom, 'introstart');
- if (needs_reverse) {
- /** @type {Animation | TickAnimation} */ (animation).reverse();
- }
- /** @type {Animation | TickAnimation} */ (animation).play();
+ outro?.abort();
+ reset?.();
}
},
- // 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();
+ out(fn) {
+ if (is_outro) {
+ element.inert = true;
+
+ dispatch_event(element, 'outrostart');
+ outro = animate(element, get_options(), intro, 0, () => {
+ dispatch_event(element, 'outroend');
+ outro = current_options = undefined;
+ fn?.();
+ });
+
+ // TODO arguably the outro should never null itself out until _all_ outros for this effect have completed...
+ // in that case we wouldn't need to store `reset` separately
+ reset = outro.reset;
} 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();
- }
+ fn?.();
}
},
- // cancel
- c() {
- if (animation !== null) {
- /** @type {Animation | TickAnimation} */ (animation).cancel();
- }
- cancelled = true;
- },
- // cleanup
- x() {
- run_all(subs);
- subs = [];
- },
- r: direction,
- d: dom
+ stop: () => {
+ intro?.abort();
+ outro?.abort();
+ }
};
- return transition;
-}
-/**
- * @param {import('../../types.js').Block} block
- * @returns {boolean}
- */
-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)
- );
+ var effect = /** @type {import('#client').Effect} */ (current_effect);
+
+ (effect.transitions ??= []).push(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
+ if (is_intro && should_intro) {
+ var parent = /** @type {import('#client').Effect} */ (effect.parent);
+
+ if (is_global || (parent.f & EFFECT_RAN) !== 0) {
+ user_effect(() => {
+ untrack(() => transition.in());
+ });
+ }
+ }
}
/**
- * @template P
- * @param {HTMLElement} dom
- * @param {() => import('../../types.js').TransitionFn
| import('../../types.js').AnimateFn
} get_transition_fn
- * @param {(() => P) | null} props_fn
- * @param {'in' | 'out' | 'both' | 'key'} direction
- * @param {boolean} global
- * @returns {void}
+ * Animates an element, according to the provided configuration
+ * @param {Element} element
+ * @param {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig)} options
+ * @param {import('#client').Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
+ * @param {number} t2 The target `t` value — `1` for intro, `0` for outro
+ * @param {(() => void) | undefined} callback
+ * @returns {import('#client').Animation}
*/
-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';
-
- let can_show_intro_on_mount = true;
- let can_apply_lazy_transitions = false;
+function animate(element, options, counterpart, t2, callback) {
+ if (is_function(options)) {
+ // In the case of a deferred transition (such as `crossfade`), `option` will be
+ // a function rather than an `AnimationConfig`. We need to call this function
+ // once DOM has been updated...
+ /** @type {import('#client').Animation} */
+ var a;
+
+ user_effect(() => {
+ var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' }));
+ a = animate(element, o, counterpart, t2, callback);
+ });
- if (is_keyed_transition) {
- // @ts-ignore
- dom.__animate = true;
+ // ...but we want to do so without using `async`/`await` everywhere, so
+ // we return a facade that allows everything to remain synchronous
+ return {
+ abort: () => a.abort(),
+ deactivate: () => a.deactivate(),
+ reset: () => a.reset(),
+ t: (now) => a.t(now)
+ };
}
- /** @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;
- }
- if (can_show_intro_on_mount || !global) {
- can_apply_lazy_transitions = true;
- }
- } 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;
+
+ counterpart?.deactivate();
+
+ if (!options?.duration) {
+ callback?.();
+ return {
+ abort: noop,
+ deactivate: noop,
+ reset: noop,
+ t: () => t2
+ };
}
- /** @type {import('../../types.js').Transition} */
- let transition;
+ var { delay = 0, duration, css, tick, easing = linear } = options;
- 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} */ (transition_fn)(
- dom,
- { from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() },
- props,
- {}
- )
- : /** @type {import('../../types.js').TransitionFn} */ (transition_fn)(dom, props, {
- direction
- });
- });
+ var start = raf.now() + delay;
+ var t1 = counterpart?.t(start) ?? 1 - t2;
+ var delta = t2 - t1;
- 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');
+ duration *= Math.abs(delta);
+ var end = start + duration;
- if (show_intro && !already_mounted) {
- transition.p = transition.i();
- }
+ /** @type {Animation} */
+ var animation;
- const effect = managed_pre_effect(() => {
- destroy_effect(effect);
- dom.inert = false;
+ /** @type {import('#client').Task} */
+ var task;
- if (show_intro && !already_mounted) {
- transition.in();
- }
+ if (css) {
+ // WAAPI
+ var keyframes = [];
+ var n = duration / (1000 / 60);
- /** @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;
- }
- }
- transition_block = parent;
- }
- });
- });
+ for (var i = 0; i <= n; i += 1) {
+ var t = t1 + delta * easing(i / n);
+ var styles = css(t, 1 - t);
+ keyframes.push(css_to_keyframe(styles));
+ }
- if (direction === 'key') {
- effect(() => {
- return () => {
- transition.x();
- };
+ animation = element.animate(keyframes, {
+ delay,
+ duration,
+ easing: 'linear',
+ fill: 'forwards'
});
- }
-}
-/**
- * @param {Set} 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);
- }
- transition.d.inert = true;
- mark_subtree_inert(effect, true);
+ animation.finished
+ .then(() => {
+ callback?.();
+ })
+ .catch(noop);
+ } else {
+ // Timer
+ if (t1 === 0) {
+ tick?.(0, 1); // TODO put in nested effect, to avoid interleaved reads/writes?
}
- }
- 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} */ (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));
+ task = loop((now) => {
+ if (now >= end) {
+ tick?.(t2, 1 - t2);
+ callback?.();
+ return false;
}
- });
- } else {
- const alternate_transitions = (block.a ??= new Set());
- alternate_transitions.add(transition);
- transition.f(() => {
- const a = /** @type {Set} */ (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));
+
+ if (now >= start) {
+ var p = t1 + delta * easing((now - start) / duration);
+ tick?.(p, 1 - p);
}
+
+ return true;
});
}
-}
-/**
- * @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);
+ return {
+ abort: () => {
+ animation?.cancel();
+ task?.abort();
+ },
+ deactivate: () => {
+ callback = undefined;
+ },
+ reset: () => {
+ if (t2 === 0) {
+ tick?.(1, 0);
}
+ },
+ t: (now) => {
+ var t = t1 + delta * easing((now - start) / duration);
+ return Math.min(1, Math.max(0, t));
}
- });
- transitions.add(transition);
-}
-
-/**
- *
- * @param {import('../../types.js').EachItemBlock} block
- * @param {Set} 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();
- }
- }
- schedule_raf_task(() => {
- trigger_transitions(transitions, 'key', from);
- });
+ };
}
diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js
index 1b15a9ddbb..c896454659 100644
--- a/packages/svelte/src/internal/client/dom/hydration.js
+++ b/packages/svelte/src/internal/client/dom/hydration.js
@@ -73,33 +73,26 @@ export function get_hydration_fragment(node, insert_text = false) {
}
/**
- * @param {Text | Comment | Element} anchor_node
- * @param {boolean} [is_controlled]
+ * @param {Node} node
* @returns {void}
*/
-export function hydrate_block_anchor(anchor_node, is_controlled) {
- if (hydrating) {
- /** @type {Node} */
- let target_node = anchor_node;
+export function hydrate_block_anchor(node) {
+ if (!hydrating) return;
- if (is_controlled) {
- target_node = /** @type {Node} */ (target_node.firstChild);
- }
- if (target_node.nodeType === 8) {
- // @ts-ignore
- let fragment = target_node.$$fragment;
- if (fragment === undefined) {
- fragment = get_hydration_fragment(target_node);
- } else {
- schedule_task(() => {
- // @ts-expect-error clean up memory
- target_node.$$fragment = undefined;
- });
- }
- set_current_hydration_fragment(fragment);
+ if (node.nodeType === 8) {
+ // @ts-ignore
+ let fragment = node.$$fragment;
+ if (fragment === undefined) {
+ fragment = get_hydration_fragment(node);
} else {
- const first_child = /** @type {Element | null} */ (target_node.firstChild);
- set_current_hydration_fragment(first_child === null ? [] : [first_child]);
+ schedule_task(() => {
+ // @ts-expect-error clean up memory
+ node.$$fragment = undefined;
+ });
}
+ set_current_hydration_fragment(fragment);
+ } else {
+ const first_child = /** @type {Element | null} */ (node.firstChild);
+ set_current_hydration_fragment(first_child === null ? [] : [first_child]);
}
}
diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js
index becdac20d9..ebd06796bf 100644
--- a/packages/svelte/src/internal/client/dom/reconciler.js
+++ b/packages/svelte/src/internal/client/dom/reconciler.js
@@ -30,30 +30,21 @@ export function create_fragment_with_script_from_html(html) {
/**
* @param {Array | import('../types.js').TemplateNode} current
- * @param {null | Element} parent_element
- * @param {null | Text | Element | Comment} sibling
+ * @param {Text | Element | Comment} sibling
* @returns {Text | Element | Comment}
*/
-export function insert(current, parent_element, sibling) {
+export function insert(current, sibling) {
+ if (!current) return sibling;
+
if (is_array(current)) {
- var i = 0;
- var node;
- for (; i < current.length; i++) {
- node = current[i];
- if (sibling === null) {
- append_child(/** @type {Element} */ (parent_element), /** @type {Node} */ (node));
- } else {
- sibling.before(/** @type {Node} */ (node));
- }
+ for (var i = 0; i < current.length; i++) {
+ sibling.before(/** @type {Node} */ (current[i]));
}
+
return current[0];
- } else if (current !== null) {
- if (sibling === null) {
- append_child(/** @type {Element} */ (parent_element), /** @type {Node} */ (current));
- } else {
- sibling.before(/** @type {Node} */ (current));
- }
}
+
+ sibling.before(/** @type {Node} */ (current));
return /** @type {Text | Element | Comment} */ (current);
}
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index 6ec040a317..24a0ebb6e0 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -90,7 +90,7 @@ export function svg_template_with_script(svg, return_fragment) {
function open_template(is_fragment, use_clone_node, anchor, template_element_fn) {
if (hydrating) {
if (anchor !== null) {
- hydrate_block_anchor(anchor, false);
+ hydrate_block_anchor(anchor);
}
// In ssr+hydration optimization mode, we might remove the template_element,
// so we need to is_fragment flag to properly handle hydrated content accordingly.
@@ -181,18 +181,18 @@ export function comment(anchor) {
* @returns {void}
*/
function close_template(dom, is_fragment, anchor) {
- const block = /** @type {import('#client').Block} */ (current_block);
-
/** @type {import('#client').TemplateNode | Array} */
- const current = is_fragment
+ var current = is_fragment
? is_array(dom)
? dom
: /** @type {import('#client').TemplateNode[]} */ (Array.from(dom.childNodes))
: dom;
+
if (!hydrating && anchor !== null) {
- insert(current, null, anchor);
+ insert(current, anchor);
}
- block.d = current;
+
+ /** @type {import('#client').Block} */ (current_block).d = current;
}
/**
diff --git a/packages/svelte/src/internal/client/loop.js b/packages/svelte/src/internal/client/loop.js
index 26bd3bece3..1d3aec1be4 100644
--- a/packages/svelte/src/internal/client/loop.js
+++ b/packages/svelte/src/internal/client/loop.js
@@ -1,19 +1,22 @@
import { raf } from './timing.js';
-const tasks = new Set();
+// TODO move this into timing.js where it probably belongs
/**
* @param {number} now
* @returns {void}
*/
function run_tasks(now) {
- tasks.forEach((task) => {
+ raf.tasks.forEach((task) => {
if (!task.c(now)) {
- tasks.delete(task);
+ raf.tasks.delete(task);
task.f();
}
});
- if (tasks.size !== 0) raf.tick(run_tasks);
+
+ if (raf.tasks.size !== 0) {
+ raf.tick(run_tasks);
+ }
}
/**
@@ -25,13 +28,17 @@ function run_tasks(now) {
export function loop(callback) {
/** @type {import('./types.js').TaskEntry} */
let task;
- if (tasks.size === 0) raf.tick(run_tasks);
+
+ if (raf.tasks.size === 0) {
+ raf.tick(run_tasks);
+ }
+
return {
promise: new Promise((fulfill) => {
- tasks.add((task = { c: callback, f: fulfill }));
+ raf.tasks.add((task = { c: callback, f: fulfill }));
}),
abort() {
- tasks.delete(task);
+ raf.tasks.delete(task);
}
};
}
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 4aef848b32..ebe1a422cb 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -1,22 +1,34 @@
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,
+ IS_ELSEIF
+} from '../constants.js';
import { set } from './sources.js';
+import { noop } from '../../common.js';
/**
* @param {import('./types.js').EffectType} type
- * @param {(() => void | (() => void)) | ((b: import('#client').Block) => void | (() => void))} fn
+ * @param {(() => void | (() => void))} fn
* @param {boolean} sync
* @param {null | import('#client').Block} block
* @param {boolean} init
@@ -25,6 +37,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 +47,8 @@ function create_effect(type, fn, sync, block = current_block, init = true) {
deriveds: null,
teardown: null,
ctx: current_component_context,
- ondestroy: null
+ ondestroy: null,
+ transitions: null
};
if (current_effect !== null) {
@@ -201,8 +215,7 @@ export function invalidate_effect(fn) {
}
/**
- * @template {import('#client').Block} B
- * @param {(block: B) => void | (() => void)} fn
+ * @param {(() => void)} fn
* @param {any} block
* @param {any} managed
* @param {any} sync
@@ -217,22 +230,128 @@ export function render_effect(fn, block = current_block, managed = false, sync =
}
/**
- * @param {import('#client').Effect} signal
+ * @param {import('#client').Effect} effect
* @returns {void}
*/
-export function destroy_effect(signal) {
- destroy_children(signal);
- remove_reactions(signal, 0);
- set_signal_status(signal, DESTROYED);
-
- signal.teardown?.();
- signal.ondestroy?.();
- signal.fn =
- signal.effects =
- signal.teardown =
- signal.ondestroy =
- signal.ctx =
- signal.block =
- signal.deps =
+export function destroy_effect(effect) {
+ destroy_children(effect);
+ remove_reactions(effect, 0);
+ set_signal_status(effect, DESTROYED);
+
+ if (effect.transitions) {
+ for (const transition of effect.transitions) {
+ transition.stop();
+ }
+ }
+
+ effect.teardown?.();
+ effect.ondestroy?.();
+
+ // @ts-expect-error
+ effect.fn =
+ effect.effects =
+ effect.teardown =
+ effect.ondestroy =
+ effect.ctx =
+ effect.block =
+ effect.deps =
null;
}
+
+/**
+ * When a block effect is removed, we don't immediately destroy it or yank it
+ * out of the DOM, because it might have transitions. Instead, we 'pause' it.
+ * It stays around (in memory, and in the DOM) until outro transitions have
+ * completed, and if the state change is reversed then we _resume_ it.
+ * A paused effect does not update, and the DOM subtree becomes inert.
+ * @param {import('#client').Effect} effect
+ * @param {() => void} callback
+ */
+export function pause_effect(effect, callback = noop) {
+ /** @type {import('#client').TransitionManager[]} */
+ 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.out(check);
+ }
+ } else {
+ destroy_effect(effect);
+ callback();
+ }
+}
+
+/**
+ * @param {import('#client').Effect} effect
+ * @param {import('#client').TransitionManager[]} transitions
+ * @param {boolean} local
+ */
+function pause_children(effect, transitions, local) {
+ if ((effect.f & INERT) !== 0) return;
+ effect.f ^= INERT;
+
+ if (effect.transitions !== null) {
+ for (const transition of effect.transitions) {
+ if (transition.is_global || local) {
+ transitions.push(transition);
+ }
+ }
+ }
+
+ if (effect.effects !== null) {
+ for (const child of effect.effects) {
+ var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & MANAGED) !== 0;
+ pause_children(child, transitions, transparent ? local : false);
+ }
+ }
+}
+
+/**
+ * The opposite of `pause_effect`. We call this if (for example)
+ * `x` becomes falsy then truthy: `{#if x}...{/if}`
+ * @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 ((effect.f & INERT) === 0) return;
+ effect.f ^= INERT;
+
+ // If a dependency of this effect changed while it was paused,
+ // apply the change now
+ if (check_dirtiness(effect)) {
+ execute_effect(effect);
+ }
+
+ if (effect.effects !== null) {
+ for (const child of effect.effects) {
+ var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & MANAGED) !== 0;
+ resume_children(child, transparent ? local : false);
+ }
+ }
+
+ if (effect.transitions !== null) {
+ for (const transition of effect.transitions) {
+ if (transition.is_global || local) {
+ transition.in();
+ }
+ }
+ }
+}
diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts
index 7006a7a71f..c0aceff874 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -1,4 +1,4 @@
-import type { Block, ComponentContext, Equals } from '#client';
+import type { Block, ComponentContext, Equals, TransitionManager } from '#client';
import type { EFFECT, PRE_EFFECT, RENDER_EFFECT } from '../constants';
export type EffectType = typeof EFFECT | typeof PRE_EFFECT | typeof RENDER_EFFECT;
@@ -21,7 +21,7 @@ export interface Value extends Signal {
export interface Reaction extends Signal {
/** The reaction function */
- fn: null | Function;
+ fn: Function;
/** Signals that this signal reads from */
deps: null | Value[];
/** Effects created inside this signal */
@@ -36,6 +36,7 @@ export interface Derived extends Value, Reaction {
}
export interface Effect extends Reaction {
+ parent: Effect | null;
/** The block associated with this effect */
block: null | Block;
/** The associated component context */
@@ -43,11 +44,13 @@ export interface Effect extends Reaction {
/** Stuff to do when the effect is destroyed */
ondestroy: null | (() => void);
/** The effect function */
- fn: null | (() => void | (() => void)) | ((b: Block, s: Signal) => void | (() => void));
+ fn: () => void | (() => void);
/** The teardown function returned from the effect function */
teardown: null | (() => void);
/** The depth from the root signal, used for ordering render/pre-effects topologically **/
l: number;
+ /** Transition managers created with `$.transition` */
+ transitions: null | TransitionManager[];
}
export interface ValueDebug extends Value {
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index 70d8ba31a1..c6d20d4be7 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -12,7 +12,6 @@ import {
set_current_hydration_fragment
} from './dom/hydration.js';
import { array_from } from './utils.js';
-import { ROOT_BLOCK } from './constants.js';
import { handle_event_propagation } from './dom/elements/events.js';
/** @type {Set} */
@@ -21,6 +20,18 @@ export const all_registered_events = new Set();
/** @type {Set<(events: Array) => void>} */
export const root_event_handles = new Set();
+/**
+ * This is normally true — block effects should run their intro transitions —
+ * but is false during hydration and mounting (unless `options.intro` is `true`)
+ * and when creating the children of a `` that just changed tag
+ */
+export let should_intro = true;
+
+/** @param {boolean} value */
+export function set_should_intro(value) {
+ should_intro = value;
+}
+
/**
* @param {Element} dom
* @param {() => string} value
@@ -51,19 +62,19 @@ export function text(dom, value) {
}
/**
- * @param {Comment} anchor_node
+ * @param {Comment} anchor
* @param {void | ((anchor: Comment, slot_props: Record) => void)} slot_fn
* @param {Record} slot_props
* @param {null | ((anchor: Comment) => void)} fallback_fn
*/
-export function slot(anchor_node, slot_fn, slot_props, fallback_fn) {
- hydrate_block_anchor(anchor_node);
+export function slot(anchor, slot_fn, slot_props, fallback_fn) {
+ hydrate_block_anchor(anchor);
if (slot_fn === undefined) {
if (fallback_fn !== null) {
- fallback_fn(anchor_node);
+ fallback_fn(anchor);
}
} else {
- slot_fn(anchor_node, slot_props);
+ slot_fn(anchor, slot_props);
}
}
@@ -198,6 +209,8 @@ function _mount(Component, options) {
const registered_events = new Set();
const container = options.target;
+ should_intro = options.intro ?? false;
+
/** @type {import('#client').RootBlock} */
const block = {
// dom
@@ -207,11 +220,7 @@ function _mount(Component, options) {
// intro
i: options.intro || false,
// parent
- p: null,
- // transition
- r: null,
- // type
- t: ROOT_BLOCK
+ p: null
};
/** @type {Exports} */
@@ -246,6 +255,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);
+ should_intro = true;
+
/** @param {Array} events */
const event_handle = (events) => {
for (let i = 0; i < events.length; i++) {
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 745342bf6e..fe2c323c32 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -21,7 +21,8 @@ import {
DESTROYED,
INERT,
MANAGED,
- STATE_SYMBOL
+ STATE_SYMBOL,
+ EFFECT_RAN
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
@@ -54,9 +55,19 @@ let flush_count = 0;
/** @type {null | import('./types.js').Reaction} */
export let current_reaction = null;
+/** @param {null | import('./types.js').Reaction} reaction */
+export function set_current_reaction(reaction) {
+ current_reaction = reaction;
+}
+
/** @type {null | import('./types.js').Effect} */
export let current_effect = null;
+/** @param {null | import('./types.js').Effect} effect */
+export function set_current_effect(effect) {
+ current_effect = effect;
+}
+
/** @type {null | import('./types.js').Value[]} */
export let current_dependencies = null;
let current_dependencies_index = 0;
@@ -104,6 +115,11 @@ export let current_block = null;
/** @type {import('./types.js').ComponentContext | null} */
export let current_component_context = null;
+/** @param {import('./types.js').ComponentContext | null} context */
+export function set_current_component_context(context) {
+ current_component_context = context;
+}
+
/** @returns {boolean} */
export function is_runes() {
return current_component_context !== null && current_component_context.r;
@@ -147,7 +163,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) {
@@ -200,7 +216,6 @@ function check_dirtiness(reaction) {
export function execute_reaction_fn(signal) {
const fn = signal.fn;
const flags = signal.f;
- const is_render_effect = (flags & RENDER_EFFECT) !== 0;
const previous_dependencies = current_dependencies;
const previous_dependencies_index = current_dependencies_index;
@@ -217,19 +232,7 @@ export function execute_reaction_fn(signal) {
current_untracking = false;
try {
- let res;
- if (is_render_effect) {
- res = /** @type {(block: import('#client').Block, signal: import('#client').Signal) => V} */ (
- fn
- )(
- /** @type {import('#client').Block} */ (
- /** @type {import('#client').Effect} */ (signal).block
- ),
- /** @type {import('#client').Signal} */ (signal)
- );
- } else {
- res = /** @type {() => V} */ (fn)();
- }
+ let res = fn();
let dependencies = /** @type {import('./types.js').Value[]} **/ (signal.deps);
if (current_dependencies !== null) {
let i;
@@ -548,6 +551,8 @@ export function schedule_effect(signal, sync) {
}
}
}
+
+ signal.f |= EFFECT_RAN;
}
/**
diff --git a/packages/svelte/src/internal/client/timing.js b/packages/svelte/src/internal/client/timing.js
index 161e92a733..241fdcec73 100644
--- a/packages/svelte/src/internal/client/timing.js
+++ b/packages/svelte/src/internal/client/timing.js
@@ -9,5 +9,6 @@ const now = is_client ? () => performance.now() : () => Date.now();
/** @type {import('./types.js').Raf} */
export const raf = {
tick: /** @param {any} _ */ (_) => request_animation_frame(_),
- now: () => now()
+ now: () => now(),
+ tasks: new Set()
};
diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts
index e4f776715a..489f786b23 100644
--- a/packages/svelte/src/internal/client/types.d.ts
+++ b/packages/svelte/src/internal/client/types.d.ts
@@ -1,16 +1,4 @@
-import {
- ROOT_BLOCK,
- EACH_BLOCK,
- EACH_ITEM_BLOCK,
- IF_BLOCK,
- AWAIT_BLOCK,
- KEY_BLOCK,
- HEAD_BLOCK,
- DYNAMIC_COMPONENT_BLOCK,
- DYNAMIC_ELEMENT_BLOCK,
- SNIPPET_BLOCK,
- STATE_SYMBOL
-} from './constants.js';
+import { STATE_SYMBOL } from './constants.js';
import type { Effect, Source, Value } from './reactivity/types.js';
type EventCallback = (event: Event) => boolean;
@@ -59,42 +47,8 @@ export type ComponentContext = {
export type Equals = (this: Value, value: unknown) => boolean;
-export type BlockType =
- | typeof ROOT_BLOCK
- | typeof EACH_BLOCK
- | typeof EACH_ITEM_BLOCK
- | typeof IF_BLOCK
- | typeof AWAIT_BLOCK
- | typeof KEY_BLOCK
- | typeof SNIPPET_BLOCK
- | typeof HEAD_BLOCK
- | typeof DYNAMIC_COMPONENT_BLOCK
- | typeof DYNAMIC_ELEMENT_BLOCK;
-
export type TemplateNode = Text | Element | Comment;
-export type Transition = {
- /** effect */
- e: Effect;
- /** payload */
- p: null | TransitionPayload;
- /** init */
- i: (from?: DOMRect) => TransitionPayload;
- /** finished */
- f: (fn: () => void) => void;
- in: () => void;
- /** out */
- o: () => void;
- /** cancel */
- c: () => void;
- /** cleanup */
- x: () => void;
- /** direction */
- r: 'in' | 'out' | 'both' | 'key';
- /** dom */
- d: HTMLElement;
-};
-
export type RootBlock = {
/** dom */
d: null | TemplateNode | Array;
@@ -104,10 +58,6 @@ export type RootBlock = {
i: boolean;
/** parent */
p: null;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** type */
- t: typeof ROOT_BLOCK;
};
export type IfBlock = {
@@ -119,31 +69,6 @@ export type IfBlock = {
e: null | Effect;
/** parent */
p: Block;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** consequent transitions */
- c: null | Set;
- /** alternate transitions */
- a: null | Set;
- /** effect */
- ce: null | Effect;
- /** effect */
- ae: null | Effect;
- /** type */
- t: typeof IF_BLOCK;
-};
-
-export type KeyBlock = {
- /** dom */
- d: null | TemplateNode | Array;
- /** effect */
- e: null | Effect;
- /** parent */
- p: Block;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** type */
- t: typeof KEY_BLOCK;
};
export type HeadBlock = {
@@ -153,10 +78,6 @@ export type HeadBlock = {
e: null | Effect;
/** parent */
p: Block;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** type */
- t: typeof HEAD_BLOCK;
};
export type DynamicElementBlock = {
@@ -166,10 +87,6 @@ export type DynamicElementBlock = {
e: null | Effect;
/** parent */
p: Block;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** type */
- t: typeof DYNAMIC_ELEMENT_BLOCK;
};
export type DynamicComponentBlock = {
@@ -179,10 +96,6 @@ export type DynamicComponentBlock = {
e: null | Effect;
/** parent */
p: Block;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** type */
- t: typeof DYNAMIC_COMPONENT_BLOCK;
};
export type AwaitBlock = {
@@ -194,15 +107,9 @@ export type AwaitBlock = {
p: Block;
/** pending */
n: boolean;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** type */
- t: typeof AWAIT_BLOCK;
};
export type EachBlock = {
- /** anchor */
- a: Element | Comment;
/** flags */
f: number;
/** dom */
@@ -213,35 +120,21 @@ export type EachBlock = {
e: null | Effect;
/** parent */
p: Block;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** transitions */
- s: Array;
- /** type */
- t: typeof EACH_BLOCK;
};
export type EachItemBlock = {
- /** transition */
- a: null | ((block: EachItemBlock, transitions: Set) => void);
+ /** animation manager */
+ a: AnimationManager | null;
/** dom */
d: null | TemplateNode | Array;
/** effect */
- e: null | Effect;
+ e: Effect;
/** item */
v: any | Source;
/** index */
i: number | Source;
/** key */
k: unknown;
- /** parent */
- p: EachBlock;
- /** transition */
- r: null | ((transition: Transition) => void);
- /** transitions */
- s: null | Set;
- /** type */
- t: typeof EACH_ITEM_BLOCK;
};
export type SnippetBlock = {
@@ -251,10 +144,6 @@ export type SnippetBlock = {
p: Block;
/** effect */
e: null | Effect;
- /** transition */
- r: null;
- /** type */
- t: typeof SNIPPET_BLOCK;
};
export type Block =
@@ -264,25 +153,54 @@ export type Block =
| DynamicElementBlock
| DynamicComponentBlock
| HeadBlock
- | KeyBlock
| EachBlock
| EachItemBlock
| SnippetBlock;
+export interface TransitionManager {
+ /** Whether the `global` modifier was used (i.e. `transition:fade|global`) */
+ is_global: boolean;
+ /** Called inside `resume_effect` */
+ in: () => void;
+ /** Called inside `pause_effect` */
+ out: (callback?: () => void) => void;
+ /** Called inside `destroy_effect` */
+ stop: () => void;
+}
+
+export interface AnimationManager {
+ /** An element with an `animate:` directive */
+ element: Element;
+ /** Called during keyed each block reconciliation, before updates */
+ measure: () => void;
+ /** Called during keyed each block reconciliation, after updates — this triggers the animation */
+ apply: () => void;
+}
+
+export interface Animation {
+ /** Abort the animation */
+ abort: () => void;
+ /** Allow the animation to continue running, but remove any callback. This prevents the removal of an outroing block if the corresponding intro has a `delay` */
+ deactivate: () => void;
+ /** Resets an animation to its starting state, if it uses `tick`. Exposed as a separate method so that an aborted `out:` can still reset even if the `outro` had already completed */
+ reset: () => void;
+ /** Get the `t` value (between `0` and `1`) of the animation, so that its counterpart can start from the right place */
+ t: (now: number) => number;
+}
+
export type TransitionFn = (
element: Element,
props: P,
options: { direction?: 'in' | 'out' | 'both' }
-) => TransitionPayload;
+) => AnimationConfig | ((options: { direction?: 'in' | 'out' }) => AnimationConfig);
export type AnimateFn
= (
element: Element,
rects: { from: DOMRect; to: DOMRect },
- props: P,
- options: {}
-) => TransitionPayload;
+ props: P
+) => AnimationConfig;
-export type TransitionPayload = {
+export type AnimationConfig = {
delay?: number;
duration?: number;
easing?: (t: number) => number;
@@ -307,15 +225,17 @@ export type Render = {
d: null | TemplateNode | Array;
/** effect */
e: null | Effect;
- /** transitions */
- s: Set;
/** prev */
p: Render | null;
};
export type Raf = {
+ /** Alias for `requestAnimationFrame`, exposed in such a way that we can override in tests */
tick: (callback: (time: DOMHighResTimeStamp) => void) => any;
+ /** Alias for `performance.now()`, exposed in such a way that we can override in tests */
now: () => number;
+ /** A set of tasks that will run to completion, unless aborted */
+ tasks: Set;
};
export interface Task {
diff --git a/packages/svelte/tests/animation-helpers.js b/packages/svelte/tests/animation-helpers.js
index 94bb1839a0..a941a643cc 100644
--- a/packages/svelte/tests/animation-helpers.js
+++ b/packages/svelte/tests/animation-helpers.js
@@ -14,6 +14,7 @@ export const raf = {
raf.ticks.add(f);
};
svelte_raf.now = () => raf.time;
+ svelte_raf.tasks.clear();
}
};
@@ -31,125 +32,144 @@ function tick(time) {
}
class Animation {
+ #target;
#keyframes;
#duration;
- #timeline_offset;
- #reversed;
- #target;
- #paused;
+
+ #offset = raf.time;
+
+ #finished = () => {};
+ #cancelled = () => {};
+
+ currentTime = 0;
/**
* @param {HTMLElement} target
* @param {Keyframe[]} keyframes
- * @param {{duration?: number}} options
+ * @param {{ duration: number }} options // TODO add delay
*/
- constructor(target, keyframes, options = {}) {
+ constructor(target, keyframes, { duration }) {
this.#target = target;
this.#keyframes = keyframes;
- this.#duration = options.duration || 0;
- this.#timeline_offset = 0;
- this.#reversed = false;
- this.#paused = false;
- this.onfinish = () => {};
- this.pending = true;
- this.currentTime = 0;
- this.playState = 'running';
- this.effect = {
- setKeyframes: (/** @type {Keyframe[]} */ keyframes) => {
- this.#keyframes = keyframes;
+ this.#duration = duration;
+
+ // 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() {
- this.#paused = false;
- raf.animations.add(this);
- this.playState = 'running';
this._update();
}
_update() {
- if (this.#reversed) {
- if (this.#timeline_offset === 0) {
- this.currentTime = this.#duration - raf.time;
- } else {
- this.currentTime = this.#timeline_offset + (this.#timeline_offset - raf.time);
- }
- } else {
- this.currentTime = raf.time - this.#timeline_offset;
- }
+ this.currentTime = raf.time - this.#offset;
const target_frame = this.currentTime / this.#duration;
- this._applyKeyFrame(target_frame);
+ this.#apply_keyframe(target_frame);
+
+ if (this.currentTime >= this.#duration) {
+ this.#finished();
+ raf.animations.delete(this);
+ }
}
/**
- * @param {number} target_frame
+ * @param {number} t
*/
- _applyKeyFrame(target_frame) {
- const keyframes = this.#keyframes;
- const keyframes_size = keyframes.length - 1;
- const frame = keyframes[Math.min(keyframes_size, Math.floor(keyframes.length * target_frame))];
+ #apply_keyframe(t) {
+ const n = Math.min(1, Math.max(0, t)) * (this.#keyframes.length - 1);
+
+ const lower = this.#keyframes[Math.floor(n)];
+ const upper = this.#keyframes[Math.ceil(n)];
+
+ let frame = lower;
+ if (lower !== upper) {
+ frame = {};
+
+ for (const key in lower) {
+ frame[key] = interpolate(
+ /** @type {string} */ (lower[key]),
+ /** @type {string} */ (upper[key]),
+ n % 1
+ );
+ }
+ }
+
for (let prop in frame) {
// @ts-ignore
this.#target.style[prop] = frame[prop];
}
- if (this.#reversed) {
- if (this.currentTime <= 0) {
- this.finish();
- for (let prop in frame) {
- // @ts-ignore
- this.#target.style[prop] = null;
- }
- }
- } else {
- if (this.currentTime >= this.#duration) {
- this.finish();
- for (let prop in frame) {
- // @ts-ignore
- this.#target.style[prop] = null;
- }
- }
- }
- }
- finish() {
- this.onfinish();
- this.currentTime = this.#reversed ? 0 : this.#duration;
- if (this.#reversed) {
- raf.animations.delete(this);
+ if (this.currentTime >= this.#duration) {
+ this.currentTime = this.#duration;
+ for (let prop in frame) {
+ // @ts-ignore
+ this.#target.style[prop] = null;
+ }
}
- this.playState = 'idle';
}
cancel() {
- this.#paused = true;
if (this.currentTime > 0 && this.currentTime < this.#duration) {
- this._applyKeyFrame(this.#reversed ? this.#keyframes.length - 1 : 0);
+ this.#apply_keyframe(0);
}
- }
- pause() {
- this.#paused = true;
- this.playState = 'paused';
+ this.#cancelled();
+ raf.animations.delete(this);
}
+}
+
+/**
+ * @param {string} a
+ * @param {string} b
+ * @param {number} p
+ */
+function interpolate(a, b, p) {
+ if (a === b) return a;
+
+ const fallback = p < 0.5 ? a : b;
+
+ const a_match = a.match(/[\d.]+|[^\d.]+/g);
+ const b_match = b.match(/[\d.]+|[^\d.]+/g);
+
+ if (!a_match || !b_match) return fallback;
+ if (a_match.length !== b_match.length) return fallback;
- reverse() {
- if (this.#paused && !raf.animations.has(this)) {
- raf.animations.add(this);
+ let result = '';
+
+ for (let i = 0; i < a_match.length; i += 2) {
+ const a_num = parseFloat(a_match[i]);
+ const b_num = parseFloat(b_match[i]);
+ result += a_num + (b_num - a_num) * p;
+
+ if (a_match[i + 1] !== b_match[i + 1]) {
+ // bail
+ return fallback;
}
- this.#timeline_offset = this.currentTime;
- this.#reversed = !this.#reversed;
- this.playState = 'running';
+
+ result += a_match[i + 1] ?? '';
}
+
+ return result;
}
/**
* @param {Keyframe[]} keyframes
- * @param {{duration?: number}} options
+ * @param {{duration: number}} options
* @returns {globalThis.Animation}
*/
HTMLElement.prototype.animate = function (keyframes, options) {
const animation = new Animation(this, keyframes, options);
+ raf.animations.add(animation);
// @ts-ignore
return animation;
};
diff --git a/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/Component.svelte
index be0c7e36b0..f1f5971a06 100644
--- a/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/Component.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/Component.svelte
@@ -1,11 +1,13 @@
diff --git a/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/main.svelte b/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/main.svelte
index 245304be83..d8dc75288b 100644
--- a/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/main.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/main.svelte
@@ -6,7 +6,7 @@
let resolve;
let value = 0;
export let logs = [];
-
+
async function new_promise() {
promise = new Promise(r => {
resolve = r;
@@ -20,7 +20,7 @@
export async function test() {
resolve_promise();
- await Promise.resolve();
+ await tick();
new_promise();
resolve_promise();
return tick();
@@ -33,4 +33,4 @@
Loading...
{:then state}
-{/await}
\ No newline at end of file
+{/await}
diff --git a/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js b/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js
index 3df138e99a..6e3f7a3cb7 100644
--- a/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js
@@ -18,7 +18,7 @@ export default test({
raf.tick(150);
assert.htmlEqual(
target.innerHTML,
- 'foo
bar
'
+ 'foo
bar
'
);
component.open = true;
raf.tick(250);
diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js
index 6315a32d86..3d127f1375 100644
--- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js
@@ -23,7 +23,6 @@ export default test({
`,
test({ assert, component, target, raf }) {
- raf.tick(0);
component.tag = 'p';
assert.equal(target.querySelectorAll('p').length, 5);
@@ -52,11 +51,11 @@ export default test({
];
divs = target.querySelectorAll('div');
- assert.ok(~divs[0].style.transform);
- assert.equal(divs[1].style.transform, 'translate(1px, 0px)');
- assert.equal(divs[2].style.transform, 'translate(1px, 0px)');
- assert.equal(divs[3].style.transform, 'translate(1px, 0px)');
- assert.ok(~divs[4].style.transform);
+ assert.equal(divs[0].style.transform, 'translate(0px, 120px)');
+ assert.equal(divs[1].style.transform, '');
+ assert.equal(divs[2].style.transform, '');
+ assert.equal(divs[3].style.transform, '');
+ assert.equal(divs[4].style.transform, 'translate(0px, -120px)');
raf.tick(100);
assert.deepEqual([divs[0].style.transform, divs[4].style.transform], ['', '']);
diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/main.svelte
index 596d12c77a..0fa9fecf30 100644
--- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/main.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/main.svelte
@@ -8,11 +8,11 @@
return {
duration: 100,
- css: (t, u) => `transform: translate(${u + dx}px, ${u * dy}px)`
+ css: (t, u) => `transform: translate(${u * dx}px, ${u * dy}px)`
};
}
{#each things as thing (thing.id)}
{thing.name}
-{/each}
\ No newline at end of file
+{/each}
diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-transition/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-transition/_config.js
index 842a3f8f5f..6b55bcf44c 100644
--- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-transition/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-transition/_config.js
@@ -15,8 +15,9 @@ export default test({
assert.equal(h1.style.opacity, '');
assert.equal(h2.style.opacity, '');
- raf.tick(50);
+ raf.tick(200);
component.visible = false;
- assert.equal(h2.style.opacity, '0.49998000000000004');
+ raf.tick(250);
+ assert.equal(h2.style.opacity, '0.5');
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-css-deferred-removal/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-css-deferred-removal/_config.js
index 99f60053d4..909c93d969 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-css-deferred-removal/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-css-deferred-removal/_config.js
@@ -1,4 +1,4 @@
-import { ok, test } from '../../test';
+import { test } from '../../test';
export default test({
get props() {
@@ -8,14 +8,11 @@ export default test({
test({ assert, component, target, raf }) {
component.visible = false;
+ raf.tick(150);
+
const outer = /** @type {HTMLSpanElement} */ (target.querySelector('.outer'));
const inner = /** @type {HTMLSpanElement} */ (target.querySelector('.inner'));
- raf.tick(150);
-
- assert.deepEqual(
- [outer.style.cssText, inner.style.cssText],
- ['opacity: 0.24999000000000002;', '']
- );
+ assert.deepEqual([outer.style.cssText, inner.style.cssText], ['opacity: 0.25;', '']);
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-css-duration/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-css-duration/_config.js
index d681f74c7c..6567c552e1 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-css-duration/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-css-duration/_config.js
@@ -6,14 +6,12 @@ export default test({
const div = target.querySelector('div');
ok(div);
- raf.tick(25);
-
- assert.equal(div.style.opacity, '0.16666');
+ raf.tick(50);
+ assert.equal(div.style.opacity, '0.5');
component.visible = false;
- raf.tick(40);
-
- assert.ok(div.style.opacity === '0');
+ raf.tick(75);
+ assert.equal(div.style.opacity, '0.25');
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-css-iframe/Foo.svelte b/packages/svelte/tests/runtime-legacy/samples/transition-css-iframe/Foo.svelte
index c79672f21b..bca4e93daa 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-css-iframe/Foo.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-css-iframe/Foo.svelte
@@ -13,4 +13,4 @@
{#if visible}
-{/if}
\ No newline at end of file
+{/if}
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-css-iframe/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-css-iframe/_config.js
index 271ae50883..255de7dca7 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-css-iframe/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-css-iframe/_config.js
@@ -5,6 +5,7 @@ export default test({
async test({ assert, component, target, raf }) {
const frame = /** @type {HTMLIFrameElement} */ (target.querySelector('iframe'));
await tick();
+ await tick(); // TODO investigate why this second tick is necessary. without it, `Foo.svelte` initializes with `visible = true`, incorrectly
component.visible = true;
const div = frame.contentDocument?.querySelector('div');
@@ -14,8 +15,10 @@ export default test({
component.visible = false;
- raf.tick(26);
- // The exact number doesn't matter here, this test is about ensuring that transitions work in iframes
- assert.equal(Number(div.style.opacity).toFixed(4), '0.8333');
+ raf.tick(25);
+ assert.equal(div.style.opacity, '0.25');
+
+ raf.tick(35);
+ assert.equal(div.style.opacity, '0.18333333333333335');
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-css-in-out-in/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-css-in-out-in/_config.js
index 7a48af8b5b..3d87dc6e10 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-css-in-out-in/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-css-in-out-in/_config.js
@@ -6,18 +6,27 @@ export default test({
const div = target.querySelector('div');
ok(div);
- assert.equal(div.style.opacity, '0');
+ assert.equal(div.style.scale, '0');
raf.tick(50);
component.visible = false;
// both in and out styles
- assert.equal(div.style.opacity, '0.49998000000000004');
+ assert.equal(div.style.scale, '0.5');
+ assert.equal(div.style.opacity, '1');
+ assert.equal(div.style.rotate, '360deg');
raf.tick(75);
+
+ assert.equal(div.style.scale, '0.75'); // intro continues while outro plays
+ assert.equal(div.style.opacity, '0.75');
+ assert.equal(div.style.rotate, '270deg');
+
component.visible = true;
// reset original styles
+ assert.equal(div.style.scale, '0');
assert.equal(div.style.opacity, '1');
+ assert.equal(div.style.rotate, '360deg');
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-css-in-out-in/main.svelte b/packages/svelte/tests/runtime-legacy/samples/transition-css-in-out-in/main.svelte
index 5108452a39..b82c6da774 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-css-in-out-in/main.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-css-in-out-in/main.svelte
@@ -5,7 +5,7 @@
return {
duration: 100,
css: t => {
- return `opacity: ${t}`;
+ return `scale: ${t}`;
}
};
}
@@ -14,7 +14,7 @@
return {
duration: 100,
css: t => {
- return `opacity: ${t}`;
+ return `rotate: ${t * 360}deg; opacity: ${t}`;
}
};
}
@@ -22,4 +22,4 @@
{#if visible}
-{/if}
\ No newline at end of file
+{/if}
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-inert/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-inert/_config.js
index 8679b2fe7f..a474fae9d4 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-inert/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-inert/_config.js
@@ -6,29 +6,26 @@ export default test({
const b = target.querySelector('button.b');
ok(a);
ok(b);
- // jsdom doesn't set the inert attribute, and the transition checks if it exists, so set it manually to trigger the inert logic
- a.inert = false;
- b.inert = false;
// check and abort halfway through the outro transition
component.visible = false;
raf.tick(50);
- assert.strictEqual(target.querySelector('button.a')?.inert, true);
- assert.strictEqual(target.querySelector('button.b')?.inert, true);
+ assert.ok(target.querySelector('button.a')?.inert);
+ assert.ok(target.querySelector('button.b')?.inert);
component.visible = true;
- assert.strictEqual(target.querySelector('button.a')?.inert, false);
- assert.strictEqual(target.querySelector('button.b')?.inert, false);
+ assert.ok(!target.querySelector('button.a')?.inert);
+ assert.ok(!target.querySelector('button.b')?.inert);
// let it transition out completely and then back in
component.visible = false;
raf.tick(101);
component.visible = true;
raf.tick(150);
- assert.strictEqual(target.querySelector('button.a')?.inert, false);
- assert.strictEqual(target.querySelector('button.b')?.inert, false);
+ assert.ok(!target.querySelector('button.a')?.inert);
+ assert.ok(!target.querySelector('button.b')?.inert);
raf.tick(151);
- assert.strictEqual(target.querySelector('button.a')?.inert, false);
- assert.strictEqual(target.querySelector('button.b')?.inert, false);
+ assert.ok(!target.querySelector('button.a')?.inert);
+ assert.ok(!target.querySelector('button.b')?.inert);
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-js-aborted-outro-in-each/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-js-aborted-outro-in-each/_config.js
index a333fe2da5..3216f2a898 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-js-aborted-outro-in-each/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-js-aborted-outro-in-each/_config.js
@@ -23,8 +23,6 @@ export default test({
assert.equal(spans[1].foo, 0.25);
assert.equal(spans[2].foo, 0.75);
- raf.tick(7);
-
component.things = things;
raf.tick(225);
@@ -42,8 +40,8 @@ export default test({
target.querySelectorAll('span')
);
- assert.equal(spans[0].foo, undefined);
- assert.equal(spans[1].foo, undefined);
- assert.equal(spans[2].foo, undefined);
+ assert.equal(spans[0].foo, 1);
+ assert.equal(spans[1].foo, 1);
+ assert.equal(spans[2].foo, 1);
}
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block-outros/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block-outros/_config.js
index b578ef1431..29537d6ff5 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block-outros/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block-outros/_config.js
@@ -14,8 +14,6 @@ export default test({
intro: true,
async test({ assert, target, component, raf }) {
- raf.tick(0);
-
assert.htmlEqual(target.innerHTML, 'loading...
');
let time = 0;
@@ -24,10 +22,6 @@ export default test({
assert.htmlEqual(target.innerHTML, 'loading...
');
await fulfil(42);
- await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
assert.htmlEqual(
target.innerHTML,
@@ -61,9 +55,6 @@ export default test({
fulfil = f;
});
await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
assert.htmlEqual(
target.innerHTML,
@@ -83,11 +74,6 @@ export default test({
);
await fulfil(43);
- await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
-
assert.htmlEqual(
target.innerHTML,
`
@@ -109,9 +95,6 @@ export default test({
fulfil = f;
});
await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
assert.htmlEqual(
target.innerHTML,
@@ -132,11 +115,6 @@ export default test({
);
await fulfil(44);
- await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
-
assert.htmlEqual(
target.innerHTML,
`
@@ -159,10 +137,6 @@ export default test({
fulfil = f;
});
await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
-
assert.htmlEqual(
target.innerHTML,
`
@@ -172,7 +146,6 @@ export default test({
);
raf.tick((time += 40));
-
assert.htmlEqual(
target.innerHTML,
`
@@ -182,10 +155,6 @@ export default test({
);
await fulfil(45);
- await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
assert.htmlEqual(
target.innerHTML,
@@ -197,7 +166,6 @@ export default test({
);
raf.tick((time += 20));
-
assert.htmlEqual(
target.innerHTML,
`
@@ -211,9 +179,6 @@ export default test({
fulfil = f;
});
await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
assert.htmlEqual(
target.innerHTML,
@@ -238,10 +203,6 @@ export default test({
);
await fulfil(46);
- await Promise.resolve();
- await Promise.resolve();
-
- raf.tick(time);
assert.htmlEqual(
target.innerHTML,
@@ -255,7 +216,6 @@ export default test({
);
raf.tick((time += 10));
-
assert.htmlEqual(
target.innerHTML,
`
@@ -265,7 +225,6 @@ export default test({
);
raf.tick((time += 20));
-
assert.htmlEqual(
target.innerHTML,
`
@@ -274,7 +233,6 @@ export default test({
);
raf.tick((time += 70));
-
assert.htmlEqual(
target.innerHTML,
`
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block/_config.js
index 9ec5ba476b..706b306ebb 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block/_config.js
@@ -1,4 +1,4 @@
-import { ok, test } from '../../test';
+import { test } from '../../test';
/** @type {(value: any) => void} */
let fulfil;
@@ -36,8 +36,8 @@ export default test({
);
assert.equal(ps[0].className, 'pending');
assert.equal(ps[1].className, 'then');
- assert.equal(ps[0].foo, 0.8);
- assert.equal(ps[1].foo, undefined);
+ assert.equal(ps[0].foo, 0.2);
+ assert.equal(ps[1].foo, 0.3);
raf.tick(100);
});
}
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-js-each-block-intro-outro/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-js-each-block-intro-outro/_config.js
index 144dcf03bc..58de002579 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-js-each-block-intro-outro/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-js-each-block-intro-outro/_config.js
@@ -35,9 +35,9 @@ export default test({
component.visible = true;
raf.tick(100);
- assert.equal(divs[0].foo, 1);
- assert.equal(divs[1].foo, 1);
- assert.equal(divs[2].foo, 1);
+ assert.equal(divs[0].foo, 0.3);
+ assert.equal(divs[1].foo, 0.3);
+ assert.equal(divs[2].foo, 0.3);
assert.equal(divs[0].bar, 1);
assert.equal(divs[1].bar, 1);
diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-js-if-else-block-intro/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-js-if-else-block-intro/_config.js
index 130cfb9dde..0e8d3022d3 100644
--- a/packages/svelte/tests/runtime-legacy/samples/transition-js-if-else-block-intro/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/transition-js-if-else-block-intro/_config.js
@@ -15,7 +15,7 @@ export default test({
raf.tick(500);
component.x = true;
assert.equal(component.no, null);
- assert.equal(component.yes.foo, undefined);
+ assert.equal(component.yes.foo, 0);
raf.tick(700);
assert.equal(component.yes.foo, 0.5);
diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-transition/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-transition/_config.js
index f80737e8eb..9c741d2b8c 100644
--- a/packages/svelte/tests/runtime-runes/samples/dynamic-component-transition/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-transition/_config.js
@@ -11,7 +11,7 @@ export default test({
btn?.click();
});
- assert.htmlEqual(target.innerHTML, `Outside
`);
+ assert.htmlEqual(target.innerHTML, `Outside
`);
raf.tick(100);
diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-if-component-transition/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-if-component-transition/_config.js
index f80737e8eb..9c741d2b8c 100644
--- a/packages/svelte/tests/runtime-runes/samples/dynamic-if-component-transition/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/dynamic-if-component-transition/_config.js
@@ -11,7 +11,7 @@ export default test({
btn?.click();
});
- assert.htmlEqual(target.innerHTML, `Outside
`);
+ assert.htmlEqual(target.innerHTML, `Outside
`);
raf.tick(100);