Group outro callbacks — fixes #648

pull/1429/head
Rich Harris 7 years ago committed by GitHub
parent 53816ee5c5
commit e1db82773d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -418,6 +418,7 @@ export default class EachBlock extends Node {
} }
} }
@transitionManager.groupOutros();
for (; #i < ${iterations}.length; #i += 1) ${outro}(#i); for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
` `
: deindent` : deindent`

@ -702,16 +702,13 @@ export default class Element extends Node {
block.builders.intro.addBlock(deindent` block.builders.intro.addBlock(deindent`
#component.root._aftercreate.push(() => { #component.root._aftercreate.push(() => {
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true, null); if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
${name}.run(true, () => { ${name}.run(1);
#component.fire("intro.end", { node: ${this.var} });
});
}); });
`); `);
block.builders.outro.addBlock(deindent` block.builders.outro.addBlock(deindent`
${name}.run(false, () => { ${name}.run(0, () => {
#component.fire("outro.end", { node: ${this.var} });
${block.outros > 1 ? `if (--#outros === 0) #outrocallback();` : `#outrocallback();`} ${block.outros > 1 ? `if (--#outros === 0) #outrocallback();` : `#outrocallback();`}
${name} = null; ${name} = null;
}); });
@ -737,10 +734,8 @@ export default class Element extends Node {
block.builders.intro.addBlock(deindent` block.builders.intro.addBlock(deindent`
#component.root._aftercreate.push(() => { #component.root._aftercreate.push(() => {
${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true, null); ${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
${introName}.run(true, () => { ${introName}.run(1);
#component.fire("intro.end", { node: ${this.var} });
});
}); });
`); `);
} }
@ -756,9 +751,8 @@ export default class Element extends Node {
// TODO hide elements that have outro'd (unless they belong to a still-outroing // TODO hide elements that have outro'd (unless they belong to a still-outroing
// group) prior to their removal from the DOM // group) prior to their removal from the DOM
block.builders.outro.addBlock(deindent` block.builders.outro.addBlock(deindent`
${outroName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false, null); ${outroName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false);
${outroName}.run(false, () => { ${outroName}.run(0, () => {
#component.fire("outro.end", { node: ${this.var} });
${block.outros > 1 ? `if (--#outros === 0) #outrocallback();` : `#outrocallback();`} ${block.outros > 1 ? `if (--#outros === 0) #outrocallback();` : `#outrocallback();`}
}); });
`); `);

@ -283,6 +283,7 @@ export default class IfBlock extends Node {
const updateMountNode = this.getUpdateMountNode(anchor); const updateMountNode = this.getUpdateMountNode(anchor);
const destroyOldBlock = deindent` const destroyOldBlock = deindent`
@transitionManager.groupOutros();
${name}.o(function() { ${name}.o(function() {
${if_blocks}[ ${previous_block_index} ].d(1); ${if_blocks}[ ${previous_block_index} ].d(1);
${if_blocks}[ ${previous_block_index} ] = null; ${if_blocks}[ ${previous_block_index} ] = null;
@ -406,6 +407,7 @@ export default class IfBlock extends Node {
// as that will typically result in glitching // as that will typically result in glitching
const exit = branch.hasOutroMethod const exit = branch.hasOutroMethod
? deindent` ? deindent`
@transitionManager.groupOutros();
${name}.o(function() { ${name}.o(function() {
${name}.d(1); ${name}.d(1);
${name} = null; ${name} = null;

@ -1,4 +1,5 @@
import { assign, isPromise } from './utils.js'; import { assign, isPromise } from './utils.js';
import { transitionManager } from './transitions.js';
export function handlePromise(promise, info) { export function handlePromise(promise, info) {
var token = info.token = {}; var token = info.token = {};
@ -14,10 +15,13 @@ export function handlePromise(promise, info) {
if (info.block) { if (info.block) {
if (info.blocks) { if (info.blocks) {
info.blocks.forEach((block, i) => { info.blocks.forEach((block, i) => {
if (i !== index && block) block.o(() => { if (i !== index && block) {
transitionManager.groupOutros();
block.o(() => {
block.d(1); block.d(1);
info.blocks[i] = null; info.blocks[i] = null;
}); });
}
}); });
} else { } else {
info.block.d(1); info.block.d(1);

@ -1,3 +1,5 @@
import { transitionManager } from './transitions.js';
export function destroyBlock(block, lookup) { export function destroyBlock(block, lookup) {
block.d(1); block.d(1);
lookup[block.key] = null; lookup[block.key] = null;
@ -43,6 +45,7 @@ export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic
var did_move = {}; var did_move = {};
var destroy = has_outro ? outroAndDestroyBlock : destroyBlock; var destroy = has_outro ? outroAndDestroyBlock : destroyBlock;
if (has_outro) transitionManager.groupOutros();
function insert(block) { function insert(block) {
block[intro_method](node, next); block[intro_method](node, next);

@ -1,48 +1,35 @@
import { createElement } from './dom.js'; import { createElement } from './dom.js';
import { noop } from './utils.js';
export function linear(t) { export function linear(t) {
return t; return t;
} }
export function generateRule( export function generateRule({ a, b, delta, duration }, ease, fn) {
a, let keyframes = '{\n';
b,
delta,
duration,
ease,
fn
) {
var keyframes = '{\n';
for (var p = 0; p <= 1; p += 16.666 / duration) { for (let p = 0; p <= 1; p += 16.666 / duration) {
var t = a + delta * ease(p); const t = a + delta * ease(p);
keyframes += p * 100 + '%{' + fn(t) + '}\n'; keyframes += p * 100 + `%{${fn(t)}}\n`;
} }
return keyframes + '100% {' + fn(b) + '}\n}'; return keyframes + `100% {${fn(b)}}\n}`;
} }
// https://github.com/darkskyapp/string-hash/blob/master/index.js // https://github.com/darkskyapp/string-hash/blob/master/index.js
export function hash(str) { export function hash(str) {
var hash = 5381; let hash = 5381;
var i = str.length; let i = str.length;
while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
return hash >>> 0; return hash >>> 0;
} }
export function wrapTransition(component, node, fn, params, intro, outgroup) { export function wrapTransition(component, node, fn, params, intro) {
var obj = fn(node, params); const obj = fn(node, params);
var duration = obj.duration || 300; const duration = obj.duration || 300;
var ease = obj.easing || linear; const ease = obj.easing || linear;
var cssText; let cssText;
// TODO share <style> tag between all transitions?
if (obj.css && !transitionManager.stylesheet) {
var style = createElement('style');
document.head.appendChild(style);
transitionManager.stylesheet = style.sheet;
}
if (intro) { if (intro) {
if (obj.css && obj.delay) { if (obj.css && obj.delay) {
@ -58,13 +45,19 @@ export function wrapTransition(component, node, fn, params, intro, outgroup) {
running: false, running: false,
program: null, program: null,
pending: null, pending: null,
run: function(intro, callback) {
var program = { run(b, callback) {
const program = {
start: window.performance.now() + (obj.delay || 0), start: window.performance.now() + (obj.delay || 0),
intro: intro, b,
callback: callback callback: callback || noop
}; };
if (!b) {
program.group = transitionManager.outros;
transitionManager.outros.remaining += 1;
}
if (obj.delay) { if (obj.delay) {
this.pending = program; this.pending = program;
} else { } else {
@ -76,11 +69,11 @@ export function wrapTransition(component, node, fn, params, intro, outgroup) {
transitionManager.add(this); transitionManager.add(this);
} }
}, },
start: function(program) {
component.fire(program.intro ? 'intro.start' : 'outro.start', { node: node }); start(program) {
component.fire(`${program.b ? 'intro' : 'outro'}.start`, { node });
program.a = this.t; program.a = this.t;
program.b = program.intro ? 1 : 0;
program.delta = program.b - program.a; program.delta = program.b - program.a;
program.duration = duration * Math.abs(program.b - program.a); program.duration = duration * Math.abs(program.b - program.a);
program.end = program.start + program.duration; program.end = program.start + program.duration;
@ -88,48 +81,55 @@ export function wrapTransition(component, node, fn, params, intro, outgroup) {
if (obj.css) { if (obj.css) {
if (obj.delay) node.style.cssText = cssText; if (obj.delay) node.style.cssText = cssText;
program.rule = generateRule( const rule = generateRule(program, ease, obj.css);
program.a, transitionManager.addRule(rule, program.name = '__svelte_' + hash(rule));
program.b,
program.delta,
program.duration,
ease,
obj.css
);
transitionManager.addRule(program.rule, program.name = '__svelte_' + hash(program.rule));
node.style.animation = (node.style.animation || '') node.style.animation = (node.style.animation || '')
.split(', ') .split(', ')
.filter(function(anim) { .filter(anim => anim && (program.delta < 0 || !/__svelte/.test(anim)))
// when introing, discard old animations if there are any .concat(`${program.name} ${program.duration}ms linear 1 forwards`)
return anim && (program.delta < 0 || !/__svelte/.test(anim));
})
.concat(program.name + ' ' + program.duration + 'ms linear 1 forwards')
.join(', '); .join(', ');
} }
this.program = program; this.program = program;
this.pending = null; this.pending = null;
}, },
update: function(now) {
var program = this.program; update(now) {
const program = this.program;
if (!program) return; if (!program) return;
var p = now - program.start; const p = now - program.start;
this.t = program.a + program.delta * ease(p / program.duration); this.t = program.a + program.delta * ease(p / program.duration);
if (obj.tick) obj.tick(this.t); if (obj.tick) obj.tick(this.t);
}, },
done: function() {
var program = this.program; done() {
const program = this.program;
this.t = program.b; this.t = program.b;
if (obj.tick) obj.tick(this.t); if (obj.tick) obj.tick(this.t);
if (obj.css) transitionManager.deleteRule(node, program.name);
component.fire(`${program.b ? 'intro' : 'outro'}.end`, { node });
if (!program.b) {
program.group.callbacks.push(() => {
program.callback(); program.callback();
program = null; if (obj.css) transitionManager.deleteRule(node, program.name);
});
if (--program.group.remaining === 0) {
program.group.callbacks.forEach(fn => {
fn();
});
}
}
this.program = null;
this.running = !!this.pending; this.running = !!this.pending;
}, },
abort: function() {
abort() {
if (obj.tick) obj.tick(1); if (obj.tick) obj.tick(1);
if (obj.css) transitionManager.deleteRule(node, this.program.name); if (obj.css) transitionManager.deleteRule(node, this.program.name);
this.program = this.pending = null; this.program = this.pending = null;
@ -145,7 +145,7 @@ export var transitionManager = {
stylesheet: null, stylesheet: null,
activeRules: {}, activeRules: {},
add: function(transition) { add(transition) {
this.transitions.push(transition); this.transitions.push(transition);
if (!this.running) { if (!this.running) {
@ -154,21 +154,27 @@ export var transitionManager = {
} }
}, },
addRule: function(rule, name) { addRule(rule, name) {
if (!this.stylesheet) {
const style = createElement('style');
document.head.appendChild(style);
transitionManager.stylesheet = style.sheet;
}
if (!this.activeRules[name]) { if (!this.activeRules[name]) {
this.activeRules[name] = true; this.activeRules[name] = true;
this.stylesheet.insertRule('@keyframes ' + name + ' ' + rule, this.stylesheet.cssRules.length); this.stylesheet.insertRule(`@keyframes ${name} ${rule}`, this.stylesheet.cssRules.length);
} }
}, },
next: function() { next() {
this.running = false; this.running = false;
var now = window.performance.now(); const now = window.performance.now();
var i = this.transitions.length; let i = this.transitions.length;
while (i--) { while (i--) {
var transition = this.transitions[i]; const transition = this.transitions[i];
if (transition.program && now >= transition.program.end) { if (transition.program && now >= transition.program.end) {
transition.done(); transition.done();
@ -189,18 +195,23 @@ export var transitionManager = {
if (this.running) { if (this.running) {
requestAnimationFrame(this.bound); requestAnimationFrame(this.bound);
} else if (this.stylesheet) { } else if (this.stylesheet) {
var i = this.stylesheet.cssRules.length; let i = this.stylesheet.cssRules.length;
while (i--) this.stylesheet.deleteRule(i); while (i--) this.stylesheet.deleteRule(i);
this.activeRules = {}; this.activeRules = {};
} }
}, },
deleteRule: function(node, name) { deleteRule(node, name) {
node.style.animation = node.style.animation node.style.animation = node.style.animation
.split(', ') .split(', ')
.filter(function(anim) { .filter(anim => anim.indexOf(name) === -1)
return anim.indexOf(name) === -1;
})
.join(', '); .join(', ');
},
groupOutros() {
this.outros = {
remaining: 0,
callbacks: []
};
} }
}; };

@ -0,0 +1,24 @@
export default {
data: {
visible: true
},
test(assert, component, target, window, raf) {
component.set({ visible: false });
const outer = target.querySelector('.outer');
const inner = target.querySelector('.inner');
const animations = [
outer.style.animation,
inner.style.animation
];
raf.tick(150);
assert.deepEqual([
outer.style.animation,
inner.style.animation
], animations);
},
};

@ -0,0 +1,31 @@
{#if visible}
<span class='outer' out:foo>
<span class='inner' out:bar>
double transition
</span>
</span>
{/if}
<script>
export default {
transitions: {
foo(node) {
return {
duration: 200,
css: t => {
return `opacity: ${t}`;
}
};
},
bar(node) {
return {
duration: 100,
css: t => {
return `left: ${t * 100}px`;
}
};
}
}
};
</script>
Loading…
Cancel
Save