svelte/src/compile/nodes/Element.ts

1162 lines
32 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 { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary';
import Compiler from '../Compiler';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Transition from './Transition';
import Animation from './Animation';
import Action from './Action';
import Class from './Class';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren';
import { dimensions } from '../../utils/patterns';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const booleanAttributes = new Set([
'async',
'autocomplete',
'autofocus',
'autoplay',
'border',
'challenge',
'checked',
'compact',
'contenteditable',
'controls',
'default',
'defer',
'disabled',
'formnovalidate',
'frameborder',
'hidden',
'indeterminate',
'ismap',
'loop',
'multiple',
'muted',
'nohref',
'noresize',
'noshade',
'novalidate',
'nowrap',
'open',
'readonly',
'required',
'reversed',
'scoped',
'scrolling',
'seamless',
'selected',
'sortable',
'spellcheck',
'translate'
]);
export default class Element extends Node {
type: 'Element';
name: string;
scope: any; // TODO
attributes: Attribute[];
actions: Action[];
bindings: Binding[];
classes: Class[];
classDependencies: string[];
handlers: EventHandler[];
intro?: Transition;
outro?: Transition;
animation?: Animation;
children: Node[];
ref: string;
namespace: string;
constructor(compiler, parent, scope, info: any) {
super(compiler, parent, scope, info);
this.name = info.name;
this.scope = scope;
const parentElement = parent.findNearest(/^Element/);
this.namespace = this.name === 'svg' ?
namespaces.svg :
parentElement ? parentElement.namespace : this.compiler.namespace;
this.attributes = [];
this.actions = [];
this.bindings = [];
this.classes = [];
this.classDependencies = [];
this.handlers = [];
this.intro = null;
this.outro = null;
this.animation = null;
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 (info.children.length > 0) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children
});
info.children = [];
}
}
if (this.name === 'option') {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const valueAttribute = info.attributes.find((attribute: Node) => attribute.name === 'value');
if (!valueAttribute) {
info.attributes.push({
type: 'Attribute',
name: 'value',
value: info.children,
synthetic: true
});
}
}
info.attributes.forEach(node => {
switch (node.type) {
case 'Action':
this.actions.push(new Action(compiler, this, scope, node));
break;
case 'Attribute':
case 'Spread':
// special case
if (node.name === 'xmlns') this.namespace = node.value[0].data;
this.attributes.push(new Attribute(compiler, this, scope, node));
break;
case 'Binding':
this.bindings.push(new Binding(compiler, this, scope, node));
break;
case 'Class':
this.classes.push(new Class(compiler, this, scope, node));
break;
case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, scope, node));
break;
case 'Transition':
const transition = new Transition(compiler, this, scope, node);
if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition;
break;
case 'Animation':
this.animation = new Animation(compiler, this, scope, node);
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}`);
}
});
this.children = mapChildren(compiler, this, scope, info.children);
compiler.stylesheet.apply(this);
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
if (this.name === 'slot' || this.name === 'option' || this.compiler.options.dev) {
this.cannotUseInnerHTML();
}
this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
this.attributes.forEach(attr => {
if (
attr.chunks &&
attr.chunks.length &&
(attr.chunks.length > 1 || attr.chunks[0].type !== 'Text')
) {
this.parent.cannotUseInnerHTML();
}
if (attr.dependencies.size) {
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 => {
attr.dependencies.forEach((dependency: string) => {
this.compiler.indirectDependencies.get(prop).add(dependency);
});
});
}
}
}
});
this.actions.forEach(action => {
this.parent.cannotUseInnerHTML();
if (action.expression) {
block.addDependencies(action.expression.dependencies);
}
});
this.bindings.forEach(binding => {
this.parent.cannotUseInnerHTML();
block.addDependencies(binding.value.dependencies);
});
this.classes.forEach(classDir => {
this.parent.cannotUseInnerHTML();
if (classDir.expression) {
block.addDependencies(classDir.expression.dependencies);
}
});
this.handlers.forEach(handler => {
this.parent.cannotUseInnerHTML();
block.addDependencies(handler.dependencies);
});
if (this.intro || this.outro || this.animation || this.ref) {
this.parent.cannotUseInnerHTML();
}
if (this.intro) block.addIntro();
if (this.outro) block.addOutro();
if (this.animation) block.addAnimation();
const valueAttribute = this.attributes.find((attribute: Attribute) => attribute.name === 'value');
// 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.bindings.find(node => node.name === 'value');
if (binding) {
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
const dependencies = binding.value.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.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 node = this.var;
const nodes = parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parentNodes is null
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
const prop = slot && quotePropIfNecessary(slot.chunks[0].data);
const initialMountNode = this.slotted ?
`${this.findNearest(/^Component/).var}._slotted${prop}` : // TODO this looks bonkers
parentNode;
block.addVariable(node);
const renderStatement = getRenderStatement(this.namespace, this.name);
block.builders.create.addLine(
`${node} = ${renderStatement};`
);
if (this.compiler.options.hydratable) {
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${node} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
var ${nodes} = @children(${this.name === 'template' ? `${node}.content` : node});
`);
} else {
block.builders.claim.addLine(
`${node} = ${renderStatement};`
);
}
}
if (initialMountNode) {
block.builders.mount.addLine(
`@append(${initialMountNode}, ${node});`
);
if (initialMountNode === 'document.head') {
block.builders.destroy.addLine(`@detachNode(${node});`);
}
} else {
block.builders.mount.addLine(`@insert(#target, ${node}, anchor);`);
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.destroy.addConditional('detach', `@detachNode(${node});`);
}
// 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(
`${node}.textContent = ${stringify(this.children[0].data)};`
);
} else {
block.builders.create.addLine(
`${node}.innerHTML = ${stringify(this.children.map(toHTML).join(''))};`
);
}
} else {
this.children.forEach((child: Node) => {
child.build(block, this.name === 'template' ? `${node}.content` : node, nodes);
});
}
let hasHoistedEventHandlerOrBinding = (
//(this.hasAncestor('EachBlock') && this.bindings.length > 0) ||
this.handlers.some(handler => handler.shouldHoist)
);
const eventHandlerOrBindingUsesComponent = (
this.bindings.length > 0 ||
this.handlers.some(handler => handler.usesComponent)
);
const eventHandlerOrBindingUsesContext = (
this.bindings.some(binding => binding.usesContext) ||
this.handlers.some(handler => handler.usesContext)
);
if (hasHoistedEventHandlerOrBinding) {
const initialProps: string[] = [];
const updates: string[] = [];
if (eventHandlerOrBindingUsesComponent) {
const component = block.alias('component');
initialProps.push(component === 'component' ? 'component' : `component: ${component}`);
}
if (eventHandlerOrBindingUsesContext) {
initialProps.push(`ctx`);
block.builders.update.addLine(`${node}._svelte.ctx = ctx;`);
}
if (initialProps.length) {
block.builders.hydrate.addBlock(deindent`
${node}._svelte = { ${initialProps.join(', ')} };
`);
}
} else {
if (eventHandlerOrBindingUsesContext) {
block.maintainContext = true;
}
}
this.addBindings(block);
this.addEventHandlers(block);
if (this.ref) this.addRef(block);
this.addAttributes(block);
this.addTransitions(block);
this.addAnimation(block);
this.addActions(block);
this.addClasses(block);
if (this.initialUpdate) {
block.builders.mount.addBlock(this.initialUpdate);
}
if (nodes) {
block.builders.claim.addLine(
`${nodes}.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}`;
node.attributes.forEach((attr: Node) => {
open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.chunks)}`
});
if (isVoidElementName(node.name)) return open + '>';
return `${open}>${node.children.map(toHTML).join('')}</${node.name}>`;
}
if (this.compiler.options.dev) {
const loc = this.compiler.locate(this.start);
block.builders.hydrate.addLine(
`@addLoc(${this.var}, ${this.compiler.fileVar}, ${loc.line}, ${loc.column}, ${this.start});`
);
}
}
addBindings(
block: Block
) {
if (this.bindings.length === 0) return;
if (this.name === 'select' || this.isMediaNode()) this.compiler.target.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));
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});`
}
${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 => {
if (name === 'resize') {
// special case
const resize_listener = block.getUniqueName(`${this.var}_resize_listener`);
block.addVariable(resize_listener);
block.builders.mount.addLine(
`${resize_listener} = @addResizeListener(${this.var}, ${handler});`
);
block.builders.destroy.addLine(
`${resize_listener}.cancel();`
);
} else {
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 ctx`)
.join(' && ');
if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
this.compiler.target.hasComplexBindings = true;
block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
);
}
if (group.events[0] === 'resize') {
this.compiler.target.hasComplexBindings = true;
block.builders.hydrate.addLine(
`#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) => {
if (attribute.name === 'class' && attribute.isDynamic) {
this.classDependencies.push(...attribute.dependencies);
}
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 => {
const condition = attr.dependencies.size > 0
? `(${[...attr.dependencies].map(d => `changed.${d}`).join(' || ')})`
: null;
if (attr.isSpread) {
const { snippet, dependencies } = attr.expression;
initialProps.push(snippet);
updates.push(condition ? `${condition} && ${snippet}` : snippet);
} else {
const snippet = `{ ${quoteNameIfNecessary(attr.name)}: ${attr.getValue()} }`;
initialProps.push(snippet);
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) {
const { compiler } = this;
this.handlers.forEach(handler => {
const isCustomEvent = compiler.events.has(handler.name);
if (handler.callee) {
handler.render(this.compiler, block, handler.shouldHoist);
}
const target = handler.shouldHoist ? 'this' : this.var;
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (handler.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`
${handler.shouldHoist && (
handler.usesComponent || handler.usesContext
? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;`
: null
)}
${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 (handler.shouldHoist) {
compiler.target.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});`
);
}
});
}
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.addConditional(`#component.root._intro`, deindent`
if (${name}) ${name}.invalidate();
#component.root._aftercreate.push(() => {
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
${name}.run(1);
});
`);
block.builders.outro.addBlock(deindent`
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false);
${name}.run(0, () => {
#outrocallback();
${name} = null;
});
`);
block.builders.destroy.addConditional('detach', `if (${name}) ${name}.abort();`);
} 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(1);
if (${outroName}) ${outroName}.abort(1);
`);
}
block.builders.intro.addConditional(`#component.root._intro`, deindent`
#component.root._aftercreate.push(() => {
${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
${introName}.run(1);
});
`);
}
if (outro) {
block.addVariable(outroName);
const snippet = outro.expression
? outro.expression.snippet
: '{}';
const fn = `%transitions-${outro.name}`;
block.builders.intro.addBlock(deindent`
if (${outroName}) ${outroName}.abort(1);
`);
// 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);
${outroName}.run(0, #outrocallback);
`);
block.builders.destroy.addConditional('detach', `if (${outroName}) ${outroName}.abort();`);
}
}
}
addAnimation(block: Block) {
if (!this.animation) return;
const rect = block.getUniqueName('rect');
const animation = block.getUniqueName('animation');
block.addVariable(rect);
block.addVariable(animation);
block.builders.measure.addBlock(deindent`
${rect} = ${this.var}.getBoundingClientRect();
`);
block.builders.fix.addBlock(deindent`
@fixPosition(${this.var});
if (${animation}) ${animation}.stop();
`);
const params = this.animation.expression ? this.animation.expression.snippet : '{}';
block.builders.animate.addBlock(deindent`
if (${animation}) ${animation}.stop();
${animation} = @wrapAnimation(${this.var}, ${rect}, %animations-${this.animation.name}, ${params});
`);
}
addActions(block: Block) {
this.actions.forEach(action => {
const { expression } = action;
let snippet, dependencies;
if (expression) {
snippet = action.expression.snippet;
dependencies = action.expression.dependencies;
}
const name = block.getUniqueName(
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
);
block.addVariable(name);
const fn = `%actions-${action.name}`;
block.builders.mount.addLine(
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
);
if (dependencies && dependencies.size > 0) {
let conditional = `typeof ${name}.update === 'function' && `;
const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.size > 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);`
);
});
}
addClasses(block: Block) {
this.classes.forEach(classDir => {
const { expression: { snippet, dependencies}, name } = classDir;
const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`;
block.builders.hydrate.addLine(updater);
if ((dependencies && dependencies.size > 0) || this.classDependencies.length) {
const allDeps = this.classDependencies.concat(...dependencies);
const deps = allDeps.map(dependency => `changed.${dependency}`).join(' || ');
const condition = allDeps.length > 1 ? `(${deps})` : deps;
block.builders.update.addConditional(
condition,
updater
);
}
});
}
getStaticAttributeValue(name: string) {
const attribute = this.attributes.find(
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
);
if (!attribute) return null;
if (attribute.isTrue) return true;
if (attribute.chunks.length === 0) return '';
if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
return attribute.chunks[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) {
const prop = quotePropIfNecessary(slot.chunks[0].data);
return `@append(${name}._slotted${prop}, ${this.var});`;
}
return `@append(${name}._slotted.default, ${this.var});`;
}
addCssClass(className = this.compiler.stylesheet.id) {
const classAttribute = this.attributes.find(a => a.name === 'class');
if (classAttribute && !classAttribute.isTrue) {
if (classAttribute.chunks.length === 1 && classAttribute.chunks[0].type === 'Text') {
(<Text>classAttribute.chunks[0]).data += ` ${className}`;
} else {
(<Node[]>classAttribute.chunks).push(
new Text(this.compiler, this, this.scope, {
type: 'Text',
data: ` ${className}`
})
);
}
} else {
this.attributes.push(
new Attribute(this.compiler, this, this.scope, {
type: 'Attribute',
name: 'class',
value: [{ type: 'Text', data: className }]
})
);
}
}
ssr() {
const { compiler } = this;
let openingTag = `<${this.name}`;
let textareaContents; // awkward special case
const slot = this.getStaticAttributeValue('slot');
if (slot && this.hasAncestor('Component')) {
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
const slotName = slot.chunks[0].data;
const appendTarget = compiler.target.appendTargets[compiler.target.appendTargets.length - 1];
appendTarget.slotStack.push(slotName);
appendTarget.slots[slotName] = '';
}
const classExpr = this.classes.map((classDir: Class) => {
const { expression: { snippet }, name } = classDir;
return `${snippet} ? "${name}" : ""`;
}).join(', ');
let addClassAttribute = classExpr ? true : false;
if (this.attributes.find(attr => attr.isSpread)) {
// TODO dry this out
const args = [];
this.attributes.forEach(attribute => {
if (attribute.isSpread) {
args.push(attribute.expression.snippet);
} else {
if (attribute.name === 'value' && this.name === 'textarea') {
textareaContents = attribute.stringifyForSsr();
} else if (attribute.isTrue) {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: true }`);
} else if (
booleanAttributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`);
} else {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${attribute.stringifyForSsr()}\` }`);
}
}
});
openingTag += "${@spread([" + args.join(', ') + "])}";
} else {
this.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && this.name === 'textarea') {
textareaContents = attribute.stringifyForSsr();
} else if (attribute.isTrue) {
openingTag += ` ${attribute.name}`;
} else if (
booleanAttributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
} else if (attribute.name === 'class' && classExpr) {
addClassAttribute = false;
openingTag += ` class="\${ [\`${attribute.stringifyForSsr()}\`, ${classExpr} ].join(' ') }"`;
} else {
openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`;
}
});
}
if (addClassAttribute) {
openingTag += ` class="\${ [${classExpr}].join(' ') }"`;
}
openingTag += '>';
compiler.target.append(openingTag);
if (this.name === 'textarea' && textareaContents !== undefined) {
compiler.target.append(textareaContents);
} else {
this.children.forEach((child: Node) => {
child.ssr();
});
}
if (!isVoidElementName(this.name)) {
compiler.target.append(`</${this.name}>`);
}
}
}
function getRenderStatement(
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: Compiler,
namespace: string,
nodes: string,
node: Node
) {
const attributes = node.attributes
.filter((attr: Node) => attr.type === 'Attribute')
.map((attr: Node) => `${quoteNameIfNecessary(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|range/.test(node.getStaticAttributeValue('type'))
},
{
eventNames: ['change'],
filter: (node: Element, name: string) =>
node.name === 'select' ||
node.name === 'input' && /radio|checkbox/.test(node.getStaticAttributeValue('type'))
},
{
eventNames: ['change', 'input'],
filter: (node: Element, name: string) =>
node.name === 'input' && node.getStaticAttributeValue('type') === 'range'
},
{
eventNames: ['resize'],
filter: (node: Element, name: string) =>
dimensions.test(name)
},
// 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'
}
];