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 { extract_names } from 'periscopic';
import Action from '../../../nodes/Action';
import Transition from '../../../nodes/Transition';
const events = [
{
event_names: ['input'],
filter: (node: Element, _name: string) =>
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'],
filter: (node: Element, name: string) =>
(name === 'textContent' || name === 'innerHTML') &&
node.attributes.some(attribute => attribute.name === 'contenteditable')
node.attributes.some((attribute) => attribute.name === 'contenteditable'),
},
{
event_names: ['change'],
filter: (node: Element, _name: string) =>
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'],
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'],
filter: (_node: Element, name: string) =>
dimensions.test(name)
filter: (_node: Element, name: string) => dimensions.test(name),
},
// media events
{
event_names: ['timeupdate'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
(name === 'currentTime' || name === 'played' || name === 'ended')
node.is_media_node() && (name === 'currentTime' || name === 'played' || name === 'ended'),
},
{
event_names: ['durationchange'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
name === 'duration'
filter: (node: Element, name: string) => node.is_media_node() && name === 'duration',
},
{
event_names: ['play', 'pause'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
name === 'paused'
filter: (node: Element, name: string) => node.is_media_node() && name === 'paused',
},
{
event_names: ['progress'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
name === 'buffered'
filter: (node: Element, name: string) => node.is_media_node() && name === 'buffered',
},
{
event_names: ['loadedmetadata'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
(name === 'buffered' || name === 'seekable')
filter: (node: Element, name: string) => node.is_media_node() && (name === 'buffered' || name === 'seekable'),
},
{
event_names: ['volumechange'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
name === 'volume'
filter: (node: Element, name: string) => node.is_media_node() && name === 'volume',
},
{
event_names: ['ratechange'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
name === 'playbackRate'
filter: (node: Element, name: string) => node.is_media_node() && name === 'playbackRate',
},
{
event_names: ['seeking', 'seeked'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
(name === 'seeking')
filter: (node: Element, name: string) => node.is_media_node() && name === 'seeking',
},
{
event_names: ['ended'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
name === 'ended'
filter: (node: Element, name: string) => node.is_media_node() && name === 'ended',
},
{
event_names: ['resize'],
filter: (node: Element, name: string) =>
node.is_media_node() &&
(name === 'videoHeight' || name === 'videoWidth')
filter: (node: Element, name: string) => node.is_media_node() && (name === 'videoHeight' || name === 'videoWidth'),
},
// details event
{
event_names: ['toggle'],
filter: (node: Element, _name: string) =>
node.name === 'details'
filter: (node: Element, _name: string) => node.name === 'details',
},
];
@ -153,7 +133,7 @@ export default class ElementWrapper extends Wrapper {
super(renderer, block, parent, node);
this.var = {
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);
@ -161,14 +141,14 @@ export default class ElementWrapper extends Wrapper {
this.class_dependencies = [];
if (this.node.children.length) {
this.node.lets.forEach(l => {
extract_names(l.value || l.name).forEach(name => {
this.node.lets.forEach((l) => {
extract_names(l.value || l.name).forEach((name) => {
renderer.add_to_context(name, true);
});
});
}
this.attributes = this.node.attributes.map(attribute => {
this.attributes = this.node.attributes.map((attribute) => {
if (attribute.name === 'slot') {
// TODO make separate subclass for this?
let owner = this.parent;
@ -187,28 +167,28 @@ export default class ElementWrapper extends Wrapper {
if (owner && owner.node.type === 'InlineComponent') {
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({
comment: create_debugging_comment(node, this.renderer.component),
name: this.renderer.component.get_unique_name(`create_${sanitize(name)}_slot`),
type: 'slot'
type: 'slot',
});
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);
});
(owner as unknown as InlineComponentWrapper).slots.set(
((owner as unknown) as InlineComponentWrapper).slots.set(
name,
get_slot_definition(child_block, scope, lets)
);
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;
}
}
@ -221,9 +201,9 @@ export default class ElementWrapper extends Wrapper {
// ordinarily, there'll only be one... but we need to handle
// the rare case where an element can have multiple bindings,
// 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) block.add_intro(node.intro.is_local);
@ -235,27 +215,29 @@ export default class ElementWrapper extends Wrapper {
}
// 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) {
block.add_dependencies(directive.expression.dependencies);
}
});
node.handlers.forEach(handler => {
node.handlers.forEach((handler) => {
if (handler.expression) {
block.add_dependencies(handler.expression.dependencies);
}
});
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.bindings.length > 0 ||
node.classes.length > 0 ||
node.intro || node.outro ||
node.handlers.length > 0 ||
this.node.name === 'option' ||
renderer.options.dev
this.node.name === 'option'
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
@ -289,32 +271,22 @@ export default class ElementWrapper extends Wrapper {
block.add_variable(node);
const render_statement = this.get_render_statement(block);
block.chunks.create.push(
b`${node} = ${render_statement};`
);
block.chunks.create.push(b`${node} = ${render_statement};`);
if (renderer.options.hydratable) {
if (parent_nodes) {
block.chunks.claim.push(b`
${node} = ${this.get_claim_statement(parent_nodes)};
`);
block.chunks.claim.push(b`${node} = ${this.get_claim_statement(parent_nodes)};`);
if (!this.void && this.node.children.length > 0) {
block.chunks.claim.push(b`
var ${nodes} = ${children};
`);
block.chunks.claim.push(b`var ${nodes} = ${children};`);
}
} else {
block.chunks.claim.push(
b`${node} = ${render_statement};`
);
block.chunks.claim.push(b`${node} = ${render_statement};`);
}
}
if (parent_node) {
block.chunks.mount.push(
b`@append(${parent_node}, ${node});`
);
block.chunks.mount.push(b`@append(${parent_node}, ${node});`);
if (is_head(parent_node)) {
block.chunks.destroy.push(b`@detach(${node});`);
@ -339,39 +311,38 @@ export default class ElementWrapper extends Wrapper {
const state = {
quasi: {
type: 'TemplateElement',
value: { raw: '' }
}
value: { raw: '' },
},
};
const literal = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
quasis: [],
};
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);
block.chunks.create.push(
b`${node}.${this.can_use_innerhtml ? 'innerHTML': 'textContent'} = ${literal};`
);
block.chunks.create.push(b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};`);
}
} else {
this.fragment.nodes.forEach((child: Wrapper) => {
child.render(
block,
this.node.name === 'template' ? x`${node}.content` : node,
nodes
);
child.render(block, this.node.name === 'template' ? x`${node}.content` : node, nodes);
});
}
const event_handler_or_binding_uses_context = (
this.bindings.some(binding => binding.handler.uses_context) ||
this.node.handlers.some(handler => handler.uses_context) ||
this.node.actions.some(action => action.uses_context)
);
const event_handler_or_binding_uses_context =
this.bindings.some((binding) => binding.handler.uses_context) ||
this.node.handlers.some((handler) => handler.uses_context) ||
this.node.actions.some((action) => action.uses_context);
if (event_handler_or_binding_uses_context) {
block.maintain_context = true;
@ -379,15 +350,23 @@ export default class ElementWrapper extends Wrapper {
this.add_attributes(block);
this.add_directives_in_order(block);
this.add_transitions(block);
this.add_animation(block);
const { intro, outro } = this.node;
if (intro || outro) {
if (intro === outro) {
this.add_bidi_transition(block, intro);
} else {
this.add_intro(block, intro, outro);
this.add_outro(block, intro, outro);
}
}
if (this.node.animation) {
this.add_animation(block, intro, outro);
}
this.add_classes(block);
this.add_manual_style_scoping(block);
if (nodes && this.renderer.options.hydratable && !this.void) {
block.chunks.claim.push(
b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`
);
block.chunks.claim.push(b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`);
}
if (renderer.options.dev) {
@ -399,7 +378,10 @@ export default class ElementWrapper extends Wrapper {
}
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) {
@ -413,7 +395,7 @@ export default class ElementWrapper extends Wrapper {
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) {
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')
.map((attr) => p`${attr.name}: true`);
const name = this.node.namespace
? this.node.name
: this.node.name.toUpperCase();
const name = this.node.namespace ? this.node.name : this.node.name.toUpperCase();
const svg = this.node.namespace === namespaces.svg ? 1 : null;
return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`;
}
add_directives_in_order (block: Block) {
add_directives_in_order(block: Block) {
interface BindingGroup {
events: string[];
bindings: Binding[];
@ -444,17 +424,17 @@ export default class ElementWrapper extends Wrapper {
type OrderedAttribute = EventHandler | BindingGroup | Binding | Action;
const bindingGroups = events
.map(event => ({
.map((event) => ({
events: event.event_names,
bindings: this.bindings
.filter(binding => binding.node.name !== 'this')
.filter(binding => event.filter(this.node, binding.node.name))
.filter((binding) => binding.node.name !== 'this')
.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) {
return item.node.start;
} 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)
.sort((a, b) => getOrder(a) - getOrder(b))
.forEach(item => {
.forEach((item) => {
if (item instanceof EventHandler) {
add_event_handler(block, this.var, item);
} else if (item instanceof Binding) {
@ -494,23 +469,23 @@ export default class ElementWrapper extends Wrapper {
renderer.component.has_reactive_assignments = true;
const lock = bindingGroup.bindings.some(binding => binding.needs_lock) ?
block.get_unique_name(`${this.var.name}_updating`) :
null;
const lock = bindingGroup.bindings.some((binding) => binding.needs_lock)
? block.get_unique_name(`${this.var.name}_updating`)
: null;
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`);
renderer.add_to_context(handler.name);
// 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 contextual_dependencies: Set<string> = new Set();
group.bindings.forEach(binding => {
group.bindings.forEach((binding) => {
// TODO this is a mess
add_to_set(dependencies, binding.get_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
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
if (animation_frame) {
@ -560,22 +535,22 @@ export default class ElementWrapper extends Wrapper {
callee = handler;
}
const params = Array.from(contextual_dependencies).map(name => ({
const params = Array.from(contextual_dependencies).map((name) => ({
type: 'Identifier',
name
name,
}));
this.renderer.component.partly_hoisted.push(b`
function ${handler}(${params}) {
${group.bindings.map(b => b.handler.mutation)}
${group.bindings.map((b) => b.handler.mutation)}
${Array.from(dependencies)
.filter(dep => dep[0] !== '$')
.filter(dep => !contextual_dependencies.has(dep))
.map(dep => b`${this.renderer.invalidate(dep)};`)}
.filter((dep) => dep[0] !== '$')
.filter((dep) => !contextual_dependencies.has(dep))
.map((dep) => b`${this.renderer.invalidate(dep)};`)}
}
`);
group.events.forEach(name => {
group.events.forEach((name) => {
if (name === 'elementresize') {
// special case
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}));`
);
block.chunks.destroy.push(
b`${resize_listener}();`
);
block.chunks.destroy.push(b`${resize_listener}();`);
} else {
block.event_listeners.push(
x`@listen(${this.var}, "${name}", ${callee})`
);
block.event_listeners.push(x`@listen(${this.var}, "${name}", ${callee})`);
}
});
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}`);
const should_initialise = (
const should_initialise =
this.node.name === 'select' ||
group.bindings.find(binding => {
group.bindings.find((binding) => {
return (
binding.node.name === 'indeterminate' ||
binding.node.name === 'textContent' ||
binding.node.name === 'innerHTML' ||
binding.is_readonly_media_attribute()
);
})
);
});
if (should_initialise) {
const callback = has_local_function ? handler : x`() => ${callee}.call(${this.var})`;
block.chunks.hydrate.push(
b`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`
);
block.chunks.hydrate.push(b`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`);
}
if (group.events[0] === 'elementresize') {
block.chunks.hydrate.push(
b`@add_render_callback(() => ${callee}.call(${this.var}));`
);
block.chunks.hydrate.push(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);
return;
}
@ -665,11 +631,9 @@ export default class ElementWrapper extends Wrapper {
const initial_props = [];
const updates = [];
this.attributes
.forEach(attr => {
const condition = attr.node.dependencies.size > 0
? block.renderer.dirty(Array.from(attr.node.dependencies))
: null;
this.attributes.forEach((attr) => {
const condition =
attr.node.dependencies.size > 0 ? block.renderer.dirty(Array.from(attr.node.dependencies)) : null;
if (attr.node.is_spread) {
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`;
block.chunks.hydrate.push(
b`${fn}(${this.var}, ${data});`
);
block.chunks.hydrate.push(b`${fn}(${this.var}, ${data});`);
block.chunks.update.push(b`
${fn}(${this.var}, @get_spread_update(${levels}, [
${updates}
]));
`);
block.chunks.update.push(b`${fn}(${this.var}, @get_spread_update(${levels}, [${updates}]));`);
}
add_transitions(
block: Block
) {
const { intro, outro } = this.node;
if (!intro && !outro) return;
if (intro === outro) {
// bidirectional transition
const name = block.get_unique_name(`${this.var.name}_transition`);
const snippet = intro.expression
? intro.expression.manipulate(block)
: x`{}`;
add_bidi_transition(block: Block, intro: Transition) {
const name = block.get_unique_name(`${this.var.name}_bidi_transition`);
const snippet = intro.expression ? intro.expression.manipulate(block) : x`{}`;
block.add_variable(name);
const fn = this.renderer.reference(intro.name);
const intro_block = b`
@add_render_callback(() => {
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true);
${name}.run(1);
});
`;
let intro_block = b`${name} = @run_bidirectional_transition(${this.var}, ${fn}, true, ${snippet});`;
const outro_block = b`
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, false);
${name}.run(0);
`;
let outro_block = b`${name} = @run_bidirectional_transition(${this.var}, ${fn}, false, ${snippet});`;
if (intro.is_local) {
block.chunks.intro.push(b`
if (#local) {
${intro_block}
}
`);
block.chunks.outro.push(b`
if (#local) {
${outro_block}
intro_block = b`if (#local) {${intro_block}}`;
outro_block = b`if (#local) {${outro_block}}`;
}
`);
} else {
block.chunks.intro.push(intro_block);
block.chunks.outro.push(outro_block);
}
block.chunks.destroy.push(b`if (detaching && ${name}) ${name}.end();`);
block.chunks.destroy.push(b`if (detaching && ${name}) ${name}();`);
}
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;
add_intro(block: Block, intro: Transition, outro: Transition) {
if (outro) {
intro_block = b`
@add_render_callback(() => {
if (${outro_name}) ${outro_name}.end(1);
if (!${intro_name}) ${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
${intro_name}.start();
});
`;
block.chunks.outro.push(b`if (${intro_name}) ${intro_name}.invalidate();`);
} else {
intro_block = b`
if (!${intro_name}) {
@add_render_callback(() => {
${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
${intro_name}.start();
});
}
`;
}
if (intro.is_local) {
intro_block = b`
if (#local) {
${intro_block}
}
`;
const outro_var = block.alias(`${this.var.name}_outro`);
if (this.node.animation) {
const [unfreeze_var, rect_var, stop_animation_var, animationFn, params] = run_animation(this, block);
block.chunks.intro.push(b`
if (${outro_var}){
${outro_var}(1);
if (${unfreeze_var}) {
${unfreeze_var}(), (unfreeze = undefined);
${stop_animation_var} = @run_animation(${this.var}, ${rect_var}, ${animationFn}, ${params});
}
block.chunks.intro.push(intro_block);
}
if (outro) {
block.add_variable(outro_name);
const snippet = outro.expression
? outro.expression.manipulate(block)
: x`{}`;
const fn = this.renderer.reference(outro.name);
if (!intro) {
`);
} else {
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
// group) prior to their removal from the DOM
let outro_block = b`
${outro_name} = @create_out_transition(${this.var}, ${fn}, ${snippet});
`;
const [intro_var, node, transitionFn, params] = run_transition(this, block, intro, `intro`);
block.add_variable(intro_var);
if (outro.is_local) {
outro_block = b`
if (#local) {
${outro_block}
let start_intro = b`@add_render_callback(()=>{${intro_var} = @run_transition(${node}, ${transitionFn}, true, ${params});})`;
if (intro.is_local) start_intro = b`if (#local) ${start_intro};`;
block.chunks.intro.push(start_intro);
}
`;
// TODO
// hide elements that have outro'd prior to their removal from the DOM
// ( ...unless they belong to a still-outroing group )
add_outro(block: Block, intro: Transition, outro: Transition) {
if (intro) {
const intro_var = block.alias(`${this.var.name}_intro`);
block.chunks.outro.push(b`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) {
if (!this.node.animation) return;
block.chunks.destroy.push(b`if (detaching && ${outro_var}) ${outro_var}();`);
}
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 stop_animation = block.get_unique_name('stop_animation');
const [unfreeze_var, rect_var, stop_animation_var, name_var, params_var] = run_animation(this, block);
block.add_variable(rect);
block.add_variable(stop_animation, x`@noop`);
block.add_variable(unfreeze_var);
block.add_variable(rect_var);
block.add_variable(stop_animation_var, x`@noop`);
block.chunks.measure.push(b`
${rect} = ${this.var}.getBoundingClientRect();
${rect_var} = ${this.var}.getBoundingClientRect();
${intro && b`if(${intro_var}) ${intro_var}();`}
`);
block.chunks.fix.push(b`
@fix_position(${this.var});
${stop_animation}();
${outro && b`@add_transform(${this.var}, ${rect});`}
${unfreeze_var} = @fix_position(${this.var});
${stop_animation_var}();
${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`
${stop_animation}();
${stop_animation} = @create_animation(${this.var}, ${rect}, ${name}, ${params});
if(${unfreeze_var}) return
${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) {
const has_spread = this.node.attributes.some(attr => attr.is_spread);
this.node.classes.forEach(class_directive => {
const has_spread = this.node.attributes.some((attr) => attr.is_spread);
this.node.classes.forEach((class_directive) => {
const { expression, name } = class_directive;
let snippet;
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) {
wrappers.forEach(wrapper => {
function to_html(
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 as TextWrapper).use_space()) state.quasi.value.raw += ' ';
const parent = wrapper.node.parent as Element;
const raw = parent && (
parent.name === 'script' ||
parent.name === 'style' ||
can_use_raw_text
);
const raw = parent && (parent.name === 'script' || parent.name === 'style' || can_use_raw_text);
state.quasi.value.raw += (raw ? wrapper.node.data : escape_html(wrapper.node.data))
.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.expressions.push(wrapper.node.expression.manipulate(block));
state.quasi = {
type: 'TemplateElement',
value: { raw: '' }
value: { raw: '' },
};
}
else if (wrapper.node.name === 'noscript') {
} else if (wrapper.node.name === 'noscript') {
// do nothing
}
else {
} else {
// element
state.quasi.value.raw += `<${wrapper.node.name}`;
(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
state.quasi.value.raw += ` ${fix_attribute_casing(attr.node.name)}="`;
attr.node.chunks.forEach(chunk => {
attr.node.chunks.forEach((chunk) => {
if (chunk.type === 'Text') {
state.quasi.value.raw += escape_html(chunk.data);
} else {
@ -960,7 +853,7 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | TagWrapper>, blo
state.quasi = {
type: 'TemplateElement',
value: { raw: '' }
value: { raw: '' },
};
}
});
@ -971,10 +864,36 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | TagWrapper>, blo
state.quasi.value.raw += '>';
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}>`;
}
}
});
}
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';
interface Fragment {
key: string|null;
key: string | null;
first: null;
/* create */ c: () => void;
/* claim */ l: (nodes: any) => void;
@ -17,18 +17,18 @@ interface Fragment {
/* animate */ a: () => void;
/* intro */ i: (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
interface T$$ {
dirty: number[];
ctx: null|any;
ctx: null | any;
bound: any;
update: () => void;
callbacks: any;
after_update: any[];
props: Record<string, 0 | string>;
fragment: null|false|Fragment;
fragment: null | false | Fragment;
not_equal: any;
before_update: any[];
context: Map<any, any>;
@ -38,24 +38,23 @@ interface T$$ {
export function bind(component, name, callback) {
const index = component.$$.props[name];
if (index !== undefined) {
if (index === undefined) return;
component.$$.bound[index] = callback;
callback(component.$$.ctx[index]);
}
}
export function create_component(block) {
block && block.c();
if (block) block.c();
}
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) {
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
add_render_callback(() => {
@ -73,18 +72,16 @@ export function mount_component(component, target, anchor) {
after_update.forEach(add_render_callback);
}
export function destroy_component(component, detaching) {
const $$ = component.$$;
if ($$.fragment !== null) {
run_all($$.on_destroy);
export function destroy_component({ $$ }, detaching) {
if ($$.fragment === null) return;
$$.fragment && $$.fragment.d(detaching);
run_all($$.on_destroy);
if ($$.fragment) $$.fragment.d(detaching);
// TODO null out other refs, including component.$$ (but need to
// preserve final state?)
// TODO null out other refs, including component.$$
// (need to preserve final state?)
$$.on_destroy = $$.fragment = null;
$$.ctx = [];
}
}
function make_dirty(component, i) {
@ -93,16 +90,23 @@ function make_dirty(component, i) {
schedule_update();
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;
set_current_component(component);
const prop_values = options.props || {};
const $$: T$$ = component.$$ = {
const $$: T$$ = (component.$$ = {
fragment: null,
ctx: null,
@ -121,45 +125,40 @@ export function init(component, options, instance, create_fragment, not_equal, p
// everything else
callbacks: blank_object(),
dirty
};
let ready = false;
dirty,
});
$$.ctx = instance
? instance(component, prop_values, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if ($$.bound[i]) $$.bound[i](value);
? instance(component, prop_values, (i, res, val = res) => {
if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = val))) {
if ($$.bound[i]) $$.bound[i](val);
if (ready) make_dirty(component, i);
}
return ret;
return res;
})
: [];
$$.update();
ready = true;
run_all($$.before_update);
// `false` as a special case of no DOM component
// false when empty fragment
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
if (options.target) {
if (options.hydrate) {
const nodes = children(options.target);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment!.l(nodes);
if (target) {
if (hydrate) {
const nodes = children(target);
if ($$.fragment) $$.fragment.l(nodes);
nodes.forEach(detach);
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment!.c();
if ($$.fragment) $$.fragment.c();
}
if (options.intro) transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor);
if (intro) transition_in(component.$$.fragment);
mount_component(component, target, anchor);
flush();
}
set_current_component(parent_component);
}
@ -191,40 +190,33 @@ if (typeof HTMLElement === 'function') {
$on(type, callback) {
// 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);
return () => {
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
}
$set() {}
};
}
export class SvelteComponent {
$$: T$$;
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on(type, callback) {
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
callbacks.push(callback);
return () => {
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
}
$set() {}
}

@ -1,103 +1,49 @@
import { identity as linear, noop } from './utils';
import { now } from './environment';
import { loop } from './loop';
import { create_rule, delete_rule } from './style_manager';
import { noop } from './utils';
import { AnimationConfig } from '../animate';
import { run_transition } from './transitions';
//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;
const to = node.getBoundingClientRect();
if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom) return noop;
const {
delay = 0,
duration = 300,
easing = linear,
// @ts-ignore todo: should this be separated from destructuring? Or start/end added to public api and documentation?
start: start_time = now() + delay,
// @ts-ignore todo:
end = start_time + duration,
tick = noop,
css
} = fn(node, { from, to }, params);
let running = true;
let started = false;
let name;
function start() {
if (css) {
name = create_rule(node, 0, 1, duration, delay, easing, css);
}
if (!delay) {
started = true;
}
}
function stop() {
if (css) delete_rule(node, name);
running = false;
}
loop(now => {
if (!started && now >= start_time) {
started = true;
}
if (started && now >= end) {
tick(1, 0);
stop();
}
if (!running) {
return false;
}
if (started) {
const p = now - start_time;
const t = 0 + 1 * easing(p / duration);
tick(t, 1 - t);
}
return true;
});
start();
tick(0, 1);
return stop;
return run_transition(node, (node, params) => fn(node, { from, to }, params), true, params);
}
export function fix_position(node: Element & ElementCSSInlineStyle) {
export function fix_position(node: HTMLElement) {
const style = getComputedStyle(node);
if (style.position !== 'absolute' && style.position !== 'fixed') {
const { width, height } = style;
const a = node.getBoundingClientRect();
const { position: og_position, width: og_width, height: og_height } = node.style;
node.style.position = 'absolute';
node.style.width = width;
node.style.height = height;
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();
if (a.left !== b.left || a.top !== b.top) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`;
}
}

@ -1,21 +1,22 @@
import { raf } from './environment';
export interface Task { abort(): void; promise: Promise<void> }
import { raf, now } from './environment';
import { noop } from './utils';
export interface Task {
abort(): void;
promise: Promise<void>;
}
type TaskCallback = (now: number) => boolean | void;
type TaskEntry = { c: TaskCallback; f: () => void };
const tasks = new Set<TaskEntry>();
function run_tasks(now: number) {
tasks.forEach(task => {
if (!task.c(now)) {
tasks.forEach((task) => {
if (task.c(now)) return;
tasks.delete(task);
task.f();
}
});
if (tasks.size !== 0) raf(run_tasks);
if (tasks.size) raf(run_tasks);
}
/**
@ -31,15 +32,62 @@ export function clear_loops() {
*/
export function loop(callback: TaskCallback): Task {
let task: TaskEntry;
if (tasks.size === 0) raf(run_tasks);
if (!tasks.size) raf(run_tasks);
return {
promise: new Promise(fulfill => {
tasks.add(task = { c: callback, f: fulfill });
promise: new Promise((fulfill) => {
tasks.add((task = { c: callback, f: fulfill }));
}),
abort() {
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;
export function schedule_update() {
if (!update_scheduled) {
if (update_scheduled) return;
update_scheduled = true;
resolved_promise.then(flush);
}
}
export function tick() {
@ -33,10 +32,45 @@ export function add_flush_callback(fn) {
let flushing = false;
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() {
if (flushing) return;
flushing = true;
do {
// first, call beforeUpdate functions
// and update components
@ -45,45 +79,35 @@ export function flush() {
set_current_component(component);
update(component.$$);
}
dirty_components.length = 0;
while (binding_callbacks.length) binding_callbacks.pop()();
// then, once components are updated, call
// afterUpdate functions. This may cause
// subsequent updates...
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
// ...so guard against infinite loops
seen_callbacks.add(callback);
callback();
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
flushing = false;
seen_callbacks.clear();
}
function update($$) {
if ($$.fragment !== null) {
if ($$.fragment === null) return;
$$.update();
run_all($$.before_update);
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
$$.after_update.forEach(add_render_callback);
}
}

@ -1,14 +1,29 @@
import { element } from './dom';
import { raf } from './environment';
import { next_frame } from './loop';
const svelte_rule = `__svelte_`;
interface ExtendedDoc extends Document {
__svelte_stylesheet: CSSStyleSheet;
__svelte_rules: Record<string, true>;
__svelte_rules: Set<string>;
}
const active_docs = new Set<ExtendedDoc>();
let active = 0;
const active_documents = new Set<ExtendedDoc>();
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
function hash(str: string) {
let hash = 5381;
@ -17,58 +32,56 @@ function hash(str: string) {
while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
return hash >>> 0;
}
export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b: number, duration: number, delay: number, ease: (t: number) => number, fn: (t: number, u: number) => string, uid: number = 0) {
const step = 16.666 / duration;
let keyframes = '{\n';
for (let p = 0; p <= 1; p += step) {
const t = a + (b - a) * ease(p);
keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`;
export function generate_rule(
node: HTMLElement,
a: number,
b: number,
duration: number,
delay: number,
ease: (t: number) => number,
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`;
}
const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`;
const name = `__svelte_${hash(rule)}_${uid}`;
const doc = node.ownerDocument as ExtendedDoc;
active_docs.add(doc);
const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = doc.head.appendChild(element('style') as HTMLStyleElement).sheet as CSSStyleSheet);
const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {});
if (!current_rules[name]) {
current_rules[name] = true;
rule += `100% {${fn(b, 1 - b)}}\n}`;
const name = `${svelte_rule}${hash(rule)}`;
const [stylesheet, rules] = rulesheet(node);
if (!rules.has(name)) {
rules.add(name);
stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length);
}
const animation = node.style.animation || '';
node.style.animation = `${animation ? `${animation}, ` : ``}${name} ${duration}ms linear ${delay}ms 1 both`;
active += 1;
return name;
const previous = node.style.animation || '';
node.style.animation = `${previous ? `${previous}, ` : ``}${name} ${duration}ms linear ${delay}ms 1 both`;
running_animations++;
return () => {
const prev = (node.style.animation || '').split(', ');
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: Element & ElementCSSInlineStyle, name?: string) {
export function delete_rule(node: HTMLElement, name?: string) {
const previous = (node.style.animation || '').split(', ');
const next = previous.filter(name
? anim => anim.indexOf(name) < 0 // remove specific animation
: anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations
const next = previous.filter(
name
? (anim) => anim.indexOf(name) < 0 // remove specific animation
: (anim) => anim.indexOf('__svelte') === -1 // remove all Svelte animations
);
const deleted = previous.length - next.length;
if (deleted) {
node.style.animation = next.join(', ');
active -= deleted;
if (!active) clear_rules();
running_animations -= deleted;
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 { now } from "./environment";
import { loop } from './loop';
import { create_rule, delete_rule } from './style_manager';
import { identity as linear, run_all, is_function } from './utils';
import { now } from './environment';
import { raf_timeout, loopThen } from './loop';
import { generate_rule } from './style_manager';
import { custom_event } from './dom';
import { add_render_callback } from './scheduler';
import { TransitionConfig } from '../transition';
import { add_render_callback } from './scheduler';
let promise: Promise<void>|null;
function wait() {
if (!promise) {
promise = Promise.resolve();
promise.then(() => {
promise = null;
});
}
return promise;
}
function dispatch(node: Element, direction: boolean, kind: 'start' | 'end') {
node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`));
function startStopDispatcher(node: Element, direction: boolean) {
add_render_callback(() => node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}start`)));
return () => node.dispatchEvent(custom_event(`${!direction ? 'intro' : 'outro'}end`));
}
const outroing = new Set();
@ -28,326 +16,95 @@ let outros;
export function group_outros() {
outros = {
r: 0, // remaining outros
c: [], // callbacks
p: outros // parent group
/* parent group */ p: outros,
/* remaining outros */ r: 0,
/* callbacks */ c: [],
};
}
export function check_outros() {
if (!outros.r) {
run_all(outros.c);
}
if (!outros.r) run_all(outros.c);
outros = outros.p;
}
export function transition_in(block, local?: 0 | 1) {
if (block && block.i) {
if (!block || !block.i) return;
outroing.delete(block);
block.i(local);
}
}
export function transition_out(block, local: 0 | 1, detach: 0 | 1, callback) {
if (block && block.o) {
if (outroing.has(block)) return;
export function transition_out(block, local?: 0 | 1, detach?: 0 | 1, callback?: () => void) {
if (!block || !block.o || outroing.has(block)) return;
outroing.add(block);
outros.c.push(() => {
outroing.delete(block);
if (callback) {
if (!callback) return;
if (detach) block.d(1);
callback();
}
});
block.o(local);
}
}
const null_transition: TransitionConfig = { duration: 0 };
type TransitionFn = (node: Element, params: any) => TransitionConfig;
export function create_in_transition(node: Element & ElementCSSInlineStyle, fn: TransitionFn, params: any) {
let config = fn(node, params);
let running = false;
let animation_name;
let task;
let uid = 0;
function cleanup() {
if (animation_name) delete_rule(node, animation_name);
}
function go() {
const {
delay = 0,
duration = 300,
easing = linear,
tick = noop,
css
} = config || null_transition;
if (css) animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++);
tick(0, 1);
const start_time = now() + delay;
const end_time = start_time + duration;
if (task) task.abort();
running = true;
add_render_callback(() => dispatch(node, true, 'start'));
task = loop(now => {
if (running) {
if (now >= end_time) {
tick(1, 0);
dispatch(node, true, 'end');
cleanup();
return running = false;
}
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) {
type TransitionFn = (node: HTMLElement, params: any) => TransitionConfig;
export function run_transition(
node: HTMLElement,
fn: TransitionFn,
is_intro: boolean,
params?: any,
reversed_from?: number
): StopResetReverse {
let config = fn(node, params);
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 {
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 group = outros;
if (!is_intro) group.r++;
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;
add_render_callback(() => dispatch(node, false, 'start'));
loop(now => {
if (running) {
if (now >= end_time) {
tick(0, 1);
dispatch(node, false, 'end');
if (!--group.r) {
// this will result in `end()` being called,
// so we don't need to clean up here
run_all(group.c);
}
return false;
}
if (now >= start_time) {
const t = easing((now - start_time) / duration);
tick(1 - t, t);
}
}
return running;
});
}
if (is_function(config)) {
wait().then(() => {
// @ts-ignore
config = config();
go();
});
} else {
go();
}
return {
end(reset) {
if (reset && config.tick) {
config.tick(1, 0);
}
if (running) {
if (animation_name) delete_rule(node, animation_name);
running = false;
}
}
};
cancel_css = css && generate_rule(node, +!is_intro, +is_intro, duration, delay, easing, css);
dispatch_end = startStopDispatcher(node, is_intro);
cancel_raf = cancel_raf = !run
? raf_timeout(stop, end_time)
: loopThen(
delay,
(t) => ((t = easing((t - start_time) / duration)), run(t, 1 - t)),
() => (run(1, 0), stop()),
end_time
);
}
function stop(reset_reverse?: 1 | 2) {
if (!is_intro && reset_reverse === 1 && config && 'tick' in config) config.tick(1, 0);
if (!running) return;
else running = false;
if (cancel_css) cancel_css();
if (cancel_raf) cancel_raf();
if (dispatch_end) dispatch_end();
if (!is_intro && !--group.r) run_all(group.c);
if (reset_reverse === 2) return run_transition(node, fn, !is_intro, params, start_time);
else if (!~reversed_from) running_bidi.delete(node);
}
if (is_function(config)) add_render_callback(() => start((config = config())));
else start(config);
return stop;
}
export function create_bidirectional_transition(node: Element & ElementCSSInlineStyle, fn: TransitionFn, params: any, intro: boolean) {
let config = fn(node, params);
let t = intro ? 0 : 1;
let running_program = null;
let pending_program = null;
let animation_name = null;
function clear_animation() {
if (animation_name) delete_rule(node, animation_name);
}
function init(program, duration) {
const d = program.b - t;
duration *= Math.abs(d);
return {
a: t,
b: program.b,
d,
duration,
start: program.start,
end: program.start + duration,
group: program.group
};
}
function go(b) {
const {
delay = 0,
duration = 300,
easing = linear,
tick = noop,
css
} = config || null_transition;
const program = {
start: now() + delay,
b
};
if (!b) {
// @ts-ignore todo: improve typings
program.group = outros;
outros.r += 1;
}
if (running_program) {
pending_program = program;
export type StopResetReverse = (reset_reverse?: 1 | 2) => StopResetReverse;
const running_bidi: Map<HTMLElement, StopResetReverse> = new Map();
export function run_bidirectional_transition(node: HTMLElement, fn: TransitionFn, is_intro: boolean, params: any) {
if (running_bidi.has(node)) {
running_bidi.set(node, running_bidi.get(node)(2));
} else {
// if this is an intro, and there's a delay, we need to do
// an initial tick and/or apply CSS animation immediately
if (css) {
clear_animation();
animation_name = create_rule(node, t, b, duration, delay, easing, css);
}
if (b) tick(0, 1);
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;
running_bidi.set(node, run_transition(node, fn, is_intro, params, -1));
}
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 { assign, is_function } from 'svelte/internal';
import { is_function } from 'svelte/internal';
type EasingFunction = (t: number) => number;
@ -19,24 +19,19 @@ interface BlurParams {
opacity: number;
}
export function blur(node: Element, {
delay = 0,
duration = 400,
easing = cubicInOut,
amount = 5,
opacity = 0
}: BlurParams): TransitionConfig {
export function blur(
node: Element,
{ delay = 0, duration = 400, easing = cubicInOut, amount = 5, opacity = 0 }: BlurParams
): TransitionConfig {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const f = style.filter === 'none' ? '' : style.filter;
const od = target_opacity * (1 - opacity);
return {
delay,
duration,
easing,
css: (_t, u) => `opacity: ${target_opacity - (od * u)}; filter: ${f} blur(${u * amount}px);`
css: (_t, u) => `opacity: ${target_opacity - od * u}; filter: ${f} blur(${u * amount}px);`,
};
}
@ -46,18 +41,13 @@ interface FadeParams {
easing: EasingFunction;
}
export function fade(node: Element, {
delay = 0,
duration = 400,
easing = linear
}: FadeParams): TransitionConfig {
export function fade(node: Element, { delay = 0, duration = 400, easing = linear }: FadeParams): TransitionConfig {
const o = +getComputedStyle(node).opacity;
return {
delay,
duration,
easing,
css: t => `opacity: ${t * o}`
css: (t) => `opacity: ${t * o};`,
};
}
@ -70,14 +60,10 @@ interface FlyParams {
opacity: number;
}
export function fly(node: Element, {
delay = 0,
duration = 400,
easing = cubicOut,
x = 0,
y = 0,
opacity = 0
}: FlyParams): TransitionConfig {
export function fly(
node: Element,
{ delay = 0, duration = 400, easing = cubicOut, x = 0, y = 0, opacity = 0 }: FlyParams
): TransitionConfig {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
@ -90,7 +76,7 @@ export function fly(node: Element, {
easing,
css: (t, u) => `
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;
}
export function slide(node: Element, {
delay = 0,
duration = 400,
easing = cubicOut
}: SlideParams): TransitionConfig {
export function slide(node: Element, { delay = 0, duration = 400, easing = cubicOut }: SlideParams): TransitionConfig {
const style = getComputedStyle(node);
const opacity = +style.opacity;
const height = parseFloat(style.height);
@ -119,7 +101,7 @@ export function slide(node: Element, {
delay,
duration,
easing,
css: t =>
css: (t) =>
`overflow: hidden;` +
`opacity: ${Math.min(t * 20, 1) * opacity};` +
`height: ${t * height}px;` +
@ -128,7 +110,7 @@ export function slide(node: Element, {
`margin-top: ${t * margin_top}px;` +
`margin-bottom: ${t * margin_bottom}px;` +
`border-top-width: ${t * border_top_width}px;` +
`border-bottom-width: ${t * border_bottom_width}px;`
`border-bottom-width: ${t * border_bottom_width}px;`,
};
}
@ -140,13 +122,10 @@ interface ScaleParams {
opacity: number;
}
export function scale(node: Element, {
delay = 0,
duration = 400,
easing = cubicOut,
start = 0,
opacity = 0
}: ScaleParams): TransitionConfig {
export function scale(
node: Element,
{ delay = 0, duration = 400, easing = cubicOut, start = 0, opacity = 0 }: ScaleParams
): TransitionConfig {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
@ -159,9 +138,9 @@ export function scale(node: Element, {
duration,
easing,
css: (_t, u) => `
transform: ${transform} scale(${1 - (sd * u)});
opacity: ${target_opacity - (od * u)}
`
transform: ${transform} scale(${1 - sd * u});
opacity: ${target_opacity - od * u};
`,
};
}
@ -172,12 +151,10 @@ interface DrawParams {
easing: EasingFunction;
}
export function draw(node: SVGElement & { getTotalLength(): number }, {
delay = 0,
speed,
duration,
easing = cubicInOut
}: DrawParams): TransitionConfig {
export function draw(
node: SVGElement & { getTotalLength(): number },
{ delay = 0, speed, duration, easing = cubicInOut }: DrawParams
): TransitionConfig {
const len = node.getTotalLength();
if (duration === undefined) {
@ -194,7 +171,7 @@ export function draw(node: SVGElement & { getTotalLength(): number }, {
delay,
duration,
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);
easing: EasingFunction;
}
type ClientRectMap = Map<any, { rect: ClientRect }>;
export function crossfade({ fallback, ...defaults }: CrossfadeParams & {
interface CrossFadeConfig extends CrossfadeParams {
fallback: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig;
}) {
const to_receive: ClientRectMap = new Map();
const to_send: ClientRectMap = new Map();
function crossfade(from: ClientRect, node: Element, params: CrossfadeParams): TransitionConfig {
const {
delay = 0,
duration = d => Math.sqrt(d) * 30,
easing = cubicOut
} = assign(assign({}, defaults), params);
const to = node.getBoundingClientRect();
}
interface MarkedCrossFadeConfig extends CrossfadeParams {
key: any;
}
type ElementMap = Map<string, Element>;
export function crossfade({
delay: default_delay = 0,
easing: default_easing = cubicOut,
duration: default_duration = (d) => Math.sqrt(d) * 30,
fallback,
}: 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 dy = from.top - to.top;
const dw = from.width / to.width;
const dh = from.height / to.height;
const d = Math.sqrt(dx * dx + dy * dy);
const style = getComputedStyle(node);
const style = getComputedStyle(to_node);
const transform = style.transform === 'none' ? '' : style.transform;
const opacity = +style.opacity;
return {
delay,
duration: is_function(duration) ? duration(d) : duration,
easing,
duration: is_function(duration) ? duration(d) : duration,
css: (t, u) => `
opacity: ${t * opacity};
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(items: ClientRectMap, counterparts: ClientRectMap, intro: boolean) {
return (node: Element, params: CrossfadeParams & { key: any }) => {
items.set(params.key, {
rect: node.getBoundingClientRect()
});
function transition(a: ElementMap, b: ElementMap, is_intro: boolean) {
return (node: Element, params: MarkedCrossFadeConfig) => {
a.set(params.key, node);
return () => {
if (counterparts.has(params.key)) {
const { rect } = counterparts.get(params.key);
counterparts.delete(params.key);
return crossfade(rect, node, params);
if (b.has(params.key)) {
const from_node = b.get(params.key);
b.delete(params.key);
return crossfade(from_node, 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 [
transition(to_send, to_receive, false),
transition(to_receive, to_send, true)
];
return [transition(to_send, to_receive, false), transition(to_receive, to_send, true)];
}

Loading…
Cancel
Save