pull/4742/head
pushkine 6 years ago
parent aabb23cc34
commit 431f567908

@ -0,0 +1,9 @@
{
"printWidth": 120,
"singleQuote": true,
"jsxBracketSameLine": true,
"semi": true,
"useTabs": true,
"jsxSingleQuote": true,
"quoteProps": "consistent"
}

@ -26,105 +26,85 @@ import { Identifier } from 'estree';
import EventHandler from './EventHandler'; import EventHandler from './EventHandler';
import { extract_names } from 'periscopic'; import { extract_names } from 'periscopic';
import Action from '../../../nodes/Action'; import Action from '../../../nodes/Action';
import Transition from '../../../nodes/Transition';
const events = [ const events = [
{ {
event_names: ['input'], event_names: ['input'],
filter: (node: Element, _name: string) => filter: (node: Element, _name: string) =>
node.name === 'textarea' || node.name === 'textarea' ||
node.name === 'input' && !/radio|checkbox|range|file/.test(node.get_static_attribute_value('type') as string) (node.name === 'input' && !/radio|checkbox|range|file/.test(node.get_static_attribute_value('type') as string)),
}, },
{ {
event_names: ['input'], event_names: ['input'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) =>
(name === 'textContent' || name === 'innerHTML') && (name === 'textContent' || name === 'innerHTML') &&
node.attributes.some(attribute => attribute.name === 'contenteditable') node.attributes.some((attribute) => attribute.name === 'contenteditable'),
}, },
{ {
event_names: ['change'], event_names: ['change'],
filter: (node: Element, _name: string) => filter: (node: Element, _name: string) =>
node.name === 'select' || node.name === 'select' ||
node.name === 'input' && /radio|checkbox|file/.test(node.get_static_attribute_value('type') as string) (node.name === 'input' && /radio|checkbox|file/.test(node.get_static_attribute_value('type') as string)),
}, },
{ {
event_names: ['change', 'input'], event_names: ['change', 'input'],
filter: (node: Element, _name: string) => filter: (node: Element, _name: string) =>
node.name === 'input' && node.get_static_attribute_value('type') === 'range' node.name === 'input' && node.get_static_attribute_value('type') === 'range',
}, },
{ {
event_names: ['elementresize'], event_names: ['elementresize'],
filter: (_node: Element, name: string) => filter: (_node: Element, name: string) => dimensions.test(name),
dimensions.test(name)
}, },
// media events // media events
{ {
event_names: ['timeupdate'], event_names: ['timeupdate'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) =>
node.is_media_node() && node.is_media_node() && (name === 'currentTime' || name === 'played' || name === 'ended'),
(name === 'currentTime' || name === 'played' || name === 'ended')
}, },
{ {
event_names: ['durationchange'], event_names: ['durationchange'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && name === 'duration',
node.is_media_node() &&
name === 'duration'
}, },
{ {
event_names: ['play', 'pause'], event_names: ['play', 'pause'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && name === 'paused',
node.is_media_node() &&
name === 'paused'
}, },
{ {
event_names: ['progress'], event_names: ['progress'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && name === 'buffered',
node.is_media_node() &&
name === 'buffered'
}, },
{ {
event_names: ['loadedmetadata'], event_names: ['loadedmetadata'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && (name === 'buffered' || name === 'seekable'),
node.is_media_node() &&
(name === 'buffered' || name === 'seekable')
}, },
{ {
event_names: ['volumechange'], event_names: ['volumechange'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && name === 'volume',
node.is_media_node() &&
name === 'volume'
}, },
{ {
event_names: ['ratechange'], event_names: ['ratechange'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && name === 'playbackRate',
node.is_media_node() &&
name === 'playbackRate'
}, },
{ {
event_names: ['seeking', 'seeked'], event_names: ['seeking', 'seeked'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && name === 'seeking',
node.is_media_node() &&
(name === 'seeking')
}, },
{ {
event_names: ['ended'], event_names: ['ended'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && name === 'ended',
node.is_media_node() &&
name === 'ended'
}, },
{ {
event_names: ['resize'], event_names: ['resize'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) => node.is_media_node() && (name === 'videoHeight' || name === 'videoWidth'),
node.is_media_node() &&
(name === 'videoHeight' || name === 'videoWidth')
}, },
// details event // details event
{ {
event_names: ['toggle'], event_names: ['toggle'],
filter: (node: Element, _name: string) => filter: (node: Element, _name: string) => node.name === 'details',
node.name === 'details'
}, },
]; ];
@ -153,7 +133,7 @@ export default class ElementWrapper extends Wrapper {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.var = { this.var = {
type: 'Identifier', type: 'Identifier',
name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_') name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_'),
}; };
this.void = is_void(node.name); this.void = is_void(node.name);
@ -161,14 +141,14 @@ export default class ElementWrapper extends Wrapper {
this.class_dependencies = []; this.class_dependencies = [];
if (this.node.children.length) { if (this.node.children.length) {
this.node.lets.forEach(l => { this.node.lets.forEach((l) => {
extract_names(l.value || l.name).forEach(name => { extract_names(l.value || l.name).forEach((name) => {
renderer.add_to_context(name, true); renderer.add_to_context(name, true);
}); });
}); });
} }
this.attributes = this.node.attributes.map(attribute => { this.attributes = this.node.attributes.map((attribute) => {
if (attribute.name === 'slot') { if (attribute.name === 'slot') {
// TODO make separate subclass for this? // TODO make separate subclass for this?
let owner = this.parent; let owner = this.parent;
@ -187,28 +167,28 @@ export default class ElementWrapper extends Wrapper {
if (owner && owner.node.type === 'InlineComponent') { if (owner && owner.node.type === 'InlineComponent') {
const name = attribute.get_static_value() as string; const name = attribute.get_static_value() as string;
if (!(owner as unknown as InlineComponentWrapper).slots.has(name)) { if (!((owner as unknown) as InlineComponentWrapper).slots.has(name)) {
const child_block = block.child({ const child_block = block.child({
comment: create_debugging_comment(node, this.renderer.component), comment: create_debugging_comment(node, this.renderer.component),
name: this.renderer.component.get_unique_name(`create_${sanitize(name)}_slot`), name: this.renderer.component.get_unique_name(`create_${sanitize(name)}_slot`),
type: 'slot' type: 'slot',
}); });
const { scope, lets } = this.node; const { scope, lets } = this.node;
const seen = new Set(lets.map(l => l.name.name)); const seen = new Set(lets.map((l) => l.name.name));
(owner as unknown as InlineComponentWrapper).node.lets.forEach(l => { ((owner as unknown) as InlineComponentWrapper).node.lets.forEach((l) => {
if (!seen.has(l.name.name)) lets.push(l); if (!seen.has(l.name.name)) lets.push(l);
}); });
(owner as unknown as InlineComponentWrapper).slots.set( ((owner as unknown) as InlineComponentWrapper).slots.set(
name, name,
get_slot_definition(child_block, scope, lets) get_slot_definition(child_block, scope, lets)
); );
this.renderer.blocks.push(child_block); this.renderer.blocks.push(child_block);
} }
this.slot_block = (owner as unknown as InlineComponentWrapper).slots.get(name).block; this.slot_block = ((owner as unknown) as InlineComponentWrapper).slots.get(name).block;
block = this.slot_block; block = this.slot_block;
} }
} }
@ -221,9 +201,9 @@ export default class ElementWrapper extends Wrapper {
// ordinarily, there'll only be one... but we need to handle // ordinarily, there'll only be one... but we need to handle
// the rare case where an element can have multiple bindings, // the rare case where an element can have multiple bindings,
// e.g. <audio bind:paused bind:currentTime> // e.g. <audio bind:paused bind:currentTime>
this.bindings = this.node.bindings.map(binding => new Binding(block, binding, this)); this.bindings = this.node.bindings.map((binding) => new Binding(block, binding, this));
this.event_handlers = this.node.handlers.map(event_handler => new EventHandler(event_handler, this)); this.event_handlers = this.node.handlers.map((event_handler) => new EventHandler(event_handler, this));
if (node.intro || node.outro) { if (node.intro || node.outro) {
if (node.intro) block.add_intro(node.intro.is_local); if (node.intro) block.add_intro(node.intro.is_local);
@ -235,27 +215,29 @@ export default class ElementWrapper extends Wrapper {
} }
// add directive and handler dependencies // add directive and handler dependencies
[node.animation, node.outro, ...node.actions, ...node.classes].forEach(directive => { [node.animation, node.outro, ...node.actions, ...node.classes].forEach((directive) => {
if (directive && directive.expression) { if (directive && directive.expression) {
block.add_dependencies(directive.expression.dependencies); block.add_dependencies(directive.expression.dependencies);
} }
}); });
node.handlers.forEach(handler => { node.handlers.forEach((handler) => {
if (handler.expression) { if (handler.expression) {
block.add_dependencies(handler.expression.dependencies); block.add_dependencies(handler.expression.dependencies);
} }
}); });
if (this.parent) { if (this.parent) {
if (node.actions.length > 0 || if (
renderer.options.dev ||
node.actions.length ||
node.bindings.length ||
node.handlers.length ||
node.classes.length ||
node.intro ||
node.outro ||
node.animation || node.animation ||
node.bindings.length > 0 || this.node.name === 'option'
node.classes.length > 0 ||
node.intro || node.outro ||
node.handlers.length > 0 ||
this.node.name === 'option' ||
renderer.options.dev
) { ) {
this.parent.cannot_use_innerhtml(); // need to use add_location this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content(); this.parent.not_static_content();
@ -289,32 +271,22 @@ export default class ElementWrapper extends Wrapper {
block.add_variable(node); block.add_variable(node);
const render_statement = this.get_render_statement(block); const render_statement = this.get_render_statement(block);
block.chunks.create.push( block.chunks.create.push(b`${node} = ${render_statement};`);
b`${node} = ${render_statement};`
);
if (renderer.options.hydratable) { if (renderer.options.hydratable) {
if (parent_nodes) { if (parent_nodes) {
block.chunks.claim.push(b` block.chunks.claim.push(b`${node} = ${this.get_claim_statement(parent_nodes)};`);
${node} = ${this.get_claim_statement(parent_nodes)};
`);
if (!this.void && this.node.children.length > 0) { if (!this.void && this.node.children.length > 0) {
block.chunks.claim.push(b` block.chunks.claim.push(b`var ${nodes} = ${children};`);
var ${nodes} = ${children};
`);
} }
} else { } else {
block.chunks.claim.push( block.chunks.claim.push(b`${node} = ${render_statement};`);
b`${node} = ${render_statement};`
);
} }
} }
if (parent_node) { if (parent_node) {
block.chunks.mount.push( block.chunks.mount.push(b`@append(${parent_node}, ${node});`);
b`@append(${parent_node}, ${node});`
);
if (is_head(parent_node)) { if (is_head(parent_node)) {
block.chunks.destroy.push(b`@detach(${node});`); block.chunks.destroy.push(b`@detach(${node});`);
@ -339,39 +311,38 @@ export default class ElementWrapper extends Wrapper {
const state = { const state = {
quasi: { quasi: {
type: 'TemplateElement', type: 'TemplateElement',
value: { raw: '' } value: { raw: '' },
} },
}; };
const literal = { const literal = {
type: 'TemplateLiteral', type: 'TemplateLiteral',
expressions: [], expressions: [],
quasis: [] quasis: [],
}; };
const can_use_raw_text = !this.can_use_innerhtml && can_use_textcontent; const can_use_raw_text = !this.can_use_innerhtml && can_use_textcontent;
to_html((this.fragment.nodes as unknown as Array<ElementWrapper | TextWrapper>), block, literal, state, can_use_raw_text); to_html(
(this.fragment.nodes as unknown) as Array<ElementWrapper | TextWrapper>,
block,
literal,
state,
can_use_raw_text
);
literal.quasis.push(state.quasi); literal.quasis.push(state.quasi);
block.chunks.create.push( block.chunks.create.push(b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};`);
b`${node}.${this.can_use_innerhtml ? 'innerHTML': 'textContent'} = ${literal};`
);
} }
} else { } else {
this.fragment.nodes.forEach((child: Wrapper) => { this.fragment.nodes.forEach((child: Wrapper) => {
child.render( child.render(block, this.node.name === 'template' ? x`${node}.content` : node, nodes);
block,
this.node.name === 'template' ? x`${node}.content` : node,
nodes
);
}); });
} }
const event_handler_or_binding_uses_context = ( const event_handler_or_binding_uses_context =
this.bindings.some(binding => binding.handler.uses_context) || this.bindings.some((binding) => binding.handler.uses_context) ||
this.node.handlers.some(handler => handler.uses_context) || this.node.handlers.some((handler) => handler.uses_context) ||
this.node.actions.some(action => action.uses_context) this.node.actions.some((action) => action.uses_context);
);
if (event_handler_or_binding_uses_context) { if (event_handler_or_binding_uses_context) {
block.maintain_context = true; block.maintain_context = true;
@ -379,15 +350,23 @@ export default class ElementWrapper extends Wrapper {
this.add_attributes(block); this.add_attributes(block);
this.add_directives_in_order(block); this.add_directives_in_order(block);
this.add_transitions(block); const { intro, outro } = this.node;
this.add_animation(block); 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, outro);
}
this.add_classes(block); this.add_classes(block);
this.add_manual_style_scoping(block); this.add_manual_style_scoping(block);
if (nodes && this.renderer.options.hydratable && !this.void) { if (nodes && this.renderer.options.hydratable && !this.void) {
block.chunks.claim.push( block.chunks.claim.push(b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`);
b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`
);
} }
if (renderer.options.dev) { if (renderer.options.dev) {
@ -399,7 +378,10 @@ export default class ElementWrapper extends Wrapper {
} }
can_use_textcontent() { can_use_textcontent() {
return this.is_static_content && this.fragment.nodes.every(node => node.node.type === 'Text' || node.node.type === 'MustacheTag'); return (
this.is_static_content &&
this.fragment.nodes.every((node) => node.node.type === 'Text' || node.node.type === 'MustacheTag')
);
} }
get_render_statement(block: Block) { get_render_statement(block: Block) {
@ -413,7 +395,7 @@ export default class ElementWrapper extends Wrapper {
return x`@_document.createElementNS("${namespace}", "${name}")`; return x`@_document.createElementNS("${namespace}", "${name}")`;
} }
const is = this.attributes.find(attr => attr.node.name === 'is'); const is = this.attributes.find((attr) => attr.node.name === 'is');
if (is) { if (is) {
return x`@element_is("${name}", ${is.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`)})`; return x`@element_is("${name}", ${is.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`)})`;
} }
@ -426,16 +408,14 @@ export default class ElementWrapper extends Wrapper {
.filter((attr) => attr.type === 'Attribute') .filter((attr) => attr.type === 'Attribute')
.map((attr) => p`${attr.name}: true`); .map((attr) => p`${attr.name}: true`);
const name = this.node.namespace const name = this.node.namespace ? this.node.name : this.node.name.toUpperCase();
? this.node.name
: this.node.name.toUpperCase();
const svg = this.node.namespace === namespaces.svg ? 1 : null; const svg = this.node.namespace === namespaces.svg ? 1 : null;
return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`; return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`;
} }
add_directives_in_order (block: Block) { add_directives_in_order(block: Block) {
interface BindingGroup { interface BindingGroup {
events: string[]; events: string[];
bindings: Binding[]; bindings: Binding[];
@ -444,17 +424,17 @@ export default class ElementWrapper extends Wrapper {
type OrderedAttribute = EventHandler | BindingGroup | Binding | Action; type OrderedAttribute = EventHandler | BindingGroup | Binding | Action;
const bindingGroups = events const bindingGroups = events
.map(event => ({ .map((event) => ({
events: event.event_names, events: event.event_names,
bindings: this.bindings bindings: this.bindings
.filter(binding => binding.node.name !== 'this') .filter((binding) => binding.node.name !== 'this')
.filter(binding => event.filter(this.node, binding.node.name)) .filter((binding) => event.filter(this.node, binding.node.name)),
})) }))
.filter(group => group.bindings.length); .filter((group) => group.bindings.length);
const this_binding = this.bindings.find(b => b.node.name === 'this'); const this_binding = this.bindings.find((b) => b.node.name === 'this');
function getOrder (item: OrderedAttribute) { function getOrder(item: OrderedAttribute) {
if (item instanceof EventHandler) { if (item instanceof EventHandler) {
return item.node.start; return item.node.start;
} else if (item instanceof Binding) { } else if (item instanceof Binding) {
@ -466,15 +446,10 @@ export default class ElementWrapper extends Wrapper {
} }
} }
([ ([...bindingGroups, ...this.event_handlers, this_binding, ...this.node.actions] as OrderedAttribute[])
...bindingGroups,
...this.event_handlers,
this_binding,
...this.node.actions
] as OrderedAttribute[])
.filter(Boolean) .filter(Boolean)
.sort((a, b) => getOrder(a) - getOrder(b)) .sort((a, b) => getOrder(a) - getOrder(b))
.forEach(item => { .forEach((item) => {
if (item instanceof EventHandler) { if (item instanceof EventHandler) {
add_event_handler(block, this.var, item); add_event_handler(block, this.var, item);
} else if (item instanceof Binding) { } else if (item instanceof Binding) {
@ -494,23 +469,23 @@ export default class ElementWrapper extends Wrapper {
renderer.component.has_reactive_assignments = true; renderer.component.has_reactive_assignments = true;
const lock = bindingGroup.bindings.some(binding => binding.needs_lock) ? const lock = bindingGroup.bindings.some((binding) => binding.needs_lock)
block.get_unique_name(`${this.var.name}_updating`) : ? block.get_unique_name(`${this.var.name}_updating`)
null; : null;
if (lock) block.add_variable(lock, x`false`); if (lock) block.add_variable(lock, x`false`);
[bindingGroup].forEach(group => { [bindingGroup].forEach((group) => {
const handler = renderer.component.get_unique_name(`${this.var.name}_${group.events.join('_')}_handler`); const handler = renderer.component.get_unique_name(`${this.var.name}_${group.events.join('_')}_handler`);
renderer.add_to_context(handler.name); renderer.add_to_context(handler.name);
// TODO figure out how to handle locks // TODO figure out how to handle locks
const needs_lock = group.bindings.some(binding => binding.needs_lock); const needs_lock = group.bindings.some((binding) => binding.needs_lock);
const dependencies: Set<string> = new Set(); const dependencies: Set<string> = new Set();
const contextual_dependencies: Set<string> = new Set(); const contextual_dependencies: Set<string> = new Set();
group.bindings.forEach(binding => { group.bindings.forEach((binding) => {
// TODO this is a mess // TODO this is a mess
add_to_set(dependencies, binding.get_dependencies()); add_to_set(dependencies, binding.get_dependencies());
add_to_set(contextual_dependencies, binding.node.expression.contextual_dependencies); add_to_set(contextual_dependencies, binding.node.expression.contextual_dependencies);
@ -534,7 +509,7 @@ export default class ElementWrapper extends Wrapper {
// TODO dry this out — similar code for event handlers and component bindings // TODO dry this out — similar code for event handlers and component bindings
if (has_local_function) { if (has_local_function) {
const args = Array.from(contextual_dependencies).map(name => renderer.reference(name)); const args = Array.from(contextual_dependencies).map((name) => renderer.reference(name));
// need to create a block-local function that calls an instance-level function // need to create a block-local function that calls an instance-level function
if (animation_frame) { if (animation_frame) {
@ -560,22 +535,22 @@ export default class ElementWrapper extends Wrapper {
callee = handler; callee = handler;
} }
const params = Array.from(contextual_dependencies).map(name => ({ const params = Array.from(contextual_dependencies).map((name) => ({
type: 'Identifier', type: 'Identifier',
name name,
})); }));
this.renderer.component.partly_hoisted.push(b` this.renderer.component.partly_hoisted.push(b`
function ${handler}(${params}) { function ${handler}(${params}) {
${group.bindings.map(b => b.handler.mutation)} ${group.bindings.map((b) => b.handler.mutation)}
${Array.from(dependencies) ${Array.from(dependencies)
.filter(dep => dep[0] !== '$') .filter((dep) => dep[0] !== '$')
.filter(dep => !contextual_dependencies.has(dep)) .filter((dep) => !contextual_dependencies.has(dep))
.map(dep => b`${this.renderer.invalidate(dep)};`)} .map((dep) => b`${this.renderer.invalidate(dep)};`)}
} }
`); `);
group.events.forEach(name => { group.events.forEach((name) => {
if (name === 'elementresize') { if (name === 'elementresize') {
// special case // special case
const resize_listener = block.get_unique_name(`${this.var.name}_resize_listener`); const resize_listener = block.get_unique_name(`${this.var.name}_resize_listener`);
@ -585,43 +560,34 @@ export default class ElementWrapper extends Wrapper {
b`${resize_listener} = @add_resize_listener(${this.var}, ${callee}.bind(${this.var}));` b`${resize_listener} = @add_resize_listener(${this.var}, ${callee}.bind(${this.var}));`
); );
block.chunks.destroy.push( block.chunks.destroy.push(b`${resize_listener}();`);
b`${resize_listener}();`
);
} else { } else {
block.event_listeners.push( block.event_listeners.push(x`@listen(${this.var}, "${name}", ${callee})`);
x`@listen(${this.var}, "${name}", ${callee})`
);
} }
}); });
const some_initial_state_is_undefined = group.bindings const some_initial_state_is_undefined = group.bindings
.map(binding => x`${binding.snippet} === void 0`) .map((binding) => x`${binding.snippet} === void 0`)
.reduce((lhs, rhs) => x`${lhs} || ${rhs}`); .reduce((lhs, rhs) => x`${lhs} || ${rhs}`);
const should_initialise = ( const should_initialise =
this.node.name === 'select' || this.node.name === 'select' ||
group.bindings.find(binding => { group.bindings.find((binding) => {
return ( return (
binding.node.name === 'indeterminate' || binding.node.name === 'indeterminate' ||
binding.node.name === 'textContent' || binding.node.name === 'textContent' ||
binding.node.name === 'innerHTML' || binding.node.name === 'innerHTML' ||
binding.is_readonly_media_attribute() binding.is_readonly_media_attribute()
); );
}) });
);
if (should_initialise) { if (should_initialise) {
const callback = has_local_function ? handler : x`() => ${callee}.call(${this.var})`; const callback = has_local_function ? handler : x`() => ${callee}.call(${this.var})`;
block.chunks.hydrate.push( block.chunks.hydrate.push(b`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`);
b`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`
);
} }
if (group.events[0] === 'elementresize') { if (group.events[0] === 'elementresize') {
block.chunks.hydrate.push( block.chunks.hydrate.push(b`@add_render_callback(() => ${callee}.call(${this.var}));`);
b`@add_render_callback(() => ${callee}.call(${this.var}));`
);
} }
}); });
@ -648,7 +614,7 @@ export default class ElementWrapper extends Wrapper {
} }
}); });
if (this.node.attributes.some(attr => attr.is_spread)) { if (this.node.attributes.some((attr) => attr.is_spread)) {
this.add_spread_attributes(block); this.add_spread_attributes(block);
return; return;
} }
@ -665,11 +631,9 @@ export default class ElementWrapper extends Wrapper {
const initial_props = []; const initial_props = [];
const updates = []; const updates = [];
this.attributes this.attributes.forEach((attr) => {
.forEach(attr => { const condition =
const condition = attr.node.dependencies.size > 0 attr.node.dependencies.size > 0 ? block.renderer.dirty(Array.from(attr.node.dependencies)) : null;
? block.renderer.dirty(Array.from(attr.node.dependencies))
: null;
if (attr.node.is_spread) { if (attr.node.is_spread) {
const snippet = attr.node.expression.manipulate(block); const snippet = attr.node.expression.manipulate(block);
@ -700,181 +664,114 @@ export default class ElementWrapper extends Wrapper {
const fn = this.node.namespace === namespaces.svg ? x`@set_svg_attributes` : x`@set_attributes`; const fn = this.node.namespace === namespaces.svg ? x`@set_svg_attributes` : x`@set_attributes`;
block.chunks.hydrate.push( block.chunks.hydrate.push(b`${fn}(${this.var}, ${data});`);
b`${fn}(${this.var}, ${data});`
);
block.chunks.update.push(b` block.chunks.update.push(b`${fn}(${this.var}, @get_spread_update(${levels}, [${updates}]));`);
${fn}(${this.var}, @get_spread_update(${levels}, [
${updates}
]));
`);
} }
add_bidi_transition(block: Block, intro: Transition) {
add_transitions( const name = block.get_unique_name(`${this.var.name}_bidi_transition`);
block: Block const snippet = intro.expression ? intro.expression.manipulate(block) : x`{}`;
) {
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); block.add_variable(name);
const fn = this.renderer.reference(intro.name); const fn = this.renderer.reference(intro.name);
const intro_block = b` let intro_block = b`${name} = @run_bidirectional_transition(${this.var}, ${fn}, true, ${snippet});`;
@add_render_callback(() => {
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true);
${name}.run(1);
});
`;
const outro_block = b` let outro_block = b`${name} = @run_bidirectional_transition(${this.var}, ${fn}, false, ${snippet});`;
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, false);
${name}.run(0);
`;
if (intro.is_local) { if (intro.is_local) {
block.chunks.intro.push(b` intro_block = b`if (#local) {${intro_block}}`;
if (#local) { outro_block = b`if (#local) {${outro_block}}`;
${intro_block}
}
`);
block.chunks.outro.push(b`
if (#local) {
${outro_block}
} }
`);
} else {
block.chunks.intro.push(intro_block); block.chunks.intro.push(intro_block);
block.chunks.outro.push(outro_block); block.chunks.outro.push(outro_block);
}
block.chunks.destroy.push(b`if (detaching && ${name}) ${name}.end();`); block.chunks.destroy.push(b`if (detaching && ${name}) ${name}();`);
} }
add_intro(block: Block, intro: Transition, outro: Transition) {
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) { if (outro) {
intro_block = b` const outro_var = block.alias(`${this.var.name}_outro`);
@add_render_callback(() => { if (this.node.animation) {
if (${outro_name}) ${outro_name}.end(1); const [unfreeze_var, rect_var, stop_animation_var, animationFn, params] = run_animation(this, block);
if (!${intro_name}) ${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet}); block.chunks.intro.push(b`
${intro_name}.start(); if (${outro_var}){
}); ${outro_var}(1);
`; if (${unfreeze_var}) {
${unfreeze_var}(), (unfreeze = undefined);
block.chunks.outro.push(b`if (${intro_name}) ${intro_name}.invalidate();`); ${stop_animation_var} = @run_animation(${this.var}, ${rect_var}, ${animationFn}, ${params});
} 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.intro.push(intro_block);
} }
`);
if (outro) { } else {
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` block.chunks.intro.push(b`
if (${outro_name}) ${outro_name}.end(1); if (${outro_var}){
${outro_var}(1);
}
`); `);
} }
}
if (!intro) return;
// TODO hide elements that have outro'd (unless they belong to a still-outroing const [intro_var, node, transitionFn, params] = run_transition(this, block, intro, `intro`);
// group) prior to their removal from the DOM block.add_variable(intro_var);
let outro_block = b`
${outro_name} = @create_out_transition(${this.var}, ${fn}, ${snippet});
`;
if (outro.is_local) { let start_intro = b`@add_render_callback(()=>{${intro_var} = @run_transition(${node}, ${transitionFn}, true, ${params});})`;
outro_block = b` if (intro.is_local) start_intro = b`if (#local) ${start_intro};`;
if (#local) { block.chunks.intro.push(start_intro);
${outro_block}
} }
`; // 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`if (${intro_var}) ${intro_var}();`);
} }
if (!outro) return;
block.chunks.outro.push(outro_block); const [outro_var, node, transitionFn, params] = run_transition(this, block, outro, `outro`);
block.add_variable(outro_var);
block.chunks.destroy.push(b`if (detaching && ${outro_name}) ${outro_name}.end();`); let start_outro = b`${outro_var} = @run_transition(${node}, ${transitionFn}, false, ${params});`;
} if (intro.is_local) start_outro = b`if (#local) ${start_outro};`;
} block.chunks.outro.push(start_outro);
}
add_animation(block: Block) { block.chunks.destroy.push(b`if (detaching && ${outro_var}) ${outro_var}();`);
if (!this.node.animation) return; }
const { outro } = this.node; add_animation(block: Block, intro: Transition, outro: Transition) {
const intro_var = intro && block.alias(`${this.var.name}_intro`);
const rect = block.get_unique_name('rect'); const [unfreeze_var, rect_var, stop_animation_var, name_var, params_var] = run_animation(this, block);
const stop_animation = block.get_unique_name('stop_animation');
block.add_variable(rect); block.add_variable(unfreeze_var);
block.add_variable(stop_animation, x`@noop`); block.add_variable(rect_var);
block.add_variable(stop_animation_var, x`@noop`);
block.chunks.measure.push(b` block.chunks.measure.push(b`
${rect} = ${this.var}.getBoundingClientRect(); ${rect_var} = ${this.var}.getBoundingClientRect();
${intro && b`if(${intro_var}) ${intro_var}();`}
`); `);
block.chunks.fix.push(b` block.chunks.fix.push(b`
@fix_position(${this.var}); ${unfreeze_var} = @fix_position(${this.var});
${stop_animation}(); ${stop_animation_var}();
${outro && b`@add_transform(${this.var}, ${rect});`} ${outro && b`@add_transform(${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` block.chunks.animate.push(b`
${stop_animation}(); if(${unfreeze_var}) return
${stop_animation} = @create_animation(${this.var}, ${rect}, ${name}, ${params}); ${stop_animation_var}();
@add_render_callback(()=>{${stop_animation_var} = @run_animation(${this.var}, ${rect_var}, ${name_var}, ${params_var});});
`); `);
block.chunks.destroy.push(b`${unfreeze_var} = undefined;`);
} }
add_classes(block: Block) { add_classes(block: Block) {
const has_spread = this.node.attributes.some(attr => attr.is_spread); const has_spread = this.node.attributes.some((attr) => attr.is_spread);
this.node.classes.forEach(class_directive => { this.node.classes.forEach((class_directive) => {
const { expression, name } = class_directive; const { expression, name } = class_directive;
let snippet; let snippet;
let dependencies; let dependencies;
@ -912,46 +809,42 @@ export default class ElementWrapper extends Wrapper {
} }
} }
function to_html(wrappers: Array<ElementWrapper | TextWrapper | TagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) { function to_html(
wrappers.forEach(wrapper => { wrappers: Array<ElementWrapper | TextWrapper | TagWrapper>,
block: Block,
literal: any,
state: any,
can_use_raw_text?: boolean
) {
wrappers.forEach((wrapper) => {
if (wrapper.node.type === 'Text') { if (wrapper.node.type === 'Text') {
if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' '; if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' ';
const parent = wrapper.node.parent as Element; const parent = wrapper.node.parent as Element;
const raw = parent && ( const raw = parent && (parent.name === 'script' || parent.name === 'style' || can_use_raw_text);
parent.name === 'script' ||
parent.name === 'style' ||
can_use_raw_text
);
state.quasi.value.raw += (raw ? wrapper.node.data : escape_html(wrapper.node.data)) state.quasi.value.raw += (raw ? wrapper.node.data : escape_html(wrapper.node.data))
.replace(/\\/g, '\\\\') .replace(/\\/g, '\\\\')
.replace(/`/g, '\\`') .replace(/`/g, '\\`')
.replace(/\$/g, '\\$'); .replace(/\$/g, '\\$');
} } else if (wrapper.node.type === 'MustacheTag' || wrapper.node.type === 'RawMustacheTag') {
else if (wrapper.node.type === 'MustacheTag' || wrapper.node.type === 'RawMustacheTag' ) {
literal.quasis.push(state.quasi); literal.quasis.push(state.quasi);
literal.expressions.push(wrapper.node.expression.manipulate(block)); literal.expressions.push(wrapper.node.expression.manipulate(block));
state.quasi = { state.quasi = {
type: 'TemplateElement', type: 'TemplateElement',
value: { raw: '' } value: { raw: '' },
}; };
} } else if (wrapper.node.name === 'noscript') {
else if (wrapper.node.name === 'noscript') {
// do nothing // do nothing
} } else {
else {
// element // element
state.quasi.value.raw += `<${wrapper.node.name}`; state.quasi.value.raw += `<${wrapper.node.name}`;
(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => { (wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
state.quasi.value.raw += ` ${fix_attribute_casing(attr.node.name)}="`; state.quasi.value.raw += ` ${fix_attribute_casing(attr.node.name)}="`;
attr.node.chunks.forEach(chunk => { attr.node.chunks.forEach((chunk) => {
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
state.quasi.value.raw += escape_html(chunk.data); state.quasi.value.raw += escape_html(chunk.data);
} else { } else {
@ -960,7 +853,7 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | TagWrapper>, blo
state.quasi = { state.quasi = {
type: 'TemplateElement', type: 'TemplateElement',
value: { raw: '' } value: { raw: '' },
}; };
} }
}); });
@ -971,10 +864,36 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | TagWrapper>, blo
state.quasi.value.raw += '>'; state.quasi.value.raw += '>';
if (!(wrapper as ElementWrapper).void) { if (!(wrapper as ElementWrapper).void) {
to_html((wrapper as ElementWrapper).fragment.nodes as Array<ElementWrapper | TextWrapper>, block, literal, state); to_html(
(wrapper as ElementWrapper).fragment.nodes as Array<ElementWrapper | TextWrapper>,
block,
literal,
state
);
state.quasi.value.raw += `</${wrapper.node.name}>`; state.quasi.value.raw += `</${wrapper.node.name}>`;
} }
} }
}); });
} }
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) : x`{}`,
];
}
function run_transition(element: ElementWrapper, block: Block, transition: Transition, type: string) {
// const run = b`
// 0 = @run_transition(1, `2`, ${
// type === 'intro' ? `true` : `false`
// }, ${params});
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) : x`{}`,
];
}

@ -5,7 +5,7 @@ import { children, detach } from './dom';
import { transition_in } from './transitions'; import { transition_in } from './transitions';
interface Fragment { interface Fragment {
key: string|null; key: string | null;
first: null; first: null;
/* create */ c: () => void; /* create */ c: () => void;
/* claim */ l: (nodes: any) => void; /* claim */ l: (nodes: any) => void;
@ -17,18 +17,18 @@ interface Fragment {
/* animate */ a: () => void; /* animate */ a: () => void;
/* intro */ i: (local: any) => void; /* intro */ i: (local: any) => void;
/* outro */ o: (local: any) => void; /* outro */ o: (local: any) => void;
/* destroy */ d: (detaching: 0|1) => void; /* destroy */ d: (detaching: 0 | 1) => void;
} }
// eslint-disable-next-line @typescript-eslint/class-name-casing // eslint-disable-next-line @typescript-eslint/class-name-casing
interface T$$ { interface T$$ {
dirty: number[]; dirty: number[];
ctx: null|any; ctx: null | any;
bound: any; bound: any;
update: () => void; update: () => void;
callbacks: any; callbacks: any;
after_update: any[]; after_update: any[];
props: Record<string, 0 | string>; props: Record<string, 0 | string>;
fragment: null|false|Fragment; fragment: null | false | Fragment;
not_equal: any; not_equal: any;
before_update: any[]; before_update: any[];
context: Map<any, any>; context: Map<any, any>;
@ -38,24 +38,23 @@ interface T$$ {
export function bind(component, name, callback) { export function bind(component, name, callback) {
const index = component.$$.props[name]; const index = component.$$.props[name];
if (index !== undefined) { if (index === undefined) return;
component.$$.bound[index] = callback; component.$$.bound[index] = callback;
callback(component.$$.ctx[index]); callback(component.$$.ctx[index]);
}
} }
export function create_component(block) { export function create_component(block) {
block && block.c(); if (block) block.c();
} }
export function claim_component(block, parent_nodes) { export function claim_component(block, parent_nodes) {
block && block.l(parent_nodes); if (block) block.l(parent_nodes);
} }
export function mount_component(component, target, anchor) { export function mount_component(component, target, anchor) {
const { fragment, on_mount, on_destroy, after_update } = component.$$; const { fragment, on_mount, on_destroy, after_update } = component.$$;
fragment && fragment.m(target, anchor); if (fragment) fragment.m(target, anchor);
// onMount happens before the initial afterUpdate // onMount happens before the initial afterUpdate
add_render_callback(() => { add_render_callback(() => {
@ -73,18 +72,16 @@ export function mount_component(component, target, anchor) {
after_update.forEach(add_render_callback); after_update.forEach(add_render_callback);
} }
export function destroy_component(component, detaching) { export function destroy_component({ $$ }, detaching) {
const $$ = component.$$; if ($$.fragment === null) return;
if ($$.fragment !== null) {
run_all($$.on_destroy);
$$.fragment && $$.fragment.d(detaching); run_all($$.on_destroy);
if ($$.fragment) $$.fragment.d(detaching);
// TODO null out other refs, including component.$$ (but need to // TODO null out other refs, including component.$$
// preserve final state?) // (need to preserve final state?)
$$.on_destroy = $$.fragment = null; $$.on_destroy = $$.fragment = null;
$$.ctx = []; $$.ctx = [];
}
} }
function make_dirty(component, i) { function make_dirty(component, i) {
@ -93,16 +90,23 @@ function make_dirty(component, i) {
schedule_update(); schedule_update();
component.$$.dirty.fill(0); component.$$.dirty.fill(0);
} }
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); component.$$.dirty[(i / 31) | 0] |= 1 << i % 31;
} }
export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { export function init(
component,
{ props: prop_values = {}, target, hydrate, intro, anchor },
instance,
create_fragment,
not_equal,
props,
dirty = [-1]
) {
let ready = false;
const parent_component = current_component; const parent_component = current_component;
set_current_component(component); set_current_component(component);
const prop_values = options.props || {}; const $$: T$$ = (component.$$ = {
const $$: T$$ = component.$$ = {
fragment: null, fragment: null,
ctx: null, ctx: null,
@ -121,45 +125,40 @@ export function init(component, options, instance, create_fragment, not_equal, p
// everything else // everything else
callbacks: blank_object(), callbacks: blank_object(),
dirty dirty,
}; });
let ready = false;
$$.ctx = instance $$.ctx = instance
? instance(component, prop_values, (i, ret, ...rest) => { ? instance(component, prop_values, (i, res, val = res) => {
const value = rest.length ? rest[0] : ret; if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = val))) {
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { if ($$.bound[i]) $$.bound[i](val);
if ($$.bound[i]) $$.bound[i](value);
if (ready) make_dirty(component, i); if (ready) make_dirty(component, i);
} }
return ret; return res;
}) })
: []; : [];
$$.update(); $$.update();
ready = true; ready = true;
run_all($$.before_update); run_all($$.before_update);
// `false` as a special case of no DOM component // false when empty fragment
$$.fragment = create_fragment ? create_fragment($$.ctx) : false; $$.fragment = create_fragment ? create_fragment($$.ctx) : false;
if (options.target) { if (target) {
if (options.hydrate) { if (hydrate) {
const nodes = children(options.target); const nodes = children(target);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion if ($$.fragment) $$.fragment.l(nodes);
$$.fragment && $$.fragment!.l(nodes);
nodes.forEach(detach); nodes.forEach(detach);
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion if ($$.fragment) $$.fragment.c();
$$.fragment && $$.fragment!.c();
} }
if (intro) transition_in(component.$$.fragment);
if (options.intro) transition_in(component.$$.fragment); mount_component(component, target, anchor);
mount_component(component, options.target, options.anchor);
flush(); flush();
} }
set_current_component(parent_component); set_current_component(parent_component);
} }
@ -191,40 +190,33 @@ if (typeof HTMLElement === 'function') {
$on(type, callback) { $on(type, callback) {
// TODO should this delegate to addEventListener? // TODO should this delegate to addEventListener?
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
callbacks.push(callback); callbacks.push(callback);
return () => { return () => {
const index = callbacks.indexOf(callback); const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1); if (~index) callbacks.splice(index, 1);
}; };
} }
$set() {
// overridden by instance, if it has props // overridden by instance, if it has props
} $set() {}
}; };
} }
export class SvelteComponent { export class SvelteComponent {
$$: T$$; $$: T$$;
$destroy() { $destroy() {
destroy_component(this, 1); destroy_component(this, 1);
this.$destroy = noop; this.$destroy = noop;
} }
$on(type, callback) { $on(type, callback) {
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
callbacks.push(callback); callbacks.push(callback);
return () => { return () => {
const index = callbacks.indexOf(callback); const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1); if (~index) callbacks.splice(index, 1);
}; };
} }
$set() {
// overridden by instance, if it has props // overridden by instance, if it has props
} $set() {}
} }

@ -1,103 +1,49 @@
import { identity as linear, noop } from './utils'; import { noop } from './utils';
import { now } from './environment';
import { loop } from './loop';
import { create_rule, delete_rule } from './style_manager';
import { AnimationConfig } from '../animate'; import { AnimationConfig } from '../animate';
import { run_transition } from './transitions';
//todo: documentation says it is DOMRect, but in IE it would be ClientRect //todo: documentation says it is DOMRect, but in IE it would be ClientRect
type PositionRect = DOMRect|ClientRect; type PositionRect = DOMRect | ClientRect;
type AnimationFn = (node: Element, { from, to }: { from: PositionRect; to: PositionRect }, params: any) => AnimationConfig; 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) { export function run_animation(node: HTMLElement, from: PositionRect, fn: AnimationFn, params) {
if (!from) return noop; if (!from) return noop;
const to = node.getBoundingClientRect(); const to = node.getBoundingClientRect();
if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) return noop; if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) return noop;
return run_transition(node, (node, params) => fn(node, { from, to }, params), true, params);
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) { export function fix_position(node: HTMLElement) {
const style = getComputedStyle(node); const style = getComputedStyle(node);
if (style.position !== 'absolute' && style.position !== 'fixed') { if (style.position !== 'absolute' && style.position !== 'fixed') {
const { width, height } = style; const { width, height } = style;
const a = node.getBoundingClientRect(); const a = node.getBoundingClientRect();
const { position: og_position, width: og_width, height: og_height } = node.style;
node.style.position = 'absolute'; node.style.position = 'absolute';
node.style.width = width; node.style.width = width;
node.style.height = height; node.style.height = height;
add_transform(node, a); add_transform(node, a);
return () => {
node.style.position = og_position;
node.style.width = og_width;
node.style.height = og_height;
node.style.transform = '';
};
} }
} }
export function add_transform(node: Element & ElementCSSInlineStyle, a: PositionRect) { export function add_transform(node: HTMLElement, a: PositionRect) {
const b = node.getBoundingClientRect(); const b = node.getBoundingClientRect();
if (a.left !== b.left || a.top !== b.top) { if (a.left !== b.left || a.top !== b.top) {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`; node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`;
} }
} }

@ -1,21 +1,22 @@
import { raf } from './environment'; import { raf, now } from './environment';
import { noop } from './utils';
export interface Task { abort(): void; promise: Promise<void> }
export interface Task {
abort(): void;
promise: Promise<void>;
}
type TaskCallback = (now: number) => boolean | void; type TaskCallback = (now: number) => boolean | void;
type TaskEntry = { c: TaskCallback; f: () => void }; type TaskEntry = { c: TaskCallback; f: () => void };
const tasks = new Set<TaskEntry>(); const tasks = new Set<TaskEntry>();
function run_tasks(now: number) { function run_tasks(now: number) {
tasks.forEach(task => { tasks.forEach((task) => {
if (!task.c(now)) { if (task.c(now)) return;
tasks.delete(task); tasks.delete(task);
task.f(); task.f();
}
}); });
if (tasks.size) raf(run_tasks);
if (tasks.size !== 0) raf(run_tasks);
} }
/** /**
@ -31,15 +32,62 @@ export function clear_loops() {
*/ */
export function loop(callback: TaskCallback): Task { export function loop(callback: TaskCallback): Task {
let task: TaskEntry; let task: TaskEntry;
if (!tasks.size) raf(run_tasks);
if (tasks.size === 0) raf(run_tasks);
return { return {
promise: new Promise(fulfill => { promise: new Promise((fulfill) => {
tasks.add(task = { c: callback, f: fulfill }); tasks.add((task = { c: callback, f: fulfill }));
}), }),
abort() { abort() {
tasks.delete(task); tasks.delete(task);
},
};
}
function add(c) {
const task = { c, f: noop };
if (!tasks.size) raf(run_tasks);
tasks.add(task);
return () => tasks.delete(task);
}
const timed_tasks = [];
// callback on 1st frame after timestamp
export function raf_timeout(callback: () => void, timestamp: number) {
let i = timed_tasks.length;
let v;
const task = { c: callback, t: timestamp };
if (i) {
while (i > 0 && timestamp > (v = timed_tasks[i - 1]).t) {
// bubble sort descending until timestamp < task.timestamp
timed_tasks[i--] = v;
}
timed_tasks[i] = task;
} else {
timed_tasks.push(task);
add((now) => {
let i = timed_tasks.length;
while (i > 0 && now >= timed_tasks[--i].t) {
// pop() until now < task.timestamp
timed_tasks.pop().c(now);
}
console.log(i, timed_tasks, now);
return timed_tasks.length;
});
} }
return () => {
const index = timed_tasks.indexOf(task);
if (~index) timed_tasks.splice(index, 1);
console.log(timed_tasks);
}; };
} }
export function loopThen(delay: number, run: (now: number) => void, stop: () => void, end_time: number) {
const fn = () => add((now) => (now < end_time ? (run(now), true) : (stop(), false)));
if (delay < 16) return fn();
else {
let cancel = raf_timeout(() => (cancel = fn()), now() + delay - 16.6667);
return () => cancel();
}
}
export function next_frame(callback) {
return add(() => (callback(), false));
}

@ -12,10 +12,9 @@ const resolved_promise = Promise.resolve();
let update_scheduled = false; let update_scheduled = false;
export function schedule_update() { export function schedule_update() {
if (!update_scheduled) { if (update_scheduled) return;
update_scheduled = true; update_scheduled = true;
resolved_promise.then(flush); resolved_promise.then(flush);
}
} }
export function tick() { export function tick() {
@ -33,10 +32,45 @@ export function add_flush_callback(fn) {
let flushing = false; let flushing = false;
const seen_callbacks = new Set(); const seen_callbacks = new Set();
// export function flush() {
// if (flushing) return;
// flushing = true;
// do {
// // update components + beforeUpdate
// for (let i = 0, component; i < dirty_components.length; i++) {
// set_current_component((component = dirty_components[i]));
// update(component.$$);
// }
// dirty_components.length = 0;
// // update bindings
// for (let i = 0; i < binding_callbacks.length; i++) {
// binding_callbacks[i]();
// }
// binding_callbacks.length = 0;
// // afterUpdate
// for (let i = 0, callback; i < render_callbacks.length; i++) {
// if (seen_callbacks.has((callback = render_callbacks[i]))) continue;
// seen_callbacks.add(callback);
// callback();
// }
// render_callbacks.length = 0;
// } while (dirty_components.length);
// for (let i = 0; i < flush_callbacks.length; i++) {
// flush_callbacks[i]();
// }
// flush_callbacks.length = 0;
// update_scheduled = false;
// flushing = false;
// seen_callbacks.clear();
// }
export function flush() { export function flush() {
if (flushing) return; if (flushing) return;
flushing = true; flushing = true;
do { do {
// first, call beforeUpdate functions // first, call beforeUpdate functions
// and update components // and update components
@ -45,45 +79,35 @@ export function flush() {
set_current_component(component); set_current_component(component);
update(component.$$); update(component.$$);
} }
dirty_components.length = 0; dirty_components.length = 0;
while (binding_callbacks.length) binding_callbacks.pop()(); while (binding_callbacks.length) binding_callbacks.pop()();
// then, once components are updated, call // then, once components are updated, call
// afterUpdate functions. This may cause // afterUpdate functions. This may cause
// subsequent updates... // subsequent updates...
for (let i = 0; i < render_callbacks.length; i += 1) { for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i]; const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) { if (!seen_callbacks.has(callback)) {
// ...so guard against infinite loops // ...so guard against infinite loops
seen_callbacks.add(callback); seen_callbacks.add(callback);
callback(); callback();
} }
} }
render_callbacks.length = 0; render_callbacks.length = 0;
} while (dirty_components.length); } while (dirty_components.length);
while (flush_callbacks.length) { while (flush_callbacks.length) {
flush_callbacks.pop()(); flush_callbacks.pop()();
} }
update_scheduled = false; update_scheduled = false;
flushing = false; flushing = false;
seen_callbacks.clear(); seen_callbacks.clear();
} }
function update($$) { function update($$) {
if ($$.fragment !== null) { if ($$.fragment === null) return;
$$.update(); $$.update();
run_all($$.before_update); run_all($$.before_update);
const dirty = $$.dirty; const dirty = $$.dirty;
$$.dirty = [-1]; $$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty); $$.fragment && $$.fragment.p($$.ctx, dirty);
$$.after_update.forEach(add_render_callback); $$.after_update.forEach(add_render_callback);
}
} }

@ -1,14 +1,29 @@
import { element } from './dom'; import { element } from './dom';
import { raf } from './environment'; import { next_frame } from './loop';
const svelte_rule = `__svelte_`;
interface ExtendedDoc extends Document { interface ExtendedDoc extends Document {
__svelte_stylesheet: CSSStyleSheet; __svelte_stylesheet: CSSStyleSheet;
__svelte_rules: Record<string, true>; __svelte_rules: Set<string>;
} }
const active_docs = new Set<ExtendedDoc>(); const active_documents = new Set<ExtendedDoc>();
let active = 0; let running_animations = 0;
function rulesheet({ ownerDocument }): [CSSStyleSheet, Set<string>] {
const doc = ownerDocument as ExtendedDoc;
if (!active_documents.has(doc)) {
active_documents.add(doc);
if (!doc.__svelte_stylesheet) {
doc.__svelte_stylesheet = doc.head.appendChild(element('style')).sheet as CSSStyleSheet;
}
if (!doc.__svelte_rules) {
doc.__svelte_rules = new Set();
}
}
return [doc.__svelte_stylesheet, doc.__svelte_rules];
}
// https://github.com/darkskyapp/string-hash/blob/master/index.js // https://github.com/darkskyapp/string-hash/blob/master/index.js
function hash(str: string) { function hash(str: string) {
let hash = 5381; let hash = 5381;
@ -17,58 +32,56 @@ function hash(str: string) {
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 generate_rule(
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) { node: HTMLElement,
const step = 16.666 / duration; a: number,
let keyframes = '{\n'; b: number,
duration: number,
for (let p = 0; p <= 1; p += step) { delay: number,
const t = a + (b - a) * ease(p); ease: (t: number) => number,
keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; fn: (t: number, u: number) => string
) {
const step = 16.6667 / duration;
let rule = '{\n';
for (let p = 0, t = 0; p <= 1; p += step) {
t = a + (b - a) * ease(p);
rule += p * 100 + `%{${fn(t, 1 - t)}}\n`;
} }
rule += `100% {${fn(b, 1 - b)}}\n}`;
const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; const name = `${svelte_rule}${hash(rule)}`;
const name = `__svelte_${hash(rule)}_${uid}`; const [stylesheet, rules] = rulesheet(node);
const doc = node.ownerDocument as ExtendedDoc; if (!rules.has(name)) {
active_docs.add(doc); rules.add(name);
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;
stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length); stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length);
} }
const previous = node.style.animation || '';
const animation = node.style.animation || ''; node.style.animation = `${previous ? `${previous}, ` : ``}${name} ${duration}ms linear ${delay}ms 1 both`;
node.style.animation = `${animation ? `${animation}, ` : ``}${name} ${duration}ms linear ${delay}ms 1 both`; running_animations++;
return () => {
active += 1; const prev = (node.style.animation || '').split(', ');
return name; const next = prev.filter((anim) => !anim.includes(name));
if (prev.length === next.length) return;
node.style.animation = next.join(', ');
if (--running_animations) return;
active_documents.forEach(({ __svelte_stylesheet, __svelte_rules }) => {
let i = __svelte_stylesheet.cssRules.length;
while (i--) __svelte_stylesheet.deleteRule(i);
__svelte_rules.clear();
});
active_documents.clear();
};
} }
export function delete_rule(node: HTMLElement, name?: string) {
export function delete_rule(node: Element & ElementCSSInlineStyle, name?: string) {
const previous = (node.style.animation || '').split(', '); const previous = (node.style.animation || '').split(', ');
const next = previous.filter(name const next = previous.filter(
? anim => anim.indexOf(name) < 0 // remove specific animation name
: anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations ? (anim) => anim.indexOf(name) < 0 // remove specific animation
: (anim) => anim.indexOf('__svelte') === -1 // remove all Svelte animations
); );
const deleted = previous.length - next.length; const deleted = previous.length - next.length;
if (deleted) { if (deleted) {
node.style.animation = next.join(', '); node.style.animation = next.join(', ');
active -= deleted; running_animations -= deleted;
if (!active) clear_rules(); if (!active_documents) active_documents.clear();
} }
} }
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();
});
}

@ -1,26 +1,14 @@
import { identity as linear, is_function, noop, run_all } from './utils'; import { identity as linear, run_all, is_function } from './utils';
import { now } from "./environment"; import { now } from './environment';
import { loop } from './loop'; import { raf_timeout, loopThen } from './loop';
import { create_rule, delete_rule } from './style_manager'; import { generate_rule } from './style_manager';
import { custom_event } from './dom'; import { custom_event } from './dom';
import { add_render_callback } from './scheduler';
import { TransitionConfig } from '../transition'; import { TransitionConfig } from '../transition';
import { add_render_callback } from './scheduler';
let promise: Promise<void>|null; function startStopDispatcher(node: Element, direction: boolean) {
add_render_callback(() => node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}start`)));
function wait() { return () => node.dispatchEvent(custom_event(`${!direction ? 'intro' : 'outro'}end`));
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}`));
} }
const outroing = new Set(); const outroing = new Set();
@ -28,326 +16,95 @@ let outros;
export function group_outros() { export function group_outros() {
outros = { outros = {
r: 0, // remaining outros /* parent group */ p: outros,
c: [], // callbacks /* remaining outros */ r: 0,
p: outros // parent group /* callbacks */ c: [],
}; };
} }
export function check_outros() { export function check_outros() {
if (!outros.r) { if (!outros.r) run_all(outros.c);
run_all(outros.c);
}
outros = outros.p; outros = outros.p;
} }
export function transition_in(block, local?: 0 | 1) { export function transition_in(block, local?: 0 | 1) {
if (block && block.i) { if (!block || !block.i) return;
outroing.delete(block); outroing.delete(block);
block.i(local); block.i(local);
}
} }
export function transition_out(block, local: 0 | 1, detach: 0 | 1, callback) { export function transition_out(block, local?: 0 | 1, detach?: 0 | 1, callback?: () => void) {
if (block && block.o) { if (!block || !block.o || outroing.has(block)) return;
if (outroing.has(block)) return;
outroing.add(block); outroing.add(block);
outros.c.push(() => { outros.c.push(() => {
outroing.delete(block); outroing.delete(block);
if (callback) { if (!callback) return;
if (detach) block.d(1); if (detach) block.d(1);
callback(); callback();
}
}); });
block.o(local); block.o(local);
}
} }
const null_transition: TransitionConfig = { duration: 0 }; const null_transition: TransitionConfig = { duration: 0 };
type TransitionFn = (node: Element, params: any) => TransitionConfig; type TransitionFn = (node: HTMLElement, params: any) => TransitionConfig;
export function run_transition(
export function create_in_transition(node: Element & ElementCSSInlineStyle, fn: TransitionFn, params: any) { node: HTMLElement,
let config = fn(node, params); fn: TransitionFn,
let running = false; is_intro: boolean,
let animation_name; params?: any,
let task; reversed_from?: number
let uid = 0; ): StopResetReverse {
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;
}
if (now >= start_time) {
const t = easing((now - start_time) / duration);
tick(t, 1 - t);
}
}
return running;
});
}
let started = false;
return {
start() {
if (started) return;
delete_rule(node);
if (is_function(config)) {
config = config();
wait().then(go);
} else {
go();
}
},
invalidate() {
started = false;
},
end() {
if (running) {
cleanup();
running = false;
}
}
};
}
export function create_out_transition(node: Element & ElementCSSInlineStyle, fn: TransitionFn, params: any) {
let config = fn(node, params); let config = fn(node, params);
let running = true; let running = true;
let animation_name;
const group = outros;
group.r += 1; let cancel_css;
let cancel_raf;
let dispatch_end;
let start_time;
function go() { const group = outros;
const { if (!is_intro) group.r++;
delay = 0,
duration = 300,
easing = linear,
tick = noop,
css
} = config || null_transition;
if (css) animation_name = create_rule(node, 1, 0, duration, delay, easing, css);
const start_time = now() + delay; function start({ delay = 0, duration = 300, easing = linear, tick, css } = null_transition) {
if (!running) return;
const run = tick && (is_intro ? tick : (a, b) => tick(b, a));
if (reversed_from) delay += duration - (now() - reversed_from);
start_time = now() + delay;
const end_time = start_time + duration; const end_time = start_time + duration;
cancel_css = css && generate_rule(node, +!is_intro, +is_intro, duration, delay, easing, css);
add_render_callback(() => dispatch(node, false, 'start')); dispatch_end = startStopDispatcher(node, is_intro);
cancel_raf = cancel_raf = !run
loop(now => { ? raf_timeout(stop, end_time)
if (running) { : loopThen(
if (now >= end_time) { delay,
tick(0, 1); (t) => ((t = easing((t - start_time) / duration)), run(t, 1 - t)),
() => (run(1, 0), stop()),
dispatch(node, false, 'end'); end_time
);
if (!--group.r) { }
// this will result in `end()` being called,
// so we don't need to clean up here function stop(reset_reverse?: 1 | 2) {
run_all(group.c); if (!is_intro && reset_reverse === 1 && config && 'tick' in config) config.tick(1, 0);
} if (!running) return;
else running = false;
return false; if (cancel_css) cancel_css();
} if (cancel_raf) cancel_raf();
if (dispatch_end) dispatch_end();
if (now >= start_time) { if (!is_intro && !--group.r) run_all(group.c);
const t = easing((now - start_time) / duration); if (reset_reverse === 2) return run_transition(node, fn, !is_intro, params, start_time);
tick(1 - t, t); else if (!~reversed_from) running_bidi.delete(node);
} }
} if (is_function(config)) add_render_callback(() => start((config = config())));
else start(config);
return running; return stop;
});
}
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;
}
}
};
} }
export type StopResetReverse = (reset_reverse?: 1 | 2) => StopResetReverse;
export function create_bidirectional_transition(node: Element & ElementCSSInlineStyle, fn: TransitionFn, params: any, intro: boolean) { const running_bidi: Map<HTMLElement, StopResetReverse> = new Map();
let config = fn(node, params); export function run_bidirectional_transition(node: HTMLElement, fn: TransitionFn, is_intro: boolean, params: any) {
if (running_bidi.has(node)) {
let t = intro ? 0 : 1; running_bidi.set(node, running_bidi.get(node)(2));
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 { } else {
// if this is an intro, and there's a delay, we need to do running_bidi.set(node, run_transition(node, fn, is_intro, params, -1));
// 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);
running_program = init(program, duration);
add_render_callback(() => dispatch(node, b, 'start'));
loop(now => {
if (pending_program && now > pending_program.start) {
running_program = init(pending_program, duration);
pending_program = null;
dispatch(node, running_program.b, 'start');
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);
}
}
return !!(running_program || pending_program);
});
}
}
return {
run(b) {
if (is_function(config)) {
wait().then(() => {
// @ts-ignore
config = config();
go(b);
});
} else {
go(b);
}
},
end() {
clear_animation();
running_program = pending_program = null;
}
};
} }

