mirror of https://github.com/sveltejs/svelte
parent
0c6682f71b
commit
fa19cb3b42
@ -0,0 +1,69 @@
|
|||||||
|
import Block from './Block';
|
||||||
|
import { CompileOptions } from '../../interfaces';
|
||||||
|
import Component from '../Component';
|
||||||
|
import FragmentWrapper from './wrappers/Fragment';
|
||||||
|
|
||||||
|
export default class Renderer {
|
||||||
|
component: Component; // TODO Maybe Renderer shouldn't know about Component?
|
||||||
|
options: CompileOptions;
|
||||||
|
|
||||||
|
blocks: (Block | string)[];
|
||||||
|
readonly: Set<string>;
|
||||||
|
slots: Set<string>;
|
||||||
|
metaBindings: string[];
|
||||||
|
|
||||||
|
block: Block;
|
||||||
|
fragment: FragmentWrapper;
|
||||||
|
|
||||||
|
usedNames: Set<string>;
|
||||||
|
fileVar: string;
|
||||||
|
|
||||||
|
hasIntroTransitions: boolean;
|
||||||
|
hasOutroTransitions: boolean;
|
||||||
|
hasComplexBindings: boolean;
|
||||||
|
|
||||||
|
constructor(component: Component, options: CompileOptions) {
|
||||||
|
this.component = component;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
this.readonly = new Set();
|
||||||
|
this.slots = new Set();
|
||||||
|
|
||||||
|
this.usedNames = new Set();
|
||||||
|
this.fileVar = options.dev && this.component.getUniqueName('file');
|
||||||
|
|
||||||
|
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
|
||||||
|
this.metaBindings = [];
|
||||||
|
|
||||||
|
// main block
|
||||||
|
this.block = new Block({
|
||||||
|
component,
|
||||||
|
name: '@create_main_fragment',
|
||||||
|
key: null,
|
||||||
|
|
||||||
|
bindings: new Map(),
|
||||||
|
|
||||||
|
dependencies: new Set(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.block.hasUpdateMethod = true;
|
||||||
|
this.blocks = [this.block];
|
||||||
|
|
||||||
|
this.fragment = new FragmentWrapper(
|
||||||
|
this,
|
||||||
|
this.block,
|
||||||
|
component.fragment.children,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
this.blocks.forEach(block => {
|
||||||
|
if (typeof block !== 'string') {
|
||||||
|
block.assignVariableNames();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fragment.render(this.block, null, 'nodes');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,461 @@
|
|||||||
|
import Renderer from '../Renderer';
|
||||||
|
import Block from '../Block';
|
||||||
|
import Node from '../../nodes/shared/Node';
|
||||||
|
import Wrapper from './shared/wrapper';
|
||||||
|
import createDebuggingComment from '../../../utils/createDebuggingComment';
|
||||||
|
import EachBlock from '../../nodes/EachBlock';
|
||||||
|
import FragmentWrapper from './Fragment';
|
||||||
|
import deindent from '../../../utils/deindent';
|
||||||
|
|
||||||
|
export default class EachBlockWrapper extends Wrapper {
|
||||||
|
block: Block;
|
||||||
|
node: EachBlock;
|
||||||
|
fragment: FragmentWrapper;
|
||||||
|
var: string;
|
||||||
|
vars: {
|
||||||
|
anchor: string;
|
||||||
|
create_each_block: string;
|
||||||
|
each_block_value: string;
|
||||||
|
get_each_context: string;
|
||||||
|
iterations: string;
|
||||||
|
length: string;
|
||||||
|
mountOrIntro: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: Renderer,
|
||||||
|
block: Block,
|
||||||
|
parent: Wrapper,
|
||||||
|
node: EachBlock,
|
||||||
|
stripWhitespace: boolean,
|
||||||
|
nextSibling: Wrapper
|
||||||
|
) {
|
||||||
|
super(renderer, block, parent, node);
|
||||||
|
this.cannotUseInnerHTML();
|
||||||
|
|
||||||
|
this.var = 'each';
|
||||||
|
|
||||||
|
const { dependencies } = node.expression;
|
||||||
|
block.addDependencies(dependencies);
|
||||||
|
|
||||||
|
this.block = block.child({
|
||||||
|
comment: createDebuggingComment(this.node, this.renderer.component),
|
||||||
|
name: renderer.component.getUniqueName('create_each_block'),
|
||||||
|
key: <string>node.key, // TODO...
|
||||||
|
|
||||||
|
bindings: new Map(block.bindings)
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO this seems messy
|
||||||
|
this.block.hasAnimation = this.node.hasAnimation;
|
||||||
|
|
||||||
|
// node.contexts.forEach(prop => {
|
||||||
|
// this.block.bindings.set(prop.key.name, `ctx.${this.vars.each_block_value}[ctx.${indexName}]${prop.tail}`);
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (this.node.index) {
|
||||||
|
this.block.getUniqueName(this.node.index); // this prevents name collisions (#1254)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.blocks.push(this.block);
|
||||||
|
|
||||||
|
this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling);
|
||||||
|
block.addDependencies(this.block.dependencies);
|
||||||
|
this.block.hasUpdateMethod = this.block.dependencies.size > 0; // TODO should this logic be in Block?
|
||||||
|
|
||||||
|
if (this.else) {
|
||||||
|
this.else.block = block.child({
|
||||||
|
comment: createDebuggingComment(this.else, this.renderer.component),
|
||||||
|
name: renderer.component.getUniqueName(`${this.block.name}_else`),
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.blocks.push(this.else.block);
|
||||||
|
this.else.initChildren(
|
||||||
|
this.else.block,
|
||||||
|
stripWhitespace,
|
||||||
|
nextSibling
|
||||||
|
);
|
||||||
|
this.else.block.hasUpdateMethod = this.else.block.dependencies.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.block.hasOutros || (this.else && this.else.block.hasOutros)) {
|
||||||
|
block.addOutro();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block, parentNode: string, parentNodes: string) {
|
||||||
|
if (this.fragment.nodes.length === 0) return;
|
||||||
|
|
||||||
|
const { renderer } = this;
|
||||||
|
const { component } = renderer;
|
||||||
|
|
||||||
|
// hack the sourcemap, so that if data is missing the bug
|
||||||
|
// is easy to find
|
||||||
|
let c = this.node.start + 2;
|
||||||
|
while (component.source[c] !== 'e') c += 1;
|
||||||
|
component.code.overwrite(c, c + 4, 'length');
|
||||||
|
const length = `[✂${c}-${c+4}✂]`;
|
||||||
|
|
||||||
|
const needsAnchor = this.next
|
||||||
|
? !this.next.isDomNode() :
|
||||||
|
!parentNode || !this.parent.isDomNode();
|
||||||
|
|
||||||
|
this.vars = {
|
||||||
|
anchor: needsAnchor
|
||||||
|
? block.getUniqueName(`${this.var}_anchor`)
|
||||||
|
: (this.next && this.next.var) || 'null',
|
||||||
|
create_each_block: this.block.name,
|
||||||
|
each_block_value: renderer.component.getUniqueName('each_value'),
|
||||||
|
get_each_context: renderer.component.getUniqueName(`get_${this.var}_context`),
|
||||||
|
iterations: block.getUniqueName(`${this.var}_blocks`),
|
||||||
|
length: `[✂${c}-${c+4}✂]`,
|
||||||
|
mountOrIntro: (this.block.hasIntroMethod || this.block.hasOutroMethod)
|
||||||
|
? 'i'
|
||||||
|
: 'm'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.contextProps = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = list[i]${prop.tail};`);
|
||||||
|
|
||||||
|
const indexName = this.node.index || renderer.component.getUniqueName(`${this.node.context}_index`);
|
||||||
|
|
||||||
|
// TODO only add these if necessary
|
||||||
|
this.contextProps.push(
|
||||||
|
`child_ctx.${this.vars.each_block_value} = list;`,
|
||||||
|
`child_ctx.${indexName} = i;`
|
||||||
|
);
|
||||||
|
|
||||||
|
const { snippet } = this.node.expression;
|
||||||
|
|
||||||
|
block.builders.init.addLine(`var ${this.vars.each_block_value} = ${snippet};`);
|
||||||
|
|
||||||
|
renderer.blocks.push(deindent`
|
||||||
|
function ${this.vars.get_each_context}(ctx, list, i) {
|
||||||
|
const child_ctx = Object.create(ctx);
|
||||||
|
${this.contextProps}
|
||||||
|
return child_ctx;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (this.node.key) {
|
||||||
|
this.renderKeyed(block, parentNode, parentNodes, snippet);
|
||||||
|
} else {
|
||||||
|
this.renderUnkeyed(block, parentNode, parentNodes, snippet);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsAnchor) {
|
||||||
|
block.addElement(
|
||||||
|
this.vars.anchor,
|
||||||
|
`@createComment()`,
|
||||||
|
parentNodes && `@createComment()`,
|
||||||
|
parentNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.else) {
|
||||||
|
const each_block_else = component.getUniqueName(`${this.var}_else`);
|
||||||
|
const mountOrIntro = (this.else.block.hasIntroMethod || this.else.block.hasOutroMethod) ? 'i' : 'm';
|
||||||
|
|
||||||
|
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 (!${this.vars.each_block_value}.${length}) {
|
||||||
|
${each_block_else} = ${this.else.block.name}(#component, ctx);
|
||||||
|
${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 (!${this.vars.each_block_value}.${length} && ${each_block_else}) {
|
||||||
|
${each_block_else}.p(changed, ctx);
|
||||||
|
} else if (!${this.vars.each_block_value}.${length}) {
|
||||||
|
${each_block_else} = ${this.else.block.name}(#component, ctx);
|
||||||
|
${each_block_else}.c();
|
||||||
|
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${this.vars.anchor});
|
||||||
|
} else if (${each_block_else}) {
|
||||||
|
${each_block_else}.d(1);
|
||||||
|
${each_block_else} = null;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
if (${this.vars.each_block_value}.${length}) {
|
||||||
|
if (${each_block_else}) {
|
||||||
|
${each_block_else}.d(1);
|
||||||
|
${each_block_else} = null;
|
||||||
|
}
|
||||||
|
} else if (!${each_block_else}) {
|
||||||
|
${each_block_else} = ${this.else.block.name}(#component, ctx);
|
||||||
|
${each_block_else}.c();
|
||||||
|
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.destroy.addBlock(deindent`
|
||||||
|
if (${each_block_else}) ${each_block_else}.d(${parentNode ? '' : 'detach'});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fragment.nodes.forEach((child: Wrapper) => {
|
||||||
|
child.render(this.block, null, 'nodes');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.else) {
|
||||||
|
this.else.children.forEach((child: Node) => {
|
||||||
|
child.build(this.else.block, null, 'nodes');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderKeyed(
|
||||||
|
block: Block,
|
||||||
|
parentNode: string,
|
||||||
|
parentNodes: string,
|
||||||
|
snippet: string
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
create_each_block,
|
||||||
|
length,
|
||||||
|
anchor,
|
||||||
|
mountOrIntro,
|
||||||
|
} = this.vars;
|
||||||
|
|
||||||
|
const get_key = block.getUniqueName('get_key');
|
||||||
|
const blocks = block.getUniqueName(`${this.var}_blocks`);
|
||||||
|
const lookup = block.getUniqueName(`${this.var}_lookup`);
|
||||||
|
|
||||||
|
block.addVariable(blocks, '[]');
|
||||||
|
block.addVariable(lookup, `@blankObject()`);
|
||||||
|
|
||||||
|
if (this.fragment.nodes[0].isDomNode()) {
|
||||||
|
this.block.first = this.fragment.nodes[0].var;
|
||||||
|
} else {
|
||||||
|
this.block.first = this.block.getUniqueName('first');
|
||||||
|
this.block.addElement(
|
||||||
|
this.block.first,
|
||||||
|
`@createComment()`,
|
||||||
|
parentNodes && `@createComment()`,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
const ${get_key} = ctx => ${this.node.key.snippet};
|
||||||
|
|
||||||
|
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
|
||||||
|
let child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
|
||||||
|
let key = ${get_key}(child_ctx);
|
||||||
|
${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const initialMountNode = parentNode || '#target';
|
||||||
|
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||||
|
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||||
|
|
||||||
|
block.builders.create.addBlock(deindent`
|
||||||
|
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c();
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (parentNodes) {
|
||||||
|
block.builders.claim.addBlock(deindent`
|
||||||
|
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.mount.addBlock(deindent`
|
||||||
|
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
|
||||||
|
`);
|
||||||
|
|
||||||
|
const dynamic = this.block.hasUpdateMethod;
|
||||||
|
|
||||||
|
const rects = block.getUniqueName('rects');
|
||||||
|
const destroy = this.node.hasAnimation
|
||||||
|
? `@fixAndOutroAndDestroyBlock`
|
||||||
|
: this.block.hasOutros
|
||||||
|
? `@outroAndDestroyBlock`
|
||||||
|
: `@destroyBlock`;
|
||||||
|
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
const ${this.vars.each_block_value} = ${snippet};
|
||||||
|
|
||||||
|
${this.block.hasOutros && `@groupOutros();`}
|
||||||
|
${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].r();`}
|
||||||
|
${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.vars.each_block_value}, ${lookup}, ${updateMountNode}, ${destroy}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.vars.get_each_context});
|
||||||
|
${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`}
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (this.block.hasOutros && this.renderer.component.options.nestedTransitions) {
|
||||||
|
const countdown = block.getUniqueName('countdown');
|
||||||
|
block.builders.outro.addBlock(deindent`
|
||||||
|
const ${countdown} = @callAfter(#outrocallback, ${blocks}.length);
|
||||||
|
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].o(${countdown});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.destroy.addBlock(deindent`
|
||||||
|
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d(${parentNode ? '' : 'detach'});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUnkeyed(
|
||||||
|
block: Block,
|
||||||
|
parentNode: string,
|
||||||
|
parentNodes: string,
|
||||||
|
snippet: string
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
create_each_block,
|
||||||
|
length,
|
||||||
|
iterations,
|
||||||
|
anchor,
|
||||||
|
mountOrIntro,
|
||||||
|
} = this.vars;
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
var ${iterations} = [];
|
||||||
|
|
||||||
|
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
|
||||||
|
${iterations}[#i] = ${create_each_block}(#component, ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i));
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (parentNodes) {
|
||||||
|
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.node.expression;
|
||||||
|
dependencies.forEach((dependency: string) => {
|
||||||
|
allDependencies.add(dependency);
|
||||||
|
});
|
||||||
|
|
||||||
|
const outroBlock = this.block.hasOutros && block.getUniqueName('outroBlock')
|
||||||
|
if (outroBlock) {
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
function ${outroBlock}(i, detach, fn) {
|
||||||
|
if (${iterations}[i]) {
|
||||||
|
${iterations}[i].o(() => {
|
||||||
|
if (detach) {
|
||||||
|
${iterations}[i].d(detach);
|
||||||
|
${iterations}[i] = null;
|
||||||
|
}
|
||||||
|
if (fn) fn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.hasIntros || this.block.hasOutros)
|
||||||
|
? deindent`
|
||||||
|
if (${iterations}[#i]) {
|
||||||
|
${iterations}[#i].p(changed, child_ctx);
|
||||||
|
} else {
|
||||||
|
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
|
||||||
|
${iterations}[#i].c();
|
||||||
|
}
|
||||||
|
${iterations}[#i].i(${updateMountNode}, ${anchor});
|
||||||
|
`
|
||||||
|
: deindent`
|
||||||
|
if (${iterations}[#i]) {
|
||||||
|
${iterations}[#i].p(changed, child_ctx);
|
||||||
|
} else {
|
||||||
|
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
|
||||||
|
${iterations}[#i].c();
|
||||||
|
${iterations}[#i].m(${updateMountNode}, ${anchor});
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: deindent`
|
||||||
|
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
|
||||||
|
${iterations}[#i].c();
|
||||||
|
${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`;
|
||||||
|
|
||||||
|
let destroy;
|
||||||
|
|
||||||
|
if (this.block.hasOutros) {
|
||||||
|
destroy = deindent`
|
||||||
|
@groupOutros();
|
||||||
|
for (; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 1);
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
destroy = deindent`
|
||||||
|
for (; #i < ${iterations}.length; #i += 1) {
|
||||||
|
${iterations}[#i].d(1);
|
||||||
|
}
|
||||||
|
${iterations}.length = ${this.vars.each_block_value}.${length};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
if (${condition}) {
|
||||||
|
${this.vars.each_block_value} = ${snippet};
|
||||||
|
|
||||||
|
for (var #i = ${start}; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
|
||||||
|
const child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
|
||||||
|
|
||||||
|
${forLoopBody}
|
||||||
|
}
|
||||||
|
|
||||||
|
${destroy}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outroBlock && this.renderer.component.options.nestedTransitions) {
|
||||||
|
const countdown = block.getUniqueName('countdown');
|
||||||
|
block.builders.outro.addBlock(deindent`
|
||||||
|
${iterations} = ${iterations}.filter(Boolean);
|
||||||
|
const ${countdown} = @callAfter(#outrocallback, ${iterations}.length);
|
||||||
|
for (let #i = 0; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 0, ${countdown});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.destroy.addBlock(`@destroyEach(${iterations}, detach);`);
|
||||||
|
}
|
||||||
|
|
||||||
|
remount(name: string) {
|
||||||
|
// TODO consider keyed blocks
|
||||||
|
return `for (var #i = 0; #i < ${this.vars.iterations}.length; #i += 1) ${this.vars.iterations}[#i].m(${name}._slotted.default, null);`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,182 @@
|
|||||||
|
import Attribute from '../../../nodes/Attribute';
|
||||||
|
import Block from '../../Block';
|
||||||
|
import AttributeWrapper from './Attribute';
|
||||||
|
import Node from '../../../nodes/shared/Node';
|
||||||
|
import ElementWrapper from '.';
|
||||||
|
import { stringify } from '../../../../utils/stringify';
|
||||||
|
|
||||||
|
export interface StyleProp {
|
||||||
|
key: string;
|
||||||
|
value: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class StyleAttributeWrapper extends AttributeWrapper {
|
||||||
|
node: Attribute;
|
||||||
|
parent: ElementWrapper;
|
||||||
|
|
||||||
|
constructor(node: Attribute, parent: ElementWrapper) {
|
||||||
|
super(node, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block) {
|
||||||
|
const styleProps = optimizeStyle(this.node.chunks);
|
||||||
|
if (!styleProps) return super.render(block);
|
||||||
|
|
||||||
|
styleProps.forEach((prop: StyleProp) => {
|
||||||
|
let value;
|
||||||
|
|
||||||
|
if (isDynamic(prop.value)) {
|
||||||
|
const propDependencies = new Set();
|
||||||
|
let shouldCache;
|
||||||
|
|
||||||
|
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 { dependencies, snippet } = chunk;
|
||||||
|
|
||||||
|
dependencies.forEach(d => {
|
||||||
|
propDependencies.add(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(' + ');
|
||||||
|
|
||||||
|
if (propDependencies.size) {
|
||||||
|
const dependencies = Array.from(propDependencies);
|
||||||
|
const condition = (
|
||||||
|
(block.hasOutros ? `!#current || ` : '') +
|
||||||
|
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.update.addConditional(
|
||||||
|
condition,
|
||||||
|
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = stringify(prop.value[0].data);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function optimizeStyle(value: Node[]) {
|
||||||
|
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';
|
||||||
|
}
|
@ -0,0 +1,759 @@
|
|||||||
|
import Renderer from '../../Renderer';
|
||||||
|
import Element from '../../../nodes/Element';
|
||||||
|
import Wrapper from '../shared/wrapper';
|
||||||
|
import Block from '../../Block';
|
||||||
|
import Node from '../../../nodes/shared/Node';
|
||||||
|
import { CompileOptions } from '../../../../interfaces';
|
||||||
|
import { quotePropIfNecessary, quoteNameIfNecessary } from '../../../../utils/quoteIfNecessary';
|
||||||
|
import isVoidElementName from '../../../../utils/isVoidElementName';
|
||||||
|
import FragmentWrapper from '../Fragment';
|
||||||
|
import { stringify, escapeHTML } from '../../../../utils/stringify';
|
||||||
|
import TextWrapper from '../Text';
|
||||||
|
import fixAttributeCasing from '../../../../utils/fixAttributeCasing';
|
||||||
|
import deindent from '../../../../utils/deindent';
|
||||||
|
import namespaces from '../../../../utils/namespaces';
|
||||||
|
import AttributeWrapper from './Attribute';
|
||||||
|
import StyleAttributeWrapper from './StyleAttribute';
|
||||||
|
|
||||||
|
export default class ElementWrapper extends Wrapper {
|
||||||
|
node: Element;
|
||||||
|
fragment: FragmentWrapper;
|
||||||
|
attributes: AttributeWrapper[];
|
||||||
|
classDependencies: string[];
|
||||||
|
|
||||||
|
var: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: Renderer,
|
||||||
|
block: Block,
|
||||||
|
parent: Wrapper,
|
||||||
|
node: Element,
|
||||||
|
stripWhitespace: boolean,
|
||||||
|
nextSibling: Wrapper
|
||||||
|
) {
|
||||||
|
super(renderer, block, parent, node);
|
||||||
|
this.var = node.name;
|
||||||
|
|
||||||
|
this.classDependencies = [];
|
||||||
|
|
||||||
|
this.attributes = this.node.attributes.map(attribute => {
|
||||||
|
if (attribute.name === 'style') {
|
||||||
|
return new StyleAttributeWrapper(attribute, this);
|
||||||
|
}
|
||||||
|
return new AttributeWrapper(attribute, this);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block, parentNode: string, parentNodes: string) {
|
||||||
|
const { renderer } = this;
|
||||||
|
|
||||||
|
if (this.node.name === 'slot') {
|
||||||
|
const slotName = this.getStaticAttributeValue('name') || 'default';
|
||||||
|
renderer.slots.add(slotName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.node.name === 'noscript') return;
|
||||||
|
|
||||||
|
const node = this.var;
|
||||||
|
const nodes = parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parentNodes is null
|
||||||
|
|
||||||
|
const slot = this.node.attributes.find((attribute: Node) => attribute.name === 'slot');
|
||||||
|
const prop = slot && quotePropIfNecessary(slot.chunks[0].data);
|
||||||
|
const initialMountNode = this.slotted ?
|
||||||
|
`${this.findNearest(/^InlineComponent/).var}._slotted${prop}` : // TODO this looks bonkers
|
||||||
|
parentNode;
|
||||||
|
|
||||||
|
block.addVariable(node);
|
||||||
|
const renderStatement = this.getRenderStatement();
|
||||||
|
block.builders.create.addLine(
|
||||||
|
`${node} = ${renderStatement};`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (renderer.options.hydratable) {
|
||||||
|
if (parentNodes) {
|
||||||
|
block.builders.claim.addBlock(deindent`
|
||||||
|
${node} = ${this.getClaimStatement(parentNodes)};
|
||||||
|
var ${nodes} = @children(${this.name === 'template' ? `${node}.content` : node});
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
block.builders.claim.addLine(
|
||||||
|
`${node} = ${renderStatement};`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialMountNode) {
|
||||||
|
block.builders.mount.addLine(
|
||||||
|
`@append(${initialMountNode}, ${node});`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (initialMountNode === 'document.head') {
|
||||||
|
block.builders.destroy.addLine(`@detachNode(${node});`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
block.builders.mount.addLine(`@insert(#target, ${node}, anchor);`);
|
||||||
|
|
||||||
|
// TODO we eventually need to consider what happens to elements
|
||||||
|
// that belong to the same outgroup as an outroing element...
|
||||||
|
block.builders.destroy.addConditional('detach', `@detachNode(${node});`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert static children with textContent or innerHTML
|
||||||
|
if (!this.node.namespace && this.canUseInnerHTML && this.fragment.nodes.length > 0) {
|
||||||
|
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].type === 'Text') {
|
||||||
|
block.builders.create.addLine(
|
||||||
|
`${node}.textContent = ${stringify(this.fragment.nodes[0].data)};`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
block.builders.create.addLine(
|
||||||
|
`${node}.innerHTML = ${stringify(this.fragment.nodes.map(toHTML).join(''))};`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.fragment.nodes.forEach((child: Wrapper) => {
|
||||||
|
child.render(
|
||||||
|
block,
|
||||||
|
this.node.name === 'template' ? `${node}.content` : node,
|
||||||
|
nodes
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasHoistedEventHandlerOrBinding = (
|
||||||
|
//(this.hasAncestor('EachBlock') && this.node.bindings.length > 0) ||
|
||||||
|
this.node.handlers.some(handler => handler.shouldHoist)
|
||||||
|
);
|
||||||
|
const eventHandlerOrBindingUsesComponent = (
|
||||||
|
this.node.bindings.length > 0 ||
|
||||||
|
this.node.handlers.some(handler => handler.usesComponent)
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventHandlerOrBindingUsesContext = (
|
||||||
|
this.node.bindings.some(binding => binding.usesContext) ||
|
||||||
|
this.node.handlers.some(handler => handler.usesContext)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasHoistedEventHandlerOrBinding) {
|
||||||
|
const initialProps: string[] = [];
|
||||||
|
const updates: string[] = [];
|
||||||
|
|
||||||
|
if (eventHandlerOrBindingUsesComponent) {
|
||||||
|
const component = block.alias('component');
|
||||||
|
initialProps.push(component === 'component' ? 'component' : `component: ${component}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventHandlerOrBindingUsesContext) {
|
||||||
|
initialProps.push(`ctx`);
|
||||||
|
block.builders.update.addLine(`${node}._svelte.ctx = ctx;`);
|
||||||
|
block.maintainContext = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialProps.length) {
|
||||||
|
block.builders.hydrate.addBlock(deindent`
|
||||||
|
${node}._svelte = { ${initialProps.join(', ')} };
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (eventHandlerOrBindingUsesContext) {
|
||||||
|
block.maintainContext = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addBindings(block);
|
||||||
|
this.addEventHandlers(block);
|
||||||
|
if (this.ref) this.addRef(block);
|
||||||
|
this.addAttributes(block);
|
||||||
|
this.addTransitions(block);
|
||||||
|
this.addAnimation(block);
|
||||||
|
this.addActions(block);
|
||||||
|
this.addClasses(block);
|
||||||
|
|
||||||
|
if (this.initialUpdate) {
|
||||||
|
block.builders.mount.addBlock(this.initialUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes) {
|
||||||
|
block.builders.claim.addLine(
|
||||||
|
`${nodes}.forEach(@detachNode);`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toHTML(wrapper: ElementWrapper | TextWrapper) {
|
||||||
|
if (wrapper.node.type === 'Text') {
|
||||||
|
return wrapper.node.parent &&
|
||||||
|
wrapper.node.parent.type === 'Element' &&
|
||||||
|
(wrapper.node.parent.name === 'script' || wrapper.node.parent.name === 'style')
|
||||||
|
? wrapper.node.data
|
||||||
|
: escapeHTML(wrapper.node.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wrapper.node.name === 'noscript') return '';
|
||||||
|
|
||||||
|
let open = `<${wrapper.node.name}`;
|
||||||
|
|
||||||
|
(<ElementWrapper>wrapper).node.attributes.forEach((attr: Node) => {
|
||||||
|
open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.chunks)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isVoidElementName(wrapper.node.name)) return open + '>';
|
||||||
|
|
||||||
|
return `${open}>${wrapper.fragment.nodes.map(toHTML).join('')}</${wrapper.name}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderer.options.dev) {
|
||||||
|
const loc = renderer.locate(this.start);
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`@addLoc(${this.var}, ${renderer.fileVar}, ${loc.line}, ${loc.column}, ${this.start});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRenderStatement() {
|
||||||
|
const { name, namespace } = this.node;
|
||||||
|
|
||||||
|
if (namespace === 'http://www.w3.org/2000/svg') {
|
||||||
|
return `@createSvgElement("${name}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namespace) {
|
||||||
|
return `document.createElementNS("${namespace}", "${name}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `@createElement("${name}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getClaimStatement(nodes: string) {
|
||||||
|
const attributes = this.node.attributes
|
||||||
|
.filter((attr: Node) => attr.type === 'Attribute')
|
||||||
|
.map((attr: Node) => `${quoteNameIfNecessary(attr.name)}: true`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const name = this.node.namespace
|
||||||
|
? this.node.name
|
||||||
|
: this.node.name.toUpperCase();
|
||||||
|
|
||||||
|
return `@claimElement(${nodes}, "${name}", ${attributes
|
||||||
|
? `{ ${attributes} }`
|
||||||
|
: `{}`}, ${this.node.namespace === namespaces.svg ? true : false})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addBindings(
|
||||||
|
block: Block
|
||||||
|
) {
|
||||||
|
if (this.node.bindings.length === 0) return;
|
||||||
|
|
||||||
|
if (this.name === 'select' || this.isMediaNode()) this.component.target.hasComplexBindings = true;
|
||||||
|
|
||||||
|
const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
|
||||||
|
|
||||||
|
// TODO munge in constructor
|
||||||
|
const mungedBindings = this.bindings.map(binding => binding.munge(block));
|
||||||
|
|
||||||
|
const lock = mungedBindings.some(binding => binding.needsLock) ?
|
||||||
|
block.getUniqueName(`${this.var}_updating`) :
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (lock) block.addVariable(lock, 'false');
|
||||||
|
|
||||||
|
const groups = events
|
||||||
|
.map(event => {
|
||||||
|
return {
|
||||||
|
events: event.eventNames,
|
||||||
|
bindings: mungedBindings.filter(binding => event.filter(this, binding.name))
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(group => group.bindings.length);
|
||||||
|
|
||||||
|
groups.forEach(group => {
|
||||||
|
const handler = block.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);
|
||||||
|
|
||||||
|
const needsLock = group.bindings.some(binding => binding.needsLock);
|
||||||
|
|
||||||
|
group.bindings.forEach(binding => {
|
||||||
|
if (!binding.updateDom) return;
|
||||||
|
|
||||||
|
const updateConditions = needsLock ? [`!${lock}`] : [];
|
||||||
|
if (binding.updateCondition) updateConditions.push(binding.updateCondition);
|
||||||
|
|
||||||
|
block.builders.update.addLine(
|
||||||
|
updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const usesContext = group.bindings.some(binding => binding.handler.usesContext);
|
||||||
|
const usesState = group.bindings.some(binding => binding.handler.usesState);
|
||||||
|
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
|
||||||
|
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
|
||||||
|
|
||||||
|
const props = new Set();
|
||||||
|
const storeProps = new Set();
|
||||||
|
group.bindings.forEach(binding => {
|
||||||
|
binding.handler.props.forEach(prop => {
|
||||||
|
props.add(prop);
|
||||||
|
});
|
||||||
|
|
||||||
|
binding.handler.storeProps.forEach(prop => {
|
||||||
|
storeProps.add(prop);
|
||||||
|
});
|
||||||
|
}); // TODO use stringifyProps here, once indenting is fixed
|
||||||
|
|
||||||
|
// media bindings — awkward special case. The native timeupdate events
|
||||||
|
// fire too infrequently, so we need to take matters into our
|
||||||
|
// own hands
|
||||||
|
let animation_frame;
|
||||||
|
if (group.events[0] === 'timeupdate') {
|
||||||
|
animation_frame = block.getUniqueName(`${this.var}_animationframe`);
|
||||||
|
block.addVariable(animation_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
function ${handler}() {
|
||||||
|
${
|
||||||
|
animation_frame && deindent`
|
||||||
|
cancelAnimationFrame(${animation_frame});
|
||||||
|
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
|
||||||
|
}
|
||||||
|
${usesStore && `var $ = #component.store.get();`}
|
||||||
|
${needsLock && `${lock} = true;`}
|
||||||
|
${mutations.length > 0 && mutations}
|
||||||
|
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
|
||||||
|
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
|
||||||
|
${needsLock && `${lock} = false;`}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
group.events.forEach(name => {
|
||||||
|
if (name === 'resize') {
|
||||||
|
// special case
|
||||||
|
const resize_listener = block.getUniqueName(`${this.var}_resize_listener`);
|
||||||
|
block.addVariable(resize_listener);
|
||||||
|
|
||||||
|
block.builders.mount.addLine(
|
||||||
|
`${resize_listener} = @addResizeListener(${this.var}, ${handler});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(
|
||||||
|
`${resize_listener}.cancel();`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`@addListener(${this.var}, "${name}", ${handler});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(
|
||||||
|
`@removeListener(${this.var}, "${name}", ${handler});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const allInitialStateIsDefined = group.bindings
|
||||||
|
.map(binding => `'${binding.object}' in ctx`)
|
||||||
|
.join(' && ');
|
||||||
|
|
||||||
|
if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
|
||||||
|
this.component.target.hasComplexBindings = true;
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.events[0] === 'resize') {
|
||||||
|
this.component.target.hasComplexBindings = true;
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`#component.root._beforecreate.push(${handler});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
addAttributes(block: Block) {
|
||||||
|
if (this.node.attributes.find(attr => attr.type === 'Spread')) {
|
||||||
|
this.addSpreadAttributes(block);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attributes.forEach((attribute: Attribute) => {
|
||||||
|
if (attribute.node.name === 'class' && attribute.node.isDynamic) {
|
||||||
|
this.classDependencies.push(...attribute.node.dependencies);
|
||||||
|
}
|
||||||
|
attribute.render(block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addSpreadAttributes(block: Block) {
|
||||||
|
const levels = block.getUniqueName(`${this.var}_levels`);
|
||||||
|
const data = block.getUniqueName(`${this.var}_data`);
|
||||||
|
|
||||||
|
const initialProps = [];
|
||||||
|
const updates = [];
|
||||||
|
|
||||||
|
this.node.attributes
|
||||||
|
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
|
||||||
|
.forEach(attr => {
|
||||||
|
const condition = attr.dependencies.size > 0
|
||||||
|
? `(${[...attr.dependencies].map(d => `changed.${d}`).join(' || ')})`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (attr.isSpread) {
|
||||||
|
const { snippet, dependencies } = attr.expression;
|
||||||
|
|
||||||
|
initialProps.push(snippet);
|
||||||
|
|
||||||
|
updates.push(condition ? `${condition} && ${snippet}` : snippet);
|
||||||
|
} else {
|
||||||
|
const snippet = `{ ${quoteNameIfNecessary(attr.name)}: ${attr.getValue()} }`;
|
||||||
|
initialProps.push(snippet);
|
||||||
|
|
||||||
|
updates.push(condition ? `${condition} && ${snippet}` : snippet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
var ${levels} = [
|
||||||
|
${initialProps.join(',\n')}
|
||||||
|
];
|
||||||
|
|
||||||
|
var ${data} = {};
|
||||||
|
for (var #i = 0; #i < ${levels}.length; #i += 1) {
|
||||||
|
${data} = @assign(${data}, ${levels}[#i]);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`@setAttributes(${this.var}, ${data});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
@setAttributes(${this.var}, @getSpreadUpdate(${levels}, [
|
||||||
|
${updates.join(',\n')}
|
||||||
|
]));
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventHandlers(block: Block) {
|
||||||
|
const { renderer } = this;
|
||||||
|
const { component } = renderer;
|
||||||
|
|
||||||
|
this.node.handlers.forEach(handler => {
|
||||||
|
const isCustomEvent = component.events.has(handler.name);
|
||||||
|
|
||||||
|
if (handler.callee) {
|
||||||
|
handler.render(this.component, block, handler.shouldHoist);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = handler.shouldHoist ? 'this' : this.var;
|
||||||
|
|
||||||
|
// get a name for the event handler that is globally unique
|
||||||
|
// if hoisted, locally unique otherwise
|
||||||
|
const handlerName = (handler.shouldHoist ? component : block).getUniqueName(
|
||||||
|
`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
|
||||||
|
);
|
||||||
|
|
||||||
|
const component_name = block.alias('component'); // can't use #component, might be hoisted
|
||||||
|
|
||||||
|
// create the handler body
|
||||||
|
const handlerBody = deindent`
|
||||||
|
${handler.shouldHoist && (
|
||||||
|
handler.usesComponent || handler.usesContext
|
||||||
|
? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;`
|
||||||
|
: null
|
||||||
|
)}
|
||||||
|
|
||||||
|
${handler.snippet ?
|
||||||
|
handler.snippet :
|
||||||
|
`${component_name}.fire("${handler.name}", event);`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (isCustomEvent) {
|
||||||
|
block.addVariable(handlerName);
|
||||||
|
|
||||||
|
block.builders.hydrate.addBlock(deindent`
|
||||||
|
${handlerName} = %events-${handler.name}.call(${component_name}, ${this.var}, function(event) {
|
||||||
|
${handlerBody}
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(deindent`
|
||||||
|
${handlerName}.destroy();
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
const handlerFunction = deindent`
|
||||||
|
function ${handlerName}(event) {
|
||||||
|
${handlerBody}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (handler.shouldHoist) {
|
||||||
|
component.target.blocks.push(handlerFunction);
|
||||||
|
} else {
|
||||||
|
block.builders.init.addBlock(handlerFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(
|
||||||
|
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addRef(block: Block) {
|
||||||
|
const ref = `#component.refs.${this.ref.name}`;
|
||||||
|
|
||||||
|
block.builders.mount.addLine(
|
||||||
|
`${ref} = ${this.var};`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(
|
||||||
|
`if (${ref} === ${this.var}) ${ref} = null;`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addTransitions(
|
||||||
|
block: Block
|
||||||
|
) {
|
||||||
|
const { intro, outro } = this;
|
||||||
|
|
||||||
|
if (!intro && !outro) return;
|
||||||
|
|
||||||
|
if (intro === outro) {
|
||||||
|
const name = block.getUniqueName(`${this.var}_transition`);
|
||||||
|
const snippet = intro.expression
|
||||||
|
? intro.expression.snippet
|
||||||
|
: '{}';
|
||||||
|
|
||||||
|
block.addVariable(name);
|
||||||
|
|
||||||
|
const fn = `%transitions-${intro.name}`;
|
||||||
|
|
||||||
|
block.builders.intro.addConditional(`#component.root._intro`, deindent`
|
||||||
|
if (${name}) ${name}.invalidate();
|
||||||
|
|
||||||
|
#component.root._aftercreate.push(() => {
|
||||||
|
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
|
||||||
|
${name}.run(1);
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.outro.addBlock(deindent`
|
||||||
|
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false);
|
||||||
|
${name}.run(0, () => {
|
||||||
|
#outrocallback();
|
||||||
|
${name} = null;
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.destroy.addConditional('detach', `if (${name}) ${name}.abort();`);
|
||||||
|
} else {
|
||||||
|
const introName = intro && block.getUniqueName(`${this.var}_intro`);
|
||||||
|
const outroName = outro && block.getUniqueName(`${this.var}_outro`);
|
||||||
|
|
||||||
|
if (intro) {
|
||||||
|
block.addVariable(introName);
|
||||||
|
const snippet = intro.expression
|
||||||
|
? intro.expression.snippet
|
||||||
|
: '{}';
|
||||||
|
|
||||||
|
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
|
||||||
|
|
||||||
|
if (outro) {
|
||||||
|
block.builders.intro.addBlock(deindent`
|
||||||
|
if (${introName}) ${introName}.abort(1);
|
||||||
|
if (${outroName}) ${outroName}.abort(1);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.intro.addConditional(`#component.root._intro`, deindent`
|
||||||
|
#component.root._aftercreate.push(() => {
|
||||||
|
${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
|
||||||
|
${introName}.run(1);
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outro) {
|
||||||
|
block.addVariable(outroName);
|
||||||
|
const snippet = outro.expression
|
||||||
|
? outro.expression.snippet
|
||||||
|
: '{}';
|
||||||
|
|
||||||
|
const fn = `%transitions-${outro.name}`;
|
||||||
|
|
||||||
|
block.builders.intro.addBlock(deindent`
|
||||||
|
if (${outroName}) ${outroName}.abort(1);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// TODO hide elements that have outro'd (unless they belong to a still-outroing
|
||||||
|
// group) prior to their removal from the DOM
|
||||||
|
block.builders.outro.addBlock(deindent`
|
||||||
|
${outroName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false);
|
||||||
|
${outroName}.run(0, #outrocallback);
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.destroy.addConditional('detach', `if (${outroName}) ${outroName}.abort();`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addAnimation(block: Block) {
|
||||||
|
if (!this.node.animation) return;
|
||||||
|
|
||||||
|
const rect = block.getUniqueName('rect');
|
||||||
|
const animation = block.getUniqueName('animation');
|
||||||
|
|
||||||
|
block.addVariable(rect);
|
||||||
|
block.addVariable(animation);
|
||||||
|
|
||||||
|
block.builders.measure.addBlock(deindent`
|
||||||
|
${rect} = ${this.var}.getBoundingClientRect();
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.fix.addBlock(deindent`
|
||||||
|
@fixPosition(${this.var});
|
||||||
|
if (${animation}) ${animation}.stop();
|
||||||
|
`);
|
||||||
|
|
||||||
|
const params = this.node.animation.expression ? this.node.animation.expression.snippet : '{}';
|
||||||
|
block.builders.animate.addBlock(deindent`
|
||||||
|
if (${animation}) ${animation}.stop();
|
||||||
|
${animation} = @wrapAnimation(${this.var}, ${rect}, %animations-${this.node.animation.name}, ${params});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
addActions(block: Block) {
|
||||||
|
this.node.actions.forEach(action => {
|
||||||
|
const { expression } = action;
|
||||||
|
let snippet, dependencies;
|
||||||
|
if (expression) {
|
||||||
|
snippet = expression.snippet;
|
||||||
|
dependencies = expression.dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = block.getUniqueName(
|
||||||
|
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.addVariable(name);
|
||||||
|
const fn = `%actions-${action.name}`;
|
||||||
|
|
||||||
|
block.builders.mount.addLine(
|
||||||
|
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dependencies && dependencies.size > 0) {
|
||||||
|
let conditional = `typeof ${name}.update === 'function' && `;
|
||||||
|
const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
|
||||||
|
conditional += dependencies.size > 1 ? `(${deps})` : deps;
|
||||||
|
|
||||||
|
block.builders.update.addConditional(
|
||||||
|
conditional,
|
||||||
|
`${name}.update.call(#component, ${snippet});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(
|
||||||
|
`if (typeof ${name}.destroy === 'function') ${name}.destroy.call(#component);`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addClasses(block: Block) {
|
||||||
|
this.node.classes.forEach(classDir => {
|
||||||
|
const { expression, name } = classDir;
|
||||||
|
let snippet, dependencies;
|
||||||
|
if (expression) {
|
||||||
|
snippet = expression.snippet;
|
||||||
|
dependencies = expression.dependencies;
|
||||||
|
} else {
|
||||||
|
snippet = `ctx${quotePropIfNecessary(name)}`;
|
||||||
|
dependencies = [name];
|
||||||
|
}
|
||||||
|
const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`;
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(updater);
|
||||||
|
|
||||||
|
if ((dependencies && dependencies.size > 0) || this.classDependencies.length) {
|
||||||
|
const allDeps = this.classDependencies.concat(...dependencies);
|
||||||
|
const deps = allDeps.map(dependency => `changed${quotePropIfNecessary(dependency)}`).join(' || ');
|
||||||
|
const condition = allDeps.length > 1 ? `(${deps})` : deps;
|
||||||
|
|
||||||
|
block.builders.update.addConditional(
|
||||||
|
condition,
|
||||||
|
updater
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getStaticAttributeValue(name: string) {
|
||||||
|
const attribute = this.node.attributes.find(
|
||||||
|
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!attribute) return null;
|
||||||
|
|
||||||
|
if (attribute.isTrue) return true;
|
||||||
|
if (attribute.chunks.length === 0) return '';
|
||||||
|
|
||||||
|
if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
|
||||||
|
return attribute.chunks[0].data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMediaNode() {
|
||||||
|
return this.node.name === 'audio' || this.node.name === 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
remount(name: string) {
|
||||||
|
const slot = this.attributes.find(attribute => attribute.name === 'slot');
|
||||||
|
if (slot) {
|
||||||
|
const prop = quotePropIfNecessary(slot.chunks[0].data);
|
||||||
|
return `@append(${name}._slotted${prop}, ${this.var});`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `@append(${name}._slotted.default, ${this.var});`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addCssClass(className = this.component.stylesheet.id) {
|
||||||
|
const classAttribute = this.attributes.find(a => a.name === 'class');
|
||||||
|
if (classAttribute && !classAttribute.isTrue) {
|
||||||
|
if (classAttribute.chunks.length === 1 && classAttribute.chunks[0].type === 'Text') {
|
||||||
|
(<Text>classAttribute.chunks[0]).data += ` ${className}`;
|
||||||
|
} else {
|
||||||
|
(<Node[]>classAttribute.chunks).push(
|
||||||
|
new Text(this.component, this, this.scope, {
|
||||||
|
type: 'Text',
|
||||||
|
data: ` ${className}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.attributes.push(
|
||||||
|
new Attribute(this.component, this, this.scope, {
|
||||||
|
type: 'Attribute',
|
||||||
|
name: 'class',
|
||||||
|
value: [{ type: 'Text', data: className }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyAttributeValue(value: Node[] | true) {
|
||||||
|
if (value === true) return '';
|
||||||
|
if (value.length === 0) return `=""`;
|
||||||
|
|
||||||
|
const data = value[0].data;
|
||||||
|
return `=${JSON.stringify(data)}`;
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
import Wrapper from './shared/wrapper';
|
||||||
|
import EachBlock from './EachBlock';
|
||||||
|
import Element from './Element';
|
||||||
|
import MustacheTag from './MustacheTag';
|
||||||
|
import Text from './Text';
|
||||||
|
import Window from './Window';
|
||||||
|
import Node from '../../nodes/shared/Node';
|
||||||
|
import { trimStart, trimEnd } from '../../../utils/trim';
|
||||||
|
import TextWrapper from './Text';
|
||||||
|
import Renderer from '../Renderer';
|
||||||
|
import Block from '../Block';
|
||||||
|
|
||||||
|
const wrappers = {
|
||||||
|
Comment: null,
|
||||||
|
EachBlock,
|
||||||
|
Element,
|
||||||
|
MustacheTag,
|
||||||
|
Text,
|
||||||
|
Window
|
||||||
|
};
|
||||||
|
|
||||||
|
function link(next: Wrapper, prev: Wrapper) {
|
||||||
|
prev.next = next;
|
||||||
|
if (next) next.prev = prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FragmentWrapper {
|
||||||
|
nodes: Wrapper[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: Renderer,
|
||||||
|
block: Block,
|
||||||
|
nodes: Node[],
|
||||||
|
parent: Wrapper,
|
||||||
|
stripWhitespace: boolean,
|
||||||
|
nextSibling: Wrapper
|
||||||
|
) {
|
||||||
|
this.nodes = [];
|
||||||
|
|
||||||
|
let lastChild: Wrapper;
|
||||||
|
let windowWrapper;
|
||||||
|
|
||||||
|
let i = nodes.length;
|
||||||
|
while (i--) {
|
||||||
|
const child = nodes[i];
|
||||||
|
|
||||||
|
if (!(child.type in wrappers)) {
|
||||||
|
throw new Error(`TODO implement ${child.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// special case — this is an easy way to remove whitespace surrounding
|
||||||
|
// <svelte:window/>. lil hacky but it works
|
||||||
|
if (child.type === 'Window') {
|
||||||
|
windowWrapper = new Window(renderer, block, parent, child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.type === 'Text') {
|
||||||
|
let { data } = 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 (this.nodes.length === 0) {
|
||||||
|
const shouldTrim = (
|
||||||
|
nextSibling ? (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)) : !child.hasAncestor('EachBlock')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldTrim) {
|
||||||
|
data = trimEnd(data);
|
||||||
|
if (!data) continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// glue text nodes (which could e.g. be separated by comments) together
|
||||||
|
if (lastChild && lastChild.type === 'Text') {
|
||||||
|
lastChild.data = data + lastChild.data;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = new TextWrapper(renderer, block, parent, child);
|
||||||
|
this.nodes.unshift(wrapper);
|
||||||
|
|
||||||
|
link(lastChild, lastChild = wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
const Wrapper = wrappers[child.type];
|
||||||
|
if (!Wrapper) return;
|
||||||
|
|
||||||
|
const wrapper = new Wrapper(renderer, block, parent, child, stripWhitespace, lastChild || nextSibling);
|
||||||
|
this.nodes.unshift(wrapper);
|
||||||
|
|
||||||
|
link(lastChild, lastChild = wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowWrapper) {
|
||||||
|
this.nodes.unshift(windowWrapper);
|
||||||
|
link(lastChild, windowWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block, parentNode: string, parentNodes: string) {
|
||||||
|
for (let i = 0; i < this.nodes.length; i += 1) {
|
||||||
|
this.nodes[i].render(block, parentNode, parentNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import Renderer from '../Renderer';
|
||||||
|
import Block from '../Block';
|
||||||
|
import Node from '../../nodes/shared/Node';
|
||||||
|
import Tag from './shared/Tag';
|
||||||
|
|
||||||
|
export default class MustacheTagWrapper extends Tag {
|
||||||
|
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
|
||||||
|
super(renderer, block, parent, node);
|
||||||
|
this.cannotUseInnerHTML();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block, parentNode: string, parentNodes: string) {
|
||||||
|
const { init } = this.renameThisMethod(
|
||||||
|
block,
|
||||||
|
value => `@setData(${this.var}, ${value});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.addElement(
|
||||||
|
this.var,
|
||||||
|
`@createText(${init})`,
|
||||||
|
parentNodes && `@claimText(${parentNodes}, ${init})`,
|
||||||
|
parentNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
remount(name: string) {
|
||||||
|
return `@append(${name}._slotted.default, ${this.var});`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import Renderer from '../Renderer';
|
||||||
|
import Block from '../Block';
|
||||||
|
import Text from '../../nodes/Text';
|
||||||
|
import Wrapper from './shared/wrapper';
|
||||||
|
import { CompileOptions } from '../../../interfaces';
|
||||||
|
import { stringify } from '../../../utils/stringify';
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
'optgroup',
|
||||||
|
'select',
|
||||||
|
'video',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO this should probably be in Fragment
|
||||||
|
function shouldSkip(node: Text) {
|
||||||
|
if (/\S/.test(node.data)) return false;
|
||||||
|
|
||||||
|
const parentElement = node.findNearest(/(?:Element|InlineComponent|Head)/);
|
||||||
|
if (!parentElement) return false;
|
||||||
|
|
||||||
|
if (parentElement.type === 'Head') return true;
|
||||||
|
if (parentElement.type === 'InlineComponent') return parentElement.children.length === 1 && node === parentElement.children[0];
|
||||||
|
|
||||||
|
return parentElement.namespace || elementsWithoutText.has(parentElement.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TextWrapper extends Wrapper {
|
||||||
|
node: Text;
|
||||||
|
var: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: Renderer,
|
||||||
|
block: Block,
|
||||||
|
parent: Wrapper,
|
||||||
|
node: Text
|
||||||
|
) {
|
||||||
|
super(renderer, block, parent, node);
|
||||||
|
this.var = 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block, parentNode: string, parentNodes: string) {
|
||||||
|
block.addElement(
|
||||||
|
this.var,
|
||||||
|
`@createText(${stringify(this.node.data)})`,
|
||||||
|
parentNodes && `@claimText(${parentNodes}, ${stringify(this.node.data)})`,
|
||||||
|
parentNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
remount(name: string) {
|
||||||
|
return `@append(${name}._slotted.default, ${this.var});`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,198 @@
|
|||||||
|
import Renderer from '../Renderer';
|
||||||
|
import Block from '../Block';
|
||||||
|
import Node from '../../nodes/shared/Node';
|
||||||
|
import Wrapper from './shared/wrapper';
|
||||||
|
import deindent from '../../../utils/deindent';
|
||||||
|
|
||||||
|
const associatedEvents = {
|
||||||
|
innerWidth: 'resize',
|
||||||
|
innerHeight: 'resize',
|
||||||
|
outerWidth: 'resize',
|
||||||
|
outerHeight: 'resize',
|
||||||
|
|
||||||
|
scrollX: 'scroll',
|
||||||
|
scrollY: 'scroll',
|
||||||
|
};
|
||||||
|
|
||||||
|
const properties = {
|
||||||
|
scrollX: 'pageXOffset',
|
||||||
|
scrollY: 'pageYOffset'
|
||||||
|
};
|
||||||
|
|
||||||
|
const readonly = new Set([
|
||||||
|
'innerWidth',
|
||||||
|
'innerHeight',
|
||||||
|
'outerWidth',
|
||||||
|
'outerHeight',
|
||||||
|
'online',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default class WindowWrapper extends Wrapper {
|
||||||
|
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
|
||||||
|
super(renderer, block, parent, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block, parentNode: string, parentNodes: string) {
|
||||||
|
const { renderer } = this;
|
||||||
|
const { component } = renderer;
|
||||||
|
|
||||||
|
const events = {};
|
||||||
|
const bindings: Record<string, string> = {};
|
||||||
|
|
||||||
|
this.node.handlers.forEach(handler => {
|
||||||
|
// TODO verify that it's a valid callee (i.e. built-in or declared method)
|
||||||
|
component.addSourcemapLocations(handler.expression);
|
||||||
|
|
||||||
|
const isCustomEvent = component.events.has(handler.name);
|
||||||
|
|
||||||
|
let usesState = handler.dependencies.size > 0;
|
||||||
|
|
||||||
|
handler.render(component, block, false); // TODO hoist?
|
||||||
|
|
||||||
|
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
|
||||||
|
const handlerBody = deindent`
|
||||||
|
${usesState && `var ctx = #component.get();`}
|
||||||
|
${handler.snippet};
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (isCustomEvent) {
|
||||||
|
// TODO dry this out
|
||||||
|
block.addVariable(handlerName);
|
||||||
|
|
||||||
|
block.builders.hydrate.addBlock(deindent`
|
||||||
|
${handlerName} = %events-${handler.name}.call(#component, window, function(event) {
|
||||||
|
${handlerBody}
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(deindent`
|
||||||
|
${handlerName}.destroy();
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
function ${handlerName}(event) {
|
||||||
|
${handlerBody}
|
||||||
|
}
|
||||||
|
window.addEventListener("${handler.name}", ${handlerName});
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.destroy.addBlock(deindent`
|
||||||
|
window.removeEventListener("${handler.name}", ${handlerName});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.node.bindings.forEach(binding => {
|
||||||
|
// in dev mode, throw if read-only values are written to
|
||||||
|
if (readonly.has(binding.name)) {
|
||||||
|
renderer.readonly.add(binding.value.node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings[binding.name] = binding.value.node.name;
|
||||||
|
|
||||||
|
// bind:online is a special case, we need to listen for two separate events
|
||||||
|
if (binding.name === 'online') return;
|
||||||
|
|
||||||
|
const associatedEvent = associatedEvents[binding.name];
|
||||||
|
const property = properties[binding.name] || binding.name;
|
||||||
|
|
||||||
|
if (!events[associatedEvent]) events[associatedEvent] = [];
|
||||||
|
events[associatedEvent].push(
|
||||||
|
`${binding.value.node.name}: this.${property}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// add initial value
|
||||||
|
renderer.metaBindings.push(
|
||||||
|
`this._state.${binding.value.node.name} = window.${property};`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const lock = block.getUniqueName(`window_updating`);
|
||||||
|
const clear = block.getUniqueName(`clear_window_updating`);
|
||||||
|
const timeout = block.getUniqueName(`window_updating_timeout`);
|
||||||
|
|
||||||
|
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');
|
||||||
|
block.addVariable(clear, `function() { ${lock} = false; }`);
|
||||||
|
block.addVariable(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerBody = deindent`
|
||||||
|
${event === 'scroll' && deindent`
|
||||||
|
if (${lock}) return;
|
||||||
|
${lock} = true;
|
||||||
|
`}
|
||||||
|
${component.options.dev && `component._updatingReadonlyProperty = true;`}
|
||||||
|
|
||||||
|
#component.set({
|
||||||
|
${props}
|
||||||
|
});
|
||||||
|
|
||||||
|
${component.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) {
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
#component.on("state", ({ changed, current }) => {
|
||||||
|
if (${
|
||||||
|
[bindings.scrollX, bindings.scrollY].map(
|
||||||
|
binding => binding && `changed["${binding}"]`
|
||||||
|
).filter(Boolean).join(' || ')
|
||||||
|
}) {
|
||||||
|
${lock} = true;
|
||||||
|
clearTimeout(${timeout});
|
||||||
|
window.scrollTo(${
|
||||||
|
bindings.scrollX ? `current["${bindings.scrollX}"]` : `window.pageXOffset`
|
||||||
|
}, ${
|
||||||
|
bindings.scrollY ? `current["${bindings.scrollY}"]` : `window.pageYOffset`
|
||||||
|
});
|
||||||
|
${timeout} = setTimeout(${clear}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.options.dev && `component._updatingReadonlyProperty = true;`}
|
||||||
|
#component.set({ ${bindings.online}: navigator.onLine });
|
||||||
|
${component.options.dev && `component._updatingReadonlyProperty = false;`}
|
||||||
|
}
|
||||||
|
window.addEventListener("online", ${handlerName});
|
||||||
|
window.addEventListener("offline", ${handlerName});
|
||||||
|
`);
|
||||||
|
|
||||||
|
// add initial value
|
||||||
|
renderer.metaBindings.push(
|
||||||
|
`this._state.${bindings.online} = navigator.onLine;`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.destroy.addBlock(deindent`
|
||||||
|
window.removeEventListener("online", ${handlerName});
|
||||||
|
window.removeEventListener("offline", ${handlerName});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
import Wrapper from './Wrapper';
|
||||||
|
import Renderer from '../../Renderer';
|
||||||
|
import Block from '../../Block';
|
||||||
|
import Node from '../../../nodes/shared/Node';
|
||||||
|
|
||||||
|
export default class Tag extends Wrapper {
|
||||||
|
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
|
||||||
|
super(renderer, block, parent, node);
|
||||||
|
this.cannotUseInnerHTML();
|
||||||
|
|
||||||
|
this.var = this.type === 'MustacheTag' ? 'text' : 'raw';
|
||||||
|
block.addDependencies(node.expression.dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block, parentNode: string, parentNodes: string) {
|
||||||
|
const { init } = this.renameThisMethod(
|
||||||
|
block,
|
||||||
|
value => `@setData(${this.var}, ${value});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.addElement(
|
||||||
|
this.var,
|
||||||
|
`@createText(${init})`,
|
||||||
|
parentNodes && `@claimText(${parentNodes}, ${init})`,
|
||||||
|
parentNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renameThisMethod(
|
||||||
|
block: Block,
|
||||||
|
update: ((value: string) => string)
|
||||||
|
) {
|
||||||
|
const { snippet, dependencies } = this.node.expression;
|
||||||
|
|
||||||
|
const value = this.node.shouldCache && block.getUniqueName(`${this.var}_value`);
|
||||||
|
const content = this.node.shouldCache ? value : snippet;
|
||||||
|
|
||||||
|
if (this.node.shouldCache) block.addVariable(value, snippet);
|
||||||
|
|
||||||
|
if (dependencies.size) {
|
||||||
|
const changedCheck = (
|
||||||
|
(block.hasOutros ? `!#current || ` : '') +
|
||||||
|
[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
|
||||||
|
|
||||||
|
const condition = this.node.shouldCache ?
|
||||||
|
(dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
|
||||||
|
changedCheck;
|
||||||
|
|
||||||
|
block.builders.update.addConditional(
|
||||||
|
condition,
|
||||||
|
update(content)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { init: content };
|
||||||
|
}
|
||||||
|
|
||||||
|
remount(name: string) {
|
||||||
|
return `@append(${name}._slotted.default, ${this.var});`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import Renderer from '../../Renderer';
|
||||||
|
import Node from '../../../nodes/shared/Node';
|
||||||
|
import { CompileOptions } from '../../../../interfaces';
|
||||||
|
import Block from '../../Block';
|
||||||
|
|
||||||
|
export default class Wrapper {
|
||||||
|
renderer: Renderer;
|
||||||
|
parent: Wrapper;
|
||||||
|
node: Node;
|
||||||
|
|
||||||
|
prev: Wrapper | null;
|
||||||
|
next: Wrapper | null;
|
||||||
|
|
||||||
|
canUseInnerHTML: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: Renderer,
|
||||||
|
block: Block,
|
||||||
|
parent: Wrapper,
|
||||||
|
node: Node
|
||||||
|
) {
|
||||||
|
this.renderer = renderer;
|
||||||
|
this.parent = parent;
|
||||||
|
this.node = node;
|
||||||
|
|
||||||
|
this.canUseInnerHTML = !renderer.options.hydratable;
|
||||||
|
|
||||||
|
block.wrappers.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
cannotUseInnerHTML() {
|
||||||
|
this.canUseInnerHTML = false;
|
||||||
|
if (this.parent) this.parent.cannotUseInnerHTML();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdateMountNode(anchor: string) {
|
||||||
|
return (this.parent && this.parent.isDomNode())
|
||||||
|
? this.parent.var
|
||||||
|
: `${anchor}.parentNode`;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDomNode() {
|
||||||
|
return (
|
||||||
|
this.node.type === 'Element' ||
|
||||||
|
this.node.type === 'Text' ||
|
||||||
|
this.node.type === 'MustacheTag'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block, parentNode: string, parentNodes: string) {
|
||||||
|
throw new Error(`render method not implemented by subclass ${this.node.type}`);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue