mirror of https://github.com/sveltejs/svelte
927 lines
26 KiB
927 lines
26 KiB
import deindent from '../../utils/deindent';
|
|
import { stringify, escapeHTML } from '../../utils/stringify';
|
|
import flattenReference from '../../utils/flattenReference';
|
|
import isVoidElementName from '../../utils/isVoidElementName';
|
|
import validCalleeObjects from '../../utils/validCalleeObjects';
|
|
import reservedNames from '../../utils/reservedNames';
|
|
import fixAttributeCasing from '../../utils/fixAttributeCasing';
|
|
import quoteIfNecessary from '../../utils/quoteIfNecessary';
|
|
import mungeAttribute from './shared/mungeAttribute';
|
|
import Node from './shared/Node';
|
|
import Block from '../dom/Block';
|
|
import Attribute from './Attribute';
|
|
import Binding from './Binding';
|
|
import EventHandler from './EventHandler';
|
|
import Ref from './Ref';
|
|
import Transition from './Transition';
|
|
import Action from './Action';
|
|
import Text from './Text';
|
|
import * as namespaces from '../../utils/namespaces';
|
|
import mapChildren from './shared/mapChildren';
|
|
|
|
export default class Element extends Node {
|
|
type: 'Element';
|
|
name: string;
|
|
attributes: Attribute[];
|
|
bindings: Binding[];
|
|
handlers: EventHandler[];
|
|
intro: Transition;
|
|
outro: Transition;
|
|
children: Node[];
|
|
|
|
ref: string;
|
|
namespace: string;
|
|
|
|
constructor(compiler, parent, info: any) {
|
|
super(compiler, parent, info);
|
|
this.name = info.name;
|
|
|
|
const parentElement = parent.findNearest(/^Element/);
|
|
this.namespace = this.name === 'svg' ?
|
|
namespaces.svg :
|
|
parentElement ? parentElement.namespace : this.compiler.namespace;
|
|
|
|
this.attributes = [];
|
|
this.bindings = [];
|
|
this.handlers = [];
|
|
|
|
this.intro = null;
|
|
this.outro = null;
|
|
|
|
info.attributes.forEach(node => {
|
|
switch (node.type) {
|
|
case 'Attribute':
|
|
// special case
|
|
if (node.name === 'xmlns') this.namespace = node.value[0].data;
|
|
|
|
this.attributes.push(new Attribute(compiler, this, node));
|
|
break;
|
|
|
|
case 'Binding':
|
|
this.bindings.push(new Binding(compiler, this, node));
|
|
break;
|
|
|
|
case 'EventHandler':
|
|
this.handlers.push(new EventHandler(compiler, this, node));
|
|
break;
|
|
|
|
case 'Transition':
|
|
const transition = new Transition(compiler, this, node);
|
|
if (node.intro) this.intro = transition;
|
|
if (node.outro) this.outro = transition;
|
|
break;
|
|
|
|
case 'Ref':
|
|
// TODO catch this in validation
|
|
if (this.ref) throw new Error(`Duplicate refs`);
|
|
|
|
compiler.usesRefs = true
|
|
this.ref = node.name;
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Not implemented: ${node.type}`);
|
|
}
|
|
});
|
|
|
|
// TODO break out attributes and directives here
|
|
|
|
this.children = mapChildren(compiler, this, info.children);
|
|
}
|
|
|
|
init(
|
|
block: Block,
|
|
stripWhitespace: boolean,
|
|
nextSibling: Node
|
|
) {
|
|
if (this.name === 'slot' || this.name === 'option') {
|
|
this.cannotUseInnerHTML();
|
|
}
|
|
|
|
this.attributes.forEach(attr => {
|
|
if (attr.dependencies.size) {
|
|
this.parent.cannotUseInnerHTML();
|
|
block.addDependencies(attr.dependencies);
|
|
|
|
// special case — <option value={foo}> — see below
|
|
if (this.name === 'option' && attr.name === 'value') {
|
|
let select = this.parent;
|
|
while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
|
|
|
|
if (select && select.selectBindingDependencies) {
|
|
select.selectBindingDependencies.forEach(prop => {
|
|
dependencies.forEach((dependency: string) => {
|
|
this.compiler.indirectDependencies.get(prop).add(dependency);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
this.bindings.forEach(binding => {
|
|
this.cannotUseInnerHTML();
|
|
block.addDependencies(binding.value.dependencies);
|
|
});
|
|
|
|
this.handlers.forEach(handler => {
|
|
this.cannotUseInnerHTML();
|
|
block.addDependencies(handler.dependencies);
|
|
});
|
|
|
|
if (this.intro) {
|
|
this.compiler.hasIntroTransitions = block.hasIntroMethod = true;
|
|
}
|
|
|
|
if (this.outro) {
|
|
this.compiler.hasOutroTransitions = block.hasOutroMethod = true;
|
|
block.outros += 1;
|
|
}
|
|
|
|
this.attributes.forEach(attribute => {
|
|
if (attribute.type === 'Attribute' && attribute.value !== true) {
|
|
// removed
|
|
} else {
|
|
if (this.parent) this.parent.cannotUseInnerHTML();
|
|
|
|
if (attribute.type === 'Action' && attribute.expression) {
|
|
block.addDependencies(attribute.metadata.dependencies);
|
|
} else if (attribute.type === 'Spread') {
|
|
block.addDependencies(attribute.metadata.dependencies);
|
|
}
|
|
}
|
|
});
|
|
|
|
const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value');
|
|
|
|
if (this.name === 'textarea') {
|
|
// this is an egregious hack, but it's the easiest way to get <textarea>
|
|
// children treated the same way as a value attribute
|
|
if (this.children.length > 0) {
|
|
this.attributes.push(new Attribute(this.compiler, this, {
|
|
name: 'value',
|
|
value: this.children
|
|
}));
|
|
|
|
this.children = [];
|
|
}
|
|
}
|
|
|
|
// special case — in a case like this...
|
|
//
|
|
// <select bind:value='foo'>
|
|
// <option value='{{bar}}'>bar</option>
|
|
// <option value='{{baz}}'>baz</option>
|
|
// </option>
|
|
//
|
|
// ...we need to know that `foo` depends on `bar` and `baz`,
|
|
// so that if `foo.qux` changes, we know that we need to
|
|
// mark `bar` and `baz` as dirty too
|
|
if (this.name === 'select') {
|
|
const binding = this.attributes.find(node => node.type === 'Binding' && node.name === 'value');
|
|
if (binding) {
|
|
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
|
|
const dependencies = binding.metadata.dependencies;
|
|
this.selectBindingDependencies = dependencies;
|
|
dependencies.forEach((prop: string) => {
|
|
this.compiler.indirectDependencies.set(prop, new Set());
|
|
});
|
|
} else {
|
|
this.selectBindingDependencies = null;
|
|
}
|
|
}
|
|
|
|
const slot = this.getStaticAttributeValue('slot');
|
|
if (slot && this.hasAncestor('Component')) {
|
|
this.cannotUseInnerHTML();
|
|
this.slotted = true;
|
|
// TODO validate slots — no nesting, no dynamic names...
|
|
const component = this.findNearest(/^Component/);
|
|
component._slots.add(slot);
|
|
}
|
|
|
|
if (this.spread) {
|
|
block.addDependencies(this.spread.metadata.dependencies);
|
|
}
|
|
|
|
this.var = block.getUniqueName(
|
|
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
);
|
|
|
|
if (this.children.length) {
|
|
if (this.name === 'pre' || this.name === 'textarea') stripWhitespace = false;
|
|
this.initChildren(block, stripWhitespace, nextSibling);
|
|
}
|
|
}
|
|
|
|
build(
|
|
block: Block,
|
|
parentNode: string,
|
|
parentNodes: string
|
|
) {
|
|
const { compiler } = this;
|
|
|
|
if (this.name === 'slot') {
|
|
const slotName = this.getStaticAttributeValue('name') || 'default';
|
|
this.compiler.slots.add(slotName);
|
|
}
|
|
|
|
if (this.name === 'noscript') return;
|
|
|
|
const childState = {
|
|
parentNode: this.var,
|
|
parentNodes: parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parentNodes is null
|
|
};
|
|
|
|
const name = this.var;
|
|
const allUsedContexts: Set<string> = new Set();
|
|
|
|
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
|
|
const initialMountNode = this.slotted ?
|
|
`${this.findNearest(/^Component/).var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
|
|
parentNode;
|
|
|
|
block.addVariable(name);
|
|
const renderStatement = getRenderStatement(this.compiler, this.namespace, this.name);
|
|
block.builders.create.addLine(
|
|
`${name} = ${renderStatement};`
|
|
);
|
|
|
|
if (this.compiler.hydratable) {
|
|
if (parentNodes) {
|
|
block.builders.claim.addBlock(deindent`
|
|
${name} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
|
|
var ${childState.parentNodes} = @children(${name});
|
|
`);
|
|
} else {
|
|
block.builders.claim.addLine(
|
|
`${name} = ${renderStatement};`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (initialMountNode) {
|
|
block.builders.mount.addLine(
|
|
`@appendNode(${name}, ${initialMountNode});`
|
|
);
|
|
|
|
if (initialMountNode === 'document.head') {
|
|
block.builders.unmount.addLine(`@detachNode(${name});`);
|
|
}
|
|
} else {
|
|
block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`);
|
|
|
|
// TODO we eventually need to consider what happens to elements
|
|
// that belong to the same outgroup as an outroing element...
|
|
block.builders.unmount.addLine(`@detachNode(${name});`);
|
|
}
|
|
|
|
// TODO move this into a class as well?
|
|
if (this._cssRefAttribute) {
|
|
block.builders.hydrate.addLine(
|
|
`@setAttribute(${name}, "svelte-ref-${this._cssRefAttribute}", "");`
|
|
)
|
|
}
|
|
|
|
// insert static children with textContent or innerHTML
|
|
if (!this.namespace && this.canUseInnerHTML && this.children.length > 0) {
|
|
if (this.children.length === 1 && this.children[0].type === 'Text') {
|
|
block.builders.create.addLine(
|
|
`${name}.textContent = ${stringify(this.children[0].data)};`
|
|
);
|
|
} else {
|
|
block.builders.create.addLine(
|
|
`${name}.innerHTML = ${stringify(this.children.map(toHTML).join(''))};`
|
|
);
|
|
}
|
|
} else {
|
|
this.children.forEach((child: Node) => {
|
|
child.build(block, childState.parentNode, childState.parentNodes);
|
|
});
|
|
}
|
|
|
|
this.addBindings(block, allUsedContexts);
|
|
const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
|
|
if (this.ref) this.addRef(block);
|
|
this.addAttributes(block);
|
|
this.addTransitions(block);
|
|
this.addActions(block);
|
|
|
|
if (allUsedContexts.size || eventHandlerUsesComponent) {
|
|
const initialProps: string[] = [];
|
|
const updates: string[] = [];
|
|
|
|
if (eventHandlerUsesComponent) {
|
|
initialProps.push(`component: #component`);
|
|
}
|
|
|
|
allUsedContexts.forEach((contextName: string) => {
|
|
if (contextName === 'state') return;
|
|
if (block.contextTypes.get(contextName) !== 'each') return;
|
|
|
|
const listName = block.listNames.get(contextName);
|
|
const indexName = block.indexNames.get(contextName);
|
|
|
|
initialProps.push(
|
|
`${listName}: state.${listName},\n${indexName}: state.${indexName}`
|
|
);
|
|
updates.push(
|
|
`${name}._svelte.${listName} = state.${listName};\n${name}._svelte.${indexName} = state.${indexName};`
|
|
);
|
|
});
|
|
|
|
if (initialProps.length) {
|
|
block.builders.hydrate.addBlock(deindent`
|
|
${name}._svelte = {
|
|
${initialProps.join(',\n')}
|
|
};
|
|
`);
|
|
}
|
|
|
|
if (updates.length) {
|
|
block.builders.update.addBlock(updates.join('\n'));
|
|
}
|
|
}
|
|
|
|
if (this.initialUpdate) {
|
|
block.builders.mount.addBlock(this.initialUpdate);
|
|
}
|
|
|
|
if (childState.parentNodes) {
|
|
block.builders.claim.addLine(
|
|
`${childState.parentNodes}.forEach(@detachNode);`
|
|
);
|
|
}
|
|
|
|
function toHTML(node: Element | Text) {
|
|
if (node.type === 'Text') {
|
|
return node.parent &&
|
|
node.parent.type === 'Element' &&
|
|
(node.parent.name === 'script' || node.parent.name === 'style')
|
|
? node.data
|
|
: escapeHTML(node.data);
|
|
}
|
|
|
|
if (node.name === 'noscript') return '';
|
|
|
|
let open = `<${node.name}`;
|
|
|
|
if (node._cssRefAttribute) {
|
|
open += ` svelte-ref-${node._cssRefAttribute}`;
|
|
}
|
|
|
|
node.attributes.forEach((attr: Node) => {
|
|
open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.value)}`
|
|
});
|
|
|
|
if (isVoidElementName(node.name)) return open + '>';
|
|
|
|
return `${open}>${node.children.map(toHTML).join('')}</${node.name}>`;
|
|
}
|
|
}
|
|
|
|
addBindings(
|
|
block: Block,
|
|
allUsedContexts: Set<string>
|
|
) {
|
|
if (this.bindings.length === 0) return;
|
|
|
|
if (this.name === 'select' || this.isMediaNode()) this.compiler.hasComplexBindings = true;
|
|
|
|
const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
|
|
|
|
// TODO munge in constructor
|
|
const mungedBindings = this.bindings.map(binding => binding.munge(block, allUsedContexts));
|
|
|
|
const lock = mungedBindings.some(binding => binding.needsLock) ?
|
|
block.getUniqueName(`${this.var}_updating`) :
|
|
null;
|
|
|
|
if (lock) block.addVariable(lock, 'false');
|
|
|
|
const groups = events
|
|
.map(event => {
|
|
return {
|
|
events: event.eventNames,
|
|
bindings: mungedBindings.filter(binding => event.filter(this, binding.name))
|
|
};
|
|
})
|
|
.filter(group => group.bindings.length);
|
|
|
|
groups.forEach(group => {
|
|
const handler = block.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);
|
|
|
|
const needsLock = group.bindings.some(binding => binding.needsLock);
|
|
|
|
group.bindings.forEach(binding => {
|
|
if (!binding.updateDom) return;
|
|
|
|
const updateConditions = needsLock ? [`!${lock}`] : [];
|
|
if (binding.updateCondition) updateConditions.push(binding.updateCondition);
|
|
|
|
block.builders.update.addLine(
|
|
updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom
|
|
);
|
|
});
|
|
|
|
const usesContext = group.bindings.some(binding => binding.handler.usesContext);
|
|
const usesState = group.bindings.some(binding => binding.handler.usesState);
|
|
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
|
|
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
|
|
|
|
const props = new Set();
|
|
const storeProps = new Set();
|
|
group.bindings.forEach(binding => {
|
|
binding.handler.props.forEach(prop => {
|
|
props.add(prop);
|
|
});
|
|
|
|
binding.handler.storeProps.forEach(prop => {
|
|
storeProps.add(prop);
|
|
});
|
|
}); // TODO use stringifyProps here, once indenting is fixed
|
|
|
|
// media bindings — awkward special case. The native timeupdate events
|
|
// fire too infrequently, so we need to take matters into our
|
|
// own hands
|
|
let animation_frame;
|
|
if (group.events[0] === 'timeupdate') {
|
|
animation_frame = block.getUniqueName(`${this.var}_animationframe`);
|
|
block.addVariable(animation_frame);
|
|
}
|
|
|
|
block.builders.init.addBlock(deindent`
|
|
function ${handler}() {
|
|
${
|
|
animation_frame && deindent`
|
|
cancelAnimationFrame(${animation_frame});
|
|
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
|
|
}
|
|
${usesContext && `var context = ${this.var}._svelte;`}
|
|
${usesState && `var state = #component.get();`}
|
|
${usesStore && `var $ = #component.store.get();`}
|
|
${needsLock && `${lock} = true;`}
|
|
${mutations.length > 0 && mutations}
|
|
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
|
|
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
|
|
${needsLock && `${lock} = false;`}
|
|
}
|
|
`);
|
|
|
|
group.events.forEach(name => {
|
|
block.builders.hydrate.addLine(
|
|
`@addListener(${this.var}, "${name}", ${handler});`
|
|
);
|
|
|
|
block.builders.destroy.addLine(
|
|
`@removeListener(${this.var}, "${name}", ${handler});`
|
|
);
|
|
});
|
|
|
|
const allInitialStateIsDefined = group.bindings
|
|
.map(binding => `'${binding.object}' in state`)
|
|
.join(' && ');
|
|
|
|
if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
|
|
this.compiler.hasComplexBindings = true;
|
|
|
|
block.builders.hydrate.addLine(
|
|
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
|
|
);
|
|
}
|
|
});
|
|
|
|
this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
|
|
}
|
|
|
|
addAttributes(block: Block) {
|
|
if (this.attributes.find(attr => attr.type === 'Spread')) {
|
|
this.addSpreadAttributes(block);
|
|
return;
|
|
}
|
|
|
|
this.attributes.forEach((attribute: Attribute) => {
|
|
attribute.render(block);
|
|
});
|
|
}
|
|
|
|
addSpreadAttributes(block: Block) {
|
|
const levels = block.getUniqueName(`${this.var}_levels`);
|
|
const data = block.getUniqueName(`${this.var}_data`);
|
|
|
|
const initialProps = [];
|
|
const updates = [];
|
|
|
|
this.attributes
|
|
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
|
|
.forEach(attr => {
|
|
if (attr.type === 'Attribute') {
|
|
const { dynamic, value, dependencies } = mungeAttribute(attr, block);
|
|
|
|
const snippet = `{ ${quoteIfNecessary(attr.name)}: ${value} }`;
|
|
initialProps.push(snippet);
|
|
|
|
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
|
|
updates.push(condition ? `${condition} && ${snippet}` : snippet);
|
|
}
|
|
|
|
else {
|
|
block.contextualise(attr.expression); // TODO gah
|
|
const { snippet, dependencies } = attr.metadata;
|
|
|
|
initialProps.push(snippet);
|
|
|
|
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
|
|
updates.push(condition ? `${condition} && ${snippet}` : snippet);
|
|
}
|
|
});
|
|
|
|
block.builders.init.addBlock(deindent`
|
|
var ${levels} = [
|
|
${initialProps.join(',\n')}
|
|
];
|
|
|
|
var ${data} = {};
|
|
for (var #i = 0; #i < ${levels}.length; #i += 1) {
|
|
${data} = @assign(${data}, ${levels}[#i]);
|
|
}
|
|
`);
|
|
|
|
block.builders.hydrate.addLine(
|
|
`@setAttributes(${this.var}, ${data});`
|
|
);
|
|
|
|
block.builders.update.addBlock(deindent`
|
|
@setAttributes(${this.var}, @getSpreadUpdate(${levels}, [
|
|
${updates.join(',\n')}
|
|
]));
|
|
`);
|
|
}
|
|
|
|
addEventHandlers(block: Block, allUsedContexts) {
|
|
const { compiler } = this;
|
|
let eventHandlerUsesComponent = false;
|
|
|
|
this.handlers.forEach(handler => {
|
|
const isCustomEvent = compiler.events.has(handler.name);
|
|
const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');
|
|
|
|
const context = shouldHoist ? null : this.var;
|
|
const usedContexts: string[] = [];
|
|
|
|
if (handler.callee) {
|
|
handler.render(this.compiler, block);
|
|
|
|
if (!validCalleeObjects.has(handler.callee.name)) {
|
|
if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
|
|
}
|
|
|
|
// handler.expression.arguments.forEach((arg: Node) => {
|
|
// const { contexts } = block.contextualise(arg, context, true);
|
|
|
|
// contexts.forEach(context => {
|
|
// if (!~usedContexts.indexOf(context)) usedContexts.push(context);
|
|
// allUsedContexts.add(context);
|
|
// });
|
|
// });
|
|
}
|
|
|
|
const ctx = context || 'this';
|
|
|
|
// get a name for the event handler that is globally unique
|
|
// if hoisted, locally unique otherwise
|
|
const handlerName = (shouldHoist ? compiler : block).getUniqueName(
|
|
`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
|
|
);
|
|
|
|
const component = block.alias('component'); // can't use #component, might be hoisted
|
|
|
|
// create the handler body
|
|
const handlerBody = deindent`
|
|
${eventHandlerUsesComponent &&
|
|
`var #component = ${ctx}._svelte.component;`}
|
|
${handler.dependencies.size > 0 && `const ctx = #component.get();`}
|
|
${handler.snippet ?
|
|
handler.snippet :
|
|
`#component.fire("${handler.name}", event);`}
|
|
`;
|
|
|
|
if (isCustomEvent) {
|
|
block.addVariable(handlerName);
|
|
|
|
block.builders.hydrate.addBlock(deindent`
|
|
${handlerName} = %events-${handler.name}.call(#component, ${this.var}, function(event) {
|
|
${handlerBody}
|
|
});
|
|
`);
|
|
|
|
block.builders.destroy.addLine(deindent`
|
|
${handlerName}.destroy();
|
|
`);
|
|
} else {
|
|
const handlerFunction = deindent`
|
|
function ${handlerName}(event) {
|
|
${handlerBody}
|
|
}
|
|
`;
|
|
|
|
if (shouldHoist) {
|
|
compiler.blocks.push(handlerFunction);
|
|
} else {
|
|
block.builders.init.addBlock(handlerFunction);
|
|
}
|
|
|
|
block.builders.hydrate.addLine(
|
|
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
|
|
);
|
|
|
|
block.builders.destroy.addLine(
|
|
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
|
|
);
|
|
}
|
|
});
|
|
return eventHandlerUsesComponent;
|
|
}
|
|
|
|
addRef(block: Block) {
|
|
const ref = `#component.refs.${this.ref}`;
|
|
|
|
block.builders.mount.addLine(
|
|
`${ref} = ${this.var};`
|
|
);
|
|
|
|
block.builders.destroy.addLine(
|
|
`if (${ref} === ${this.var}) ${ref} = null;`
|
|
);
|
|
}
|
|
|
|
addTransitions(
|
|
block: Block
|
|
) {
|
|
const { intro, outro } = this;
|
|
|
|
if (!intro && !outro) return;
|
|
|
|
if (intro === outro) {
|
|
const name = block.getUniqueName(`${this.var}_transition`);
|
|
const snippet = intro.expression
|
|
? intro.expression.snippet
|
|
: '{}';
|
|
|
|
block.addVariable(name);
|
|
|
|
const fn = `%transitions-${intro.name}`;
|
|
|
|
block.builders.intro.addBlock(deindent`
|
|
#component.root._aftercreate.push(function() {
|
|
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true, null);
|
|
${name}.run(true, function() {
|
|
#component.fire("intro.end", { node: ${this.var} });
|
|
});
|
|
});
|
|
`);
|
|
|
|
block.builders.outro.addBlock(deindent`
|
|
${name}.run(false, function() {
|
|
#component.fire("outro.end", { node: ${this.var} });
|
|
if (--#outros === 0) #outrocallback();
|
|
${name} = null;
|
|
});
|
|
`);
|
|
} else {
|
|
const introName = intro && block.getUniqueName(`${this.var}_intro`);
|
|
const outroName = outro && block.getUniqueName(`${this.var}_outro`);
|
|
|
|
if (intro) {
|
|
block.addVariable(introName);
|
|
const snippet = intro.expression
|
|
? intro.expression.snippet
|
|
: '{}';
|
|
|
|
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
|
|
|
|
if (outro) {
|
|
block.builders.intro.addBlock(deindent`
|
|
if (${introName}) ${introName}.abort();
|
|
if (${outroName}) ${outroName}.abort();
|
|
`);
|
|
}
|
|
|
|
block.builders.intro.addBlock(deindent`
|
|
#component.root._aftercreate.push(function() {
|
|
${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true, null);
|
|
${introName}.run(true, function() {
|
|
#component.fire("intro.end", { node: ${this.var} });
|
|
});
|
|
});
|
|
`);
|
|
}
|
|
|
|
if (outro) {
|
|
block.addVariable(outroName);
|
|
const snippet = outro.expression
|
|
? outro.expression.snippet
|
|
: '{}';
|
|
|
|
const fn = `%transitions-${outro.name}`;
|
|
|
|
// TODO hide elements that have outro'd (unless they belong to a still-outroing
|
|
// group) prior to their removal from the DOM
|
|
block.builders.outro.addBlock(deindent`
|
|
${outroName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false, null);
|
|
${outroName}.run(false, function() {
|
|
#component.fire("outro.end", { node: ${this.var} });
|
|
if (--#outros === 0) #outrocallback();
|
|
});
|
|
`);
|
|
}
|
|
}
|
|
}
|
|
|
|
addActions(block: Block) {
|
|
this.attributes.filter((a: Action) => a.type === 'Action').forEach((attribute:Action) => {
|
|
const { expression } = attribute;
|
|
let snippet, dependencies;
|
|
if (expression) {
|
|
this.compiler.addSourcemapLocations(expression);
|
|
block.contextualise(expression);
|
|
snippet = attribute.metadata.snippet;
|
|
dependencies = attribute.metadata.dependencies;
|
|
}
|
|
|
|
const name = block.getUniqueName(
|
|
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
|
|
);
|
|
|
|
block.addVariable(name);
|
|
const fn = `%actions-${attribute.name}`;
|
|
|
|
block.builders.hydrate.addLine(
|
|
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
|
|
);
|
|
|
|
if (dependencies && dependencies.length) {
|
|
let conditional = `typeof ${name}.update === 'function' && `;
|
|
const deps = dependencies.map(dependency => `changed.${dependency}`).join(' || ');
|
|
conditional += dependencies.length > 1 ? `(${deps})` : deps;
|
|
|
|
block.builders.update.addConditional(
|
|
conditional,
|
|
`${name}.update.call(#component, ${snippet});`
|
|
);
|
|
}
|
|
|
|
block.builders.destroy.addLine(
|
|
`if (typeof ${name}.destroy === 'function') ${name}.destroy.call(#component);`
|
|
);
|
|
});
|
|
}
|
|
|
|
getStaticAttributeValue(name: string) {
|
|
const attribute = this.attributes.find(
|
|
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
|
|
);
|
|
|
|
if (!attribute) return null;
|
|
|
|
if (attribute.value === true) return true;
|
|
if (attribute.value.length === 0) return '';
|
|
|
|
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') {
|
|
return attribute.value[0].data;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
isMediaNode() {
|
|
return this.name === 'audio' || this.name === 'video';
|
|
}
|
|
|
|
remount(name: string) {
|
|
const slot = this.attributes.find(attribute => attribute.name === 'slot');
|
|
if (slot) {
|
|
return `@appendNode(${this.var}, ${name}._slotted.${this.getStaticAttributeValue('slot')});`;
|
|
}
|
|
|
|
return `@appendNode(${this.var}, ${name}._slotted.default);`;
|
|
}
|
|
|
|
addCssClass() {
|
|
const classAttribute = this.attributes.find(a => a.name === 'class');
|
|
if (classAttribute && classAttribute.value !== true) {
|
|
if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') {
|
|
classAttribute.value[0].data += ` ${this.compiler.stylesheet.id}`;
|
|
} else {
|
|
(<Node[]>classAttribute.value).push(
|
|
new Node({ type: 'Text', data: ` ${this.compiler.stylesheet.id}` })
|
|
);
|
|
}
|
|
} else {
|
|
this.attributes.push(
|
|
new Attribute({
|
|
compiler: this.compiler,
|
|
name: 'class',
|
|
value: [new Node({ type: 'Text', data: `${this.compiler.stylesheet.id}` })],
|
|
parent: this,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getRenderStatement(
|
|
compiler: DomGenerator,
|
|
namespace: string,
|
|
name: string
|
|
) {
|
|
if (namespace === 'http://www.w3.org/2000/svg') {
|
|
return `@createSvgElement("${name}")`;
|
|
}
|
|
|
|
if (namespace) {
|
|
return `document.createElementNS("${namespace}", "${name}")`;
|
|
}
|
|
|
|
return `@createElement("${name}")`;
|
|
}
|
|
|
|
function getClaimStatement(
|
|
compiler: DomGenerator,
|
|
namespace: string,
|
|
nodes: string,
|
|
node: Node
|
|
) {
|
|
const attributes = node.attributes
|
|
.filter((attr: Node) => attr.type === 'Attribute')
|
|
.map((attr: Node) => `${quoteIfNecessary(attr.name)}: true`)
|
|
.join(', ');
|
|
|
|
const name = namespace ? node.name : node.name.toUpperCase();
|
|
|
|
return `@claimElement(${nodes}, "${name}", ${attributes
|
|
? `{ ${attributes} }`
|
|
: `{}`}, ${namespace === namespaces.svg ? true : false})`;
|
|
}
|
|
|
|
function stringifyAttributeValue(value: Node | true) {
|
|
if (value === true) return '';
|
|
if (value.length === 0) return `=""`;
|
|
|
|
const data = value[0].data;
|
|
return `=${JSON.stringify(data)}`;
|
|
}
|
|
|
|
const events = [
|
|
{
|
|
eventNames: ['input'],
|
|
filter: (node: Element, name: string) =>
|
|
node.name === 'textarea' ||
|
|
node.name === 'input' && !/radio|checkbox/.test(node.getStaticAttributeValue('type'))
|
|
},
|
|
{
|
|
eventNames: ['change'],
|
|
filter: (node: Element, name: string) =>
|
|
node.name === 'select' ||
|
|
node.name === 'input' && /radio|checkbox|range/.test(node.getStaticAttributeValue('type'))
|
|
},
|
|
|
|
// media events
|
|
{
|
|
eventNames: ['timeupdate'],
|
|
filter: (node: Element, name: string) =>
|
|
node.isMediaNode() &&
|
|
(name === 'currentTime' || name === 'played')
|
|
},
|
|
{
|
|
eventNames: ['durationchange'],
|
|
filter: (node: Element, name: string) =>
|
|
node.isMediaNode() &&
|
|
name === 'duration'
|
|
},
|
|
{
|
|
eventNames: ['play', 'pause'],
|
|
filter: (node: Element, name: string) =>
|
|
node.isMediaNode() &&
|
|
name === 'paused'
|
|
},
|
|
{
|
|
eventNames: ['progress'],
|
|
filter: (node: Element, name: string) =>
|
|
node.isMediaNode() &&
|
|
name === 'buffered'
|
|
},
|
|
{
|
|
eventNames: ['loadedmetadata'],
|
|
filter: (node: Element, name: string) =>
|
|
node.isMediaNode() &&
|
|
(name === 'buffered' || name === 'seekable')
|
|
},
|
|
{
|
|
eventNames: ['volumechange'],
|
|
filter: (node: Element, name: string) =>
|
|
node.isMediaNode() &&
|
|
name === 'volume'
|
|
}
|
|
];
|