mirror of https://github.com/sveltejs/svelte
parent
e8595978cf
commit
a8eaa7e95c
@ -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,607 +0,0 @@
|
||||
import Block from './Block';
|
||||
import { trimStart, trimEnd } from '../../utils/trim';
|
||||
import { assign } from '../../shared/index.js';
|
||||
import getStaticAttributeValue from '../../utils/getStaticAttributeValue';
|
||||
import isChildOfComponent from '../shared/utils/isChildOfComponent';
|
||||
import { DomGenerator } from './index';
|
||||
import { Node } from '../../interfaces';
|
||||
import { State } from './interfaces';
|
||||
|
||||
function isElseIf(node: Node) {
|
||||
return (
|
||||
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
|
||||
);
|
||||
}
|
||||
|
||||
function getChildState(parent: State, child = {}) {
|
||||
return assign(
|
||||
{},
|
||||
parent,
|
||||
{ parentNode: null, parentNodes: 'nodes' },
|
||||
child || {}
|
||||
);
|
||||
}
|
||||
|
||||
function createDebuggingComment(node: Node, generator: DomGenerator) {
|
||||
const { locate, source } = generator;
|
||||
|
||||
let c = node.start;
|
||||
if (node.type === 'ElseBlock') {
|
||||
while (source[c] !== '{') c -= 1;
|
||||
c -= 1;
|
||||
}
|
||||
|
||||
let d = node.expression ? node.expression.end : c;
|
||||
while (source[d] !== '}') d += 1;
|
||||
d += 2;
|
||||
|
||||
const start = locate(c);
|
||||
const loc = `(${start.line + 1}:${start.column})`;
|
||||
|
||||
return `${loc} ${source.slice(c, d)}`.replace(/\n/g, ' ');
|
||||
}
|
||||
|
||||
function cannotUseInnerHTML(node: Node) {
|
||||
while (node && node.canUseInnerHTML) {
|
||||
node.canUseInnerHTML = false;
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// Whitespace inside one of these elements will not result in
|
||||
// a whitespace node being created in any circumstances. (This
|
||||
// list is almost certainly very incomplete)
|
||||
const elementsWithoutText = new Set([
|
||||
'audio',
|
||||
'datalist',
|
||||
'dl',
|
||||
'ol',
|
||||
'optgroup',
|
||||
'select',
|
||||
'ul',
|
||||
'video',
|
||||
]);
|
||||
|
||||
const preprocessors = {
|
||||
MustacheTag: (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[],
|
||||
stripWhitespace: boolean
|
||||
) => {
|
||||
cannotUseInnerHTML(node);
|
||||
node.var = block.getUniqueName('text');
|
||||
block.addDependencies(node.metadata.dependencies);
|
||||
},
|
||||
|
||||
RawMustacheTag: (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[],
|
||||
stripWhitespace: boolean
|
||||
) => {
|
||||
cannotUseInnerHTML(node);
|
||||
node.var = block.getUniqueName('raw');
|
||||
block.addDependencies(node.metadata.dependencies);
|
||||
},
|
||||
|
||||
Text: (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[],
|
||||
stripWhitespace: boolean
|
||||
) => {
|
||||
if (!/\S/.test(node.data) && (state.namespace || elementsWithoutText.has(state.parentNodeName))) {
|
||||
node.shouldSkip = true;
|
||||
return;
|
||||
}
|
||||
|
||||
node.var = block.getUniqueName(`text`);
|
||||
},
|
||||
|
||||
AwaitBlock: (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
inEachBlock: boolean,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[],
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) => {
|
||||
cannotUseInnerHTML(node);
|
||||
|
||||
node.var = block.getUniqueName('await_block');
|
||||
block.addDependencies(node.metadata.dependencies);
|
||||
|
||||
let dynamic = false;
|
||||
|
||||
[
|
||||
['pending', null],
|
||||
['then', node.value],
|
||||
['catch', node.error]
|
||||
].forEach(([status, arg]) => {
|
||||
const child = node[status];
|
||||
|
||||
const context = block.getUniqueName(arg || '_');
|
||||
const contexts = new Map(block.contexts);
|
||||
contexts.set(arg, context);
|
||||
|
||||
child._block = block.child({
|
||||
comment: createDebuggingComment(child, generator),
|
||||
name: generator.getUniqueName(`create_${status}_block`),
|
||||
params: block.params.concat(context),
|
||||
context,
|
||||
contexts
|
||||
});
|
||||
|
||||
child._state = getChildState(state);
|
||||
|
||||
preprocessChildren(generator, child._block, child._state, child, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling);
|
||||
generator.blocks.push(child._block);
|
||||
|
||||
if (child._block.dependencies.size > 0) {
|
||||
dynamic = true;
|
||||
block.addDependencies(child._block.dependencies);
|
||||
}
|
||||
});
|
||||
|
||||
node.pending._block.hasUpdateMethod = dynamic;
|
||||
node.then._block.hasUpdateMethod = dynamic;
|
||||
node.catch._block.hasUpdateMethod = dynamic;
|
||||
},
|
||||
|
||||
IfBlock: (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
inEachBlock: boolean,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[],
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) => {
|
||||
cannotUseInnerHTML(node);
|
||||
|
||||
const blocks: Block[] = [];
|
||||
let dynamic = false;
|
||||
let hasIntros = false;
|
||||
let hasOutros = false;
|
||||
|
||||
function attachBlocks(node: Node) {
|
||||
node.var = block.getUniqueName(`if_block`);
|
||||
|
||||
block.addDependencies(node.metadata.dependencies);
|
||||
|
||||
node._block = block.child({
|
||||
comment: createDebuggingComment(node, generator),
|
||||
name: generator.getUniqueName(`create_if_block`),
|
||||
});
|
||||
|
||||
node._state = getChildState(state);
|
||||
|
||||
blocks.push(node._block);
|
||||
preprocessChildren(generator, node._block, node._state, node, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling);
|
||||
|
||||
if (node._block.dependencies.size > 0) {
|
||||
dynamic = true;
|
||||
block.addDependencies(node._block.dependencies);
|
||||
}
|
||||
|
||||
if (node._block.hasIntroMethod) hasIntros = true;
|
||||
if (node._block.hasOutroMethod) hasOutros = true;
|
||||
|
||||
if (isElseIf(node.else)) {
|
||||
attachBlocks(node.else.children[0]);
|
||||
} else if (node.else) {
|
||||
node.else._block = block.child({
|
||||
comment: createDebuggingComment(node.else, generator),
|
||||
name: generator.getUniqueName(`create_if_block`),
|
||||
});
|
||||
|
||||
node.else._state = getChildState(state);
|
||||
|
||||
blocks.push(node.else._block);
|
||||
preprocessChildren(
|
||||
generator,
|
||||
node.else._block,
|
||||
node.else._state,
|
||||
node.else,
|
||||
inEachBlock,
|
||||
elementStack,
|
||||
componentStack,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
|
||||
if (node.else._block.dependencies.size > 0) {
|
||||
dynamic = true;
|
||||
block.addDependencies(node.else._block.dependencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attachBlocks(node);
|
||||
|
||||
blocks.forEach(block => {
|
||||
block.hasUpdateMethod = dynamic;
|
||||
block.hasIntroMethod = hasIntros;
|
||||
block.hasOutroMethod = hasOutros;
|
||||
});
|
||||
|
||||
generator.blocks.push(...blocks);
|
||||
},
|
||||
|
||||
EachBlock: (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
inEachBlock: boolean,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[],
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) => {
|
||||
cannotUseInnerHTML(node);
|
||||
node.var = block.getUniqueName(`each`);
|
||||
node.iterations = block.getUniqueName(`${node.var}_blocks`);
|
||||
|
||||
const { dependencies } = node.metadata;
|
||||
block.addDependencies(dependencies);
|
||||
|
||||
const indexNames = new Map(block.indexNames);
|
||||
const indexName =
|
||||
node.index || block.getUniqueName(`${node.context}_index`);
|
||||
indexNames.set(node.context, indexName);
|
||||
|
||||
const listNames = new Map(block.listNames);
|
||||
const listName = block.getUniqueName(
|
||||
(node.expression.type === 'MemberExpression' && !node.expression.computed) ? node.expression.property.name :
|
||||
node.expression.type === 'Identifier' ? node.expression.name :
|
||||
`each_value`
|
||||
);
|
||||
listNames.set(node.context, listName);
|
||||
|
||||
const context = block.getUniqueName(node.context);
|
||||
const contexts = new Map(block.contexts);
|
||||
contexts.set(node.context, context);
|
||||
|
||||
const indexes = new Map(block.indexes);
|
||||
if (node.index) indexes.set(node.index, node.context);
|
||||
|
||||
const changeableIndexes = new Map(block.changeableIndexes);
|
||||
if (node.index) changeableIndexes.set(node.index, node.key);
|
||||
|
||||
if (node.destructuredContexts) {
|
||||
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
|
||||
contexts.set(node.destructuredContexts[i], `${context}[${i}]`);
|
||||
}
|
||||
}
|
||||
|
||||
node._block = block.child({
|
||||
comment: createDebuggingComment(node, generator),
|
||||
name: generator.getUniqueName('create_each_block'),
|
||||
context: node.context,
|
||||
key: node.key,
|
||||
|
||||
contexts,
|
||||
indexes,
|
||||
changeableIndexes,
|
||||
|
||||
listName,
|
||||
indexName,
|
||||
|
||||
indexNames,
|
||||
listNames,
|
||||
params: block.params.concat(listName, context, indexName),
|
||||
});
|
||||
|
||||
node._state = getChildState(state, {
|
||||
inEachBlock: true,
|
||||
});
|
||||
|
||||
generator.blocks.push(node._block);
|
||||
preprocessChildren(generator, node._block, node._state, node, true, elementStack, componentStack, stripWhitespace, nextSibling);
|
||||
block.addDependencies(node._block.dependencies);
|
||||
node._block.hasUpdateMethod = node._block.dependencies.size > 0;
|
||||
|
||||
if (node.else) {
|
||||
node.else._block = block.child({
|
||||
comment: createDebuggingComment(node.else, generator),
|
||||
name: generator.getUniqueName(`${node._block.name}_else`),
|
||||
});
|
||||
|
||||
node.else._state = getChildState(state);
|
||||
|
||||
generator.blocks.push(node.else._block);
|
||||
preprocessChildren(
|
||||
generator,
|
||||
node.else._block,
|
||||
node.else._state,
|
||||
node.else,
|
||||
inEachBlock,
|
||||
elementStack,
|
||||
componentStack,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
node.else._block.hasUpdateMethod = node.else._block.dependencies.size > 0;
|
||||
}
|
||||
},
|
||||
|
||||
Element: (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
inEachBlock: boolean,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[],
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) => {
|
||||
if (node.name === 'slot' || node.name === 'option') {
|
||||
cannotUseInnerHTML(node);
|
||||
}
|
||||
|
||||
node.attributes.forEach((attribute: Node) => {
|
||||
if (attribute.type === 'Attribute' && attribute.value !== true) {
|
||||
attribute.value.forEach((chunk: Node) => {
|
||||
if (chunk.type !== 'Text') {
|
||||
if (node.parent) cannotUseInnerHTML(node.parent);
|
||||
|
||||
const dependencies = chunk.metadata.dependencies;
|
||||
block.addDependencies(dependencies);
|
||||
|
||||
// special case — <option value='{{foo}}'> — see below
|
||||
if (
|
||||
node.name === 'option' &&
|
||||
attribute.name === 'value' &&
|
||||
state.selectBindingDependencies
|
||||
) {
|
||||
state.selectBindingDependencies.forEach(prop => {
|
||||
dependencies.forEach((dependency: string) => {
|
||||
generator.indirectDependencies.get(prop).add(dependency);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (node.parent) cannotUseInnerHTML(node.parent);
|
||||
|
||||
if (attribute.type === 'EventHandler' && attribute.expression) {
|
||||
attribute.expression.arguments.forEach((arg: Node) => {
|
||||
block.addDependencies(arg.metadata.dependencies);
|
||||
});
|
||||
} else if (attribute.type === 'Binding') {
|
||||
block.addDependencies(attribute.metadata.dependencies);
|
||||
} else if (attribute.type === 'Transition') {
|
||||
if (attribute.intro)
|
||||
generator.hasIntroTransitions = block.hasIntroMethod = true;
|
||||
if (attribute.outro) {
|
||||
generator.hasOutroTransitions = block.hasOutroMethod = true;
|
||||
block.outros += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
|
||||
|
||||
// Treat these the same way:
|
||||
// <option>{{foo}}</option>
|
||||
// <option value='{{foo}}'>{{foo}}</option>
|
||||
if (node.name === 'option' && !valueAttribute) {
|
||||
node.attributes.push({
|
||||
type: 'Attribute',
|
||||
name: 'value',
|
||||
value: node.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 (node.name === 'select') {
|
||||
const binding = node.attributes.find((node: 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;
|
||||
state.selectBindingDependencies = dependencies;
|
||||
dependencies.forEach((prop: string) => {
|
||||
generator.indirectDependencies.set(prop, new Set());
|
||||
});
|
||||
} else {
|
||||
state.selectBindingDependencies = null;
|
||||
}
|
||||
}
|
||||
|
||||
const isComponent =
|
||||
generator.components.has(node.name) || node.name === ':Self' || node.name === ':Component';
|
||||
|
||||
if (isComponent) {
|
||||
cannotUseInnerHTML(node);
|
||||
|
||||
node.var = block.getUniqueName(
|
||||
(
|
||||
node.name === ':Self' ? generator.name :
|
||||
node.name === ':Component' ? 'switch_instance' :
|
||||
node.name
|
||||
).toLowerCase()
|
||||
);
|
||||
|
||||
node._state = getChildState(state, {
|
||||
parentNode: `${node.var}._slotted.default`
|
||||
});
|
||||
} else {
|
||||
const slot = getStaticAttributeValue(node, 'slot');
|
||||
if (slot && isChildOfComponent(node, generator)) {
|
||||
cannotUseInnerHTML(node);
|
||||
node.slotted = true;
|
||||
// TODO validate slots — no nesting, no dynamic names...
|
||||
const component = componentStack[componentStack.length - 1];
|
||||
component._slots.add(slot);
|
||||
}
|
||||
|
||||
node.var = block.getUniqueName(
|
||||
node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
|
||||
);
|
||||
|
||||
node._state = getChildState(state, {
|
||||
parentNode: node.var,
|
||||
parentNodes: block.getUniqueName(`${node.var}_nodes`),
|
||||
parentNodeName: node.name,
|
||||
namespace: node.name === 'svg'
|
||||
? 'http://www.w3.org/2000/svg'
|
||||
: state.namespace,
|
||||
allUsedContexts: [],
|
||||
});
|
||||
|
||||
generator.stylesheet.apply(node, elementStack);
|
||||
}
|
||||
|
||||
if (node.children.length) {
|
||||
if (isComponent) {
|
||||
if (node.children) node._slots = new Set(['default']);
|
||||
preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack, componentStack.concat(node), stripWhitespace, nextSibling);
|
||||
} else {
|
||||
if (node.name === 'pre' || node.name === 'textarea') stripWhitespace = false;
|
||||
preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack.concat(node), componentStack, stripWhitespace, nextSibling);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function preprocessChildren(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
inEachBlock: boolean,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[],
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
// glue text nodes together
|
||||
const cleaned: Node[] = [];
|
||||
let lastChild: Node;
|
||||
|
||||
let windowComponent;
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
if (child.type === 'Comment') return;
|
||||
|
||||
// special case — this is an easy way to remove whitespace surrounding
|
||||
// <:Window/>. lil hacky but it works
|
||||
if (child.type === 'Element' && child.name === ':Window') {
|
||||
windowComponent = child;
|
||||
return;
|
||||
}
|
||||
|
||||
if (child.type === 'Text' && lastChild && lastChild.type === 'Text') {
|
||||
lastChild.data += child.data;
|
||||
lastChild.end = child.end;
|
||||
} else {
|
||||
if (child.type === 'Text' && stripWhitespace && cleaned.length === 0) {
|
||||
child.data = trimStart(child.data);
|
||||
if (child.data) cleaned.push(child);
|
||||
} else {
|
||||
cleaned.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
lastChild = child;
|
||||
});
|
||||
|
||||
lastChild = null;
|
||||
|
||||
cleaned.forEach((child: Node, i: number) => {
|
||||
child.parent = node;
|
||||
child.canUseInnerHTML = !generator.hydratable;
|
||||
|
||||
const preprocessor = preprocessors[child.type];
|
||||
if (preprocessor) preprocessor(generator, block, state, child, inEachBlock, elementStack, componentStack, stripWhitespace, cleaned[i + 1] || nextSibling);
|
||||
|
||||
if (child.shouldSkip) return;
|
||||
|
||||
if (lastChild) lastChild.next = child;
|
||||
child.prev = lastChild;
|
||||
|
||||
lastChild = child;
|
||||
});
|
||||
|
||||
// We want to remove trailing whitespace inside an element/component/block,
|
||||
// *unless* there is no whitespace between this node and its next sibling
|
||||
if (stripWhitespace && lastChild && lastChild.type === 'Text') {
|
||||
const shouldTrim = (
|
||||
nextSibling ? (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)) : !inEachBlock
|
||||
);
|
||||
|
||||
if (shouldTrim) {
|
||||
lastChild.data = trimEnd(lastChild.data);
|
||||
if (!lastChild.data) {
|
||||
cleaned.pop();
|
||||
lastChild = cleaned[cleaned.length - 1];
|
||||
lastChild.next = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.children = cleaned;
|
||||
if (windowComponent) cleaned.unshift(windowComponent);
|
||||
}
|
||||
|
||||
export default function preprocess(
|
||||
generator: DomGenerator,
|
||||
namespace: string,
|
||||
node: Node
|
||||
) {
|
||||
const block = new Block({
|
||||
generator,
|
||||
name: '@create_main_fragment',
|
||||
key: null,
|
||||
|
||||
contexts: new Map(),
|
||||
indexes: new Map(),
|
||||
changeableIndexes: new Map(),
|
||||
|
||||
params: ['state'],
|
||||
indexNames: new Map(),
|
||||
listNames: new Map(),
|
||||
|
||||
dependencies: new Set(),
|
||||
});
|
||||
|
||||
const state: State = {
|
||||
namespace,
|
||||
parentNode: null,
|
||||
parentNodes: 'nodes'
|
||||
};
|
||||
|
||||
generator.blocks.push(block);
|
||||
preprocessChildren(generator, block, state, node, false, [], [], true, null);
|
||||
block.hasUpdateMethod = true;
|
||||
|
||||
return { block, state };
|
||||
}
|
@ -1,17 +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[]
|
||||
) {
|
||||
const visitor = visitors[node.type];
|
||||
visitor(generator, block, state, node, elementStack, componentStack);
|
||||
}
|
@ -1,159 +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';
|
||||
|
||||
export default function visitAwaitBlock(
|
||||
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(', ');
|
||||
|
||||
block.contextualise(node.expression);
|
||||
const { snippet } = node.metadata;
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
anchor,
|
||||
`@createComment()`,
|
||||
`@createComment()`,
|
||||
state.parentNode
|
||||
);
|
||||
}
|
||||
|
||||
const promise = block.getUniqueName(`promise`);
|
||||
const resolved = block.getUniqueName(`resolved`);
|
||||
const await_block = block.getUniqueName(`await_block`);
|
||||
const await_block_type = block.getUniqueName(`await_block_type`);
|
||||
const token = block.getUniqueName(`token`);
|
||||
const await_token = block.getUniqueName(`await_token`);
|
||||
const handle_promise = block.getUniqueName(`handle_promise`);
|
||||
const replace_await_block = block.getUniqueName(`replace_await_block`);
|
||||
const old_block = block.getUniqueName(`old_block`);
|
||||
const value = block.getUniqueName(`value`);
|
||||
const error = block.getUniqueName(`error`);
|
||||
const create_pending_block = node.pending._block.name;
|
||||
const create_then_block = node.then._block.name;
|
||||
const create_catch_block = node.catch._block.name;
|
||||
|
||||
block.addVariable(await_block);
|
||||
block.addVariable(await_block_type);
|
||||
block.addVariable(await_token);
|
||||
block.addVariable(promise);
|
||||
block.addVariable(resolved);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${replace_await_block}(${token}, type, ${value}, ${params}) {
|
||||
if (${token} !== ${await_token}) return;
|
||||
|
||||
var ${old_block} = ${await_block};
|
||||
${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component);
|
||||
|
||||
if (${old_block}) {
|
||||
${old_block}.u();
|
||||
${old_block}.d();
|
||||
${await_block}.c();
|
||||
${await_block}.m(${state.parentNode || `${anchor}.parentNode`}, ${anchor});
|
||||
}
|
||||
}
|
||||
|
||||
function ${handle_promise}(${promise}, ${params}) {
|
||||
var ${token} = ${await_token} = {};
|
||||
|
||||
if (@isPromise(${promise})) {
|
||||
${promise}.then(function(${value}) {
|
||||
${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params});
|
||||
}, function (${error}) {
|
||||
${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params});
|
||||
});
|
||||
|
||||
// if we previously had a then/catch block, destroy it
|
||||
if (${await_block_type} !== ${create_pending_block}) {
|
||||
${replace_await_block}(${token}, ${create_pending_block}, null, ${params});
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
${resolved} = ${promise};
|
||||
if (${await_block_type} !== ${create_then_block}) {
|
||||
${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${handle_promise}(${promise} = ${snippet}, ${params});
|
||||
`);
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
${await_block}.c();
|
||||
`);
|
||||
|
||||
block.builders.claim.addBlock(deindent`
|
||||
${await_block}.l(${state.parentNodes});
|
||||
`);
|
||||
|
||||
const targetNode = state.parentNode || '#target';
|
||||
const anchorNode = state.parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
${await_block}.m(${targetNode}, ${anchorNode});
|
||||
`);
|
||||
|
||||
const conditions = [];
|
||||
if (node.metadata.dependencies) {
|
||||
conditions.push(
|
||||
`(${node.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
|
||||
);
|
||||
}
|
||||
|
||||
conditions.push(
|
||||
`${promise} !== (${promise} = ${snippet})`,
|
||||
`${handle_promise}(${promise}, ${params})`
|
||||
);
|
||||
|
||||
if (node.pending._block.hasUpdateMethod) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${conditions.join(' && ')}) {
|
||||
// nothing
|
||||
} else {
|
||||
${await_block}.p(changed, ${params}, ${resolved});
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${conditions.join(' && ')}) {
|
||||
${await_block}.c();
|
||||
${await_block}.m(${anchor}.parentNode, ${anchor});
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
${await_block}.u();
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
${await_token} = null;
|
||||
${await_block}.d();
|
||||
`);
|
||||
|
||||
[node.pending, node.then, node.catch].forEach(status => {
|
||||
status.children.forEach(child => {
|
||||
visit(generator, status._block, status._state, child, elementStack, componentStack);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,491 +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';
|
||||
|
||||
export default function visitEachBlock(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[]
|
||||
) {
|
||||
const each = node.var;
|
||||
|
||||
const create_each_block = node._block.name;
|
||||
const each_block_value = node._block.listName;
|
||||
const iterations = node.iterations;
|
||||
const params = block.params.join(', ');
|
||||
|
||||
const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator);
|
||||
const anchor = needsAnchor
|
||||
? block.getUniqueName(`${each}_anchor`)
|
||||
: (node.next && node.next.var) || 'null';
|
||||
|
||||
// hack the sourcemap, so that if data is missing the bug
|
||||
// is easy to find
|
||||
let c = node.start + 3;
|
||||
while (generator.source[c] !== 'e') c += 1;
|
||||
generator.code.overwrite(c, c + 4, 'length');
|
||||
const length = `[✂${c}-${c+4}✂]`;
|
||||
|
||||
const mountOrIntro = node._block.hasIntroMethod ? 'i' : 'm';
|
||||
const vars = {
|
||||
each,
|
||||
create_each_block,
|
||||
each_block_value,
|
||||
length,
|
||||
iterations,
|
||||
params,
|
||||
anchor,
|
||||
mountOrIntro,
|
||||
};
|
||||
|
||||
block.contextualise(node.expression);
|
||||
const { snippet } = node.metadata;
|
||||
|
||||
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
|
||||
|
||||
if (node.key) {
|
||||
keyed(generator, block, state, node, snippet, vars);
|
||||
} else {
|
||||
unkeyed(generator, block, state, node, snippet, vars);
|
||||
}
|
||||
|
||||
const isToplevel = !state.parentNode;
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
anchor,
|
||||
`@createComment()`,
|
||||
`@createComment()`,
|
||||
state.parentNode
|
||||
);
|
||||
}
|
||||
|
||||
if (node.else) {
|
||||
const each_block_else = generator.getUniqueName(`${each}_else`);
|
||||
|
||||
block.builders.init.addLine(`var ${each_block_else} = null;`);
|
||||
|
||||
// TODO neaten this up... will end up with an empty line in the block
|
||||
block.builders.init.addBlock(deindent`
|
||||
if (!${each_block_value}.${length}) {
|
||||
${each_block_else} = ${node.else._block.name}(${params}, #component);
|
||||
${each_block_else}.c();
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
if (${each_block_else}) {
|
||||
${each_block_else}.${mountOrIntro}(${state.parentNode || '#target'}, null);
|
||||
}
|
||||
`);
|
||||
|
||||
const parentNode = state.parentNode || `${anchor}.parentNode`;
|
||||
|
||||
if (node.else._block.hasUpdateMethod) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (!${each_block_value}.${length} && ${each_block_else}) {
|
||||
${each_block_else}.p( changed, ${params} );
|
||||
} else if (!${each_block_value}.${length}) {
|
||||
${each_block_else} = ${node.else._block.name}(${params}, #component);
|
||||
${each_block_else}.c();
|
||||
${each_block_else}.${mountOrIntro}(${parentNode}, ${anchor});
|
||||
} else if (${each_block_else}) {
|
||||
${each_block_else}.u();
|
||||
${each_block_else}.d();
|
||||
${each_block_else} = null;
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${each_block_value}.${length}) {
|
||||
if (${each_block_else}) {
|
||||
${each_block_else}.u();
|
||||
${each_block_else}.d();
|
||||
${each_block_else} = null;
|
||||
}
|
||||
} else if (!${each_block_else}) {
|
||||
${each_block_else} = ${node.else._block.name}(${params}, #component);
|
||||
${each_block_else}.c();
|
||||
${each_block_else}.${mountOrIntro}(${parentNode}, ${anchor});
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.unmount.addLine(
|
||||
`if (${each_block_else}) ${each_block_else}.u()`
|
||||
);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
if (${each_block_else}) ${each_block_else}.d();
|
||||
`);
|
||||
}
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, node._block, node._state, child, elementStack, componentStack);
|
||||
});
|
||||
|
||||
if (node.else) {
|
||||
node.else.children.forEach((child: Node) => {
|
||||
visit(generator, node.else._block, node.else._state, child, elementStack, componentStack);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function keyed(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
snippet: string,
|
||||
{
|
||||
each,
|
||||
create_each_block,
|
||||
each_block_value,
|
||||
length,
|
||||
params,
|
||||
anchor,
|
||||
mountOrIntro,
|
||||
}
|
||||
) {
|
||||
const key = block.getUniqueName('key');
|
||||
const lookup = block.getUniqueName(`${each}_lookup`);
|
||||
const iteration = block.getUniqueName(`${each}_iteration`);
|
||||
const head = block.getUniqueName(`${each}_head`);
|
||||
const last = block.getUniqueName(`${each}_last`);
|
||||
const expected = block.getUniqueName(`${each}_expected`);
|
||||
|
||||
block.addVariable(lookup, `@blankObject()`);
|
||||
block.addVariable(head);
|
||||
block.addVariable(last);
|
||||
|
||||
if (node.children[0] && node.children[0].type === 'Element' && !generator.components.has(node.children[0].name)) {
|
||||
// TODO or text/tag/raw
|
||||
node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing
|
||||
} else {
|
||||
node._block.first = node._block.getUniqueName('first');
|
||||
node._block.addElement(
|
||||
node._block.first,
|
||||
`@createComment()`,
|
||||
`@createComment()`,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
|
||||
var ${key} = ${each_block_value}[#i].${node.key};
|
||||
var ${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
|
||||
|
||||
if (${last}) ${last}.next = ${iteration};
|
||||
${iteration}.last = ${last};
|
||||
${last} = ${iteration};
|
||||
|
||||
if (#i === 0) ${head} = ${iteration};
|
||||
}
|
||||
`);
|
||||
|
||||
const targetNode = state.parentNode || '#target';
|
||||
const anchorNode = state.parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.c();
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.claim.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.l(${state.parentNodes});
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.${mountOrIntro}(${targetNode}, ${anchorNode});
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
|
||||
const dynamic = node._block.hasUpdateMethod;
|
||||
const parentNode = isDomNode(node.parent, generator) ? node.parent.var : `${anchor}.parentNode`;
|
||||
|
||||
let destroy;
|
||||
if (node._block.hasOutroMethod) {
|
||||
const fn = block.getUniqueName(`${each}_outro`);
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${fn}(iteration) {
|
||||
iteration.o(function() {
|
||||
iteration.u();
|
||||
iteration.d();
|
||||
${lookup}[iteration.key] = null;
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
destroy = deindent`
|
||||
while (${expected}) {
|
||||
${fn}(${expected});
|
||||
${expected} = ${expected}.next;
|
||||
}
|
||||
|
||||
for (#i = 0; #i < discard_pile.length; #i += 1) {
|
||||
if (discard_pile[#i].discard) {
|
||||
${fn}(discard_pile[#i]);
|
||||
}
|
||||
}
|
||||
`;
|
||||
} else {
|
||||
const fn = block.getUniqueName(`${each}_destroy`);
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${fn}(iteration) {
|
||||
iteration.u();
|
||||
iteration.d();
|
||||
${lookup}[iteration.key] = null;
|
||||
}
|
||||
`);
|
||||
|
||||
destroy = deindent`
|
||||
while (${expected}) {
|
||||
${fn}(${expected});
|
||||
${expected} = ${expected}.next;
|
||||
}
|
||||
|
||||
for (#i = 0; #i < discard_pile.length; #i += 1) {
|
||||
var ${iteration} = discard_pile[#i];
|
||||
if (${iteration}.discard) {
|
||||
${fn}(${iteration});
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
var ${each_block_value} = ${snippet};
|
||||
|
||||
var ${expected} = ${head};
|
||||
var ${last} = null;
|
||||
|
||||
var discard_pile = [];
|
||||
|
||||
for (#i = 0; #i < ${each_block_value}.${length}; #i += 1) {
|
||||
var ${key} = ${each_block_value}[#i].${node.key};
|
||||
var ${iteration} = ${lookup}[${key}];
|
||||
|
||||
${dynamic &&
|
||||
`if (${iteration}) ${iteration}.p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);`}
|
||||
|
||||
if (${expected}) {
|
||||
if (${key} === ${expected}.key) {
|
||||
${expected} = ${expected}.next;
|
||||
} else {
|
||||
if (${iteration}) {
|
||||
// probably a deletion
|
||||
while (${expected} && ${expected}.key !== ${key}) {
|
||||
${expected}.discard = true;
|
||||
discard_pile.push(${expected});
|
||||
${expected} = ${expected}.next;
|
||||
};
|
||||
|
||||
${expected} = ${expected} && ${expected}.next;
|
||||
${iteration}.discard = false;
|
||||
${iteration}.last = ${last};
|
||||
|
||||
if (!${expected}) ${iteration}.m(${parentNode}, ${anchor});
|
||||
} else {
|
||||
// key is being inserted
|
||||
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
|
||||
${iteration}.c();
|
||||
${iteration}.${mountOrIntro}(${parentNode}, ${expected}.first);
|
||||
|
||||
${expected}.last = ${iteration};
|
||||
${iteration}.next = ${expected};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we're appending from this point forward
|
||||
if (${iteration}) {
|
||||
${iteration}.discard = false;
|
||||
${iteration}.next = null;
|
||||
${iteration}.m(${parentNode}, ${anchor});
|
||||
} else {
|
||||
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
|
||||
${iteration}.c();
|
||||
${iteration}.${mountOrIntro}(${parentNode}, ${anchor});
|
||||
}
|
||||
}
|
||||
|
||||
if (${last}) ${last}.next = ${iteration};
|
||||
${iteration}.last = ${last};
|
||||
${node._block.hasIntroMethod && `${iteration}.i(${parentNode}, ${anchor});`}
|
||||
${last} = ${iteration};
|
||||
}
|
||||
|
||||
if (${last}) ${last}.next = null;
|
||||
|
||||
${destroy}
|
||||
|
||||
${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${node.key}];
|
||||
`);
|
||||
|
||||
if (!state.parentNode) {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.u();
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.d();
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
function unkeyed(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
snippet: string,
|
||||
{
|
||||
create_each_block,
|
||||
each_block_value,
|
||||
length,
|
||||
iterations,
|
||||
params,
|
||||
anchor,
|
||||
mountOrIntro,
|
||||
}
|
||||
) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${iterations} = [];
|
||||
|
||||
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
|
||||
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
|
||||
}
|
||||
`);
|
||||
|
||||
const targetNode = state.parentNode || '#target';
|
||||
const anchorNode = state.parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].c();
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.claim.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].l(${state.parentNodes});
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].${mountOrIntro}(${targetNode}, ${anchorNode});
|
||||
}
|
||||
`);
|
||||
|
||||
const allDependencies = new Set(node._block.dependencies);
|
||||
const { dependencies } = node.metadata;
|
||||
dependencies.forEach((dependency: string) => {
|
||||
allDependencies.add(dependency);
|
||||
});
|
||||
|
||||
// TODO do this for keyed blocks as well
|
||||
const condition = Array.from(allDependencies)
|
||||
.map(dependency => `changed.${dependency}`)
|
||||
.join(' || ');
|
||||
|
||||
const parentNode = isDomNode(node.parent, generator) ? node.parent.var : `${anchor}.parentNode`;
|
||||
|
||||
if (condition !== '') {
|
||||
const forLoopBody = node._block.hasUpdateMethod
|
||||
? node._block.hasIntroMethod
|
||||
? deindent`
|
||||
if (${iterations}[#i]) {
|
||||
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
|
||||
} else {
|
||||
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
|
||||
${iterations}[#i].c();
|
||||
}
|
||||
${iterations}[#i].i(${parentNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (${iterations}[#i]) {
|
||||
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
|
||||
} else {
|
||||
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
|
||||
${iterations}[#i].c();
|
||||
${iterations}[#i].m(${parentNode}, ${anchor});
|
||||
}
|
||||
`
|
||||
: deindent`
|
||||
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
|
||||
${iterations}[#i].c();
|
||||
${iterations}[#i].${mountOrIntro}(${parentNode}, ${anchor});
|
||||
`;
|
||||
|
||||
const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`;
|
||||
|
||||
const outro = block.getUniqueName('outro');
|
||||
const destroy = node._block.hasOutroMethod
|
||||
? deindent`
|
||||
function ${outro}(i) {
|
||||
if (${iterations}[i]) {
|
||||
${iterations}[i].o(function() {
|
||||
${iterations}[i].u();
|
||||
${iterations}[i].d();
|
||||
${iterations}[i] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
|
||||
`
|
||||
: deindent`
|
||||
for (; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].u();
|
||||
${iterations}[#i].d();
|
||||
}
|
||||
${iterations}.length = ${each_block_value}.${length};
|
||||
`;
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
var ${each_block_value} = ${snippet};
|
||||
|
||||
if (${condition}) {
|
||||
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
|
||||
${forLoopBody}
|
||||
}
|
||||
|
||||
${destroy}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].u();
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(`@destroyEach(${iterations});`);
|
||||
}
|
@ -1,365 +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);
|
||||
}
|
||||
}
|
||||
|
||||
if (generator.components.has(node.name) || node.name === ':Self' || node.name === ':Component') {
|
||||
return visitComponent(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,189 +0,0 @@
|
||||
import attributeLookup from './lookup';
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import { stringify } from '../../../../utils/stringify';
|
||||
import getExpressionPrecedence from '../../../../utils/getExpressionPrecedence';
|
||||
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
|
||||
import { DomGenerator } from '../../index';
|
||||
import Block from '../../Block';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import { State } from '../../interfaces';
|
||||
|
||||
export interface StyleProp {
|
||||
key: string;
|
||||
value: Node[];
|
||||
}
|
||||
|
||||
export default function visitStyleAttribute(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
attribute: Node,
|
||||
styleProps: StyleProp[]
|
||||
) {
|
||||
styleProps.forEach((prop: StyleProp) => {
|
||||
let value;
|
||||
|
||||
if (isDynamic(prop.value)) {
|
||||
const allDependencies = new Set();
|
||||
let shouldCache;
|
||||
let hasChangeableIndex;
|
||||
|
||||
value =
|
||||
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
|
||||
prop.value
|
||||
.map((chunk: Node) => {
|
||||
if (chunk.type === 'Text') {
|
||||
return stringify(chunk.data);
|
||||
} else {
|
||||
const { indexes } = block.contextualise(chunk.expression);
|
||||
const { dependencies, snippet } = chunk.metadata;
|
||||
|
||||
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
|
||||
hasChangeableIndex = true;
|
||||
}
|
||||
|
||||
dependencies.forEach(d => {
|
||||
allDependencies.add(d);
|
||||
});
|
||||
|
||||
return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet;
|
||||
}
|
||||
})
|
||||
.join(' + ');
|
||||
|
||||
if (allDependencies.size || hasChangeableIndex) {
|
||||
const dependencies = Array.from(allDependencies);
|
||||
const condition = (
|
||||
( block.hasOutroMethod ? `#outroing || ` : '' ) +
|
||||
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
|
||||
);
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
`@setStyle(${node.var}, "${prop.key}", ${value});`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
value = stringify(prop.value[0].data);
|
||||
}
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`@setStyle(${node.var}, "${prop.key}", ${value});`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function optimizeStyle(value: Node[]) {
|
||||
let expectingKey = true;
|
||||
let i = 0;
|
||||
|
||||
const props: { key: string, value: Node[] }[] = [];
|
||||
let chunks = value.slice();
|
||||
|
||||
while (chunks.length) {
|
||||
const chunk = chunks[0];
|
||||
|
||||
if (chunk.type !== 'Text') return null;
|
||||
|
||||
const keyMatch = /^\s*([\w-]+):\s*/.exec(chunk.data);
|
||||
if (!keyMatch) return null;
|
||||
|
||||
const key = keyMatch[1];
|
||||
|
||||
const offset = keyMatch.index + keyMatch[0].length;
|
||||
const remainingData = chunk.data.slice(offset);
|
||||
|
||||
if (remainingData) {
|
||||
chunks[0] = {
|
||||
start: chunk.start + offset,
|
||||
end: chunk.end,
|
||||
type: 'Text',
|
||||
data: remainingData
|
||||
};
|
||||
} else {
|
||||
chunks.shift();
|
||||
}
|
||||
|
||||
const result = getStyleValue(chunks);
|
||||
if (!result) return null;
|
||||
|
||||
props.push({ key, value: result.value });
|
||||
chunks = result.chunks;
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function getStyleValue(chunks: Node[]) {
|
||||
const value: Node[] = [];
|
||||
|
||||
let inUrl = false;
|
||||
let quoteMark = null;
|
||||
let escaped = false;
|
||||
|
||||
while (chunks.length) {
|
||||
const chunk = chunks.shift();
|
||||
|
||||
if (chunk.type === 'Text') {
|
||||
let c = 0;
|
||||
while (c < chunk.data.length) {
|
||||
const char = chunk.data[c];
|
||||
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === quoteMark) {
|
||||
quoteMark === null;
|
||||
} else if (char === '"' || char === "'") {
|
||||
quoteMark = char;
|
||||
} else if (char === ')' && inUrl) {
|
||||
inUrl = false;
|
||||
} else if (char === 'u' && chunk.data.slice(c, c + 4) === 'url(') {
|
||||
inUrl = true;
|
||||
} else if (char === ';' && !inUrl && !quoteMark) {
|
||||
break;
|
||||
}
|
||||
|
||||
c += 1;
|
||||
}
|
||||
|
||||
if (c > 0) {
|
||||
value.push({
|
||||
type: 'Text',
|
||||
start: chunk.start,
|
||||
end: chunk.start + c,
|
||||
data: chunk.data.slice(0, c)
|
||||
});
|
||||
}
|
||||
|
||||
while (/[;\s]/.test(chunk.data[c])) c += 1;
|
||||
const remainingData = chunk.data.slice(c);
|
||||
|
||||
if (remainingData) {
|
||||
chunks.unshift({
|
||||
start: chunk.start + c,
|
||||
end: chunk.end,
|
||||
type: 'Text',
|
||||
data: remainingData
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
value.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chunks,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
function isDynamic(value: Node[]) {
|
||||
return value.length > 1 || value[0].type !== 'Text';
|
||||
}
|
@ -1,428 +0,0 @@
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import flattenReference from '../../../../utils/flattenReference';
|
||||
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
|
||||
import { DomGenerator } from '../../index';
|
||||
import Block from '../../Block';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import { State } from '../../interfaces';
|
||||
import getObject from '../../../../utils/getObject';
|
||||
import getTailSnippet from '../../../../utils/getTailSnippet';
|
||||
import stringifyProps from '../../../../utils/stringifyProps';
|
||||
import { generateRule } from '../../../../shared/index';
|
||||
import flatten from '../../../../utils/flattenReference';
|
||||
|
||||
interface Binding {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const readOnlyMediaAttributes = new Set([
|
||||
'duration',
|
||||
'buffered',
|
||||
'seekable',
|
||||
'played'
|
||||
]);
|
||||
|
||||
function isMediaNode(name: string) {
|
||||
return name === 'audio' || name === 'video';
|
||||
}
|
||||
|
||||
const events = [
|
||||
{
|
||||
eventNames: ['input'],
|
||||
filter: (node: Node, binding: Binding) =>
|
||||
node.name === 'textarea' ||
|
||||
node.name === 'input' && !/radio|checkbox/.test(getStaticAttributeValue(node, 'type'))
|
||||
},
|
||||
{
|
||||
eventNames: ['change'],
|
||||
filter: (node: Node, binding: Binding) =>
|
||||
node.name === 'select' ||
|
||||
node.name === 'input' && /radio|checkbox|range/.test(getStaticAttributeValue(node, 'type'))
|
||||
},
|
||||
|
||||
// media events
|
||||
{
|
||||
eventNames: ['timeupdate'],
|
||||
filter: (node: Node, binding: Binding) =>
|
||||
isMediaNode(node.name) &&
|
||||
(binding.name === 'currentTime' || binding.name === 'played')
|
||||
},
|
||||
{
|
||||
eventNames: ['durationchange'],
|
||||
filter: (node: Node, binding: Binding) =>
|
||||
isMediaNode(node.name) &&
|
||||
binding.name === 'duration'
|
||||
},
|
||||
{
|
||||
eventNames: ['play', 'pause'],
|
||||
filter: (node: Node, binding: Binding) =>
|
||||
isMediaNode(node.name) &&
|
||||
binding.name === 'paused'
|
||||
},
|
||||
{
|
||||
eventNames: ['progress'],
|
||||
filter: (node: Node, binding: Binding) =>
|
||||
isMediaNode(node.name) &&
|
||||
binding.name === 'buffered'
|
||||
},
|
||||
{
|
||||
eventNames: ['loadedmetadata'],
|
||||
filter: (node: Node, binding: Binding) =>
|
||||
isMediaNode(node.name) &&
|
||||
(binding.name === 'buffered' || binding.name === 'seekable')
|
||||
}
|
||||
];
|
||||
|
||||
export default function addBindings(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node
|
||||
) {
|
||||
const bindings: Node[] = node.attributes.filter((a: Node) => a.type === 'Binding');
|
||||
if (bindings.length === 0) return;
|
||||
|
||||
if (node.name === 'select' || isMediaNode(node.name)) generator.hasComplexBindings = true;
|
||||
|
||||
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(getStaticAttributeValue(node, 'type'));
|
||||
|
||||
const mungedBindings = bindings.map(binding => {
|
||||
const isReadOnly = isMediaNode(node.name) && readOnlyMediaAttributes.has(binding.name);
|
||||
|
||||
let updateCondition: string;
|
||||
|
||||
const { name } = getObject(binding.value);
|
||||
const { contexts } = block.contextualise(binding.value);
|
||||
const { snippet } = binding.metadata;
|
||||
|
||||
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
|
||||
// and `selected` is an object chosen with a <select>, then when `checked` changes,
|
||||
// we need to tell the component to update all the values `selected` might be
|
||||
// pointing to
|
||||
// TODO should this happen in preprocess?
|
||||
const dependencies = binding.metadata.dependencies.slice();
|
||||
binding.metadata.dependencies.forEach((prop: string) => {
|
||||
const indirectDependencies = generator.indirectDependencies.get(prop);
|
||||
if (indirectDependencies) {
|
||||
indirectDependencies.forEach(indirectDependency => {
|
||||
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
contexts.forEach(context => {
|
||||
if (!~state.allUsedContexts.indexOf(context))
|
||||
state.allUsedContexts.push(context);
|
||||
});
|
||||
|
||||
// view to model
|
||||
const valueFromDom = getValueFromDom(generator, node, binding);
|
||||
const handler = getEventHandler(generator, block, name, snippet, binding, dependencies, valueFromDom);
|
||||
|
||||
// model to view
|
||||
let updateDom = getDomUpdater(node, binding, snippet);
|
||||
let initialUpdate = updateDom;
|
||||
|
||||
// special cases
|
||||
if (binding.name === 'group') {
|
||||
const bindingGroup = getBindingGroup(generator, binding.value);
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
|
||||
);
|
||||
}
|
||||
|
||||
if (binding.name === 'currentTime') {
|
||||
updateCondition = `!isNaN(${snippet})`;
|
||||
initialUpdate = null;
|
||||
}
|
||||
|
||||
if (binding.name === 'paused') {
|
||||
// this is necessary to prevent audio restarting by itself
|
||||
const last = block.getUniqueName(`${node.var}_is_paused`);
|
||||
block.addVariable(last, 'true');
|
||||
|
||||
updateCondition = `${last} !== (${last} = ${snippet})`;
|
||||
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
|
||||
initialUpdate = null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: binding.name,
|
||||
object: name,
|
||||
handler,
|
||||
updateDom,
|
||||
initialUpdate,
|
||||
needsLock: !isReadOnly && needsLock,
|
||||
updateCondition
|
||||
};
|
||||
});
|
||||
|
||||
const lock = mungedBindings.some(binding => binding.needsLock) ?
|
||||
block.getUniqueName(`${node.var}_updating`) :
|
||||
null;
|
||||
|
||||
if (lock) block.addVariable(lock, 'false');
|
||||
|
||||
const groups = events
|
||||
.map(event => {
|
||||
return {
|
||||
events: event.eventNames,
|
||||
bindings: mungedBindings.filter(binding => event.filter(node, binding))
|
||||
};
|
||||
})
|
||||
.filter(group => group.bindings.length);
|
||||
|
||||
groups.forEach(group => {
|
||||
const handler = block.getUniqueName(`${node.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(`${node.var}_animationframe`);
|
||||
block.addVariable(animation_frame);
|
||||
}
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handler}() {
|
||||
${
|
||||
animation_frame && deindent`
|
||||
cancelAnimationFrame(${animation_frame});
|
||||
if (!${node.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
|
||||
}
|
||||
${usesContext && `var context = ${node.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(${node.var}, "${name}", ${handler});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${node.var}, "${name}", ${handler});`
|
||||
);
|
||||
});
|
||||
|
||||
const allInitialStateIsDefined = group.bindings
|
||||
.map(binding => `'${binding.object}' in state`)
|
||||
.join(' && ');
|
||||
|
||||
if (node.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || readOnlyMediaAttributes.has(binding.name))) {
|
||||
generator.hasComplexBindings = true;
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`if (!(${allInitialStateIsDefined})) #component._root._beforecreate.push(${handler});`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
node.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
function getDomUpdater(
|
||||
node: Node,
|
||||
binding: Node,
|
||||
snippet: string
|
||||
) {
|
||||
if (readOnlyMediaAttributes.has(binding.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.name === 'select') {
|
||||
return getStaticAttributeValue(node, 'multiple') === true ?
|
||||
`@selectOptions(${node.var}, ${snippet})` :
|
||||
`@selectOption(${node.var}, ${snippet})`;
|
||||
}
|
||||
|
||||
if (binding.name === 'group') {
|
||||
const type = getStaticAttributeValue(node, 'type');
|
||||
|
||||
const condition = type === 'checkbox'
|
||||
? `~${snippet}.indexOf(${node.var}.__value)`
|
||||
: `${node.var}.__value === ${snippet}`;
|
||||
|
||||
return `${node.var}.checked = ${condition};`
|
||||
}
|
||||
|
||||
return `${node.var}.${binding.name} = ${snippet};`;
|
||||
}
|
||||
|
||||
function getBindingGroup(generator: DomGenerator, value: Node) {
|
||||
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
|
||||
const keypath = parts.join('.');
|
||||
|
||||
// TODO handle contextual bindings — `keypath` should include unique ID of
|
||||
// each block that provides context
|
||||
let index = generator.bindingGroups.indexOf(keypath);
|
||||
if (index === -1) {
|
||||
index = generator.bindingGroups.length;
|
||||
generator.bindingGroups.push(keypath);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function getEventHandler(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
name: string,
|
||||
snippet: string,
|
||||
attribute: Node,
|
||||
dependencies: string[],
|
||||
value: string,
|
||||
) {
|
||||
let storeDependencies = [];
|
||||
|
||||
if (generator.options.store) {
|
||||
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
|
||||
dependencies = dependencies.filter(prop => prop[0] !== '$');
|
||||
}
|
||||
|
||||
if (block.contexts.has(name)) {
|
||||
const tail = attribute.value.type === 'MemberExpression'
|
||||
? getTailSnippet(attribute.value)
|
||||
: '';
|
||||
|
||||
const list = `context.${block.listNames.get(name)}`;
|
||||
const index = `context.${block.indexNames.get(name)}`;
|
||||
|
||||
return {
|
||||
usesContext: true,
|
||||
usesState: true,
|
||||
usesStore: storeDependencies.length > 0,
|
||||
mutation: `${list}[${index}]${tail} = ${value};`,
|
||||
props: dependencies.map(prop => `${prop}: state.${prop}`),
|
||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||
};
|
||||
}
|
||||
|
||||
if (attribute.value.type === 'MemberExpression') {
|
||||
// This is a little confusing, and should probably be tidied up
|
||||
// at some point. It addresses a tricky bug (#893), wherein
|
||||
// Svelte tries to `set()` a computed property, which throws an
|
||||
// error in dev mode. a) it's possible that we should be
|
||||
// replacing computations with *their* dependencies, and b)
|
||||
// we should probably populate `generator.readonly` sooner so
|
||||
// that we don't have to do the `.some()` here
|
||||
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
|
||||
|
||||
return {
|
||||
usesContext: false,
|
||||
usesState: true,
|
||||
usesStore: storeDependencies.length > 0,
|
||||
mutation: `${snippet} = ${value}`,
|
||||
props: dependencies.map((prop: string) => `${prop}: state.${prop}`),
|
||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||
};
|
||||
}
|
||||
|
||||
let props;
|
||||
let storeProps;
|
||||
|
||||
if (generator.options.store && name[0] === '$') {
|
||||
props = [];
|
||||
storeProps = [`${name.slice(1)}: ${value}`];
|
||||
} else {
|
||||
props = [`${name}: ${value}`];
|
||||
storeProps = [];
|
||||
}
|
||||
|
||||
return {
|
||||
usesContext: false,
|
||||
usesState: false,
|
||||
usesStore: false,
|
||||
mutation: null,
|
||||
props,
|
||||
storeProps
|
||||
};
|
||||
}
|
||||
|
||||
function getValueFromDom(
|
||||
generator: DomGenerator,
|
||||
node: Node,
|
||||
binding: Node
|
||||
) {
|
||||
// <select bind:value='selected>
|
||||
if (node.name === 'select') {
|
||||
return getStaticAttributeValue(node, 'multiple') === true ?
|
||||
`@selectMultipleValue(${node.var})` :
|
||||
`@selectValue(${node.var})`;
|
||||
}
|
||||
|
||||
const type = getStaticAttributeValue(node, 'type');
|
||||
|
||||
// <input type='checkbox' bind:group='foo'>
|
||||
if (binding.name === 'group') {
|
||||
const bindingGroup = getBindingGroup(generator, binding.value);
|
||||
if (type === 'checkbox') {
|
||||
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
|
||||
}
|
||||
|
||||
return `${node.var}.__value`;
|
||||
}
|
||||
|
||||
// <input type='range|number' bind:value>
|
||||
if (type === 'range' || type === 'number') {
|
||||
return `@toNumber(${node.var}.${binding.name})`;
|
||||
}
|
||||
|
||||
if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) {
|
||||
return `@timeRangesToArray(${node.var}.${binding.name})`
|
||||
}
|
||||
|
||||
// everything else
|
||||
return `${node.var}.${binding.name}`;
|
||||
}
|
||||
|
||||
function isComputed(node: Node) {
|
||||
while (node.type === 'MemberExpression') {
|
||||
if (node.computed) return true;
|
||||
node = node.object;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import { DomGenerator } from '../../index';
|
||||
import Block from '../../Block';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import { State } from '../../interfaces';
|
||||
|
||||
export default function addTransitions(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node
|
||||
) {
|
||||
const intro = node.attributes.find((a: Node) => a.type === 'Transition' && a.intro);
|
||||
const outro = node.attributes.find((a: Node) => a.type === 'Transition' && a.outro);
|
||||
|
||||
if (!intro && !outro) return;
|
||||
|
||||
if (intro === outro) {
|
||||
block.contextualise(intro.expression); // TODO remove all these
|
||||
|
||||
const name = block.getUniqueName(`${node.var}_transition`);
|
||||
const snippet = intro.expression
|
||||
? intro.metadata.snippet
|
||||
: '{}';
|
||||
|
||||
block.addVariable(name);
|
||||
|
||||
const fn = `%transitions-${intro.name}`;
|
||||
|
||||
block.builders.intro.addBlock(deindent`
|
||||
#component._root._aftercreate.push(function() {
|
||||
if (!${name}) ${name} = @wrapTransition(#component, ${node.var}, ${fn}, ${snippet}, true, null);
|
||||
${name}.run(true, function() {
|
||||
#component.fire("intro.end", { node: ${node.var} });
|
||||
});
|
||||
});
|
||||
`);
|
||||
|
||||
block.builders.outro.addBlock(deindent`
|
||||
${name}.run(false, function() {
|
||||
#component.fire("outro.end", { node: ${node.var} });
|
||||
if (--#outros === 0) #outrocallback();
|
||||
${name} = null;
|
||||
});
|
||||
`);
|
||||
} else {
|
||||
const introName = intro && block.getUniqueName(`${node.var}_intro`);
|
||||
const outroName = outro && block.getUniqueName(`${node.var}_outro`);
|
||||
|
||||
if (intro) {
|
||||
block.contextualise(intro.expression);
|
||||
|
||||
block.addVariable(introName);
|
||||
const snippet = intro.expression
|
||||
? intro.metadata.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, ${node.var}, ${fn}, ${snippet}, true, null);
|
||||
${introName}.run(true, function() {
|
||||
#component.fire("intro.end", { node: ${node.var} });
|
||||
});
|
||||
});
|
||||
`);
|
||||
}
|
||||
|
||||
if (outro) {
|
||||
block.contextualise(outro.expression);
|
||||
|
||||
block.addVariable(outroName);
|
||||
const snippet = outro.expression
|
||||
? outro.metadata.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, ${node.var}, ${fn}, ${snippet}, false, null);
|
||||
${outroName}.run(false, function() {
|
||||
#component.fire("outro.end", { node: ${node.var} });
|
||||
if (--#outros === 0) #outrocallback();
|
||||
});
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,236 +0,0 @@
|
||||
// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
||||
const lookup = {
|
||||
accept: { appliesTo: ['form', 'input'] },
|
||||
'accept-charset': { propertyName: 'acceptCharset', appliesTo: ['form'] },
|
||||
accesskey: { propertyName: 'accessKey' },
|
||||
action: { appliesTo: ['form'] },
|
||||
align: {
|
||||
appliesTo: [
|
||||
'applet',
|
||||
'caption',
|
||||
'col',
|
||||
'colgroup',
|
||||
'hr',
|
||||
'iframe',
|
||||
'img',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'tr',
|
||||
],
|
||||
},
|
||||
allowfullscreen: { propertyName: 'allowFullscreen', appliesTo: ['iframe'] },
|
||||
alt: { appliesTo: ['applet', 'area', 'img', 'input'] },
|
||||
async: { appliesTo: ['script'] },
|
||||
autocomplete: { appliesTo: ['form', 'input'] },
|
||||
autofocus: { appliesTo: ['button', 'input', 'keygen', 'select', 'textarea'] },
|
||||
autoplay: { appliesTo: ['audio', 'video'] },
|
||||
autosave: { appliesTo: ['input'] },
|
||||
bgcolor: {
|
||||
propertyName: 'bgColor',
|
||||
appliesTo: [
|
||||
'body',
|
||||
'col',
|
||||
'colgroup',
|
||||
'marquee',
|
||||
'table',
|
||||
'tbody',
|
||||
'tfoot',
|
||||
'td',
|
||||
'th',
|
||||
'tr',
|
||||
],
|
||||
},
|
||||
border: { appliesTo: ['img', 'object', 'table'] },
|
||||
buffered: { appliesTo: ['audio', 'video'] },
|
||||
challenge: { appliesTo: ['keygen'] },
|
||||
charset: { appliesTo: ['meta', 'script'] },
|
||||
checked: { appliesTo: ['command', 'input'] },
|
||||
cite: { appliesTo: ['blockquote', 'del', 'ins', 'q'] },
|
||||
class: { propertyName: 'className' },
|
||||
code: { appliesTo: ['applet'] },
|
||||
codebase: { propertyName: 'codeBase', appliesTo: ['applet'] },
|
||||
color: { appliesTo: ['basefont', 'font', 'hr'] },
|
||||
cols: { appliesTo: ['textarea'] },
|
||||
colspan: { propertyName: 'colSpan', appliesTo: ['td', 'th'] },
|
||||
content: { appliesTo: ['meta'] },
|
||||
contenteditable: { propertyName: 'contentEditable' },
|
||||
contextmenu: {},
|
||||
controls: { appliesTo: ['audio', 'video'] },
|
||||
coords: { appliesTo: ['area'] },
|
||||
data: { appliesTo: ['object'] },
|
||||
datetime: { propertyName: 'dateTime', appliesTo: ['del', 'ins', 'time'] },
|
||||
default: { appliesTo: ['track'] },
|
||||
defer: { appliesTo: ['script'] },
|
||||
dir: {},
|
||||
dirname: { propertyName: 'dirName', appliesTo: ['input', 'textarea'] },
|
||||
disabled: {
|
||||
appliesTo: [
|
||||
'button',
|
||||
'command',
|
||||
'fieldset',
|
||||
'input',
|
||||
'keygen',
|
||||
'optgroup',
|
||||
'option',
|
||||
'select',
|
||||
'textarea',
|
||||
],
|
||||
},
|
||||
download: { appliesTo: ['a', 'area'] },
|
||||
draggable: {},
|
||||
dropzone: {},
|
||||
enctype: { appliesTo: ['form'] },
|
||||
for: { propertyName: 'htmlFor', appliesTo: ['label', 'output'] },
|
||||
form: {
|
||||
appliesTo: [
|
||||
'button',
|
||||
'fieldset',
|
||||
'input',
|
||||
'keygen',
|
||||
'label',
|
||||
'meter',
|
||||
'object',
|
||||
'output',
|
||||
'progress',
|
||||
'select',
|
||||
'textarea',
|
||||
],
|
||||
},
|
||||
formaction: { appliesTo: ['input', 'button'] },
|
||||
headers: { appliesTo: ['td', 'th'] },
|
||||
height: {
|
||||
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
||||
},
|
||||
hidden: {},
|
||||
high: { appliesTo: ['meter'] },
|
||||
href: { appliesTo: ['a', 'area', 'base', 'link'] },
|
||||
hreflang: { appliesTo: ['a', 'area', 'link'] },
|
||||
'http-equiv': { propertyName: 'httpEquiv', appliesTo: ['meta'] },
|
||||
icon: { appliesTo: ['command'] },
|
||||
id: {},
|
||||
indeterminate: { appliesTo: ['input'] },
|
||||
ismap: { propertyName: 'isMap', appliesTo: ['img'] },
|
||||
itemprop: {},
|
||||
keytype: { appliesTo: ['keygen'] },
|
||||
kind: { appliesTo: ['track'] },
|
||||
label: { appliesTo: ['track'] },
|
||||
lang: {},
|
||||
language: { appliesTo: ['script'] },
|
||||
loop: { appliesTo: ['audio', 'bgsound', 'marquee', 'video'] },
|
||||
low: { appliesTo: ['meter'] },
|
||||
manifest: { appliesTo: ['html'] },
|
||||
max: { appliesTo: ['input', 'meter', 'progress'] },
|
||||
maxlength: { propertyName: 'maxLength', appliesTo: ['input', 'textarea'] },
|
||||
media: { appliesTo: ['a', 'area', 'link', 'source', 'style'] },
|
||||
method: { appliesTo: ['form'] },
|
||||
min: { appliesTo: ['input', 'meter'] },
|
||||
multiple: { appliesTo: ['input', 'select'] },
|
||||
muted: { appliesTo: ['audio', 'video'] },
|
||||
name: {
|
||||
appliesTo: [
|
||||
'button',
|
||||
'form',
|
||||
'fieldset',
|
||||
'iframe',
|
||||
'input',
|
||||
'keygen',
|
||||
'object',
|
||||
'output',
|
||||
'select',
|
||||
'textarea',
|
||||
'map',
|
||||
'meta',
|
||||
'param',
|
||||
],
|
||||
},
|
||||
novalidate: { propertyName: 'noValidate', appliesTo: ['form'] },
|
||||
open: { appliesTo: ['details'] },
|
||||
optimum: { appliesTo: ['meter'] },
|
||||
pattern: { appliesTo: ['input'] },
|
||||
ping: { appliesTo: ['a', 'area'] },
|
||||
placeholder: { appliesTo: ['input', 'textarea'] },
|
||||
poster: { appliesTo: ['video'] },
|
||||
preload: { appliesTo: ['audio', 'video'] },
|
||||
radiogroup: { appliesTo: ['command'] },
|
||||
readonly: { propertyName: 'readOnly', appliesTo: ['input', 'textarea'] },
|
||||
rel: { appliesTo: ['a', 'area', 'link'] },
|
||||
required: { appliesTo: ['input', 'select', 'textarea'] },
|
||||
reversed: { appliesTo: ['ol'] },
|
||||
rows: { appliesTo: ['textarea'] },
|
||||
rowspan: { propertyName: 'rowSpan', appliesTo: ['td', 'th'] },
|
||||
sandbox: { appliesTo: ['iframe'] },
|
||||
scope: { appliesTo: ['th'] },
|
||||
scoped: { appliesTo: ['style'] },
|
||||
seamless: { appliesTo: ['iframe'] },
|
||||
selected: { appliesTo: ['option'] },
|
||||
shape: { appliesTo: ['a', 'area'] },
|
||||
size: { appliesTo: ['input', 'select'] },
|
||||
sizes: { appliesTo: ['link', 'img', 'source'] },
|
||||
span: { appliesTo: ['col', 'colgroup'] },
|
||||
spellcheck: {},
|
||||
src: {
|
||||
appliesTo: [
|
||||
'audio',
|
||||
'embed',
|
||||
'iframe',
|
||||
'img',
|
||||
'input',
|
||||
'script',
|
||||
'source',
|
||||
'track',
|
||||
'video',
|
||||
],
|
||||
},
|
||||
srcdoc: { appliesTo: ['iframe'] },
|
||||
srclang: { appliesTo: ['track'] },
|
||||
srcset: { appliesTo: ['img'] },
|
||||
start: { appliesTo: ['ol'] },
|
||||
step: { appliesTo: ['input'] },
|
||||
style: { propertyName: 'style.cssText' },
|
||||
summary: { appliesTo: ['table'] },
|
||||
tabindex: { propertyName: 'tabIndex' },
|
||||
target: { appliesTo: ['a', 'area', 'base', 'form'] },
|
||||
title: {},
|
||||
type: {
|
||||
appliesTo: [
|
||||
'button',
|
||||
'input',
|
||||
'command',
|
||||
'embed',
|
||||
'object',
|
||||
'script',
|
||||
'source',
|
||||
'style',
|
||||
'menu',
|
||||
],
|
||||
},
|
||||
usemap: { propertyName: 'useMap', appliesTo: ['img', 'input', 'object'] },
|
||||
value: {
|
||||
appliesTo: [
|
||||
'button',
|
||||
'option',
|
||||
'input',
|
||||
'li',
|
||||
'meter',
|
||||
'progress',
|
||||
'param',
|
||||
'select',
|
||||
'textarea',
|
||||
],
|
||||
},
|
||||
width: {
|
||||
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
||||
},
|
||||
wrap: { appliesTo: ['textarea'] },
|
||||
};
|
||||
|
||||
Object.keys(lookup).forEach(name => {
|
||||
const metadata = lookup[name];
|
||||
if (!metadata.propertyName) metadata.propertyName = name;
|
||||
});
|
||||
|
||||
export default lookup;
|
@ -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,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,104 +0,0 @@
|
||||
import { DomGenerator } from '../index';
|
||||
import deindent from '../../../utils/deindent';
|
||||
import visit from '../visit';
|
||||
import Block from '../Block';
|
||||
import getStaticAttributeValue from '../../../utils/getStaticAttributeValue';
|
||||
import { Node } from '../../../interfaces';
|
||||
import { State } from '../interfaces';
|
||||
|
||||
export default function visitSlot(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
elementStack: Node[],
|
||||
componentStack: Node[]
|
||||
) {
|
||||
const slotName = getStaticAttributeValue(node, 'name') || 'default';
|
||||
generator.slots.add(slotName);
|
||||
|
||||
const content_name = block.getUniqueName(`slot_content_${slotName}`);
|
||||
block.addVariable(content_name, `#component._slotted.${slotName}`);
|
||||
|
||||
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(`${content_name}_before`)
|
||||
: (node.prev && node.prev.var) || 'null';
|
||||
|
||||
const anchorAfter = needsAnchorAfter
|
||||
? block.getUniqueName(`${content_name}_after`)
|
||||
: (node.next && node.next.var) || 'null';
|
||||
|
||||
if (needsAnchorBefore) block.addVariable(anchorBefore);
|
||||
if (needsAnchorAfter) block.addVariable(anchorAfter);
|
||||
|
||||
block.builders.create.pushCondition(`!${content_name}`);
|
||||
block.builders.hydrate.pushCondition(`!${content_name}`);
|
||||
block.builders.mount.pushCondition(`!${content_name}`);
|
||||
block.builders.unmount.pushCondition(`!${content_name}`);
|
||||
block.builders.destroy.pushCondition(`!${content_name}`);
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, block, state, child, elementStack, componentStack);
|
||||
});
|
||||
|
||||
block.builders.create.popCondition();
|
||||
block.builders.hydrate.popCondition();
|
||||
block.builders.mount.popCondition();
|
||||
block.builders.unmount.popCondition();
|
||||
block.builders.destroy.popCondition();
|
||||
|
||||
// TODO can we use an else here?
|
||||
if (state.parentNode) {
|
||||
block.builders.mount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
${needsAnchorBefore && `@appendNode(${anchorBefore} || (${anchorBefore} = @createComment()), ${state.parentNode});`}
|
||||
@appendNode(${content_name}, ${state.parentNode});
|
||||
${needsAnchorAfter && `@appendNode(${anchorAfter} || (${anchorAfter} = @createComment()), ${state.parentNode});`}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.mount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
${needsAnchorBefore && `@insertNode(${anchorBefore} || (${anchorBefore} = @createComment()), #target, anchor);`}
|
||||
@insertNode(${content_name}, #target, anchor);
|
||||
${needsAnchorAfter && `@insertNode(${anchorAfter} || (${anchorAfter} = @createComment()), #target, anchor);`}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// if the slot is unmounted, move nodes back into the document fragment,
|
||||
// so that it can be reinserted later
|
||||
// TODO so that this can work with public API, component._slotted should
|
||||
// be all fragments, derived from options.slots. Not === options.slots
|
||||
// TODO can we use an else here?
|
||||
if (anchorBefore === 'null' && anchorAfter === 'null') {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
@reinsertChildren(${state.parentNode}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else if (anchorBefore === 'null') {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
@reinsertBefore(${anchorAfter}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else if (anchorAfter === 'null') {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
@reinsertAfter(${anchorBefore}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
@reinsertBetween(${anchorBefore}, ${anchorAfter}, ${content_name});
|
||||
@detachNode(${anchorBefore});
|
||||
@detachNode(${anchorAfter});
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { DomGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
import { State } from '../interfaces';
|
||||
import { stringify } from '../../../utils/stringify';
|
||||
|
||||
export default function visitText(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node
|
||||
) {
|
||||
if (node.shouldSkip) return;
|
||||
|
||||
block.addElement(
|
||||
node.var,
|
||||
`@createText(${stringify(node.data)})`,
|
||||
`@claimText(${state.parentNodes}, ${stringify(node.data)})`,
|
||||
state.parentNode
|
||||
);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import AwaitBlock from './AwaitBlock';
|
||||
import EachBlock from './EachBlock';
|
||||
import Element from './Element/Element';
|
||||
import IfBlock from './IfBlock';
|
||||
import MustacheTag from './MustacheTag';
|
||||
import RawMustacheTag from './RawMustacheTag';
|
||||
import Text from './Text';
|
||||
import { Visitor } from '../interfaces';
|
||||
|
||||
const visitors: Record<string, Visitor> = {
|
||||
AwaitBlock,
|
||||
EachBlock,
|
||||
Element,
|
||||
IfBlock,
|
||||
MustacheTag,
|
||||
RawMustacheTag,
|
||||
Text
|
||||
};
|
||||
|
||||
export default visitors;
|
@ -1,50 +0,0 @@
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import { DomGenerator } from '../../index';
|
||||
import Block from '../../Block';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import { State } from '../../interfaces';
|
||||
|
||||
export default function visitTag(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
name: string,
|
||||
update: (value: string) => string
|
||||
) {
|
||||
const { indexes } = block.contextualise(node.expression);
|
||||
const { dependencies, snippet } = node.metadata;
|
||||
|
||||
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
|
||||
|
||||
const shouldCache = (
|
||||
node.expression.type !== 'Identifier' ||
|
||||
block.contexts.has(node.expression.name) ||
|
||||
hasChangeableIndex
|
||||
);
|
||||
|
||||
const value = shouldCache && block.getUniqueName(`${name}_value`);
|
||||
const content = shouldCache ? value : snippet;
|
||||
|
||||
if (shouldCache) block.addVariable(value, snippet);
|
||||
|
||||
if (dependencies.length || hasChangeableIndex) {
|
||||
const changedCheck = (
|
||||
(block.hasOutroMethod ? `#outroing || ` : '') +
|
||||
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
|
||||
);
|
||||
|
||||
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
|
||||
|
||||
const condition = shouldCache ?
|
||||
(dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
|
||||
changedCheck;
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
update(content)
|
||||
);
|
||||
}
|
||||
|
||||
return { init: content };
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { DomGenerator } from '../../index';
|
||||
import { Node } from '../../../../interfaces';
|
||||
|
||||
export default function isDomNode(node: Node, generator: DomGenerator) {
|
||||
if (node.type === 'Element') return !generator.components.has(node.name);
|
||||
return node.type === 'Text' || node.type === 'MustacheTag';
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import Node from './shared/Node';
|
||||
import { DomGenerator } from '../dom/index';
|
||||
import Block from '../dom/Block';
|
||||
import PendingBlock from './PendingBlock';
|
||||
import ThenBlock from './ThenBlock';
|
||||
import CatchBlock from './CatchBlock';
|
||||
import createDebuggingComment from '../../utils/createDebuggingComment';
|
||||
|
||||
export default class AwaitBlock extends Node {
|
||||
value: string;
|
||||
error: string;
|
||||
expression: Node;
|
||||
|
||||
pending: PendingBlock;
|
||||
then: ThenBlock;
|
||||
catch: CatchBlock;
|
||||
|
||||
init(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
this.var = block.getUniqueName('await_block');
|
||||
block.addDependencies(this.metadata.dependencies);
|
||||
|
||||
let dynamic = false;
|
||||
|
||||
[
|
||||
['pending', null],
|
||||
['then', this.value],
|
||||
['catch', this.error]
|
||||
].forEach(([status, arg]) => {
|
||||
const child = this[status];
|
||||
|
||||
const context = block.getUniqueName(arg || '_');
|
||||
const contexts = new Map(block.contexts);
|
||||
contexts.set(arg, context);
|
||||
|
||||
child.block = block.child({
|
||||
comment: createDebuggingComment(child, this.generator),
|
||||
name: this.generator.getUniqueName(`create_${status}_block`),
|
||||
params: block.params.concat(context),
|
||||
context,
|
||||
contexts
|
||||
});
|
||||
|
||||
child.initChildren(child.block, stripWhitespace, nextSibling);
|
||||
this.generator.blocks.push(child.block);
|
||||
|
||||
if (child.block.dependencies.size > 0) {
|
||||
dynamic = true;
|
||||
block.addDependencies(child.block.dependencies);
|
||||
}
|
||||
});
|
||||
|
||||
this.pending.block.hasUpdateMethod = dynamic;
|
||||
this.then.block.hasUpdateMethod = dynamic;
|
||||
this.catch.block.hasUpdateMethod = dynamic;
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const name = this.var;
|
||||
|
||||
const anchor = this.getOrCreateAnchor(block, parentNode);
|
||||
|
||||
const params = block.params.join(', ');
|
||||
|
||||
block.contextualise(this.expression);
|
||||
const { snippet } = this.metadata;
|
||||
|
||||
const promise = block.getUniqueName(`promise`);
|
||||
const resolved = block.getUniqueName(`resolved`);
|
||||
const await_block = block.getUniqueName(`await_block`);
|
||||
const await_block_type = block.getUniqueName(`await_block_type`);
|
||||
const token = block.getUniqueName(`token`);
|
||||
const await_token = block.getUniqueName(`await_token`);
|
||||
const handle_promise = block.getUniqueName(`handle_promise`);
|
||||
const replace_await_block = block.getUniqueName(`replace_await_block`);
|
||||
const old_block = block.getUniqueName(`old_block`);
|
||||
const value = block.getUniqueName(`value`);
|
||||
const error = block.getUniqueName(`error`);
|
||||
const create_pending_block = this.pending.block.name;
|
||||
const create_then_block = this.then.block.name;
|
||||
const create_catch_block = this.catch.block.name;
|
||||
|
||||
block.addVariable(await_block);
|
||||
block.addVariable(await_block_type);
|
||||
block.addVariable(await_token);
|
||||
block.addVariable(promise);
|
||||
block.addVariable(resolved);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${replace_await_block}(${token}, type, ${value}, ${params}) {
|
||||
if (${token} !== ${await_token}) return;
|
||||
|
||||
var ${old_block} = ${await_block};
|
||||
${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component);
|
||||
|
||||
if (${old_block}) {
|
||||
${old_block}.u();
|
||||
${old_block}.d();
|
||||
${await_block}.c();
|
||||
${await_block}.m(${parentNode || `${anchor}.parentNode`}, ${anchor});
|
||||
}
|
||||
}
|
||||
|
||||
function ${handle_promise}(${promise}, ${params}) {
|
||||
var ${token} = ${await_token} = {};
|
||||
|
||||
if (@isPromise(${promise})) {
|
||||
${promise}.then(function(${value}) {
|
||||
${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params});
|
||||
}, function (${error}) {
|
||||
${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params});
|
||||
});
|
||||
|
||||
// if we previously had a then/catch block, destroy it
|
||||
if (${await_block_type} !== ${create_pending_block}) {
|
||||
${replace_await_block}(${token}, ${create_pending_block}, null, ${params});
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
${resolved} = ${promise};
|
||||
if (${await_block_type} !== ${create_then_block}) {
|
||||
${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${handle_promise}(${promise} = ${snippet}, ${params});
|
||||
`);
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
${await_block}.c();
|
||||
`);
|
||||
|
||||
block.builders.claim.addBlock(deindent`
|
||||
${await_block}.l(${parentNodes});
|
||||
`);
|
||||
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
${await_block}.m(${initialMountNode}, ${anchorNode});
|
||||
`);
|
||||
|
||||
const conditions = [];
|
||||
if (this.metadata.dependencies) {
|
||||
conditions.push(
|
||||
`(${this.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
|
||||
);
|
||||
}
|
||||
|
||||
conditions.push(
|
||||
`${promise} !== (${promise} = ${snippet})`,
|
||||
`${handle_promise}(${promise}, ${params})`
|
||||
);
|
||||
|
||||
if (this.pending.block.hasUpdateMethod) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${conditions.join(' && ')}) {
|
||||
// nothing
|
||||
} else {
|
||||
${await_block}.p(changed, ${params}, ${resolved});
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${conditions.join(' && ')}) {
|
||||
${await_block}.c();
|
||||
${await_block}.m(${anchor}.parentNode, ${anchor});
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
${await_block}.u();
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
${await_token} = null;
|
||||
${await_block}.d();
|
||||
`);
|
||||
|
||||
[this.pending, this.then, this.catch].forEach(status => {
|
||||
status.children.forEach(child => {
|
||||
child.build(status.block, null,'nodes');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,270 @@
|
||||
import Node from './shared/Node';
|
||||
import Element from './Element';
|
||||
import getObject from '../../utils/getObject';
|
||||
import getTailSnippet from '../../utils/getTailSnippet';
|
||||
import flattenReference from '../../utils/flattenReference';
|
||||
import { DomGenerator } from '../dom/index';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
const readOnlyMediaAttributes = new Set([
|
||||
'duration',
|
||||
'buffered',
|
||||
'seekable',
|
||||
'played'
|
||||
]);
|
||||
|
||||
export default class Binding extends Node {
|
||||
name: string;
|
||||
value: Node;
|
||||
expression: Node;
|
||||
|
||||
munge(
|
||||
block: Block,
|
||||
allUsedContexts: Set<string>
|
||||
) {
|
||||
const node: Element = this.parent;
|
||||
|
||||
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type'));
|
||||
const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name);
|
||||
|
||||
let updateCondition: string;
|
||||
|
||||
const { name } = getObject(this.value);
|
||||
const { contexts } = block.contextualise(this.value);
|
||||
const { snippet } = this.metadata;
|
||||
|
||||
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
|
||||
// and `selected` is an object chosen with a <select>, then when `checked` changes,
|
||||
// we need to tell the component to update all the values `selected` might be
|
||||
// pointing to
|
||||
// TODO should this happen in preprocess?
|
||||
const dependencies = this.metadata.dependencies.slice();
|
||||
this.metadata.dependencies.forEach((prop: string) => {
|
||||
const indirectDependencies = this.generator.indirectDependencies.get(prop);
|
||||
if (indirectDependencies) {
|
||||
indirectDependencies.forEach(indirectDependency => {
|
||||
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
contexts.forEach(context => {
|
||||
allUsedContexts.add(context);
|
||||
});
|
||||
|
||||
// view to model
|
||||
const valueFromDom = getValueFromDom(this.generator, node, this);
|
||||
const handler = getEventHandler(this.generator, block, name, snippet, this, dependencies, valueFromDom);
|
||||
|
||||
// model to view
|
||||
let updateDom = getDomUpdater(node, this, snippet);
|
||||
let initialUpdate = updateDom;
|
||||
|
||||
// special cases
|
||||
if (this.name === 'group') {
|
||||
const bindingGroup = getBindingGroup(this.generator, this.value);
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.name === 'currentTime') {
|
||||
updateCondition = `!isNaN(${snippet})`;
|
||||
initialUpdate = null;
|
||||
}
|
||||
|
||||
if (this.name === 'paused') {
|
||||
// this is necessary to prevent audio restarting by itself
|
||||
const last = block.getUniqueName(`${node.var}_is_paused`);
|
||||
block.addVariable(last, 'true');
|
||||
|
||||
updateCondition = `${last} !== (${last} = ${snippet})`;
|
||||
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
|
||||
initialUpdate = null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
object: name,
|
||||
handler,
|
||||
updateDom,
|
||||
initialUpdate,
|
||||
needsLock: !isReadOnly && needsLock,
|
||||
updateCondition,
|
||||
isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute()
|
||||
};
|
||||
}
|
||||
|
||||
isReadOnlyMediaAttribute() {
|
||||
return readOnlyMediaAttributes.has(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
function getDomUpdater(
|
||||
node: Element,
|
||||
binding: Binding,
|
||||
snippet: string
|
||||
) {
|
||||
if (binding.isReadOnlyMediaAttribute()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.name === 'select') {
|
||||
return node.getStaticAttributeValue('multiple') === true ?
|
||||
`@selectOptions(${node.var}, ${snippet})` :
|
||||
`@selectOption(${node.var}, ${snippet})`;
|
||||
}
|
||||
|
||||
if (binding.name === 'group') {
|
||||
const type = node.getStaticAttributeValue('type');
|
||||
|
||||
const condition = type === 'checkbox'
|
||||
? `~${snippet}.indexOf(${node.var}.__value)`
|
||||
: `${node.var}.__value === ${snippet}`;
|
||||
|
||||
return `${node.var}.checked = ${condition};`
|
||||
}
|
||||
|
||||
return `${node.var}.${binding.name} = ${snippet};`;
|
||||
}
|
||||
|
||||
function getBindingGroup(generator: DomGenerator, value: Node) {
|
||||
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
|
||||
const keypath = parts.join('.');
|
||||
|
||||
// TODO handle contextual bindings — `keypath` should include unique ID of
|
||||
// each block that provides context
|
||||
let index = generator.bindingGroups.indexOf(keypath);
|
||||
if (index === -1) {
|
||||
index = generator.bindingGroups.length;
|
||||
generator.bindingGroups.push(keypath);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function getEventHandler(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
name: string,
|
||||
snippet: string,
|
||||
attribute: Node,
|
||||
dependencies: string[],
|
||||
value: string,
|
||||
) {
|
||||
let storeDependencies = [];
|
||||
|
||||
if (generator.options.store) {
|
||||
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
|
||||
dependencies = dependencies.filter(prop => prop[0] !== '$');
|
||||
}
|
||||
|
||||
if (block.contexts.has(name)) {
|
||||
const tail = attribute.value.type === 'MemberExpression'
|
||||
? getTailSnippet(attribute.value)
|
||||
: '';
|
||||
|
||||
const list = `context.${block.listNames.get(name)}`;
|
||||
const index = `context.${block.indexNames.get(name)}`;
|
||||
|
||||
return {
|
||||
usesContext: true,
|
||||
usesState: true,
|
||||
usesStore: storeDependencies.length > 0,
|
||||
mutation: `${list}[${index}]${tail} = ${value};`,
|
||||
props: dependencies.map(prop => `${prop}: state.${prop}`),
|
||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||
};
|
||||
}
|
||||
|
||||
if (attribute.value.type === 'MemberExpression') {
|
||||
// This is a little confusing, and should probably be tidied up
|
||||
// at some point. It addresses a tricky bug (#893), wherein
|
||||
// Svelte tries to `set()` a computed property, which throws an
|
||||
// error in dev mode. a) it's possible that we should be
|
||||
// replacing computations with *their* dependencies, and b)
|
||||
// we should probably populate `generator.readonly` sooner so
|
||||
// that we don't have to do the `.some()` here
|
||||
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
|
||||
|
||||
return {
|
||||
usesContext: false,
|
||||
usesState: true,
|
||||
usesStore: storeDependencies.length > 0,
|
||||
mutation: `${snippet} = ${value}`,
|
||||
props: dependencies.map((prop: string) => `${prop}: state.${prop}`),
|
||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||
};
|
||||
}
|
||||
|
||||
let props;
|
||||
let storeProps;
|
||||
|
||||
if (generator.options.store && name[0] === '$') {
|
||||
props = [];
|
||||
storeProps = [`${name.slice(1)}: ${value}`];
|
||||
} else {
|
||||
props = [`${name}: ${value}`];
|
||||
storeProps = [];
|
||||
}
|
||||
|
||||
return {
|
||||
usesContext: false,
|
||||
usesState: false,
|
||||
usesStore: false,
|
||||
mutation: null,
|
||||
props,
|
||||
storeProps
|
||||
};
|
||||
}
|
||||
|
||||
function getValueFromDom(
|
||||
generator: DomGenerator,
|
||||
node: Element,
|
||||
binding: Node
|
||||
) {
|
||||
// <select bind:value='selected>
|
||||
if (node.name === 'select') {
|
||||
return node.getStaticAttributeValue('multiple') === true ?
|
||||
`@selectMultipleValue(${node.var})` :
|
||||
`@selectValue(${node.var})`;
|
||||
}
|
||||
|
||||
const type = node.getStaticAttributeValue('type');
|
||||
|
||||
// <input type='checkbox' bind:group='foo'>
|
||||
if (binding.name === 'group') {
|
||||
const bindingGroup = getBindingGroup(generator, binding.value);
|
||||
if (type === 'checkbox') {
|
||||
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
|
||||
}
|
||||
|
||||
return `${node.var}.__value`;
|
||||
}
|
||||
|
||||
// <input type='range|number' bind:value>
|
||||
if (type === 'range' || type === 'number') {
|
||||
return `@toNumber(${node.var}.${binding.name})`;
|
||||
}
|
||||
|
||||
if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) {
|
||||
return `@timeRangesToArray(${node.var}.${binding.name})`
|
||||
}
|
||||
|
||||
// everything else
|
||||
return `${node.var}.${binding.name}`;
|
||||
}
|
||||
|
||||
function isComputed(node: Node) {
|
||||
while (node.type === 'MemberExpression') {
|
||||
if (node.computed) return true;
|
||||
node = node.object;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class CatchBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class Comment extends Node {
|
||||
type: 'Comment'
|
||||
}
|
@ -0,0 +1,580 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import Node from './shared/Node';
|
||||
import ElseBlock from './ElseBlock';
|
||||
import { DomGenerator } from '../dom/index';
|
||||
import Block from '../dom/Block';
|
||||
import createDebuggingComment from '../../utils/createDebuggingComment';
|
||||
|
||||
export default class EachBlock extends Node {
|
||||
type: 'EachBlock';
|
||||
|
||||
block: Block;
|
||||
expression: Node;
|
||||
|
||||
iterations: string;
|
||||
index: string;
|
||||
context: string;
|
||||
key: string;
|
||||
destructuredContexts: string[];
|
||||
|
||||
children: Node[];
|
||||
else?: ElseBlock;
|
||||
|
||||
init(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
this.var = block.getUniqueName(`each`);
|
||||
this.iterations = block.getUniqueName(`${this.var}_blocks`);
|
||||
|
||||
const { dependencies } = this.metadata;
|
||||
block.addDependencies(dependencies);
|
||||
|
||||
const indexNames = new Map(block.indexNames);
|
||||
const indexName =
|
||||
this.index || block.getUniqueName(`${this.context}_index`);
|
||||
indexNames.set(this.context, indexName);
|
||||
|
||||
const listNames = new Map(block.listNames);
|
||||
const listName = block.getUniqueName(
|
||||
(this.expression.type === 'MemberExpression' && !this.expression.computed) ? this.expression.property.name :
|
||||
this.expression.type === 'Identifier' ? this.expression.name :
|
||||
`each_value`
|
||||
);
|
||||
listNames.set(this.context, listName);
|
||||
|
||||
const context = block.getUniqueName(this.context);
|
||||
const contexts = new Map(block.contexts);
|
||||
contexts.set(this.context, context);
|
||||
|
||||
const indexes = new Map(block.indexes);
|
||||
if (this.index) indexes.set(this.index, this.context);
|
||||
|
||||
const changeableIndexes = new Map(block.changeableIndexes);
|
||||
if (this.index) changeableIndexes.set(this.index, this.key);
|
||||
|
||||
if (this.destructuredContexts) {
|
||||
for (let i = 0; i < this.destructuredContexts.length; i += 1) {
|
||||
contexts.set(this.destructuredContexts[i], `${context}[${i}]`);
|
||||
}
|
||||
}
|
||||
|
||||
this.block = block.child({
|
||||
comment: createDebuggingComment(this, this.generator),
|
||||
name: this.generator.getUniqueName('create_each_block'),
|
||||
context: this.context,
|
||||
key: this.key,
|
||||
|
||||
contexts,
|
||||
indexes,
|
||||
changeableIndexes,
|
||||
|
||||
listName,
|
||||
indexName,
|
||||
|
||||
indexNames,
|
||||
listNames,
|
||||
params: block.params.concat(listName, context, indexName),
|
||||
});
|
||||
|
||||
this.generator.blocks.push(this.block);
|
||||
this.initChildren(this.block, stripWhitespace, nextSibling);
|
||||
block.addDependencies(this.block.dependencies);
|
||||
this.block.hasUpdateMethod = this.block.dependencies.size > 0;
|
||||
|
||||
if (this.else) {
|
||||
this.else.block = block.child({
|
||||
comment: createDebuggingComment(this.else, this.generator),
|
||||
name: this.generator.getUniqueName(`${this.block.name}_else`),
|
||||
});
|
||||
|
||||
this.generator.blocks.push(this.else.block);
|
||||
this.else.initChildren(
|
||||
this.else.block,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
this.else.block.hasUpdateMethod = this.else.block.dependencies.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const { generator } = this;
|
||||
|
||||
const each = this.var;
|
||||
|
||||
const create_each_block = this.block.name;
|
||||
const each_block_value = this.block.listName;
|
||||
const iterations = this.iterations;
|
||||
const params = block.params.join(', ');
|
||||
|
||||
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
|
||||
const anchor = needsAnchor
|
||||
? block.getUniqueName(`${each}_anchor`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
// hack the sourcemap, so that if data is missing the bug
|
||||
// is easy to find
|
||||
let c = this.start + 3;
|
||||
while (generator.source[c] !== 'e') c += 1;
|
||||
generator.code.overwrite(c, c + 4, 'length');
|
||||
const length = `[✂${c}-${c+4}✂]`;
|
||||
|
||||
const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
|
||||
const vars = {
|
||||
each,
|
||||
create_each_block,
|
||||
each_block_value,
|
||||
length,
|
||||
iterations,
|
||||
params,
|
||||
anchor,
|
||||
mountOrIntro,
|
||||
};
|
||||
|
||||
block.contextualise(this.expression);
|
||||
const { snippet } = this.metadata;
|
||||
|
||||
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
|
||||
|
||||
if (this.key) {
|
||||
this.buildKeyed(block, parentNode, parentNodes, snippet, vars);
|
||||
} else {
|
||||
this.buildUnkeyed(block, parentNode, parentNodes, snippet, vars);
|
||||
}
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
anchor,
|
||||
`@createComment()`,
|
||||
`@createComment()`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
if (this.else) {
|
||||
const each_block_else = generator.getUniqueName(`${each}_else`);
|
||||
|
||||
block.builders.init.addLine(`var ${each_block_else} = null;`);
|
||||
|
||||
// TODO neaten this up... will end up with an empty line in the block
|
||||
block.builders.init.addBlock(deindent`
|
||||
if (!${each_block_value}.${length}) {
|
||||
${each_block_else} = ${this.else.block.name}(${params}, #component);
|
||||
${each_block_else}.c();
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
if (${each_block_else}) {
|
||||
${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null);
|
||||
}
|
||||
`);
|
||||
|
||||
const initialMountNode = parentNode || `${anchor}.parentNode`;
|
||||
|
||||
if (this.else.block.hasUpdateMethod) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (!${each_block_value}.${length} && ${each_block_else}) {
|
||||
${each_block_else}.p( changed, ${params} );
|
||||
} else if (!${each_block_value}.${length}) {
|
||||
${each_block_else} = ${this.else.block.name}(${params}, #component);
|
||||
${each_block_else}.c();
|
||||
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
|
||||
} else if (${each_block_else}) {
|
||||
${each_block_else}.u();
|
||||
${each_block_else}.d();
|
||||
${each_block_else} = null;
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${each_block_value}.${length}) {
|
||||
if (${each_block_else}) {
|
||||
${each_block_else}.u();
|
||||
${each_block_else}.d();
|
||||
${each_block_else} = null;
|
||||
}
|
||||
} else if (!${each_block_else}) {
|
||||
${each_block_else} = ${this.else.block.name}(${params}, #component);
|
||||
${each_block_else}.c();
|
||||
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.unmount.addLine(
|
||||
`if (${each_block_else}) ${each_block_else}.u()`
|
||||
);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
if (${each_block_else}) ${each_block_else}.d();
|
||||
`);
|
||||
}
|
||||
|
||||
this.children.forEach((child: Node) => {
|
||||
child.build(this.block, null, 'nodes');
|
||||
});
|
||||
|
||||
if (this.else) {
|
||||
this.else.children.forEach((child: Node) => {
|
||||
child.build(this.else.block, null, 'nodes');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
buildKeyed(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
snippet: string,
|
||||
{
|
||||
each,
|
||||
create_each_block,
|
||||
each_block_value,
|
||||
length,
|
||||
params,
|
||||
anchor,
|
||||
mountOrIntro,
|
||||
}
|
||||
) {
|
||||
const key = block.getUniqueName('key');
|
||||
const lookup = block.getUniqueName(`${each}_lookup`);
|
||||
const iteration = block.getUniqueName(`${each}_iteration`);
|
||||
const head = block.getUniqueName(`${each}_head`);
|
||||
const last = block.getUniqueName(`${each}_last`);
|
||||
const expected = block.getUniqueName(`${each}_expected`);
|
||||
|
||||
block.addVariable(lookup, `@blankObject()`);
|
||||
block.addVariable(head);
|
||||
block.addVariable(last);
|
||||
|
||||
if (this.children[0].isDomNode()) {
|
||||
this.block.first = this.children[0].var;
|
||||
} else {
|
||||
this.block.first = this.block.getUniqueName('first');
|
||||
this.block.addElement(
|
||||
this.block.first,
|
||||
`@createComment()`,
|
||||
`@createComment()`,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
|
||||
var ${key} = ${each_block_value}[#i].${this.key};
|
||||
var ${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
|
||||
|
||||
if (${last}) ${last}.next = ${iteration};
|
||||
${iteration}.last = ${last};
|
||||
${last} = ${iteration};
|
||||
|
||||
if (#i === 0) ${head} = ${iteration};
|
||||
}
|
||||
`);
|
||||
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.c();
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.claim.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.l(${parentNodes});
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.${mountOrIntro}(${initialMountNode}, ${anchorNode});
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
|
||||
const dynamic = this.block.hasUpdateMethod;
|
||||
|
||||
let destroy;
|
||||
if (this.block.hasOutroMethod) {
|
||||
const fn = block.getUniqueName(`${each}_outro`);
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${fn}(iteration) {
|
||||
iteration.o(function() {
|
||||
iteration.u();
|
||||
iteration.d();
|
||||
${lookup}[iteration.key] = null;
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
destroy = deindent`
|
||||
while (${expected}) {
|
||||
${fn}(${expected});
|
||||
${expected} = ${expected}.next;
|
||||
}
|
||||
|
||||
for (#i = 0; #i < discard_pile.length; #i += 1) {
|
||||
if (discard_pile[#i].discard) {
|
||||
${fn}(discard_pile[#i]);
|
||||
}
|
||||
}
|
||||
`;
|
||||
} else {
|
||||
const fn = block.getUniqueName(`${each}_destroy`);
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${fn}(iteration) {
|
||||
iteration.u();
|
||||
iteration.d();
|
||||
${lookup}[iteration.key] = null;
|
||||
}
|
||||
`);
|
||||
|
||||
destroy = deindent`
|
||||
while (${expected}) {
|
||||
${fn}(${expected});
|
||||
${expected} = ${expected}.next;
|
||||
}
|
||||
|
||||
for (#i = 0; #i < discard_pile.length; #i += 1) {
|
||||
var ${iteration} = discard_pile[#i];
|
||||
if (${iteration}.discard) {
|
||||
${fn}(${iteration});
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
var ${each_block_value} = ${snippet};
|
||||
|
||||
var ${expected} = ${head};
|
||||
var ${last} = null;
|
||||
|
||||
var discard_pile = [];
|
||||
|
||||
for (#i = 0; #i < ${each_block_value}.${length}; #i += 1) {
|
||||
var ${key} = ${each_block_value}[#i].${this.key};
|
||||
var ${iteration} = ${lookup}[${key}];
|
||||
|
||||
${dynamic &&
|
||||
`if (${iteration}) ${iteration}.p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);`}
|
||||
|
||||
if (${expected}) {
|
||||
if (${key} === ${expected}.key) {
|
||||
${expected} = ${expected}.next;
|
||||
} else {
|
||||
if (${iteration}) {
|
||||
// probably a deletion
|
||||
while (${expected} && ${expected}.key !== ${key}) {
|
||||
${expected}.discard = true;
|
||||
discard_pile.push(${expected});
|
||||
${expected} = ${expected}.next;
|
||||
};
|
||||
|
||||
${expected} = ${expected} && ${expected}.next;
|
||||
${iteration}.discard = false;
|
||||
${iteration}.last = ${last};
|
||||
|
||||
if (!${expected}) ${iteration}.m(${updateMountNode}, ${anchor});
|
||||
} else {
|
||||
// key is being inserted
|
||||
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
|
||||
${iteration}.c();
|
||||
${iteration}.${mountOrIntro}(${updateMountNode}, ${expected}.first);
|
||||
|
||||
${expected}.last = ${iteration};
|
||||
${iteration}.next = ${expected};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we're appending from this point forward
|
||||
if (${iteration}) {
|
||||
${iteration}.discard = false;
|
||||
${iteration}.next = null;
|
||||
${iteration}.m(${updateMountNode}, ${anchor});
|
||||
} else {
|
||||
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
|
||||
${iteration}.c();
|
||||
${iteration}.${mountOrIntro}(${updateMountNode}, ${anchor});
|
||||
}
|
||||
}
|
||||
|
||||
if (${last}) ${last}.next = ${iteration};
|
||||
${iteration}.last = ${last};
|
||||
${this.block.hasIntroMethod && `${iteration}.i(${updateMountNode}, ${anchor});`}
|
||||
${last} = ${iteration};
|
||||
}
|
||||
|
||||
if (${last}) ${last}.next = null;
|
||||
|
||||
${destroy}
|
||||
|
||||
${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${this.key}];
|
||||
`);
|
||||
|
||||
if (!parentNode) {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.u();
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
var ${iteration} = ${head};
|
||||
while (${iteration}) {
|
||||
${iteration}.d();
|
||||
${iteration} = ${iteration}.next;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
buildUnkeyed(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
snippet: string,
|
||||
{
|
||||
create_each_block,
|
||||
each_block_value,
|
||||
length,
|
||||
iterations,
|
||||
params,
|
||||
anchor,
|
||||
mountOrIntro,
|
||||
}
|
||||
) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${iterations} = [];
|
||||
|
||||
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
|
||||
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
|
||||
}
|
||||
`);
|
||||
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].c();
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.claim.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].l(${parentNodes});
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
|
||||
}
|
||||
`);
|
||||
|
||||
const allDependencies = new Set(this.block.dependencies);
|
||||
const { dependencies } = this.metadata;
|
||||
dependencies.forEach((dependency: string) => {
|
||||
allDependencies.add(dependency);
|
||||
});
|
||||
|
||||
// TODO do this for keyed blocks as well
|
||||
const condition = Array.from(allDependencies)
|
||||
.map(dependency => `changed.${dependency}`)
|
||||
.join(' || ');
|
||||
|
||||
if (condition !== '') {
|
||||
const forLoopBody = this.block.hasUpdateMethod
|
||||
? this.block.hasIntroMethod
|
||||
? deindent`
|
||||
if (${iterations}[#i]) {
|
||||
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
|
||||
} else {
|
||||
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
|
||||
${iterations}[#i].c();
|
||||
}
|
||||
${iterations}[#i].i(${updateMountNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (${iterations}[#i]) {
|
||||
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
|
||||
} else {
|
||||
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
|
||||
${iterations}[#i].c();
|
||||
${iterations}[#i].m(${updateMountNode}, ${anchor});
|
||||
}
|
||||
`
|
||||
: deindent`
|
||||
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
|
||||
${iterations}[#i].c();
|
||||
${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor});
|
||||
`;
|
||||
|
||||
const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`;
|
||||
|
||||
const outro = block.getUniqueName('outro');
|
||||
const destroy = this.block.hasOutroMethod
|
||||
? deindent`
|
||||
function ${outro}(i) {
|
||||
if (${iterations}[i]) {
|
||||
${iterations}[i].o(function() {
|
||||
${iterations}[i].u();
|
||||
${iterations}[i].d();
|
||||
${iterations}[i] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
|
||||
`
|
||||
: deindent`
|
||||
for (; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].u();
|
||||
${iterations}[#i].d();
|
||||
}
|
||||
${iterations}.length = ${each_block_value}.${length};
|
||||
`;
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
var ${each_block_value} = ${snippet};
|
||||
|
||||
if (${condition}) {
|
||||
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
|
||||
${forLoopBody}
|
||||
}
|
||||
|
||||
${destroy}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].u();
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(`@destroyEach(${iterations});`);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class ElseBlock extends Node {
|
||||
type: 'ElseBlock';
|
||||
children: Node[];
|
||||
block: Block;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class EventHandler extends Node {
|
||||
name: string;
|
||||
value: Node[]
|
||||
expression: Node
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import Node from './shared/Node';
|
||||
import { DomGenerator } from '../dom/index';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class Fragment extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
|
||||
init() {
|
||||
this.block = new Block({
|
||||
generator: this.generator,
|
||||
name: '@create_main_fragment',
|
||||
key: null,
|
||||
|
||||
contexts: new Map(),
|
||||
indexes: new Map(),
|
||||
changeableIndexes: new Map(),
|
||||
|
||||
params: ['state'],
|
||||
indexNames: new Map(),
|
||||
listNames: new Map(),
|
||||
|
||||
dependencies: new Set(),
|
||||
});
|
||||
|
||||
this.generator.blocks.push(this.block);
|
||||
this.initChildren(this.block, true, null);
|
||||
|
||||
this.block.hasUpdateMethod = true;
|
||||
}
|
||||
|
||||
build() {
|
||||
this.init();
|
||||
|
||||
this.children.forEach(child => {
|
||||
child.build(this.block, null, 'nodes');
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import Node from './shared/Node';
|
||||
import Tag from './shared/Tag';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class MustacheTag extends Tag {
|
||||
init(block: Block) {
|
||||
this.cannotUseInnerHTML();
|
||||
this.var = block.getUniqueName('text');
|
||||
block.addDependencies(this.metadata.dependencies);
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const { init } = this.renameThisMethod(
|
||||
block,
|
||||
value => `${this.var}.data = ${value};`
|
||||
);
|
||||
|
||||
block.addElement(
|
||||
this.var,
|
||||
`@createText(${init})`,
|
||||
`@claimText(${parentNodes}, ${init})`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class PendingBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import Node from './shared/Node';
|
||||
import Tag from './shared/Tag';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class RawMustacheTag extends Tag {
|
||||
init(block: Block) {
|
||||
this.cannotUseInnerHTML();
|
||||
this.var = block.getUniqueName('raw');
|
||||
block.addDependencies(this.metadata.dependencies);
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const name = this.var;
|
||||
|
||||
const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !parentNode;
|
||||
const needsAnchorAfter = this.next ? this.next.type !== 'Element' : !parentNode;
|
||||
|
||||
const anchorBefore = needsAnchorBefore
|
||||
? block.getUniqueName(`${name}_before`)
|
||||
: (this.prev && this.prev.var) || 'null';
|
||||
|
||||
const anchorAfter = needsAnchorAfter
|
||||
? block.getUniqueName(`${name}_after`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
let detach: string;
|
||||
let insert: (content: string) => string;
|
||||
let useInnerHTML = false;
|
||||
|
||||
if (anchorBefore === 'null' && anchorAfter === 'null') {
|
||||
useInnerHTML = true;
|
||||
detach = `${parentNode}.innerHTML = '';`;
|
||||
insert = content => `${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 } = this.renameThisMethod(
|
||||
block,
|
||||
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')`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
function addAnchorAfter() {
|
||||
block.addElement(
|
||||
anchorAfter,
|
||||
`@createElement('noscript')`,
|
||||
`@createElement('noscript')`,
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class Ref extends Node {
|
||||
name: string;
|
||||
value: Node[]
|
||||
expression: Node
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import Node from './shared/Node';
|
||||
import Element from './Element';
|
||||
import Attribute from './Attribute';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class Slot extends Element {
|
||||
type: 'Element';
|
||||
name: string;
|
||||
attributes: Attribute[]; // TODO have more specific Attribute type
|
||||
children: Node[];
|
||||
|
||||
init(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
this.var = block.getUniqueName('slot');
|
||||
|
||||
if (this.children.length) {
|
||||
this.initChildren(block, stripWhitespace, nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const { generator } = this;
|
||||
|
||||
const slotName = this.getStaticAttributeValue('name') || 'default';
|
||||
generator.slots.add(slotName);
|
||||
|
||||
const content_name = block.getUniqueName(`slot_content_${slotName}`);
|
||||
block.addVariable(content_name, `#component._slotted.${slotName}`);
|
||||
|
||||
const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !parentNode;
|
||||
const needsAnchorAfter = this.next ? this.next.type !== 'Element' : !parentNode;
|
||||
|
||||
const anchorBefore = needsAnchorBefore
|
||||
? block.getUniqueName(`${content_name}_before`)
|
||||
: (this.prev && this.prev.var) || 'null';
|
||||
|
||||
const anchorAfter = needsAnchorAfter
|
||||
? block.getUniqueName(`${content_name}_after`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
if (needsAnchorBefore) block.addVariable(anchorBefore);
|
||||
if (needsAnchorAfter) block.addVariable(anchorAfter);
|
||||
|
||||
block.builders.create.pushCondition(`!${content_name}`);
|
||||
block.builders.hydrate.pushCondition(`!${content_name}`);
|
||||
block.builders.mount.pushCondition(`!${content_name}`);
|
||||
block.builders.unmount.pushCondition(`!${content_name}`);
|
||||
block.builders.destroy.pushCondition(`!${content_name}`);
|
||||
|
||||
this.children.forEach((child: Node) => {
|
||||
child.build(block, parentNode, parentNodes);
|
||||
});
|
||||
|
||||
block.builders.create.popCondition();
|
||||
block.builders.hydrate.popCondition();
|
||||
block.builders.mount.popCondition();
|
||||
block.builders.unmount.popCondition();
|
||||
block.builders.destroy.popCondition();
|
||||
|
||||
// TODO can we use an else here?
|
||||
if (parentNode) {
|
||||
block.builders.mount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
${needsAnchorBefore && `@appendNode(${anchorBefore} || (${anchorBefore} = @createComment()), ${parentNode});`}
|
||||
@appendNode(${content_name}, ${parentNode});
|
||||
${needsAnchorAfter && `@appendNode(${anchorAfter} || (${anchorAfter} = @createComment()), ${parentNode});`}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.mount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
${needsAnchorBefore && `@insertNode(${anchorBefore} || (${anchorBefore} = @createComment()), #target, anchor);`}
|
||||
@insertNode(${content_name}, #target, anchor);
|
||||
${needsAnchorAfter && `@insertNode(${anchorAfter} || (${anchorAfter} = @createComment()), #target, anchor);`}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// if the slot is unmounted, move nodes back into the document fragment,
|
||||
// so that it can be reinserted later
|
||||
// TODO so that this can work with public API, component._slotted should
|
||||
// be all fragments, derived from options.slots. Not === options.slots
|
||||
// TODO can we use an else here?
|
||||
if (anchorBefore === 'null' && anchorAfter === 'null') {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
@reinsertChildren(${parentNode}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else if (anchorBefore === 'null') {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
@reinsertBefore(${anchorAfter}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else if (anchorAfter === 'null') {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
@reinsertAfter(${anchorBefore}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.unmount.addBlock(deindent`
|
||||
if (${content_name}) {
|
||||
@reinsertBetween(${anchorBefore}, ${anchorAfter}, ${content_name});
|
||||
@detachNode(${anchorBefore});
|
||||
@detachNode(${anchorAfter});
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
getStaticAttributeValue(name: string) {
|
||||
const attribute = this.attributes.find(
|
||||
(attr: Node) => 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { stringify } from '../../utils/stringify';
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import { State } from '../dom/interfaces';
|
||||
|
||||
// Whitespace inside one of these elements will not result in
|
||||
// a whitespace node being created in any circumstances. (This
|
||||
// list is almost certainly very incomplete)
|
||||
const elementsWithoutText = new Set([
|
||||
'audio',
|
||||
'datalist',
|
||||
'dl',
|
||||
'ol',
|
||||
'optgroup',
|
||||
'select',
|
||||
'ul',
|
||||
'video',
|
||||
]);
|
||||
|
||||
export default class Text extends Node {
|
||||
type: 'Text';
|
||||
data: string;
|
||||
shouldSkip: boolean;
|
||||
|
||||
init(block: Block) {
|
||||
const parentElement = this.findNearest('Element');
|
||||
|
||||
if (!/\S/.test(this.data) && parentElement && (parentElement.namespace || elementsWithoutText.has(parentElement.name))) {
|
||||
this.shouldSkip = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.var = block.getUniqueName(`text`);
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
if (this.shouldSkip) return;
|
||||
|
||||
block.addElement(
|
||||
this.var,
|
||||
`@createText(${stringify(this.data)})`,
|
||||
`@claimText(${parentNodes}, ${stringify(this.data)})`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class ThenBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class Transition extends Node {
|
||||
name: string;
|
||||
value: Node[]
|
||||
expression: Node
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import { stringify } from '../../utils/stringify';
|
||||
import flattenReference from '../../utils/flattenReference';
|
||||
import isVoidElementName from '../../utils/isVoidElementName';
|
||||
import validCalleeObjects from '../../utils/validCalleeObjects';
|
||||
import reservedNames from '../../utils/reservedNames';
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import Attribute from './Attribute';
|
||||
|
||||
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 class Window extends Node {
|
||||
type: 'Window';
|
||||
attributes: Attribute[];
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const { generator } = this;
|
||||
|
||||
const events = {};
|
||||
const bindings: Record<string, string> = {};
|
||||
|
||||
this.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});
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import Node from './shared/Node';
|
||||
import Attribute from './Attribute';
|
||||
import AwaitBlock from './AwaitBlock';
|
||||
import Binding from './Binding';
|
||||
import CatchBlock from './CatchBlock';
|
||||
import Comment from './Comment';
|
||||
import Component from './Component';
|
||||
import EachBlock from './EachBlock';
|
||||
import Element from './Element';
|
||||
import ElseBlock from './ElseBlock';
|
||||
import EventHandler from './EventHandler';
|
||||
import Fragment from './Fragment';
|
||||
import IfBlock from './IfBlock';
|
||||
import MustacheTag from './MustacheTag';
|
||||
import PendingBlock from './PendingBlock';
|
||||
import RawMustacheTag from './RawMustacheTag';
|
||||
import Ref from './Ref';
|
||||
import Slot from './Slot';
|
||||
import Text from './Text';
|
||||
import ThenBlock from './ThenBlock';
|
||||
import Transition from './Transition';
|
||||
import Window from './Window';
|
||||
|
||||
const nodes: Record<string, any> = {
|
||||
Attribute,
|
||||
AwaitBlock,
|
||||
Binding,
|
||||
CatchBlock,
|
||||
Comment,
|
||||
Component,
|
||||
EachBlock,
|
||||
Element,
|
||||
ElseBlock,
|
||||
EventHandler,
|
||||
Fragment,
|
||||
IfBlock,
|
||||
MustacheTag,
|
||||
PendingBlock,
|
||||
RawMustacheTag,
|
||||
Ref,
|
||||
Slot,
|
||||
Text,
|
||||
ThenBlock,
|
||||
Transition,
|
||||
Window
|
||||
};
|
||||
|
||||
export default nodes;
|
@ -0,0 +1,164 @@
|
||||
import { DomGenerator } from '../../dom/index';
|
||||
import Block from '../../dom/Block';
|
||||
import { trimStart, trimEnd } from '../../../utils/trim';
|
||||
|
||||
export default class Node {
|
||||
type: string;
|
||||
start: number;
|
||||
end: number;
|
||||
[key: string]: any;
|
||||
|
||||
metadata?: {
|
||||
dependencies: string[];
|
||||
snippet: string;
|
||||
};
|
||||
|
||||
parent: Node;
|
||||
prev?: Node;
|
||||
next?: Node;
|
||||
generator: DomGenerator;
|
||||
|
||||
canUseInnerHTML: boolean;
|
||||
var: string;
|
||||
|
||||
constructor(data: Record<string, any>) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
cannotUseInnerHTML() {
|
||||
if (this.canUseInnerHTML !== false) {
|
||||
this.canUseInnerHTML = false;
|
||||
if (this.parent) {
|
||||
if (!this.parent.cannotUseInnerHTML) console.log(this.parent.type, this.type);
|
||||
this.parent.cannotUseInnerHTML();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
// implemented by subclasses
|
||||
}
|
||||
|
||||
initChildren(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
// glue text nodes together
|
||||
const cleaned: Node[] = [];
|
||||
let lastChild: Node;
|
||||
|
||||
let windowComponent;
|
||||
|
||||
this.children.forEach((child: Node) => {
|
||||
if (child.type === 'Comment') return;
|
||||
|
||||
// special case — this is an easy way to remove whitespace surrounding
|
||||
// <:Window/>. lil hacky but it works
|
||||
if (child.type === 'Window') {
|
||||
windowComponent = child;
|
||||
return;
|
||||
}
|
||||
|
||||
if (child.type === 'Text' && lastChild && lastChild.type === 'Text') {
|
||||
lastChild.data += child.data;
|
||||
lastChild.end = child.end;
|
||||
} else {
|
||||
if (child.type === 'Text' && stripWhitespace && cleaned.length === 0) {
|
||||
child.data = trimStart(child.data);
|
||||
if (child.data) cleaned.push(child);
|
||||
} else {
|
||||
cleaned.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
lastChild = child;
|
||||
});
|
||||
|
||||
lastChild = null;
|
||||
|
||||
cleaned.forEach((child: Node, i: number) => {
|
||||
child.canUseInnerHTML = !this.generator.hydratable;
|
||||
|
||||
child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);
|
||||
|
||||
if (child.shouldSkip) return;
|
||||
|
||||
if (lastChild) lastChild.next = child;
|
||||
child.prev = lastChild;
|
||||
|
||||
lastChild = child;
|
||||
});
|
||||
|
||||
// We want to remove trailing whitespace inside an element/component/block,
|
||||
// *unless* there is no whitespace between this node and its next sibling
|
||||
if (stripWhitespace && lastChild && lastChild.type === 'Text') {
|
||||
const shouldTrim = (
|
||||
nextSibling ? (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)) : !this.hasAncestor('EachBlock')
|
||||
);
|
||||
|
||||
if (shouldTrim) {
|
||||
lastChild.data = trimEnd(lastChild.data);
|
||||
if (!lastChild.data) {
|
||||
cleaned.pop();
|
||||
lastChild = cleaned[cleaned.length - 1];
|
||||
lastChild.next = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.children = cleaned;
|
||||
if (windowComponent) cleaned.unshift(windowComponent);
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
// implemented by subclasses
|
||||
}
|
||||
|
||||
isDomNode() {
|
||||
return this.type === 'Element' || this.type === 'Text' || this.type === 'MustacheTag';
|
||||
}
|
||||
|
||||
hasAncestor(type: string) {
|
||||
return this.parent ?
|
||||
this.parent.type === type || this.parent.hasAncestor(type) :
|
||||
false;
|
||||
}
|
||||
|
||||
findNearest(type: string) {
|
||||
if (this.type === type) return this;
|
||||
if (this.parent) return this.parent.findNearest(type);
|
||||
}
|
||||
|
||||
getOrCreateAnchor(block: Block, parentNode: string) {
|
||||
// TODO use this in EachBlock and IfBlock — tricky because
|
||||
// children need to be created first
|
||||
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
|
||||
const anchor = needsAnchor
|
||||
? block.getUniqueName(`${this.var}_anchor`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
anchor,
|
||||
`@createComment()`,
|
||||
`@createComment()`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
return anchor;
|
||||
}
|
||||
|
||||
getUpdateMountNode(anchor: string) {
|
||||
return this.parent.isDomNode() ? this.parent.var : `${anchor}.parentNode`;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import Node from './Node';
|
||||
import Block from '../../dom/Block';
|
||||
|
||||
export default class Tag extends Node {
|
||||
renameThisMethod(
|
||||
block: Block,
|
||||
update: ((value: string) => string)
|
||||
) {
|
||||
const { indexes } = block.contextualise(this.expression);
|
||||
const { dependencies, snippet } = this.metadata;
|
||||
|
||||
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
|
||||
|
||||
const shouldCache = (
|
||||
this.expression.type !== 'Identifier' ||
|
||||
block.contexts.has(this.expression.name) ||
|
||||
hasChangeableIndex
|
||||
);
|
||||
|
||||
const value = shouldCache && block.getUniqueName(`${this.var}_value`);
|
||||
const content = shouldCache ? value : snippet;
|
||||
|
||||
if (shouldCache) block.addVariable(value, snippet);
|
||||
|
||||
if (dependencies.length || hasChangeableIndex) {
|
||||
const changedCheck = (
|
||||
(block.hasOutroMethod ? `#outroing || ` : '') +
|
||||
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
|
||||
);
|
||||
|
||||
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
|
||||
|
||||
const condition = shouldCache ?
|
||||
(dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
|
||||
changedCheck;
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
update(content)
|
||||
);
|
||||
}
|
||||
|
||||
return { init: content };
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
import getStaticAttributeValue from '../../utils/getStaticAttributeValue';
|
||||
import isChildOfComponent from '../shared/utils/isChildOfComponent';
|
||||
import { SsrGenerator } from './index';
|
||||
import { Node } from '../../interfaces';
|
||||
|
||||
function noop () {}
|
||||
|
||||
const preprocessors = {
|
||||
MustacheTag: noop,
|
||||
RawMustacheTag: noop,
|
||||
Text: noop,
|
||||
|
||||
AwaitBlock: (
|
||||
generator: SsrGenerator,
|
||||
node: Node,
|
||||
elementStack: Node[]
|
||||
) => {
|
||||
preprocessChildren(generator, node.pending, elementStack);
|
||||
preprocessChildren(generator, node.then, elementStack);
|
||||
preprocessChildren(generator, node.catch, elementStack);
|
||||
},
|
||||
|
||||
IfBlock: (
|
||||
generator: SsrGenerator,
|
||||
node: Node,
|
||||
elementStack: Node[]
|
||||
) => {
|
||||
preprocessChildren(generator, node, elementStack);
|
||||
|
||||
if (node.else) {
|
||||
preprocessChildren(
|
||||
generator,
|
||||
node.else,
|
||||
elementStack
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
EachBlock: (
|
||||
generator: SsrGenerator,
|
||||
node: Node,
|
||||
elementStack: Node[]
|
||||
) => {
|
||||
preprocessChildren(generator, node, elementStack);
|
||||
|
||||
if (node.else) {
|
||||
preprocessChildren(
|
||||
generator,
|
||||
node.else,
|
||||
elementStack
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Element: (
|
||||
generator: SsrGenerator,
|
||||
node: Node,
|
||||
elementStack: Node[]
|
||||
) => {
|
||||
const isComponent =
|
||||
generator.components.has(node.name) || node.name === ':Self';
|
||||
|
||||
if (!isComponent) {
|
||||
generator.stylesheet.apply(node, elementStack);
|
||||
|
||||
const slot = getStaticAttributeValue(node, 'slot');
|
||||
if (slot && isChildOfComponent(node, generator)) {
|
||||
node.slotted = true;
|
||||
}
|
||||
|
||||
// Treat these the same way:
|
||||
// <option>{{foo}}</option>
|
||||
// <option value='{{foo}}'>{{foo}}</option>
|
||||
const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
|
||||
|
||||
if (node.name === 'option' && !valueAttribute) {
|
||||
node.attributes.push({
|
||||
type: 'Attribute',
|
||||
name: 'value',
|
||||
value: node.children
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children.length) {
|
||||
if (isComponent) {
|
||||
preprocessChildren(generator, node, elementStack);
|
||||
} else {
|
||||
preprocessChildren(generator, node, elementStack.concat(node));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function preprocessChildren(
|
||||
generator: SsrGenerator,
|
||||
node: Node,
|
||||
elementStack: Node[]
|
||||
) {
|
||||
node.children.forEach((child: Node, i: number) => {
|
||||
child.parent = node;
|
||||
|
||||
const preprocessor = preprocessors[child.type];
|
||||
if (preprocessor) preprocessor(generator, child, elementStack);
|
||||
});
|
||||
}
|
||||
|
||||
export default function preprocess(generator: SsrGenerator, html: Node) {
|
||||
preprocessChildren(generator, html, []);
|
||||
}
|
@ -1,19 +1,25 @@
|
||||
import AwaitBlock from './AwaitBlock';
|
||||
import Comment from './Comment';
|
||||
import Component from './Component';
|
||||
import EachBlock from './EachBlock';
|
||||
import Element from './Element';
|
||||
import IfBlock from './IfBlock';
|
||||
import MustacheTag from './MustacheTag';
|
||||
import RawMustacheTag from './RawMustacheTag';
|
||||
import Slot from './Slot';
|
||||
import Text from './Text';
|
||||
import Window from './Window';
|
||||
|
||||
export default {
|
||||
AwaitBlock,
|
||||
Comment,
|
||||
Component,
|
||||
EachBlock,
|
||||
Element,
|
||||
IfBlock,
|
||||
MustacheTag,
|
||||
RawMustacheTag,
|
||||
Text
|
||||
Slot,
|
||||
Text,
|
||||
Window
|
||||
};
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { Node } from '../../../interfaces';
|
||||
import Generator from '../../Generator';
|
||||
|
||||
export default function isChildOfComponent(node: Node, generator: Generator) {
|
||||
while (node = node.parent) {
|
||||
if (node.type !== 'Element') continue;
|
||||
if (node.name === ':Self' || node.name === ':Component' || generator.components.has(node.name)) return true; // TODO extract this out into a helper
|
||||
if (/-/.test(node.name)) return false;
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { Node, Visitor } from '../../../interfaces';
|
||||
|
||||
export default function walkHtml(html: Node, visitors: Record<string, Visitor>) {
|
||||
function visit(node: Node) {
|
||||
const visitor = visitors[node.type];
|
||||
if (!visitor) throw new Error(`Not implemented: ${node.type}`);
|
||||
|
||||
if (visitor.enter) visitor.enter(node);
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(child);
|
||||
});
|
||||
}
|
||||
|
||||
if (visitor.leave) visitor.leave(node);
|
||||
}
|
||||
|
||||
visit(html);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import deindent from '../../../utils/deindent';
|
||||
import list from '../../../utils/list';
|
||||
import { CompileOptions, ModuleFormat, Node } from '../../../interfaces';
|
||||
import deindent from '../utils/deindent';
|
||||
import list from '../utils/list';
|
||||
import { CompileOptions, ModuleFormat, Node } from '../interfaces';
|
||||
|
||||
interface Dependency {
|
||||
name: string;
|
@ -0,0 +1,21 @@
|
||||
import { DomGenerator } from '../generators/dom/index';
|
||||
import { Node } from '../interfaces';
|
||||
|
||||
export default function createDebuggingComment(node: Node, generator: DomGenerator) {
|
||||
const { locate, source } = generator;
|
||||
|
||||
let c = node.start;
|
||||
if (node.type === 'ElseBlock') {
|
||||
while (source[c] !== '{') c -= 1;
|
||||
c -= 1;
|
||||
}
|
||||
|
||||
let d = node.expression ? node.expression.end : c;
|
||||
while (source[d] !== '}') d += 1;
|
||||
d += 2;
|
||||
|
||||
const start = locate(c);
|
||||
const loc = `(${start.line + 1}:${start.column})`;
|
||||
|
||||
return `${loc} ${source.slice(c, d)}`.replace(/\n/g, ' ');
|
||||
}
|
Loading…
Reference in new issue