diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index b77cf61112..c1a3fdd098 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -205,6 +205,10 @@ export default class Block { this.has_animation = true; } + group_transition_out(fn) { + return this.has_outros ? b`@group_transition_out((#transition_out) => { ${fn(x`#transition_out`)} })` : fn(null); + } + add_variable(id: Identifier, init?: Node) { if (this.variables.has(id.name)) { throw new Error( diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index 1efadfb90c..e32549a79e 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -7,6 +7,7 @@ import FragmentWrapper from './Fragment'; import { b, x } from 'code-red'; import ElseBlock from '../../nodes/ElseBlock'; import { Identifier, Node } from 'estree'; +import bit_state from '../../utils/bit_state' export class ElseBlockWrapper extends Wrapper { node: ElseBlock; @@ -423,25 +424,18 @@ export default class EachBlockWrapper extends Wrapper { const dynamic = this.block.has_update_method; - const destroy = this.node.has_animation - ? (this.block.has_outros - ? `@fix_and_outro_and_destroy_block` - : `@fix_and_destroy_block`) - : this.block.has_outros - ? `@outro_and_destroy_block` - : `@destroy_block`; + const transition_state = bit_state([dynamic, this.node.has_animation, this.block.has_outros]); + const update_keyed_each = (transition_out) => + b`${iterations} = @update_keyed_each(${iterations}, #dirty, #ctx, ${transition_state}, ${get_key}, ${this.vars.each_block_value}, ${lookup}, ${update_mount_node}, ${create_each_block}, ${update_anchor_node}, ${this.vars.get_each_context}, ${transition_out});`; if (this.dependencies.size) { this.updates.push(b` const ${this.vars.each_block_value} = ${snippet}; ${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`} - - ${this.block.has_outros && b`@group_outros();`} ${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].r();`} ${this.renderer.options.dev && b`@validate_each_keys(#ctx, ${this.vars.each_block_value}, ${this.vars.get_each_context}, ${get_key});`} - ${iterations} = @update_keyed_each(${iterations}, #dirty, ${get_key}, ${dynamic ? 1 : 0}, #ctx, ${this.vars.each_block_value}, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${update_anchor_node}, ${this.vars.get_each_context}); + ${this.block.group_transition_out(update_keyed_each)} ${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].a();`} - ${this.block.has_outros && b`@check_outros();`} `); } @@ -552,20 +546,11 @@ export default class EachBlockWrapper extends Wrapper { let remove_old_blocks; if (this.block.has_outros) { - const out = block.get_unique_name('out'); - - block.chunks.init.push(b` - const ${out} = i => @transition_out(${iterations}[i], 1, 1, () => { - ${iterations}[i] = null; - }); - `); - remove_old_blocks = b` - @group_outros(); - for (#i = ${data_length}; #i < ${view_length}; #i += 1) { - ${out}(#i); - } - @check_outros(); - `; + remove_old_blocks = this.block.group_transition_out((transition_out) => + b`for (#i = ${data_length}; #i < ${view_length}; #i += 1) { + ${transition_out}(#i); + }` + ) } else { remove_old_blocks = b` for (${this.block.has_update_method ? null : x`#i = ${data_length}`}; #i < ${this.block.has_update_method ? view_length : '#old_length'}; #i += 1) { diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 83bc8be94e..f8aae39a04 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -26,6 +26,7 @@ import { Identifier } from 'estree'; import EventHandler from './EventHandler'; import { extract_names } from 'periscopic'; import Action from '../../../nodes/Action'; +import Transition from '../../../nodes/Transition'; const events = [ { @@ -379,8 +380,18 @@ export default class ElementWrapper extends Wrapper { this.add_attributes(block); this.add_directives_in_order(block); - this.add_transitions(block); - this.add_animation(block); + const { intro, outro } = this.node; + if (intro || outro) { + if (intro === outro) { + this.add_bidi_transition(block, intro); + } else { + this.add_intro(block, intro, outro); + this.add_outro(block, intro, outro); + } + } + if (this.node.animation) { + this.add_animation(block, intro); + } this.add_classes(block); this.add_manual_style_scoping(block); @@ -728,165 +739,101 @@ export default class ElementWrapper extends Wrapper { } } - add_transitions( - block: Block - ) { - const { intro, outro } = this.node; - if (!intro && !outro) return; - - if (intro === outro) { - // bidirectional transition - const name = block.get_unique_name(`${this.var.name}_transition`); - const snippet = intro.expression - ? intro.expression.manipulate(block) - : x`{}`; - - block.add_variable(name); + add_bidi_transition(block: Block, intro: Transition) { + const name = block.get_unique_name(`${this.var.name}_transition`); + const snippet = intro.expression ? intro.expression.manipulate(block) : null; - const fn = this.renderer.reference(intro.name); + block.add_variable(name, x`@noop`); - const intro_block = b` - @add_render_callback(() => { - if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true); - ${name}.run(1); - }); - `; + const fn = this.renderer.reference(intro.name); - const outro_block = b` - if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, false); - ${name}.run(0); - `; + let intro_block = b`${name} = @run_bidirectional_transition(${this.var}, ${fn}, 1, ${snippet});`; + let outro_block = b`${name} = @run_bidirectional_transition(${this.var}, ${fn}, 2, ${snippet});`; - if (intro.is_local) { - block.chunks.intro.push(b` - if (#local) { - ${intro_block} - } - `); - - block.chunks.outro.push(b` - if (#local) { - ${outro_block} - } - `); - } else { - block.chunks.intro.push(intro_block); - block.chunks.outro.push(outro_block); - } - - block.chunks.destroy.push(b`if (detaching && ${name}) ${name}.end();`); + if (intro.is_local) { + intro_block = b`if (#local) {${intro_block}}`; + outro_block = b`if (#local) {${outro_block}}`; } + block.chunks.intro.push(intro_block); + block.chunks.outro.push(outro_block); - else { - const intro_name = intro && block.get_unique_name(`${this.var.name}_intro`); - const outro_name = outro && block.get_unique_name(`${this.var.name}_outro`); - - if (intro) { - block.add_variable(intro_name); - const snippet = intro.expression - ? intro.expression.manipulate(block) - : x`{}`; - - const fn = this.renderer.reference(intro.name); - - let intro_block; - - if (outro) { - intro_block = b` - @add_render_callback(() => { - if (${outro_name}) ${outro_name}.end(1); - if (!${intro_name}) ${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet}); - ${intro_name}.start(); - }); - `; - - block.chunks.outro.push(b`if (${intro_name}) ${intro_name}.invalidate();`); - } else { - intro_block = b` - if (!${intro_name}) { - @add_render_callback(() => { - ${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet}); - ${intro_name}.start(); - }); - } - `; - } - - if (intro.is_local) { - intro_block = b` - if (#local) { - ${intro_block} - } - `; + block.chunks.destroy.push(b`if (detaching) ${name}();`); + } + add_intro(block: Block, intro: Transition, outro: Transition) { + if (outro) { + const outro_var = block.alias(`${this.var.name}_outro`); + block.chunks.intro.push(b`${outro_var}(1);`); + } + if (this.node.animation) { + const [unfreeze_var, rect_var, stop_animation_var, animationFn, params] = run_animation(this, block); + block.chunks.intro.push(b` + if (${unfreeze_var}) { + ${unfreeze_var}(); + ${unfreeze_var} = void 0; + ${stop_animation_var} = @run_animation(${this.var}, ${rect_var}, ${animationFn}, ${params}); } + `); + } + if (!intro) return; - block.chunks.intro.push(intro_block); - } - - if (outro) { - block.add_variable(outro_name); - const snippet = outro.expression - ? outro.expression.manipulate(block) - : x`{}`; - - const fn = this.renderer.reference(outro.name); - - if (!intro) { - block.chunks.intro.push(b` - if (${outro_name}) ${outro_name}.end(1); - `); - } + const [intro_var, node, transitionFn, params] = run_transition(this, block, intro, `intro`); + block.add_variable(intro_var, x`@noop`); - // TODO hide elements that have outro'd (unless they belong to a still-outroing - // group) prior to their removal from the DOM - let outro_block = b` - ${outro_name} = @create_out_transition(${this.var}, ${fn}, ${snippet}); - `; + let start_intro; + if (intro.is_local) + start_intro = b`if (#local) ${intro_var} = @run_transition(${node}, ${transitionFn}, 1, ${params});`; + else start_intro = b`${intro_var} = @run_transition(${node}, ${transitionFn}, 1, ${params});`; + block.chunks.intro.push(start_intro); + } + // TODO + // hide elements that have outro'd prior to their removal from the DOM + // ( ...unless they belong to a still-outroing group ) + add_outro(block: Block, intro: Transition, outro: Transition) { + if (intro) { + const intro_var = block.alias(`${this.var.name}_intro`); + block.chunks.outro.push(b`${intro_var}();`); + } + if (!outro) return; - if (outro.is_local) { - outro_block = b` - if (#local) { - ${outro_block} - } - `; - } + const [outro_var, node, transitionFn, params] = run_transition(this, block, outro, `outro`); + block.add_variable(outro_var, x`@noop`); - block.chunks.outro.push(outro_block); + let start_outro; + if (outro.is_local) start_outro = b`if (#local) @run_transition(${node}, ${transitionFn}, 2, ${params});`; + else start_outro = b`${outro_var} = @run_transition(${node}, ${transitionFn}, 2, ${params});`; + block.chunks.outro.push(start_outro); - block.chunks.destroy.push(b`if (detaching && ${outro_name}) ${outro_name}.end();`); - } - } + block.chunks.destroy.push(b`if (detaching) ${outro_var}();`); } - add_animation(block: Block) { - if (!this.node.animation) return; - - const { outro } = this.node; + add_animation(block: Block, intro: Transition) { + const intro_var = intro && block.alias(`${this.var.name}_intro`); - const rect = block.get_unique_name('rect'); - const stop_animation = block.get_unique_name('stop_animation'); + const [unfreeze_var, rect_var, stop_animation_var, name_var, params_var] = run_animation(this, block); - block.add_variable(rect); - block.add_variable(stop_animation, x`@noop`); + block.add_variable(unfreeze_var); + block.add_variable(rect_var); + block.add_variable(stop_animation_var, x`@noop`); block.chunks.measure.push(b` - ${rect} = ${this.var}.getBoundingClientRect(); + ${rect_var} = ${this.var}.getBoundingClientRect(); + ${intro && b`${intro_var}();`} `); block.chunks.fix.push(b` - @fix_position(${this.var}); - ${stop_animation}(); - ${outro && b`@add_transform(${this.var}, ${rect});`} + ${stop_animation_var}(); + ${unfreeze_var} = @fix_position(${this.var}, ${rect_var}); `); - const params = this.node.animation.expression ? this.node.animation.expression.manipulate(block) : x`{}`; - - const name = this.renderer.reference(this.node.animation.name); - block.chunks.animate.push(b` - ${stop_animation}(); - ${stop_animation} = @create_animation(${this.var}, ${rect}, ${name}, ${params}); + if (${unfreeze_var}) return + else { + ${stop_animation_var}(); + ${stop_animation_var} = @run_animation(${this.var}, ${rect_var}, ${name_var}, ${params_var}); + } `); + + block.chunks.destroy.push(b`${unfreeze_var} = void 0;`); } add_classes(block: Block) { @@ -995,3 +942,20 @@ function to_html(wrappers: Array, blo } }); } +function run_animation(element: ElementWrapper, block: Block) { + return [ + block.alias('unfreeze'), + block.alias('rect'), + block.alias('stop_animation'), + element.renderer.reference(element.node.animation.name), + element.node.animation.expression ? element.node.animation.expression.manipulate(block) : null, + ]; +} +function run_transition(element: ElementWrapper, block: Block, transition: Transition, type: string) { + return [ + /* node_intro */ block.alias(`${element.var.name}_${type}`), + /* node */ element.var, + /* transitionFn */ element.renderer.reference(transition.name), + /* params */ transition.expression ? transition.expression.manipulate(block) : null, + ]; +} diff --git a/src/compiler/compile/render_dom/wrappers/IfBlock.ts b/src/compiler/compile/render_dom/wrappers/IfBlock.ts index 220b529902..412b5bb37d 100644 --- a/src/compiler/compile/render_dom/wrappers/IfBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/IfBlock.ts @@ -434,13 +434,10 @@ export default class IfBlockWrapper extends Wrapper { if (this.needs_update) { const update_mount_node = this.get_update_mount_node(anchor); - const destroy_old_block = b` - @group_outros(); - @transition_out(${if_blocks}[${previous_block_index}], 1, 1, () => { - ${if_blocks}[${previous_block_index}] = null; - }); - @check_outros(); - `; + const destroy_old_block = block.group_transition_out( + (transition_out) => + b`${transition_out}(${if_blocks}[${previous_block_index}], () => {${if_blocks}[${previous_block_index}] = null;})` + ); const create_new_block = b` ${name} = ${if_blocks}[${current_block_type_index}]; @@ -556,11 +553,7 @@ export default class IfBlockWrapper extends Wrapper { if (${branch.condition}) { ${enter} } else if (${name}) { - @group_outros(); - @transition_out(${name}, 1, 1, () => { - ${name} = null; - }); - @check_outros(); + ${block.group_transition_out((transition_out) => b`${transition_out}(${name},() => {${name} = null;})`)} } `); } else { diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index 00f803bbbd..cf6a15c9fa 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -451,12 +451,11 @@ export default class InlineComponentWrapper extends Wrapper { block.chunks.update.push(b` if (${switch_value} !== (${switch_value} = ${snippet})) { if (${name}) { - @group_outros(); const old_component = ${name}; - @transition_out(old_component.$$.fragment, 1, 0, () => { - @destroy_component(old_component, 1); - }); - @check_outros(); + ${block.group_transition_out( + (transition_out) => + b`${transition_out}(old_component.$$.fragment, () => { @destroy_component(old_component, 1); }, 0);` + )} } if (${switch_value}) { diff --git a/src/compiler/compile/utils/bit_state.ts b/src/compiler/compile/utils/bit_state.ts new file mode 100644 index 0000000000..7523606c50 --- /dev/null +++ b/src/compiler/compile/utils/bit_state.ts @@ -0,0 +1 @@ +export default (arr) => arr.reduce((state, bool, index) => (bool ? (state |= 1 << index) : state), 0); \ No newline at end of file diff --git a/src/runtime/animate/index.ts b/src/runtime/animate/index.ts index 087c0f7141..58aeceff7a 100644 --- a/src/runtime/animate/index.ts +++ b/src/runtime/animate/index.ts @@ -1,41 +1,22 @@ -import { cubicOut } from 'svelte/easing'; -import { is_function } from 'svelte/internal'; - -// todo: same as Transition, should it be shared? -export interface AnimationConfig { - delay?: number; - duration?: number; - easing?: (t: number) => number; - css?: (t: number, u: number) => string; - tick?: (t: number, u: number) => void; -} - -interface FlipParams { - delay: number; - duration: number | ((len: number) => number); - easing: (t: number) => number; -} - -export function flip(node: Element, animation: { from: DOMRect; to: DOMRect }, params: FlipParams): AnimationConfig { - const style = getComputedStyle(node); - const transform = style.transform === 'none' ? '' : style.transform; +import { cubicOut } from "svelte/easing"; +import { run_duration } from "svelte/internal"; +import { CssTransitionConfig, TimeableConfig } from "svelte/transition"; +export function flip( + node: Element, + animation: { from: DOMRect; to: DOMRect }, + { delay = 0, duration = (d: number) => Math.sqrt(d) * 30, easing = cubicOut }: TimeableConfig +): CssTransitionConfig { + const style = getComputedStyle(node).transform; + const transform = style === "none" ? "" : style; const scaleX = animation.from.width / node.clientWidth; const scaleY = animation.from.height / node.clientHeight; const dx = (animation.from.left - animation.to.left) / scaleX; const dy = (animation.from.top - animation.to.top) / scaleY; - const d = Math.sqrt(dx * dx + dy * dy); - - const { - delay = 0, - duration = (d: number) => Math.sqrt(d) * 120, - easing = cubicOut - } = params; - return { delay, - duration: is_function(duration) ? duration(d) : duration, + duration: run_duration(duration, Math.sqrt(dx * dx + dy * dy)), easing, css: (_t, u) => `transform: ${transform} translate(${u * dx}px, ${u * dy}px);` }; diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 7d2a92fa1b..780e7e0eec 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,10 +1,11 @@ -import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler'; +import { add_render_callback, flush, schedule_update } from './scheduler'; import { current_component, set_current_component } from './lifecycle'; -import { blank_object, is_function, run, run_all, noop } from './utils'; +import { blank_object, is_function, run, run_all } from './utils'; import { children, detach } from './dom'; import { transition_in } from './transitions'; +import { noop } from './environment'; -interface Fragment { +export interface Fragment { key: string|null; first: null; /* create */ c: () => void; @@ -20,7 +21,7 @@ interface Fragment { /* destroy */ d: (detaching: 0|1) => void; } // eslint-disable-next-line @typescript-eslint/class-name-casing -interface T$$ { +export interface T$$ { dirty: number[]; ctx: null|any; bound: any; @@ -87,15 +88,6 @@ export function destroy_component(component, detaching) { } } -function make_dirty(component, i) { - if (component.$$.dirty[0] === -1) { - dirty_components.push(component); - schedule_update(); - component.$$.dirty.fill(0); - } - component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); -} - export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { const parent_component = current_component; set_current_component(component); @@ -127,13 +119,18 @@ export function init(component, options, instance, create_fragment, not_equal, p let ready = false; $$.ctx = instance - ? instance(component, prop_values, (i, ret, ...rest) => { - const value = rest.length ? rest[0] : ret; - if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { - if ($$.bound[i]) $$.bound[i](value); - if (ready) make_dirty(component, i); - } - return ret; + ? instance(component, prop_values, (i, res, ...rest) => { + if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = rest.length ? rest[0] : res))) { + if (i in $$.bound) $$.bound[i]($$.ctx[i]); + if (ready) { + if (-1 === $$.dirty[0]) { + schedule_update(component); + $$.dirty.fill(0); + } + $$.dirty[(i / 31) | 0] |= 1 << i % 31; + } + } + return res; }) : []; diff --git a/src/runtime/internal/animations.ts b/src/runtime/internal/animations.ts index 6dc6a446f6..d266488a8c 100644 --- a/src/runtime/internal/animations.ts +++ b/src/runtime/internal/animations.ts @@ -1,103 +1,42 @@ -import { identity as linear, noop } from './utils'; -import { now } from './environment'; -import { loop } from './loop'; -import { create_rule, delete_rule } from './style_manager'; -import { AnimationConfig } from '../animate'; - - -//todo: documentation says it is DOMRect, but in IE it would be ClientRect -type PositionRect = DOMRect|ClientRect; - -type AnimationFn = (node: Element, { from, to }: { from: PositionRect; to: PositionRect }, params: any) => AnimationConfig; - -export function create_animation(node: Element & ElementCSSInlineStyle, from: PositionRect, fn: AnimationFn, params) { - if (!from) return noop; - - const to = node.getBoundingClientRect(); - if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) return noop; - - - const { - delay = 0, - duration = 300, - easing = linear, - // @ts-ignore todo: should this be separated from destructuring? Or start/end added to public api and documentation? - start: start_time = now() + delay, - // @ts-ignore todo: - end = start_time + duration, - tick = noop, - css - } = fn(node, { from, to }, params); - - let running = true; - let started = false; - let name; - - function start() { - if (css) { - name = create_rule(node, 0, 1, duration, delay, easing, css); - } - - if (!delay) { - started = true; - } - } - - function stop() { - if (css) delete_rule(node, name); - running = false; - } - - loop(now => { - if (!started && now >= start_time) { - started = true; - } - - if (started && now >= end) { - tick(1, 0); - stop(); - } - - if (!running) { - return false; - } - - if (started) { - const p = now - start_time; - const t = 0 + 1 * easing(p / duration); - tick(t, 1 - t); - } - - return true; - }); - - start(); - - tick(0, 1); - - return stop; -} - -export function fix_position(node: Element & ElementCSSInlineStyle) { - const style = getComputedStyle(node); - - if (style.position !== 'absolute' && style.position !== 'fixed') { - const { width, height } = style; - const a = node.getBoundingClientRect(); - node.style.position = 'absolute'; - node.style.width = width; - node.style.height = height; - add_transform(node, a); +import { run_transition } from './transitions'; +import { noop } from './environment'; +import { methodify } from './utils'; +import { CssTransitionConfig } from 'svelte/transition'; + +type AnimationFn = (node: Element, { from, to }: { from: DOMRect; to: DOMRect }, params: any) => CssTransitionConfig; + +export const run_animation = /*#__PURE__*/ methodify( + function run_animation(this: HTMLElement, from: DOMRect, fn: AnimationFn, params = {}) { + if (!from) return noop; + return run_transition( + this, + (_, params) => { + const to = this.getBoundingClientRect(); + if (from.left !== to.left || from.right !== to.right || from.top !== to.top || from.bottom !== to.bottom) { + return fn(this, { from, to }, params); + } else return null; + }, + 9, + params + ); } -} - -export function add_transform(node: Element & ElementCSSInlineStyle, a: PositionRect) { - const b = node.getBoundingClientRect(); - - if (a.left !== b.left || a.top !== b.top) { - const style = getComputedStyle(node); - const transform = style.transform === 'none' ? '' : style.transform; - - node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`; +); + +export const fix_position = /*#__PURE__*/ methodify( + function fix_position(this: HTMLElement, { left, top }: DOMRect) { + const { position, width, height, transform } = getComputedStyle(this); + if (position === 'absolute' || position === 'fixed') return noop; + const { position: og_position, width: og_width, height: og_height } = this.style; + this.style.position = 'absolute'; + this.style.width = width; + this.style.height = height; + const b = this.getBoundingClientRect(); + this.style.transform = `${transform === 'none' ? '' : transform} translate(${left - b.left}px, ${top - b.top}px)`; + return () => { + this.style.position = og_position; + this.style.width = og_width; + this.style.height = og_height; + this.style.transform = ''; // unsafe + }; } -} +); diff --git a/src/runtime/internal/await_block.ts b/src/runtime/internal/await_block.ts index f70cbd6c2c..e88dc159fc 100644 --- a/src/runtime/internal/await_block.ts +++ b/src/runtime/internal/await_block.ts @@ -1,5 +1,5 @@ import { is_promise } from './utils'; -import { check_outros, group_outros, transition_in, transition_out } from './transitions'; +import { transition_in, group_transition_out } from './transitions'; import { flush } from './scheduler'; import { get_current_component, set_current_component } from './lifecycle'; @@ -26,11 +26,11 @@ export function handle_promise(promise, info) { if (info.blocks) { info.blocks.forEach((block, i) => { if (i !== index && block) { - group_outros(); - transition_out(block, 1, 1, () => { - info.blocks[i] = null; + group_transition_out((transition_out) => { + transition_out(block, () => { + info.blocks[i] = null; + }); }); - check_outros(); } }); } else { diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index 751f1f802b..d38cb8d0a0 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -1,5 +1,6 @@ import { custom_event, append, insert, detach, listen, attr } from './dom'; import { SvelteComponent } from './Component'; +import { has_Symbol } from './environment'; export function dispatch_dev(type: string, detail?: T) { document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail })); @@ -82,7 +83,7 @@ export function set_data_dev(text, data) { export function validate_each_argument(arg) { if (typeof arg !== 'string' && !(arg && typeof arg === 'object' && 'length' in arg)) { let msg = '{#each} only iterates over array-like objects.'; - if (typeof Symbol === 'function' && arg && Symbol.iterator in arg) { + if (has_Symbol && arg && Symbol.iterator in arg) { msg += ' You can use a spread to convert this iterable into an array.'; } throw new Error(msg); diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index f67fd13b2d..7fdc26808b 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -1,4 +1,5 @@ import { has_prop } from "./utils"; +import { is_cors } from "./environment"; export function append(target: Node, node: Node) { target.appendChild(node); @@ -234,26 +235,6 @@ export function select_multiple_value(select) { return [].map.call(select.querySelectorAll(':checked'), option => option.__value); } -// unfortunately this can't be a constant as that wouldn't be tree-shakeable -// so we cache the result instead -let crossorigin: boolean; - -export function is_crossorigin() { - if (crossorigin === undefined) { - crossorigin = false; - - try { - if (typeof window !== 'undefined' && window.parent) { - void window.parent.document; - } - } catch (error) { - crossorigin = true; - } - } - - return crossorigin; -} - export function add_resize_listener(node: HTMLElement, fn: () => void) { const computed_style = getComputedStyle(node); const z_index = (parseInt(computed_style.zIndex) || 0) - 1; @@ -270,11 +251,9 @@ export function add_resize_listener(node: HTMLElement, fn: () => void) { iframe.setAttribute('aria-hidden', 'true'); iframe.tabIndex = -1; - const crossorigin = is_crossorigin(); - let unsubscribe: () => void; - if (crossorigin) { + if (is_cors) { iframe.src = `data:text/html,`; unsubscribe = listen(window, 'message', (event: MessageEvent) => { if (event.source === iframe.contentWindow) fn(); @@ -289,7 +268,7 @@ export function add_resize_listener(node: HTMLElement, fn: () => void) { append(node, iframe); return () => { - if (crossorigin) { + if (is_cors) { unsubscribe(); } else if (unsubscribe && iframe.contentWindow) { unsubscribe(); diff --git a/src/runtime/internal/environment.ts b/src/runtime/internal/environment.ts index 7123399180..bda01b4d3f 100644 --- a/src/runtime/internal/environment.ts +++ b/src/runtime/internal/environment.ts @@ -1,18 +1,33 @@ -import { noop } from './utils'; +export function noop() {} +export const is_browser = typeof window !== 'undefined'; +export const is_iframe = is_browser && window.self !== window.top; +export const is_cors = + is_iframe && + /*#__PURE__*/ (() => { + try { + if (window.parent) void window.parent.document; + return false; + } catch (error) { + return true; + } + })(); +export const has_Symbol = typeof Symbol === 'function'; +/* eslint-disable no-var */ +declare var global: any; +export const globals = is_browser ? window : typeof globalThis !== 'undefined' ? globalThis : global; +export const resolved_promise = Promise.resolve(); +export let now = is_browser ? window.performance.now.bind(window.performance) : Date.now.bind(Date); +export let raf = is_browser ? requestAnimationFrame : noop; +export let framerate = 1000 / 60; +/*#__PURE__*/ raf((t1) => { + raf((d) => { + const f24 = 1000 / 24, + f144 = 1000 / 144; + framerate = (d = d - t1) > f144 ? f144 : d < f24 ? f24 : d; + }); +}); -export const is_client = typeof window !== 'undefined'; - -export let now: () => number = is_client - ? () => window.performance.now() - : () => Date.now(); - -export let raf = is_client ? cb => requestAnimationFrame(cb) : noop; - -// used internally for testing -export function set_now(fn) { - now = fn; -} - -export function set_raf(fn) { - raf = fn; -} +/* tests only */ +export const set_now = (v) => void (now = v); +export const set_raf = (fn) => void (raf = fn); +export const set_framerate = (v) => void (framerate = v); diff --git a/src/runtime/internal/globals.ts b/src/runtime/internal/globals.ts deleted file mode 100644 index b97f81ab9f..0000000000 --- a/src/runtime/internal/globals.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare const global: any; - -export const globals = (typeof window !== 'undefined' - ? window - : typeof globalThis !== 'undefined' - ? globalThis - : global) as unknown as typeof globalThis; diff --git a/src/runtime/internal/index.ts b/src/runtime/internal/index.ts index e1dd2a1fcf..daeb9b1f0a 100644 --- a/src/runtime/internal/index.ts +++ b/src/runtime/internal/index.ts @@ -2,7 +2,6 @@ export * from './animations'; export * from './await_block'; export * from './dom'; export * from './environment'; -export * from './globals'; export * from './keyed_each'; export * from './lifecycle'; export * from './loop'; diff --git a/src/runtime/internal/keyed_each.ts b/src/runtime/internal/keyed_each.ts index b397335c87..3c89c01e4b 100644 --- a/src/runtime/internal/keyed_each.ts +++ b/src/runtime/internal/keyed_each.ts @@ -1,27 +1,18 @@ -import { transition_in, transition_out } from './transitions'; - -export function destroy_block(block, lookup) { - block.d(1); - lookup.delete(block.key); -} - -export function outro_and_destroy_block(block, lookup) { - transition_out(block, 1, 1, () => { - lookup.delete(block.key); - }); -} - -export function fix_and_destroy_block(block, lookup) { - block.f(); - destroy_block(block, lookup); -} - -export function fix_and_outro_and_destroy_block(block, lookup) { - block.f(); - outro_and_destroy_block(block, lookup); -} - -export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list, lookup, node, destroy, create_each_block, next, get_context) { +import { transition_in } from './transitions'; +export const update_keyed_each = ( + old_blocks, + dirty, + ctx, + state, + get_key, + list, + lookup, + node, + create_each_block, + next, + get_context, + transition_out? +) => { let o = old_blocks.length; let n = list.length; @@ -42,11 +33,11 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list if (!block) { block = create_each_block(key, child_ctx); block.c(); - } else if (dynamic) { + } else if (state & 1) { block.p(child_ctx, dirty); } - new_lookup.set(key, new_blocks[i] = block); + new_lookup.set(key, (new_blocks[i] = block)); if (key in old_indexes) deltas.set(key, Math.abs(i - old_indexes[key])); } @@ -54,13 +45,18 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list const will_move = new Set(); const did_move = new Set(); - function insert(block) { + const insert = (block) => { transition_in(block, 1); - block.m(node, next); + block.m(node, next, lookup.has(block.key)); lookup.set(block.key, block); next = block.first; n--; - } + }; + const destroy = (block) => { + if (state & 2) block.f(); + if (state & 4) transition_out(block, lookup.delete.bind(lookup, block.key)); + else block.d(1), lookup.delete(block.key); + }; while (o && n) { const new_block = new_blocks[n - 1]; @@ -73,25 +69,17 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list next = new_block.first; o--; n--; - } - - else if (!new_lookup.has(old_key)) { + } else if (!new_lookup.has(old_key)) { // remove old block - destroy(old_block, lookup); + destroy(old_block); o--; - } - - else if (!lookup.has(new_key) || will_move.has(new_key)) { + } else if (!lookup.has(new_key) || will_move.has(new_key)) { insert(new_block); - } - - else if (did_move.has(old_key)) { + } else if (did_move.has(old_key)) { o--; - } else if (deltas.get(new_key) > deltas.get(old_key)) { did_move.add(new_key); insert(new_block); - } else { will_move.add(old_key); o--; @@ -100,21 +88,10 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list while (o--) { const old_block = old_blocks[o]; - if (!new_lookup.has(old_block.key)) destroy(old_block, lookup); + if (!new_lookup.has(old_block.key)) destroy(old_block); } while (n) insert(new_blocks[n - 1]); return new_blocks; -} - -export function validate_each_keys(ctx, list, get_context, get_key) { - const keys = new Set(); - for (let i = 0; i < list.length; i++) { - const key = get_key(get_context(ctx, list, i)); - if (keys.has(key)) { - throw new Error(`Cannot have duplicate keys in a keyed each`); - } - keys.add(key); - } -} +}; diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index a8e37e9632..c9172b3783 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -2,9 +2,7 @@ import { custom_event } from './dom'; export let current_component; -export function set_current_component(component) { - current_component = component; -} +export const set_current_component = (component) => (current_component = component); export function get_current_component() { if (!current_component) throw new Error(`Function called outside component initialization`); diff --git a/src/runtime/internal/loop.ts b/src/runtime/internal/loop.ts index 33e519732f..5cdfc434ad 100644 --- a/src/runtime/internal/loop.ts +++ b/src/runtime/internal/loop.ts @@ -1,45 +1,122 @@ -import { raf } from './environment'; +import { now, raf, framerate, noop } from './environment'; +type TaskCallback = (t: number) => boolean; +type TaskCanceller = () => void; -export interface Task { abort(): void; promise: Promise } +let i = 0, + j = 0, + n = 0, + v : TaskCallback; -type TaskCallback = (now: number) => boolean | void; -type TaskEntry = { c: TaskCallback; f: () => void }; +let running_frame : Array = [], + next_frame : Array = []; -const tasks = new Set(); - -function run_tasks(now: number) { - tasks.forEach(task => { - if (!task.c(now)) { - tasks.delete(task); - task.f(); +const run = (t: number) => { + [running_frame, next_frame] = [next_frame, running_frame]; + for (t = now(), i = n = 0, j = running_frame.length; i < j; i++) { + if ((v = running_frame[i])(t)) { + next_frame[n++] = v; } - }); + } + if ((running_frame.length = 0) < n) { + raf(run); + } +}; + +type TimeoutTask = { timestamp: number; callback: (now: number) => void }; + +const pending_insert_timed : Array = [], + timed_tasks : Array = []; + +let pending_inserts = false, + running_timed = false; + +const run_timed = (now: number) => { + let last_index = timed_tasks.length - 1; + while (~last_index && now >= timed_tasks[last_index].timestamp) timed_tasks[last_index--].callback(now); + if (pending_inserts) { + for (let i = 0, j = 0, this_task: TimeoutTask, that_task: TimeoutTask; i < pending_insert_timed.length; i++) + if (now >= (this_task = pending_insert_timed[i]).timestamp) this_task.callback(now); + else { + for (j = last_index; ~j && this_task.timestamp > (that_task = timed_tasks[j]).timestamp; j--) + timed_tasks[j + 1] = that_task; + timed_tasks[j + 1] = this_task; + last_index++; + } + pending_insert_timed.length = 0; + pending_inserts = false; + } + return (running_timed = !!(timed_tasks.length = last_index + 1)); +}; + +const unsafe_loop = (fn) => { + if (0 === n) raf(run); + next_frame[n++] = fn; +}; + +export const loop = (fn) => { + let running = true; + if (0 === n) raf(run); + next_frame[n++] = (t) => !running || fn(t); + return () => void (running = false); +}; - if (tasks.size !== 0) raf(run_tasks); -} +export const setFrameTimeout = (callback: (t: number) => void, timestamp: number): TaskCanceller => { + const task: TimeoutTask = { callback, timestamp }; + if (running_timed) { + pending_inserts = !!pending_insert_timed.push(task); + } else { + unsafe_loop(run_timed); + running_timed = true; + timed_tasks.push(task); + } + return () => void (task.callback = noop); +}; /** - * For testing purposes only! + * Calls function every frame with linear tween from 0 to 1 */ -export function clear_loops() { - tasks.clear(); -} - +export const setTweenTimeout = ( + stop: (now: number) => void, + end_time: number, + run: (now: number) => void, + duration = end_time - now() +): TaskCanceller => { + let running = true; + let t = 0.0; + unsafe_loop((now) => { + if (!running) return false; + t = 1.0 - (end_time - now) / duration; + if (t >= 1.0) return run(1), stop(now), false; + if (t >= 0.0) run(t); + return running; + }); + return (run_last = false) => { + // since outros are cancelled in group by a setFrameTimeout + // tick(0, 1) has to be called in here + if (run_last) run(1); + running = false; + }; +}; /** - * Creates a new task that runs on each raf frame - * until it returns a falsy value or is aborted + * Calls function every frame with time elapsed in seconds */ -export function loop(callback: TaskCallback): Task { - let task: TaskEntry; - - if (tasks.size === 0) raf(run_tasks); +export const onEachFrame = ( + callback: (seconds_elapsed: number) => boolean, + on_stop?, + max_skipped_frames = 4 +): TaskCanceller => { + max_skipped_frames *= framerate; + let lastTime = now(); + let running = true; + const cancel = (t) => (on_stop && on_stop(t), false); + unsafe_loop((t: number) => { + if (!running) return cancel(t); + if (t > lastTime + max_skipped_frames) t = lastTime + max_skipped_frames; + return callback((-lastTime + (lastTime = t)) / 1000) ? true : cancel(t); + }); + return () => void (running = false); +}; - return { - promise: new Promise(fulfill => { - tasks.add(task = { c: callback, f: fulfill }); - }), - abort() { - tasks.delete(task); - } - }; -} +/** tests only */ +export const clear_loops = () => + void (next_frame.length = running_frame.length = timed_tasks.length = pending_insert_timed.length = n = i = j = +(running_timed = pending_inserts = false)); diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts index b0db71035a..709999ee97 100644 --- a/src/runtime/internal/scheduler.ts +++ b/src/runtime/internal/scheduler.ts @@ -1,89 +1,106 @@ -import { run_all } from './utils'; import { set_current_component } from './lifecycle'; +import { resolved_promise, now } from './environment'; +import { T$$ } from './Component'; -export const dirty_components = []; -export const intros = { enabled: false }; +let update_scheduled = false; +let is_flushing = false; + +const dirty_components = []; +// todo : remove binding_callbacks export export const binding_callbacks = []; const render_callbacks = []; +const measure_callbacks = []; const flush_callbacks = []; -const resolved_promise = Promise.resolve(); -let update_scheduled = false; +// todo : remove add_flush_callback +export const add_flush_callback = /*#__PURE__*/ Array.prototype.push.bind(flush_callbacks); +export const add_measure_callback = /*#__PURE__*/ Array.prototype.push.bind(measure_callbacks); -export function schedule_update() { +const seen_render_callbacks = new Set(); +export const add_render_callback = (fn) => { + if (!seen_render_callbacks.has(fn)) { + seen_render_callbacks.add(fn); + render_callbacks.push(fn); + } +}; +export const schedule_update = (component) => { + dirty_components.push(component); + if (!update_scheduled) { + update_scheduled = true; + resolved_promise.then(flush); + } +}; +export const tick = () => { if (!update_scheduled) { update_scheduled = true; resolved_promise.then(flush); } -} - -export function tick() { - schedule_update(); return resolved_promise; -} - -export function add_render_callback(fn) { - render_callbacks.push(fn); -} - -export function add_flush_callback(fn) { - flush_callbacks.push(fn); -} - -let flushing = false; -const seen_callbacks = new Set(); -export function flush() { - if (flushing) return; - flushing = true; +}; +export const flush = () => { + if (is_flushing) return; + else is_flushing = true; + + let i = 0, + j = 0, + t = 0, + $$: T$$, + dirty, + before_update, + after_update; do { - // first, call beforeUpdate functions - // and update components - for (let i = 0; i < dirty_components.length; i += 1) { - const component = dirty_components[i]; - set_current_component(component); - update(component.$$); - } + while (i < dirty_components.length) { + ({ $$ } = set_current_component(dirty_components[i])); - dirty_components.length = 0; + // todo : is this check still necessary ? + if (null === $$.fragment) continue; - while (binding_callbacks.length) binding_callbacks.pop()(); + /* run reactive statements */ + $$.update(); - // then, once components are updated, call - // afterUpdate functions. This may cause - // subsequent updates... - for (let i = 0; i < render_callbacks.length; i += 1) { - const callback = render_callbacks[i]; + /* run beforeUpdate */ + for (j = 0, { before_update } = $$; j < before_update.length; j++) { + before_update[j](); + } - if (!seen_callbacks.has(callback)) { - // ...so guard against infinite loops - seen_callbacks.add(callback); + /* update blocks */ + ({ dirty } = $$).dirty = [-1]; + if (false !== $$.fragment) $$.fragment.p($$.ctx, dirty); - callback(); + /* schedule afterUpdate */ + for (j = 0, { after_update } = $$; j < after_update.length; j++) { + add_render_callback(after_update[j]); } + + i = i + 1; } + dirty_components.length = 0; - render_callbacks.length = 0; + // update bindings [ ...in reverse order (#3145) ] + i = binding_callbacks.length; + while (i--) binding_callbacks[i](); + binding_callbacks.length = i = 0; + + // run afterUpdates + // todo : remove every non afterUpdate callback from render_callbacks + for (; i < render_callbacks.length; i++) render_callbacks[i](); + render_callbacks.length = i = 0; } while (dirty_components.length); + seen_render_callbacks.clear(); + update_scheduled = false; - while (flush_callbacks.length) { - flush_callbacks.pop()(); + // measurement callbacks for animations + for (i = 0, j = flush_callbacks.length; i < measure_callbacks.length; i++) { + flush_callbacks[j++] = measure_callbacks[i](); } + measure_callbacks.length = i = 0; - update_scheduled = false; - flushing = false; - seen_callbacks.clear(); -} - -function update($$) { - if ($$.fragment !== null) { - $$.update(); - run_all($$.before_update); - const dirty = $$.dirty; - $$.dirty = [-1]; - $$.fragment && $$.fragment.p($$.ctx, dirty); - - $$.after_update.forEach(add_render_callback); - } -} + // apply styles + // todo : remove every non style callback from flush_callbacks + for (t = now(); i < j; i++) flush_callbacks[i](t); + flush_callbacks.length = i = j = 0; + + is_flushing = false; +}; diff --git a/src/runtime/internal/style_manager.ts b/src/runtime/internal/style_manager.ts index 31d7573a76..5d5b30885d 100644 --- a/src/runtime/internal/style_manager.ts +++ b/src/runtime/internal/style_manager.ts @@ -1,74 +1,62 @@ -import { element } from './dom'; -import { raf } from './environment'; - -interface ExtendedDoc extends Document { - __svelte_stylesheet: CSSStyleSheet; - __svelte_rules: Record; -} - -const active_docs = new Set(); -let active = 0; - -// https://github.com/darkskyapp/string-hash/blob/master/index.js -function hash(str: string) { - let hash = 5381; - let i = str.length; - - while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); - return hash >>> 0; -} - -export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b: number, duration: number, delay: number, ease: (t: number) => number, fn: (t: number, u: number) => string, uid: number = 0) { - const step = 16.666 / duration; - let keyframes = '{\n'; - - for (let p = 0; p <= 1; p += step) { - const t = a + (b - a) * ease(p); - keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; +import { framerate } from './environment'; + +let documents_uid = 0; +let running_animations = 0; + +const document_uid = new Map(); +const document_stylesheets = new Map(); + +const current_rules = new Set(); +export const animate_css = /*#__PURE__*/ Function.prototype.call.bind(function animate_css( + this: HTMLElement, + css: (t: number) => string, + duration: number, + delay = 0 +) { + if (!document_uid.has(this.ownerDocument)) { + document_uid.set(this.ownerDocument, documents_uid++); + document_stylesheets.set( + this.ownerDocument, + this.ownerDocument.head.appendChild(this.ownerDocument.createElement('style')).sheet + ); } - - const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; - const name = `__svelte_${hash(rule)}_${uid}`; - const doc = node.ownerDocument as ExtendedDoc; - active_docs.add(doc); - const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = doc.head.appendChild(element('style') as HTMLStyleElement).sheet as CSSStyleSheet); - const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {}); - - if (!current_rules[name]) { - current_rules[name] = true; + let rule = '{\n'; + for (let t = 0, step = framerate / duration; t < 1; t += step) rule += `${100 * t}%{${css(t)}}\n`; + rule += `100% {${css(1)}}\n}`; + + // darkskyapp/string-hash + let i = rule.length, hash = 5381; + while (i--) hash = ((hash << 5) - hash) ^ rule.charCodeAt(i); + const name = `__svelte_${hash >>> 0}${document_uid.get(this.ownerDocument)}`; + + if (!current_rules.has(name)) { + current_rules.add(name); + const stylesheet = document_stylesheets.get(this.ownerDocument); stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length); } - const animation = node.style.animation || ''; - node.style.animation = `${animation ? `${animation}, ` : ``}${name} ${duration}ms linear ${delay}ms 1 both`; - - active += 1; - return name; -} - -export function delete_rule(node: Element & ElementCSSInlineStyle, name?: string) { - const previous = (node.style.animation || '').split(', '); - const next = previous.filter(name - ? anim => anim.indexOf(name) < 0 // remove specific animation - : anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations - ); - const deleted = previous.length - next.length; - if (deleted) { - node.style.animation = next.join(', '); - active -= deleted; - if (!active) clear_rules(); - } -} - -export function clear_rules() { - raf(() => { - if (active) return; - active_docs.forEach(doc => { - const stylesheet = doc.__svelte_stylesheet; - let i = stylesheet.cssRules.length; - while (i--) stylesheet.deleteRule(i); - doc.__svelte_rules = {}; - }); - active_docs.clear(); - }); -} + const previous = this.style.animation; + this.style.animation = `${ + previous ? `${previous}, ` : '' + }${duration}ms linear ${delay}ms 1 normal both running ${name}`; + + running_animations++; + + return () => { + const prev = (this.style.animation || '').split(', '); + const next = prev.filter((anim) => !anim.includes(name)); + if (prev.length !== next.length) this.style.animation = next.join(', '); + if (--running_animations === 0) { + document_stylesheets.forEach((stylesheet) => { + let i = stylesheet.cssRules.length; + while (i--) stylesheet.deleteRule(i); + }); + current_rules.clear(); + if (1 !== documents_uid) { + document_stylesheets.clear(); + document_uid.clear(); + documents_uid = 0; + } + } + }; +}); \ No newline at end of file diff --git a/src/runtime/internal/transitions.ts b/src/runtime/internal/transitions.ts index ed23d3c1dd..48f2df750f 100644 --- a/src/runtime/internal/transitions.ts +++ b/src/runtime/internal/transitions.ts @@ -1,353 +1,203 @@ -import { identity as linear, is_function, noop, run_all } from './utils'; -import { now } from "./environment"; -import { loop } from './loop'; -import { create_rule, delete_rule } from './style_manager'; +import { CssTransitionConfig } from '../transition'; +import { Fragment } from './Component'; import { custom_event } from './dom'; -import { add_render_callback } from './scheduler'; -import { TransitionConfig } from '../transition'; - -let promise: Promise|null; - -function wait() { - if (!promise) { - promise = Promise.resolve(); - promise.then(() => { - promise = null; - }); - } - - return promise; -} - -function dispatch(node: Element, direction: boolean, kind: 'start' | 'end') { - node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`)); -} - +import { now, noop } from './environment'; +import { setFrameTimeout, setTweenTimeout } from './loop'; +import { add_measure_callback } from './scheduler'; +import { animate_css } from './style_manager'; +import { linear } from 'svelte/easing'; + +type TransitionFn = (node: HTMLElement, params: any) => CssTransitionConfig; +export type StopResetReverseFn = (t?: number | -1) => StopResetReverseFn | void; + +export const transition_in = (block: Fragment, local?) => { + if (!block || !block.i) return; + outroing.delete(block); + block.i(local); +}; + +export const transition_out = (block: Fragment, local?) => { + if (!block || !block.o || outroing.has(block)) return; + outroing.add(block); + block.o(local); +}; +type TransitionGroup = { + /* parent group */ p: TransitionGroup; + /* callbacks */ c: ((cancelled: boolean) => void)[]; + /* running outros */ r: number; + /* stop callbacks */ s: ((t: number) => void)[]; + /* outro timeout */ t: number; +}; +let transition_group: TransitionGroup; const outroing = new Set(); -let outros; - -export function group_outros() { - outros = { - r: 0, // remaining outros - c: [], // callbacks - p: outros // parent group - }; -} - -export function check_outros() { - if (!outros.r) { - run_all(outros.c); - } - outros = outros.p; -} - -export function transition_in(block, local?: 0 | 1) { - if (block && block.i) { - outroing.delete(block); - block.i(local); - } -} - -export function transition_out(block, local: 0 | 1, detach: 0 | 1, callback) { - if (block && block.o) { - if (outroing.has(block)) return; +export const group_transition_out = (fn) => { + const c = []; + const current_group = (transition_group = { p: transition_group, c, r: 0, s: [], t: 0 }); + fn((block, callback, detach = true) => { + if (!block || !block.o || outroing.has(block)) return; outroing.add(block); - - outros.c.push(() => { - outroing.delete(block); - if (callback) { + c.push((cancelled = false) => { + if (cancelled) { + // block was destroyed before outro ended + outroing.delete(block); + } else if (outroing.has(block)) { + outroing.delete(block); if (detach) block.d(1); callback(); } }); - - block.o(local); - } + block.o(1); + }); + if (!current_group.r) for (let i = 0; i < c.length; i++) c[i](); + transition_group = transition_group.p; +}; + +const swap = (fn, rx) => + fn.length === 1 + ? rx & tx.intro + ? fn + : (t) => fn(1 - t) + : rx & tx.intro + ? (t) => fn(t, 1 - t) + : (t) => fn(1 - t, t); + +const mirrored = (fn, rx, easing) => { + const run = swap(fn, rx); + return easing + ? rx & tx.intro + ? (t) => run(easing(t)) + : (t) => run(1 - easing(1 - t)) + : run; +}; +const reversed = (fn, rx, easing, start = 0, end = 1) => { + const run = swap(fn, rx); + const difference = end - start; + return easing + ? (t) => run(start + difference * easing(t)) + : (t) => run(start + difference * t); +}; +export const enum tx { + intro = 1, + outro = 2, + reverse = 3, + bidirectional = 4, + animation = 8, } +export const run_transition = /*#__PURE__*/ Function.prototype.call.bind(function transition( + this: HTMLElement, + fn: TransitionFn, + rx: tx, + params = {}, + /* internal to this file */ + elapsed_duration = 0, + delay_left = -1, + elapsed_ratio = 0 +) { + let config; + + let running = true; -const null_transition: TransitionConfig = { duration: 0 }; - -type TransitionFn = (node: Element, params: any) => TransitionConfig; - -export function create_in_transition(node: Element & ElementCSSInlineStyle, fn: TransitionFn, params: any) { - let config = fn(node, params); - let running = false; - let animation_name; - let task; - let uid = 0; - - function cleanup() { - if (animation_name) delete_rule(node, animation_name); - } - - function go() { - const { - delay = 0, - duration = 300, - easing = linear, - tick = noop, - css - } = config || null_transition; - - if (css) animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++); - tick(0, 1); - - const start_time = now() + delay; - const end_time = start_time + duration; - - if (task) task.abort(); - running = true; - - add_render_callback(() => dispatch(node, true, 'start')); - - task = loop(now => { - if (running) { - if (now >= end_time) { - tick(1, 0); - - dispatch(node, true, 'end'); - - cleanup(); - return running = false; - } + let cancel_css, + cancel_raf; - if (now >= start_time) { - const t = easing((now - start_time) / duration); - tick(t, 1 - t); - } - } + let start_time = 0, + end_time = 0; - return running; - }); - } + const current_group = transition_group; + if (rx & tx.outro) current_group.r++; - let started = false; + add_measure_callback(() => { + if (null === (config = fn(this, params))) return noop; + return (current_frame_time) => { + if (false === running) return; - return { - start() { - if (started) return; + let { delay = 0, duration = 300, easing, tick, css, strategy = 'reverse' }: CssTransitionConfig = + 'function' === typeof config ? (config = config()) : config; - delete_rule(node); + const solver = 'reverse' === strategy ? reversed : mirrored; + const runner = (fn) => solver(fn, rx, easing, elapsed_ratio, 1); - if (is_function(config)) { - config = config(); - wait().then(go); - } else { - go(); + if (rx & tx.bidirectional) { + if (-1 !== delay_left) delay = delay_left; + if (solver === reversed) duration -= elapsed_duration; + else if (solver === mirrored) delay -= elapsed_duration; } - }, - invalidate() { - started = false; - }, + end_time = (start_time = current_frame_time + delay) + duration; - end() { - if (running) { - cleanup(); - running = false; + if (0 === (rx & tx.animation)) { + this.dispatchEvent(custom_event(`${rx & tx.intro ? 'in' : 'ou'}trostart`)); } - } - }; -} - -export function create_out_transition(node: Element & ElementCSSInlineStyle, fn: TransitionFn, params: any) { - let config = fn(node, params); - let running = true; - let animation_name; - - const group = outros; - - group.r += 1; - function go() { - const { - delay = 0, - duration = 300, - easing = linear, - tick = noop, - css - } = config || null_transition; + if (css) cancel_css = animate_css(this, runner(css), duration, delay); - if (css) animation_name = create_rule(node, 1, 0, duration, delay, easing, css); - - const start_time = now() + delay; - const end_time = start_time + duration; - - add_render_callback(() => dispatch(node, false, 'start')); - - loop(now => { - if (running) { - if (now >= end_time) { - tick(0, 1); - - dispatch(node, false, 'end'); - - if (!--group.r) { - // this will result in `end()` being called, - // so we don't need to clean up here - run_all(group.c); - } - - return false; + if (rx & tx.outro) { + if (current_group.s.push(stop) === current_group.r) { + setFrameTimeout((t) => { + for (let i = 0; i < current_group.s.length; i++) current_group.s[i](t); + }, Math.max(end_time, current_group.t)); + } else { + current_group.t = Math.max(end_time, current_group.t); } - - if (now >= start_time) { - const t = easing((now - start_time) / duration); - tick(1 - t, t); - } - } - - return running; - }); - } - - if (is_function(config)) { - wait().then(() => { - // @ts-ignore - config = config(); - go(); - }); - } else { - go(); - } - - return { - end(reset) { - if (reset && config.tick) { - config.tick(1, 0); - } - - if (running) { - if (animation_name) delete_rule(node, animation_name); - running = false; + if (tick) cancel_raf = setTweenTimeout(noop, end_time, runner(tick), duration); + } else { + cancel_raf = tick ? setTweenTimeout(stop, end_time, runner(tick), duration) : setFrameTimeout(stop, end_time); } - } - }; -} - -export function create_bidirectional_transition(node: Element & ElementCSSInlineStyle, fn: TransitionFn, params: any, intro: boolean) { - let config = fn(node, params); - - let t = intro ? 0 : 1; - - let running_program = null; - let pending_program = null; - let animation_name = null; - - function clear_animation() { - if (animation_name) delete_rule(node, animation_name); - } - - function init(program, duration) { - const d = program.b - t; - duration *= Math.abs(d); - - return { - a: t, - b: program.b, - d, - duration, - start: program.start, - end: program.start + duration, - group: program.group - }; - } - - function go(b) { - const { - delay = 0, - duration = 300, - easing = linear, - tick = noop, - css - } = config || null_transition; - - const program = { - start: now() + delay, - b }; + }); - if (!b) { - // @ts-ignore todo: improve typings - program.group = outros; - outros.r += 1; - } - - if (running_program) { - pending_program = program; - } else { - // if this is an intro, and there's a delay, we need to do - // an initial tick and/or apply CSS animation immediately - if (css) { - clear_animation(); - animation_name = create_rule(node, t, b, duration, delay, easing, css); - } - - if (b) tick(0, 1); + const stop: StopResetReverseFn = (t?: number | 1 | -1) => { + // resetting `out:` in intros + if (t === 1 && rx & tx.outro && 0 === (rx & tx.bidirectional) && 'tick' in config) config.tick(1, 0); - running_program = init(program, duration); - add_render_callback(() => dispatch(node, b, 'start')); + if (false === running) return; + else running = false; - loop(now => { - if (pending_program && now > pending_program.start) { - running_program = init(pending_program, duration); - pending_program = null; + if (cancel_css) cancel_css(); + if (cancel_raf) cancel_raf(rx & tx.outro && t >= end_time); - dispatch(node, running_program.b, 'start'); + if (rx & tx.animation) return; - if (css) { - clear_animation(); - animation_name = create_rule(node, t, running_program.b, running_program.duration, 0, easing, config.css); - } - } - - if (running_program) { - if (now >= running_program.end) { - tick(t = running_program.b, 1 - t); - dispatch(node, running_program.b, 'end'); - - if (!pending_program) { - // we're done - if (running_program.b) { - // intro — we can tidy up immediately - clear_animation(); - } else { - // outro — needs to be coordinated - if (!--running_program.group.r) run_all(running_program.group.c); - } - } - - running_program = null; - } - - else if (now >= running_program.start) { - const p = now - running_program.start; - t = running_program.a + running_program.d * easing(p / running_program.duration); - tick(t, 1 - t); - } - } + if (t >= end_time) this.dispatchEvent(custom_event(`${rx & tx.intro ? 'in' : 'ou'}troend`)); - return !!(running_program || pending_program); - }); - } - } + if (rx & tx.outro && !--current_group.r) + for (let i = 0; i < current_group.c.length; i++) current_group.c[i](t === void 0); - return { - run(b) { - if (is_function(config)) { - wait().then(() => { - // @ts-ignore - config = config(); - go(b); - }); - } else { - go(b); - } - }, + if (0 === (rx & tx.bidirectional)) return; - end() { - clear_animation(); - running_program = pending_program = null; - } + if (-1 === t) + return ( + (t = now()) < end_time && + run_transition( + this, + () => config, + rx ^ tx.reverse, + params, + end_time - t, + start_time > t ? start_time - t : 0, + (1 - elapsed_ratio) * (1 - (config.easing || linear)(1 - (end_time - t) / (end_time - start_time))) + ) + ); + else running_bidi.delete(this); }; -} + + return stop; +}); + +const running_bidi: Map = new Map(); +export const run_bidirectional_transition = /*#__PURE__*/ Function.prototype.call.bind(function bidirectional( + this: HTMLElement, + fn: TransitionFn, + rx: tx.intro | tx.outro, + params: any +) { + let cancel; + running_bidi.set( + this, + (cancel = + (running_bidi.has(this) && running_bidi.get(this)(-1)) || run_transition(this, fn, rx | tx.bidirectional, params)) + ); + return cancel; +}); +export const run_duration = (duration, value1, value2?): number => + typeof duration === 'function' ? duration(value1, value2) : duration; diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index d752c9de9d..5e06aa16bf 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -1,4 +1,4 @@ -export function noop() {} +import { noop } from "./environment"; export const identity = x => x; @@ -146,4 +146,9 @@ export const has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, export function action_destroyer(action_result) { return action_result && is_function(action_result.destroy) ? action_result.destroy : noop; -} \ No newline at end of file +} + +export const methodify = /*#__PURE__*/ (function() { + const call = Function.prototype.call; + return call.bind.bind(call); +})(); \ No newline at end of file diff --git a/src/runtime/transition/index.ts b/src/runtime/transition/index.ts index 0a20c81b1f..d7ffb3b42d 100644 --- a/src/runtime/transition/index.ts +++ b/src/runtime/transition/index.ts @@ -1,110 +1,60 @@ -import { cubicOut, cubicInOut, linear } from 'svelte/easing'; -import { assign, is_function } from 'svelte/internal'; +import { cubicOut, cubicInOut } from 'svelte/easing'; +import { run_duration } from 'svelte/internal'; -type EasingFunction = (t: number) => number; - -export interface TransitionConfig { +interface CssAnimationConfig { delay?: number; duration?: number; - easing?: EasingFunction; - css?: (t: number, u: number) => string; - tick?: (t: number, u: number) => void; + easing?: (t: number) => number; + strategy?: 'reverse' | 'mirror'; } -interface BlurParams { - delay: number; - duration: number; - easing?: EasingFunction; - amount: number; - opacity: number; +export interface CssTransitionConfig extends CssAnimationConfig { + css?: (t: number, u?: number) => string; + tick?: (t: number, u?: number) => void; } -export function blur(node: Element, { - delay = 0, - duration = 400, - easing = cubicInOut, - amount = 5, - opacity = 0 -}: BlurParams): TransitionConfig { +type FlyParams = FadingConfig & { x: number; y: number; }; +type BlurParams = FadingConfig & { amount: number; }; +type ScaleParams = FadingConfig & { start: number; }; +type DrawParams = CssAnimationConfig & { speed : number }; +type FadingConfig = CssAnimationConfig & { opacity: number; }; +type MarkedCrossFadeConfig = TimeableConfig & { key: any; }; +export type TimeableConfig = Omit & { duration?: number | ((len: number) => number) }; +type CrossFadeConfig = TimeableConfig & { fallback(node: Element, params: TimeableConfig, intro: boolean): CssTransitionConfig; }; +type ElementMap = Map; + +export function blur(node: Element, { delay = 0, duration = 400, easing = cubicInOut, amount = 5, opacity = 0 }: BlurParams): CssTransitionConfig { const style = getComputedStyle(node); const target_opacity = +style.opacity; const f = style.filter === 'none' ? '' : style.filter; - const od = target_opacity * (1 - opacity); - return { delay, duration, easing, - css: (_t, u) => `opacity: ${target_opacity - (od * u)}; filter: ${f} blur(${u * amount}px);` + css: (_t, u) => `opacity: ${target_opacity - od * u}; filter:${f} blur(${u * amount}px);`, }; } -interface FadeParams { - delay: number; - duration: number; - easing: EasingFunction; -} - -export function fade(node: Element, { - delay = 0, - duration = 400, - easing = linear -}: FadeParams): TransitionConfig { +export function fade(node: Element, { delay = 0, duration = 400, easing }: CssAnimationConfig): CssTransitionConfig { const o = +getComputedStyle(node).opacity; - - return { - delay, - duration, - easing, - css: t => `opacity: ${t * o}` - }; -} - -interface FlyParams { - delay: number; - duration: number; - easing: EasingFunction; - x: number; - y: number; - opacity: number; + return { delay, duration, easing, css: (t) => `opacity: ${t * o};` }; } -export function fly(node: Element, { - delay = 0, - duration = 400, - easing = cubicOut, - x = 0, - y = 0, - opacity = 0 -}: FlyParams): TransitionConfig { +export function fly(node: Element, { delay = 0, duration = 400, easing = cubicOut, x = 0, y = 0, opacity = 0 }: FlyParams ): CssTransitionConfig { const style = getComputedStyle(node); const target_opacity = +style.opacity; - const transform = style.transform === 'none' ? '' : style.transform; - + const prev = style.transform === 'none' ? '' : style.transform; const od = target_opacity * (1 - opacity); - return { delay, duration, easing, - css: (t, u) => ` - transform: ${transform} translate(${(1 - t) * x}px, ${(1 - t) * y}px); - opacity: ${target_opacity - (od * u)}` + css: (_t, u) => `transform: ${prev} translate(${u * x}px, ${u * y}px); opacity: ${target_opacity - od * u};`, }; } -interface SlideParams { - delay: number; - duration: number; - easing: EasingFunction; -} - -export function slide(node: Element, { - delay = 0, - duration = 400, - easing = cubicOut -}: SlideParams): TransitionConfig { +export function slide(node: Element, { delay = 0, duration = 400, easing = cubicOut }: CssAnimationConfig): CssTransitionConfig { const style = getComputedStyle(node); const opacity = +style.opacity; const height = parseFloat(style.height); @@ -114,159 +64,91 @@ export function slide(node: Element, { const margin_bottom = parseFloat(style.marginBottom); const border_top_width = parseFloat(style.borderTopWidth); const border_bottom_width = parseFloat(style.borderBottomWidth); - return { delay, duration, easing, - css: t => - `overflow: hidden;` + - `opacity: ${Math.min(t * 20, 1) * opacity};` + - `height: ${t * height}px;` + - `padding-top: ${t * padding_top}px;` + - `padding-bottom: ${t * padding_bottom}px;` + - `margin-top: ${t * margin_top}px;` + - `margin-bottom: ${t * margin_bottom}px;` + - `border-top-width: ${t * border_top_width}px;` + - `border-bottom-width: ${t * border_bottom_width}px;` + css: (t) => ` + overflow: hidden; + opacity: ${Math.min(t * 20, 1) * opacity}; + height: ${t * height}px; + padding-top: ${t * padding_top}px; + padding-bottom: ${t * padding_bottom}px; + margin-top: ${t * margin_top}px; + margin-bottom: ${t * margin_bottom}px; + border-top-width: ${t * border_top_width}px; + border-bottom-width: ${t * border_bottom_width}px;`, }; } -interface ScaleParams { - delay: number; - duration: number; - easing: EasingFunction; - start: number; - opacity: number; -} - -export function scale(node: Element, { - delay = 0, - duration = 400, - easing = cubicOut, - start = 0, - opacity = 0 -}: ScaleParams): TransitionConfig { +export function scale(node: Element, { delay = 0, duration = 400, easing = cubicOut, start = 0, opacity = 0 }: ScaleParams): CssTransitionConfig { const style = getComputedStyle(node); const target_opacity = +style.opacity; const transform = style.transform === 'none' ? '' : style.transform; - const sd = 1 - start; const od = target_opacity * (1 - opacity); - return { delay, duration, easing, - css: (_t, u) => ` - transform: ${transform} scale(${1 - (sd * u)}); - opacity: ${target_opacity - (od * u)} - ` + css: (_t, u) => `transform: ${transform} scale(${1 - sd * u}); opacity: ${target_opacity - od * u};`, }; } -interface DrawParams { - delay: number; - speed: number; - duration: number | ((len: number) => number); - easing: EasingFunction; -} -export function draw(node: SVGElement & { getTotalLength(): number }, { - delay = 0, - speed, - duration, - easing = cubicInOut -}: DrawParams): TransitionConfig { +export function draw(node: SVGPathElement | SVGGeometryElement, { delay = 0, speed, duration, easing = cubicInOut }: DrawParams): CssTransitionConfig { const len = node.getTotalLength(); - - if (duration === undefined) { - if (speed === undefined) { - duration = 800; - } else { - duration = len / speed; - } - } else if (typeof duration === 'function') { - duration = duration(len); - } - - return { - delay, - duration, - easing, - css: (t, u) => `stroke-dasharray: ${t * len} ${u * len}` - }; + if (duration === undefined) duration = speed ? len / speed : 800; + else duration = run_duration(duration, len); + return { delay, duration, easing, css: (t, u) => `stroke-dasharray: ${t * len} ${u * len};` }; } -interface CrossfadeParams { - delay: number; - duration: number | ((len: number) => number); - easing: EasingFunction; -} - -type ClientRectMap = Map; - -export function crossfade({ fallback, ...defaults }: CrossfadeParams & { - fallback: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig; -}) { - const to_receive: ClientRectMap = new Map(); - const to_send: ClientRectMap = new Map(); +export function crossfade({ delay: default_delay = 0, duration: default_duration = (d) => Math.sqrt(d) * 30, easing: default_easing = cubicOut, fallback }: CrossFadeConfig) { + const a: ElementMap = new Map(); + const b: ElementMap = new Map(); - function crossfade(from: ClientRect, node: Element, params: CrossfadeParams): TransitionConfig { - const { - delay = 0, - duration = d => Math.sqrt(d) * 30, - easing = cubicOut - } = assign(assign({}, defaults), params); - - const to = node.getBoundingClientRect(); + const crossfade = (from_node: Element, to_node: Element, { delay = default_delay, easing = default_easing, duration = default_duration }: TimeableConfig ) => { + const from = from_node.getBoundingClientRect(); + const to = to_node.getBoundingClientRect(); const dx = from.left - to.left; const dy = from.top - to.top; const dw = from.width / to.width; const dh = from.height / to.height; - const d = Math.sqrt(dx * dx + dy * dy); - - const style = getComputedStyle(node); - const transform = style.transform === 'none' ? '' : style.transform; - const opacity = +style.opacity; - + const { transform, opacity } = getComputedStyle(to_node); + const op = +opacity; + const prev = transform === 'none' ? '' : transform; return { delay, - duration: is_function(duration) ? duration(d) : duration, easing, + duration: run_duration(duration, Math.sqrt(dx * dx + dy * dy)), css: (t, u) => ` - opacity: ${t * opacity}; + opacity: ${t * op}; transform-origin: top left; - transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${t + (1-t) * dw}, ${t + (1-t) * dh}); - ` - }; - } - - function transition(items: ClientRectMap, counterparts: ClientRectMap, intro: boolean) { - return (node: Element, params: CrossfadeParams & { key: any }) => { - items.set(params.key, { - rect: node.getBoundingClientRect() - }); - + transform: ${prev} translate(${u * dx}px,${u * dy}px) scale(${t + (1 - t) * dw}, ${t + (1 - t) * dh}); + `, + } as CssTransitionConfig; + }; + + const transition = (a: ElementMap, b: ElementMap, is_intro: boolean) => ( node: Element, params: MarkedCrossFadeConfig ) => { + const { key } = params; + a.set(key, node); + if (b.has(key)) { + const from_node = b.get(key); + b.delete(key); + return crossfade(from_node, node, params); + } else { return () => { - if (counterparts.has(params.key)) { - const { rect } = counterparts.get(params.key); - counterparts.delete(params.key); - - return crossfade(rect, node, params); + if (b.has(key)) { + const from_node = b.get(key); + b.delete(key); + return crossfade(from_node, node, params); + } else { + a.delete(key); + return fallback && fallback(node, params, is_intro); } - - // if the node is disappearing altogether - // (i.e. wasn't claimed by the other list) - // then we need to supply an outro - items.delete(params.key); - return fallback && fallback(node, params, intro); }; - }; - } + } + }; - return [ - transition(to_send, to_receive, false), - transition(to_receive, to_send, true) - ]; + return [transition(b, a, false), transition(a, b, true)]; }