@ -1,5 +1,5 @@
import { cubicOut, cubicInOut, linear } from 'svelte/easing'; import { cubicOut, cubicInOut, linear } from 'svelte/easing';
import { assign, is_function } from 'svelte/internal'; import { is_function } from 'svelte/internal';
type EasingFunction = (t: number) => number; type EasingFunction = (t: number) => number;
@ -19,24 +19,19 @@ interface BlurParams {
opacity: number; opacity: number;
} }
export function blur(node: Element, { export function blur(
delay = 0, node: Element,
duration = 400, { delay = 0, duration = 400, easing = cubicInOut, amount = 5, opacity = 0 }: BlurParams
easing = cubicInOut, ): TransitionConfig {
amount = 5,
opacity = 0
}: BlurParams): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const f = style.filter === 'none' ? '' : style.filter; const f = style.filter === 'none' ? '' : style.filter;
const od = target_opacity * (1 - opacity); const od = target_opacity * (1 - opacity);
return { return {
delay, delay,
duration, duration,
easing, 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);`,
}; };
} }
@ -46,18 +41,13 @@ interface FadeParams {
easing: EasingFunction; easing: EasingFunction;
} }
export function fade(node: Element, { export function fade(node: Element, { delay = 0, duration = 400, easing = linear }: FadeParams): TransitionConfig {
delay = 0,
duration = 400,
easing = linear
}: FadeParams): TransitionConfig {
const o = +getComputedStyle(node).opacity; const o = +getComputedStyle(node).opacity;
return { return {
delay, delay,
duration, duration,
easing, easing,
css: t => `opacity: ${t * o}` css: (t) => `opacity: ${t * o};`,
}; };
} }
@ -70,14 +60,10 @@ interface FlyParams {
opacity: number; opacity: number;
} }
export function fly(node: Element, { export function fly(
delay = 0, node: Element,
duration = 400, { delay = 0, duration = 400, easing = cubicOut, x = 0, y = 0, opacity = 0 }: FlyParams
easing = cubicOut, ): TransitionConfig {
x = 0,
y = 0,
opacity = 0
}: FlyParams): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -90,7 +76,7 @@ export function fly(node: Element, {
easing, easing,
css: (t, u) => ` css: (t, u) => `
transform: ${transform} translate(${(1 - t) * x}px, ${(1 - t) * y}px); transform: ${transform} translate(${(1 - t) * x}px, ${(1 - t) * y}px);
opacity: ${target_opacity - (od * u)}` opacity: ${target_opacity - od * u};`,
}; };
} }
@ -100,11 +86,7 @@ interface SlideParams {
easing: EasingFunction; easing: EasingFunction;
} }
export function slide(node: Element, { export function slide(node: Element, { delay = 0, duration = 400, easing = cubicOut }: SlideParams): TransitionConfig {
delay = 0,
duration = 400,
easing = cubicOut
}: SlideParams): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const opacity = +style.opacity; const opacity = +style.opacity;
const height = parseFloat(style.height); const height = parseFloat(style.height);
@ -119,7 +101,7 @@ export function slide(node: Element, {
delay, delay,
duration, duration,
easing, easing,
css: t => css: (t) =>
`overflow: hidden;` + `overflow: hidden;` +
`opacity: ${Math.min(t * 20, 1) * opacity};` + `opacity: ${Math.min(t * 20, 1) * opacity};` +
`height: ${t * height}px;` + `height: ${t * height}px;` +
@ -128,7 +110,7 @@ export function slide(node: Element, {
`margin-top: ${t * margin_top}px;` + `margin-top: ${t * margin_top}px;` +
`margin-bottom: ${t * margin_bottom}px;` + `margin-bottom: ${t * margin_bottom}px;` +
`border-top-width: ${t * border_top_width}px;` + `border-top-width: ${t * border_top_width}px;` +
`border-bottom-width: ${t * border_bottom_width}px;` `border-bottom-width: ${t * border_bottom_width}px;`,
}; };
} }
@ -140,13 +122,10 @@ interface ScaleParams {
opacity: number; opacity: number;
} }
export function scale(node: Element, { export function scale(
delay = 0, node: Element,
duration = 400, { delay = 0, duration = 400, easing = cubicOut, start = 0, opacity = 0 }: ScaleParams
easing = cubicOut, ): TransitionConfig {
start = 0,
opacity = 0
}: ScaleParams): TransitionConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const target_opacity = +style.opacity; const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -159,9 +138,9 @@ export function scale(node: Element, {
duration, duration,
easing, easing,
css: (_t, u) => ` css: (_t, u) => `
transform: ${transform} scale(${1 - (sd * u)}); transform: ${transform} scale(${1 - sd * u});
opacity: ${target_opacity - (od * u)} opacity: ${target_opacity - od * u};
` `,
}; };
} }
@ -172,12 +151,10 @@ interface DrawParams {
easing: EasingFunction; easing: EasingFunction;
} }
export function draw(node: SVGElement & { getTotalLength(): number }, { export function draw(
delay = 0, node: SVGElement & { getTotalLength(): number },
speed, { delay = 0, speed, duration, easing = cubicInOut }: DrawParams
duration, ): TransitionConfig {
easing = cubicInOut
}: DrawParams): TransitionConfig {
const len = node.getTotalLength(); const len = node.getTotalLength();
if (duration === undefined) { if (duration === undefined) {
@ -194,7 +171,7 @@ export function draw(node: SVGElement & { getTotalLength(): number }, {
delay, delay,
duration, duration,
easing, easing,
css: (t, u) => `stroke-dasharray: ${t * len} ${u * len}` css: (t, u) => `stroke-dasharray: ${t * len} ${u * len};`,
}; };
} }
@ -203,70 +180,63 @@ interface CrossfadeParams {
duration: number | ((len: number) => number); duration: number | ((len: number) => number);
easing: EasingFunction; easing: EasingFunction;
} }
interface CrossFadeConfig extends CrossfadeParams {
type ClientRectMap = Map<any, { rect: ClientRect }>;
export function crossfade({ fallback, ...defaults }: CrossfadeParams & {
fallback: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig; fallback: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig;
}) { }
const to_receive: ClientRectMap = new Map(); interface MarkedCrossFadeConfig extends CrossfadeParams {
const to_send: ClientRectMap = new Map(); key: any;
}
function crossfade(from: ClientRect, node: Element, params: CrossfadeParams): TransitionConfig { type ElementMap = Map<string, Element>;
const {
delay = 0, export function crossfade({
duration = d => Math.sqrt(d) * 30, delay: default_delay = 0,
easing = cubicOut easing: default_easing = cubicOut,
} = assign(assign({}, defaults), params); duration: default_duration = (d) => Math.sqrt(d) * 30,
fallback,
const to = node.getBoundingClientRect(); }: CrossFadeConfig) {
const to_receive: ElementMap = new Map();
const to_send: ElementMap = new Map();
function crossfade(
from_node: Element,
to_node: Element,
{ delay = default_delay, easing = default_easing, duration = default_duration }: CrossfadeParams
) {
const from = from_node.getBoundingClientRect();
const to = to_node.getBoundingClientRect();
const dx = from.left - to.left; const dx = from.left - to.left;
const dy = from.top - to.top; const dy = from.top - to.top;
const dw = from.width / to.width; const dw = from.width / to.width;
const dh = from.height / to.height; const dh = from.height / to.height;
const d = Math.sqrt(dx * dx + dy * dy); const d = Math.sqrt(dx * dx + dy * dy);
const style = getComputedStyle(to_node);
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
const opacity = +style.opacity; const opacity = +style.opacity;
return { return {
delay, delay,
duration: is_function(duration) ? duration(d) : duration,
easing, easing,
duration: is_function(duration) ? duration(d) : duration,
css: (t, u) => ` css: (t, u) => `
opacity: ${t * opacity}; opacity: ${t * opacity};
transform-origin: top left; transform-origin: top left;
transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${t + (1-t) * dw}, ${t + (1-t) * dh}); transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${t + (1 - t) * dw}, ${t + (1 - t) * dh});
` `,
}; } as TransitionConfig;
} }
function transition(a: ElementMap, b: ElementMap, is_intro: boolean) {
function transition(items: ClientRectMap, counterparts: ClientRectMap, intro: boolean) { return (node: Element, params: MarkedCrossFadeConfig) => {
return (node: Element, params: CrossfadeParams & { key: any }) => { a.set(params.key, node);
items.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => { return () => {
if (counterparts.has(params.key)) { if (b.has(params.key)) {
const { rect } = counterparts.get(params.key); const from_node = b.get(params.key);
counterparts.delete(params.key); b.delete(params.key);
return crossfade(from_node, node, params);
return crossfade(rect, node, params); } else {
a.delete(params.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 [ return [transition(to_send, to_receive, false), transition(to_receive, to_send, true)];
transition(to_send, to_receive, false),
transition(to_receive, to_send, true)
];
} }

Loading…
Cancel
Save