mirror of https://github.com/sveltejs/svelte
parent
c6d49b7295
commit
daaad80a0e
@ -1,23 +0,0 @@
|
||||
import { DomGenerator } from './index';
|
||||
import Block from './Block';
|
||||
import { Node } from '../../interfaces';
|
||||
|
||||
export interface State {
|
||||
namespace: string;
|
||||
parentNode: string;
|
||||
parentNodes: string;
|
||||
parentNodeName?: string;
|
||||
inEachBlock?: boolean;
|
||||
allUsedContexts?: string[];
|
||||
usesComponent?: boolean;
|
||||
selectBindingDependencies?: string[];
|
||||
}
|
||||
|
||||
export type Visitor = (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[]
|
||||
) => void;
|
@ -1,18 +0,0 @@
|
||||
import visitors from './visitors/index';
|
||||
import { DomGenerator } from './index';
|
||||
import Block from './Block';
|
||||
import { Node } from '../../interfaces';
|
||||
import { State } from './interfaces';
|
||||
|
||||
export default function visit(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[]
|
||||
) {
|
||||
throw new Error('do not use visit')
|
||||
const visitor = visitors[node.type];
|
||||
visitor(generator, block, state, node, elementStack, componentStack);
|
||||
}
|
@ -1,361 +0,0 @@
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import visit from '../../visit';
|
||||
import visitSlot from '../Slot';
|
||||
import visitComponent from '../Component';
|
||||
import visitWindow from './meta/Window';
|
||||
import visitAttribute from './Attribute';
|
||||
import addBindings from './addBindings';
|
||||
import flattenReference from '../../../../utils/flattenReference';
|
||||
import validCalleeObjects from '../../../../utils/validCalleeObjects';
|
||||
import * as namespaces from '../../../../utils/namespaces';
|
||||
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
|
||||
import isVoidElementName from '../../../../utils/isVoidElementName';
|
||||
import addTransitions from './addTransitions';
|
||||
import { DomGenerator } from '../../index';
|
||||
import Block from '../../Block';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import { State } from '../../interfaces';
|
||||
import reservedNames from '../../../../utils/reservedNames';
|
||||
import { stringify } from '../../../../utils/stringify';
|
||||
|
||||
const meta: Record<string, any> = {
|
||||
':Window': visitWindow,
|
||||
};
|
||||
|
||||
export default function visitElement(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[]
|
||||
) {
|
||||
if (node.name in meta) {
|
||||
return meta[node.name](generator, block, node);
|
||||
}
|
||||
|
||||
if (node.name === 'slot') {
|
||||
if (generator.customElement) {
|
||||
const slotName = getStaticAttributeValue(node, 'name') || 'default';
|
||||
generator.slots.add(slotName);
|
||||
} else {
|
||||
return visitSlot(generator, block, state, node, elementStack, componentStack);
|
||||
}
|
||||
}
|
||||
|
||||
const childState = node._state;
|
||||
const name = childState.parentNode;
|
||||
|
||||
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
|
||||
const parentNode = node.slotted ?
|
||||
`${componentStack[componentStack.length - 1].var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
|
||||
state.parentNode;
|
||||
|
||||
block.addVariable(name);
|
||||
block.builders.create.addLine(
|
||||
`${name} = ${getRenderStatement(
|
||||
generator,
|
||||
childState.namespace,
|
||||
node.name
|
||||
)};`
|
||||
);
|
||||
|
||||
if (generator.hydratable) {
|
||||
block.builders.claim.addBlock(deindent`
|
||||
${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, node)};
|
||||
var ${childState.parentNodes} = @children(${name});
|
||||
`);
|
||||
}
|
||||
|
||||
if (parentNode) {
|
||||
block.builders.mount.addLine(
|
||||
`@appendNode(${name}, ${parentNode});`
|
||||
);
|
||||
} 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});`);
|
||||
}
|
||||
|
||||
// add CSS encapsulation attribute
|
||||
if (node._needsCssAttribute && !generator.customElement) {
|
||||
generator.needsEncapsulateHelper = true;
|
||||
block.builders.hydrate.addLine(
|
||||
`@encapsulateStyles(${name});`
|
||||
);
|
||||
|
||||
if (node._cssRefAttribute) {
|
||||
block.builders.hydrate.addLine(
|
||||
`@setAttribute(${name}, "svelte-ref-${node._cssRefAttribute}", "");`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (node.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 (node.children.length > 0) {
|
||||
node.attributes.push({
|
||||
type: 'Attribute',
|
||||
name: 'value',
|
||||
value: node.children,
|
||||
});
|
||||
|
||||
node.children = [];
|
||||
}
|
||||
}
|
||||
|
||||
// insert static children with textContent or innerHTML
|
||||
if (!childState.namespace && node.canUseInnerHTML && node.children.length > 0) {
|
||||
if (node.children.length === 1 && node.children[0].type === 'Text') {
|
||||
block.builders.create.addLine(
|
||||
`${name}.textContent = ${stringify(node.children[0].data)};`
|
||||
);
|
||||
} else {
|
||||
block.builders.create.addLine(
|
||||
`${name}.innerHTML = ${stringify(node.children.map(toHTML).join(''))};`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, block, childState, child, elementStack.concat(node), componentStack);
|
||||
});
|
||||
}
|
||||
|
||||
addBindings(generator, block, childState, node);
|
||||
|
||||
node.attributes.filter((a: Node) => a.type === 'Attribute').forEach((attribute: Node) => {
|
||||
visitAttribute(generator, block, childState, node, attribute);
|
||||
});
|
||||
|
||||
// event handlers
|
||||
node.attributes.filter((a: Node) => a.type === 'EventHandler').forEach((attribute: Node) => {
|
||||
const isCustomEvent = generator.events.has(attribute.name);
|
||||
const shouldHoist = !isCustomEvent && state.inEachBlock;
|
||||
|
||||
const context = shouldHoist ? null : name;
|
||||
const usedContexts: string[] = [];
|
||||
|
||||
if (attribute.expression) {
|
||||
generator.addSourcemapLocations(attribute.expression);
|
||||
|
||||
const flattened = flattenReference(attribute.expression.callee);
|
||||
if (!validCalleeObjects.has(flattened.name)) {
|
||||
// allow event.stopPropagation(), this.select() etc
|
||||
// TODO verify that it's a valid callee (i.e. built-in or declared method)
|
||||
generator.code.prependRight(
|
||||
attribute.expression.start,
|
||||
`${block.alias('component')}.`
|
||||
);
|
||||
if (shouldHoist) childState.usesComponent = true; // this feels a bit hacky but it works!
|
||||
}
|
||||
|
||||
attribute.expression.arguments.forEach((arg: Node) => {
|
||||
const { contexts } = block.contextualise(arg, context, true);
|
||||
|
||||
contexts.forEach(context => {
|
||||
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
|
||||
if (!~childState.allUsedContexts.indexOf(context))
|
||||
childState.allUsedContexts.push(context);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const _this = context || 'this';
|
||||
const declarations = usedContexts.map(name => {
|
||||
if (name === 'state') {
|
||||
if (shouldHoist) childState.usesComponent = true;
|
||||
return `var state = ${block.alias('component')}.get();`;
|
||||
}
|
||||
|
||||
const listName = block.listNames.get(name);
|
||||
const indexName = block.indexNames.get(name);
|
||||
const contextName = block.contexts.get(name);
|
||||
|
||||
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
|
||||
});
|
||||
|
||||
// get a name for the event handler that is globally unique
|
||||
// if hoisted, locally unique otherwise
|
||||
const handlerName = (shouldHoist ? generator : block).getUniqueName(
|
||||
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
|
||||
);
|
||||
|
||||
// create the handler body
|
||||
const handlerBody = deindent`
|
||||
${childState.usesComponent &&
|
||||
`var ${block.alias('component')} = ${_this}._svelte.component;`}
|
||||
${declarations}
|
||||
${attribute.expression ?
|
||||
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
|
||||
`${block.alias('component')}.fire("${attribute.name}", event);`}
|
||||
`;
|
||||
|
||||
if (isCustomEvent) {
|
||||
block.addVariable(handlerName);
|
||||
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
${handlerName} = %events-${attribute.name}.call(#component, ${name}, function(event) {
|
||||
${handlerBody}
|
||||
});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
${handlerName}.teardown();
|
||||
`);
|
||||
} else {
|
||||
const handler = deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
`;
|
||||
|
||||
if (shouldHoist) {
|
||||
generator.blocks.push(handler);
|
||||
} else {
|
||||
block.builders.init.addBlock(handler);
|
||||
}
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`@addListener(${name}, "${attribute.name}", ${handlerName});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${name}, "${attribute.name}", ${handlerName});`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// refs
|
||||
node.attributes.filter((a: Node) => a.type === 'Ref').forEach((attribute: Node) => {
|
||||
const ref = `#component.refs.${attribute.name}`;
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`${ref} = ${name};`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`if (${ref} === ${name}) ${ref} = null;`
|
||||
);
|
||||
|
||||
generator.usesRefs = true; // so component.refs object is created
|
||||
});
|
||||
|
||||
addTransitions(generator, block, childState, node);
|
||||
|
||||
if (childState.allUsedContexts.length || childState.usesComponent) {
|
||||
const initialProps: string[] = [];
|
||||
const updates: string[] = [];
|
||||
|
||||
if (childState.usesComponent) {
|
||||
initialProps.push(`component: #component`);
|
||||
}
|
||||
|
||||
childState.allUsedContexts.forEach((contextName: string) => {
|
||||
if (contextName === 'state') return;
|
||||
|
||||
const listName = block.listNames.get(contextName);
|
||||
const indexName = block.indexNames.get(contextName);
|
||||
|
||||
initialProps.push(
|
||||
`${listName}: ${listName},\n${indexName}: ${indexName}`
|
||||
);
|
||||
updates.push(
|
||||
`${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${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 (node.initialUpdate) {
|
||||
block.builders.mount.addBlock(node.initialUpdate);
|
||||
}
|
||||
|
||||
block.builders.claim.addLine(
|
||||
`${childState.parentNodes}.forEach(@detachNode);`
|
||||
);
|
||||
|
||||
function toHTML(node: Node) {
|
||||
if (node.type === 'Text') return node.data;
|
||||
|
||||
let open = `<${node.name}`;
|
||||
|
||||
if (node._needsCssAttribute) {
|
||||
open += ` ${generator.stylesheet.id}`;
|
||||
}
|
||||
|
||||
if (node._cssRefAttribute) {
|
||||
open += ` svelte-ref-${node._cssRefAttribute}`;
|
||||
}
|
||||
|
||||
node.attributes.forEach((attr: Node) => {
|
||||
open += ` ${attr.name}${stringifyAttributeValue(attr.value)}`
|
||||
});
|
||||
|
||||
if (isVoidElementName(node.name)) return open + '>';
|
||||
|
||||
return `${open}>${node.children.map(toHTML).join('')}</${node.name}>`;
|
||||
}
|
||||
}
|
||||
|
||||
function getRenderStatement(
|
||||
generator: 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(
|
||||
generator: DomGenerator,
|
||||
namespace: string,
|
||||
nodes: string,
|
||||
node: Node
|
||||
) {
|
||||
const attributes = node.attributes
|
||||
.filter((attr: Node) => attr.type === 'Attribute')
|
||||
.map((attr: Node) => `${quoteProp(attr.name, generator.legacy)}: true`)
|
||||
.join(', ');
|
||||
|
||||
const name = namespace ? node.name : node.name.toUpperCase();
|
||||
|
||||
return `@claimElement(${nodes}, "${name}", ${attributes
|
||||
? `{ ${attributes} }`
|
||||
: `{}`}, ${namespace === namespaces.svg ? true : false})`;
|
||||
}
|
||||
|
||||
function quoteProp(name: string, legacy: boolean) {
|
||||
const isLegacyPropName = legacy && reservedNames.has(name);
|
||||
|
||||
if (/[^a-zA-Z_$0-9]/.test(name) || isLegacyPropName) return `"${name}"`;
|
||||
return name;
|
||||
}
|
||||
|
||||
function stringifyAttributeValue(value: Node | true) {
|
||||
if (value === true) return '';
|
||||
if (value.length === 0) return `=""`;
|
||||
|
||||
const data = value[0].data;
|
||||
return `=${JSON.stringify(data)}`;
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
import flattenReference from '../../../../../utils/flattenReference';
|
||||
import deindent from '../../../../../utils/deindent';
|
||||
import { DomGenerator } from '../../../index';
|
||||
import Block from '../../../Block';
|
||||
import { Node } from '../../../../../interfaces';
|
||||
|
||||
const associatedEvents = {
|
||||
innerWidth: 'resize',
|
||||
innerHeight: 'resize',
|
||||
outerWidth: 'resize',
|
||||
outerHeight: 'resize',
|
||||
|
||||
scrollX: 'scroll',
|
||||
scrollY: 'scroll',
|
||||
};
|
||||
|
||||
const readonly = new Set([
|
||||
'innerWidth',
|
||||
'innerHeight',
|
||||
'outerWidth',
|
||||
'outerHeight',
|
||||
'online',
|
||||
]);
|
||||
|
||||
export default function visitWindow(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
const events = {};
|
||||
const bindings: Record<string, string> = {};
|
||||
|
||||
node.attributes.forEach((attribute: Node) => {
|
||||
if (attribute.type === 'EventHandler') {
|
||||
// TODO verify that it's a valid callee (i.e. built-in or declared method)
|
||||
generator.addSourcemapLocations(attribute.expression);
|
||||
|
||||
let usesState = false;
|
||||
|
||||
attribute.expression.arguments.forEach((arg: Node) => {
|
||||
block.contextualise(arg, null, true);
|
||||
const { dependencies } = arg.metadata;
|
||||
if (dependencies.length) usesState = true;
|
||||
});
|
||||
|
||||
const flattened = flattenReference(attribute.expression.callee);
|
||||
if (flattened.name !== 'event' && flattened.name !== 'this') {
|
||||
// allow event.stopPropagation(), this.select() etc
|
||||
generator.code.prependRight(
|
||||
attribute.expression.start,
|
||||
`${block.alias('component')}.`
|
||||
);
|
||||
}
|
||||
|
||||
const handlerName = block.getUniqueName(`onwindow${attribute.name}`);
|
||||
const handlerBody = deindent`
|
||||
${usesState && `var state = #component.get();`}
|
||||
[✂${attribute.expression.start}-${attribute.expression.end}✂];
|
||||
`;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
window.addEventListener("${attribute.name}", ${handlerName});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("${attribute.name}", ${handlerName});
|
||||
`);
|
||||
}
|
||||
|
||||
if (attribute.type === 'Binding') {
|
||||
// in dev mode, throw if read-only values are written to
|
||||
if (readonly.has(attribute.name)) {
|
||||
generator.readonly.add(attribute.value.name);
|
||||
}
|
||||
|
||||
bindings[attribute.name] = attribute.value.name;
|
||||
|
||||
// bind:online is a special case, we need to listen for two separate events
|
||||
if (attribute.name === 'online') return;
|
||||
|
||||
const associatedEvent = associatedEvents[attribute.name];
|
||||
|
||||
if (!events[associatedEvent]) events[associatedEvent] = [];
|
||||
events[associatedEvent].push(
|
||||
`${attribute.value.name}: this.${attribute.name}`
|
||||
);
|
||||
|
||||
// add initial value
|
||||
generator.metaBindings.push(
|
||||
`this._state.${attribute.value.name} = window.${attribute.name};`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const lock = block.getUniqueName(`window_updating`);
|
||||
|
||||
Object.keys(events).forEach(event => {
|
||||
const handlerName = block.getUniqueName(`onwindow${event}`);
|
||||
const props = events[event].join(',\n');
|
||||
|
||||
if (event === 'scroll') {
|
||||
// TODO other bidirectional bindings...
|
||||
block.addVariable(lock, 'false');
|
||||
}
|
||||
|
||||
const handlerBody = deindent`
|
||||
${event === 'scroll' && `${lock} = true;`}
|
||||
${generator.options.dev && `component._updatingReadonlyProperty = true;`}
|
||||
|
||||
#component.set({
|
||||
${props}
|
||||
});
|
||||
|
||||
${generator.options.dev && `component._updatingReadonlyProperty = false;`}
|
||||
${event === 'scroll' && `${lock} = false;`}
|
||||
`;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
window.addEventListener("${event}", ${handlerName});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("${event}", ${handlerName});
|
||||
`);
|
||||
});
|
||||
|
||||
// special case... might need to abstract this out if we add more special cases
|
||||
if (bindings.scrollX && bindings.scrollY) {
|
||||
const observerCallback = block.getUniqueName(`scrollobserver`);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${observerCallback}() {
|
||||
if (${lock}) return;
|
||||
var x = ${bindings.scrollX
|
||||
? `#component.get("${bindings.scrollX}")`
|
||||
: `window.scrollX`};
|
||||
var y = ${bindings.scrollY
|
||||
? `#component.get("${bindings.scrollY}")`
|
||||
: `window.scrollY`};
|
||||
window.scrollTo(x, y);
|
||||
}
|
||||
`);
|
||||
|
||||
if (bindings.scrollX)
|
||||
block.builders.init.addLine(
|
||||
`#component.observe("${bindings.scrollX}", ${observerCallback});`
|
||||
);
|
||||
if (bindings.scrollY)
|
||||
block.builders.init.addLine(
|
||||
`#component.observe("${bindings.scrollY}", ${observerCallback});`
|
||||
);
|
||||
} else if (bindings.scrollX || bindings.scrollY) {
|
||||
const isX = !!bindings.scrollX;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
#component.observe("${bindings.scrollX || bindings.scrollY}", function(${isX ? 'x' : 'y'}) {
|
||||
if (${lock}) return;
|
||||
window.scrollTo(${isX ? 'x, window.scrollY' : 'window.scrollX, y'});
|
||||
});
|
||||
`);
|
||||
}
|
||||
|
||||
// another special case. (I'm starting to think these are all special cases.)
|
||||
if (bindings.online) {
|
||||
const handlerName = block.getUniqueName(`onlinestatuschanged`);
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
#component.set({ ${bindings.online}: navigator.onLine });
|
||||
}
|
||||
window.addEventListener("online", ${handlerName});
|
||||
window.addEventListener("offline", ${handlerName});
|
||||
`);
|
||||
|
||||
// add initial value
|
||||
generator.metaBindings.push(
|
||||
`this._state.${bindings.online} = navigator.onLine;`
|
||||
);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("online", ${handlerName});
|
||||
window.removeEventListener("offline", ${handlerName});
|
||||
`);
|
||||
}
|
||||
}
|
@ -1,419 +0,0 @@
|
||||
import deindent from '../../../utils/deindent';
|
||||
import visit from '../visit';
|
||||
import { DomGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import isDomNode from './shared/isDomNode';
|
||||
import { Node } from '../../../interfaces';
|
||||
import { State } from '../interfaces';
|
||||
|
||||
function isElseIf(node: Node) {
|
||||
return (
|
||||
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
|
||||
);
|
||||
}
|
||||
|
||||
function isElseBranch(branch) {
|
||||
return branch.block && !branch.condition;
|
||||
}
|
||||
|
||||
function getBranches(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[]
|
||||
) {
|
||||
block.contextualise(node.expression); // TODO remove
|
||||
|
||||
const branches = [
|
||||
{
|
||||
condition: node.metadata.snippet,
|
||||
block: node._block.name,
|
||||
hasUpdateMethod: node._block.hasUpdateMethod,
|
||||
hasIntroMethod: node._block.hasIntroMethod,
|
||||
hasOutroMethod: node._block.hasOutroMethod,
|
||||
},
|
||||
];
|
||||
|
||||
visitChildren(generator, block, state, node, elementStack, componentStack);
|
||||
|
||||
if (isElseIf(node.else)) {
|
||||
branches.push(
|
||||
...getBranches(generator, block, state, node.else.children[0], elementStack, componentStack)
|
||||
);
|
||||
} else {
|
||||
branches.push({
|
||||
condition: null,
|
||||
block: node.else ? node.else._block.name : null,
|
||||
hasUpdateMethod: node.else ? node.else._block.hasUpdateMethod : false,
|
||||
hasIntroMethod: node.else ? node.else._block.hasIntroMethod : false,
|
||||
hasOutroMethod: node.else ? node.else._block.hasOutroMethod : false,
|
||||
});
|
||||
|
||||
if (node.else) {
|
||||
visitChildren(generator, block, state, node.else, elementStack, componentStack);
|
||||
}
|
||||
}
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
function visitChildren(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[]
|
||||
) {
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, node._block, node._state, child, elementStack, componentStack);
|
||||
});
|
||||
}
|
||||
|
||||
export default function visitIfBlock(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[]
|
||||
) {
|
||||
const name = node.var;
|
||||
|
||||
const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator);
|
||||
const anchor = needsAnchor
|
||||
? block.getUniqueName(`${name}_anchor`)
|
||||
: (node.next && node.next.var) || 'null';
|
||||
const params = block.params.join(', ');
|
||||
|
||||
const branches = getBranches(generator, block, state, node, elementStack, componentStack);
|
||||
|
||||
const hasElse = isElseBranch(branches[branches.length - 1]);
|
||||
const if_name = hasElse ? '' : `if (${name}) `;
|
||||
|
||||
const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
|
||||
const hasOutros = branches[0].hasOutroMethod;
|
||||
|
||||
const vars = { name, needsAnchor, anchor, params, if_name, hasElse };
|
||||
|
||||
if (node.else) {
|
||||
if (hasOutros) {
|
||||
compoundWithOutros(
|
||||
generator,
|
||||
block,
|
||||
state,
|
||||
node,
|
||||
branches,
|
||||
dynamic,
|
||||
vars
|
||||
);
|
||||
} else {
|
||||
compound(generator, block, state, node, branches, dynamic, vars);
|
||||
}
|
||||
} else {
|
||||
simple(generator, block, state, node, branches[0], dynamic, vars);
|
||||
}
|
||||
|
||||
block.builders.create.addLine(`${if_name}${name}.c();`);
|
||||
|
||||
block.builders.claim.addLine(
|
||||
`${if_name}${name}.l(${state.parentNodes});`
|
||||
);
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
anchor,
|
||||
`@createComment()`,
|
||||
`@createComment()`,
|
||||
state.parentNode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function simple(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
branch,
|
||||
dynamic,
|
||||
{ name, needsAnchor, anchor, params, if_name }
|
||||
) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${name} = (${branch.condition}) && ${branch.block}(${params}, #component);
|
||||
`);
|
||||
|
||||
const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm';
|
||||
const targetNode = state.parentNode || '#target';
|
||||
const anchorNode = state.parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`if (${name}) ${name}.${mountOrIntro}(${targetNode}, ${anchorNode});`
|
||||
);
|
||||
|
||||
const parentNode = isDomNode(node.parent, generator) ? node.parent.var : `${anchor}.parentNode`;
|
||||
|
||||
const enter = dynamic
|
||||
? branch.hasIntroMethod
|
||||
? deindent`
|
||||
if (${name}) {
|
||||
${name}.p(changed, ${params});
|
||||
} else {
|
||||
${name} = ${branch.block}(${params}, #component);
|
||||
if (${name}) ${name}.c();
|
||||
}
|
||||
|
||||
${name}.i(${parentNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (${name}) {
|
||||
${name}.p(changed, ${params});
|
||||
} else {
|
||||
${name} = ${branch.block}(${params}, #component);
|
||||
${name}.c();
|
||||
${name}.m(${parentNode}, ${anchor});
|
||||
}
|
||||
`
|
||||
: branch.hasIntroMethod
|
||||
? deindent`
|
||||
if (!${name}) {
|
||||
${name} = ${branch.block}(${params}, #component);
|
||||
${name}.c();
|
||||
}
|
||||
${name}.i(${parentNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (!${name}) {
|
||||
${name} = ${branch.block}(${params}, #component);
|
||||
${name}.c();
|
||||
${name}.m(${parentNode}, ${anchor});
|
||||
}
|
||||
`;
|
||||
|
||||
// no `p()` here — we don't want to update outroing nodes,
|
||||
// as that will typically result in glitching
|
||||
const exit = branch.hasOutroMethod
|
||||
? deindent`
|
||||
${name}.o(function() {
|
||||
${name}.u();
|
||||
${name}.d();
|
||||
${name} = null;
|
||||
});
|
||||
`
|
||||
: deindent`
|
||||
${name}.u();
|
||||
${name}.d();
|
||||
${name} = null;
|
||||
`;
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${branch.condition}) {
|
||||
${enter}
|
||||
} else if (${name}) {
|
||||
${exit}
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.unmount.addLine(`${if_name}${name}.u();`);
|
||||
|
||||
block.builders.destroy.addLine(`${if_name}${name}.d();`);
|
||||
}
|
||||
|
||||
function compound(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
branches,
|
||||
dynamic,
|
||||
{ name, needsAnchor, anchor, params, hasElse, if_name }
|
||||
) {
|
||||
const select_block_type = generator.getUniqueName(`select_block_type`);
|
||||
const current_block_type = block.getUniqueName(`current_block_type`);
|
||||
const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
|
||||
|
||||
generator.blocks.push(deindent`
|
||||
function ${select_block_type}(${params}) {
|
||||
${branches
|
||||
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`)
|
||||
.join('\n')}
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${current_block_type} = ${select_block_type}(${params});
|
||||
var ${name} = ${current_block_type_and}${current_block_type}(${params}, #component);
|
||||
`);
|
||||
|
||||
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
|
||||
|
||||
const targetNode = state.parentNode || '#target';
|
||||
const anchorNode = state.parentNode ? 'null' : 'anchor';
|
||||
block.builders.mount.addLine(
|
||||
`${if_name}${name}.${mountOrIntro}(${targetNode}, ${anchorNode});`
|
||||
);
|
||||
|
||||
const parentNode = isDomNode(node.parent, generator) ? node.parent.var : `${anchor}.parentNode`;
|
||||
|
||||
const changeBlock = deindent`
|
||||
${hasElse
|
||||
? deindent`
|
||||
${name}.u();
|
||||
${name}.d();
|
||||
`
|
||||
: deindent`
|
||||
if (${name}) {
|
||||
${name}.u();
|
||||
${name}.d();
|
||||
}`}
|
||||
${name} = ${current_block_type_and}${current_block_type}(${params}, #component);
|
||||
${if_name}${name}.c();
|
||||
${if_name}${name}.${mountOrIntro}(${parentNode}, ${anchor});
|
||||
`;
|
||||
|
||||
if (dynamic) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${current_block_type} === (${current_block_type} = ${select_block_type}(${params})) && ${name}) {
|
||||
${name}.p(changed, ${params});
|
||||
} else {
|
||||
${changeBlock}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(${params}))) {
|
||||
${changeBlock}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.unmount.addLine(`${if_name}${name}.u();`);
|
||||
|
||||
block.builders.destroy.addLine(`${if_name}${name}.d();`);
|
||||
}
|
||||
|
||||
// if any of the siblings have outros, we need to keep references to the blocks
|
||||
// (TODO does this only apply to bidi transitions?)
|
||||
function compoundWithOutros(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
branches,
|
||||
dynamic,
|
||||
{ name, needsAnchor, anchor, params, hasElse }
|
||||
) {
|
||||
const select_block_type = block.getUniqueName(`select_block_type`);
|
||||
const current_block_type_index = block.getUniqueName(`current_block_type_index`);
|
||||
const previous_block_index = block.getUniqueName(`previous_block_index`);
|
||||
const if_block_creators = block.getUniqueName(`if_block_creators`);
|
||||
const if_blocks = block.getUniqueName(`if_blocks`);
|
||||
|
||||
const if_current_block_type_index = hasElse
|
||||
? ''
|
||||
: `if (~${current_block_type_index}) `;
|
||||
|
||||
block.addVariable(current_block_type_index);
|
||||
block.addVariable(name);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${if_block_creators} = [
|
||||
${branches.map(branch => branch.block).join(',\n')}
|
||||
];
|
||||
|
||||
var ${if_blocks} = [];
|
||||
|
||||
function ${select_block_type}(${params}) {
|
||||
${branches
|
||||
.map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`)
|
||||
.join('\n')}
|
||||
}
|
||||
`);
|
||||
|
||||
if (hasElse) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
${current_block_type_index} = ${select_block_type}(${params});
|
||||
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${params}, #component);
|
||||
`);
|
||||
} else {
|
||||
block.builders.init.addBlock(deindent`
|
||||
if (~(${current_block_type_index} = ${select_block_type}(${params}))) {
|
||||
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${params}, #component);
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
|
||||
const targetNode = state.parentNode || '#target';
|
||||
const anchorNode = state.parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`${if_current_block_type_index}${if_blocks}[${current_block_type_index}].${mountOrIntro}(${targetNode}, ${anchorNode});`
|
||||
);
|
||||
|
||||
const parentNode = (state.parentNode && !needsAnchor) ? state.parentNode : `${anchor}.parentNode`;
|
||||
|
||||
const destroyOldBlock = deindent`
|
||||
${name}.o(function() {
|
||||
${if_blocks}[ ${previous_block_index} ].u();
|
||||
${if_blocks}[ ${previous_block_index} ].d();
|
||||
${if_blocks}[ ${previous_block_index} ] = null;
|
||||
});
|
||||
`;
|
||||
|
||||
const createNewBlock = deindent`
|
||||
${name} = ${if_blocks}[${current_block_type_index}];
|
||||
if (!${name}) {
|
||||
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${params}, #component);
|
||||
${name}.c();
|
||||
}
|
||||
${name}.${mountOrIntro}(${parentNode}, ${anchor});
|
||||
`;
|
||||
|
||||
const changeBlock = hasElse
|
||||
? deindent`
|
||||
${destroyOldBlock}
|
||||
|
||||
${createNewBlock}
|
||||
`
|
||||
: deindent`
|
||||
if (${name}) {
|
||||
${destroyOldBlock}
|
||||
}
|
||||
|
||||
if (~${current_block_type_index}) {
|
||||
${createNewBlock}
|
||||
} else {
|
||||
${name} = null;
|
||||
}
|
||||
`;
|
||||
|
||||
if (dynamic) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
var ${previous_block_index} = ${current_block_type_index};
|
||||
${current_block_type_index} = ${select_block_type}(${params});
|
||||
if (${current_block_type_index} === ${previous_block_index}) {
|
||||
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ${params});
|
||||
} else {
|
||||
${changeBlock}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
var ${previous_block_index} = ${current_block_type_index};
|
||||
${current_block_type_index} = ${select_block_type}(${params});
|
||||
if (${current_block_type_index} !== ${previous_block_index}) {
|
||||
${changeBlock}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
${if_current_block_type_index}{
|
||||
${if_blocks}[${current_block_type_index}].u();
|
||||
${if_blocks}[${current_block_type_index}].d();
|
||||
}
|
||||
`);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import deindent from '../../../utils/deindent';
|
||||
import visitTag from './shared/Tag';
|
||||
import { DomGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
import { State } from '../interfaces';
|
||||
|
||||
export default function visitMustacheTag(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node
|
||||
) {
|
||||
const { init } = visitTag(
|
||||
generator,
|
||||
block,
|
||||
state,
|
||||
node,
|
||||
node.var,
|
||||
value => `${node.var}.data = ${value};`
|
||||
);
|
||||
|
||||
block.addElement(
|
||||
node.var,
|
||||
`@createText(${init})`,
|
||||
`@claimText(${state.parentNodes}, ${init})`,
|
||||
state.parentNode
|
||||
);
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
import deindent from '../../../utils/deindent';
|
||||
import visitTag from './shared/Tag';
|
||||
import { DomGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
import { State } from '../interfaces';
|
||||
|
||||
export default function visitRawMustacheTag(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node
|
||||
) {
|
||||
const name = node.var;
|
||||
|
||||
const needsAnchorBefore = node.prev ? node.prev.type !== 'Element' : !state.parentNode;
|
||||
const needsAnchorAfter = node.next ? node.next.type !== 'Element' : !state.parentNode;
|
||||
|
||||
const anchorBefore = needsAnchorBefore
|
||||
? block.getUniqueName(`${name}_before`)
|
||||
: (node.prev && node.prev.var) || 'null';
|
||||
|
||||
const anchorAfter = needsAnchorAfter
|
||||
? block.getUniqueName(`${name}_after`)
|
||||
: (node.next && node.next.var) || 'null';
|
||||
|
||||
let detach: string;
|
||||
let insert: (content: string) => string;
|
||||
let useInnerHTML = false;
|
||||
|
||||
if (anchorBefore === 'null' && anchorAfter === 'null') {
|
||||
useInnerHTML = true;
|
||||
detach = `${state.parentNode}.innerHTML = '';`;
|
||||
insert = content => `${state.parentNode}.innerHTML = ${content};`;
|
||||
} else if (anchorBefore === 'null') {
|
||||
detach = `@detachBefore(${anchorAfter});`;
|
||||
insert = content => `${anchorAfter}.insertAdjacentHTML("beforebegin", ${content});`;
|
||||
} else if (anchorAfter === 'null') {
|
||||
detach = `@detachAfter(${anchorBefore});`;
|
||||
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
|
||||
} else {
|
||||
detach = `@detachBetween(${anchorBefore}, ${anchorAfter});`;
|
||||
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
|
||||
}
|
||||
|
||||
const { init } = visitTag(
|
||||
generator,
|
||||
block,
|
||||
state,
|
||||
node,
|
||||
name,
|
||||
content => deindent`
|
||||
${!useInnerHTML && detach}
|
||||
${insert(content)}
|
||||
`
|
||||
);
|
||||
|
||||
// we would have used comments here, but the `insertAdjacentHTML` api only
|
||||
// exists for `Element`s.
|
||||
if (needsAnchorBefore) {
|
||||
block.addElement(
|
||||
anchorBefore,
|
||||
`@createElement('noscript')`,
|
||||
`@createElement('noscript')`,
|
||||
state.parentNode
|
||||
);
|
||||
}
|
||||
|
||||
function addAnchorAfter() {
|
||||
block.addElement(
|
||||
anchorAfter,
|
||||
`@createElement('noscript')`,
|
||||
`@createElement('noscript')`,
|
||||
state.parentNode
|
||||
);
|
||||
}
|
||||
|
||||
if (needsAnchorAfter && anchorBefore === 'null') {
|
||||
// anchorAfter needs to be in the DOM before we
|
||||
// insert the HTML...
|
||||
addAnchorAfter();
|
||||
}
|
||||
|
||||
block.builders.mount.addLine(insert(init));
|
||||
block.builders.detachRaw.addBlock(detach);
|
||||
|
||||
if (needsAnchorAfter && anchorBefore !== 'null') {
|
||||
// ...otherwise it should go afterwards
|
||||
addAnchorAfter();
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import Element from './Element/Element';
|
||||
import IfBlock from './IfBlock';
|
||||
import MustacheTag from './MustacheTag';
|
||||
import RawMustacheTag from './RawMustacheTag';
|
||||
import { Visitor } from '../interfaces';
|
||||
|
||||
const visitors: Record<string, Visitor> = {
|
||||
Element,
|
||||
IfBlock,
|
||||
MustacheTag,
|
||||
RawMustacheTag
|
||||
};
|
||||
|
||||
export default visitors;
|
Loading…
Reference in new issue