mirror of https://github.com/sveltejs/svelte
416 lines
9.9 KiB
416 lines
9.9 KiB
import CodeBuilder from '../../utils/CodeBuilder';
|
|
import deindent from '../../utils/deindent';
|
|
import Renderer from './Renderer';
|
|
import Wrapper from './wrappers/shared/Wrapper';
|
|
import EachBlockWrapper from './wrappers/EachBlock';
|
|
import InlineComponentWrapper from './wrappers/InlineComponent';
|
|
import ElementWrapper from './wrappers/Element';
|
|
|
|
export interface BlockOptions {
|
|
parent?: Block;
|
|
name: string;
|
|
renderer?: Renderer;
|
|
comment?: string;
|
|
key?: string;
|
|
bindings?: Map<string, () => { object: string, property: string, snippet: string }>;
|
|
dependencies?: Set<string>;
|
|
}
|
|
|
|
export default class Block {
|
|
parent?: Block;
|
|
renderer: Renderer;
|
|
name: string;
|
|
comment?: string;
|
|
|
|
wrappers: Wrapper[];
|
|
|
|
key: string;
|
|
first: string;
|
|
|
|
dependencies: Set<string>;
|
|
|
|
bindings: Map<string, { object: string, property: string, snippet: string }>;
|
|
|
|
builders: {
|
|
init: CodeBuilder;
|
|
create: CodeBuilder;
|
|
claim: CodeBuilder;
|
|
hydrate: CodeBuilder;
|
|
mount: CodeBuilder;
|
|
measure: CodeBuilder;
|
|
fix: CodeBuilder;
|
|
animate: CodeBuilder;
|
|
intro: CodeBuilder;
|
|
update: CodeBuilder;
|
|
outro: CodeBuilder;
|
|
destroy: CodeBuilder;
|
|
};
|
|
|
|
event_listeners: string[] = [];
|
|
|
|
maintainContext: boolean;
|
|
hasAnimation: boolean;
|
|
hasIntros: boolean;
|
|
hasOutros: boolean;
|
|
hasIntroMethod: boolean; // could have the method without the transition, due to siblings
|
|
hasOutroMethod: boolean;
|
|
outros: number;
|
|
|
|
aliases: Map<string, string>;
|
|
variables: Map<string, string>;
|
|
getUniqueName: (name: string) => string;
|
|
|
|
hasUpdateMethod = false;
|
|
autofocus: string;
|
|
|
|
constructor(options: BlockOptions) {
|
|
this.parent = options.parent;
|
|
this.renderer = options.renderer;
|
|
this.name = options.name;
|
|
this.comment = options.comment;
|
|
|
|
this.wrappers = [];
|
|
|
|
// for keyed each blocks
|
|
this.key = options.key;
|
|
this.first = null;
|
|
|
|
this.dependencies = new Set();
|
|
|
|
this.bindings = options.bindings;
|
|
|
|
this.builders = {
|
|
init: new CodeBuilder(),
|
|
create: new CodeBuilder(),
|
|
claim: new CodeBuilder(),
|
|
hydrate: new CodeBuilder(),
|
|
mount: new CodeBuilder(),
|
|
measure: new CodeBuilder(),
|
|
fix: new CodeBuilder(),
|
|
animate: new CodeBuilder(),
|
|
intro: new CodeBuilder(),
|
|
update: new CodeBuilder(),
|
|
outro: new CodeBuilder(),
|
|
destroy: new CodeBuilder(),
|
|
};
|
|
|
|
this.hasAnimation = false;
|
|
this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
|
|
this.hasOutroMethod = false;
|
|
this.outros = 0;
|
|
|
|
this.getUniqueName = this.renderer.component.getUniqueNameMaker();
|
|
this.variables = new Map();
|
|
|
|
this.aliases = new Map().set('ctx', this.getUniqueName('ctx'));
|
|
if (this.key) this.aliases.set('key', this.getUniqueName('key'));
|
|
}
|
|
|
|
assignVariableNames() {
|
|
const seen = new Set();
|
|
const dupes = new Set();
|
|
|
|
let i = this.wrappers.length;
|
|
|
|
while (i--) {
|
|
const wrapper = this.wrappers[i];
|
|
|
|
if (!wrapper.var) continue;
|
|
if (wrapper.parent && wrapper.parent.canUseInnerHTML) continue;
|
|
|
|
if (seen.has(wrapper.var)) {
|
|
dupes.add(wrapper.var);
|
|
}
|
|
|
|
seen.add(wrapper.var);
|
|
}
|
|
|
|
const counts = new Map();
|
|
i = this.wrappers.length;
|
|
|
|
while (i--) {
|
|
const wrapper = this.wrappers[i];
|
|
|
|
if (!wrapper.var) continue;
|
|
|
|
if (dupes.has(wrapper.var)) {
|
|
const i = counts.get(wrapper.var) || 0;
|
|
counts.set(wrapper.var, i + 1);
|
|
wrapper.var = this.getUniqueName(wrapper.var + i);
|
|
} else {
|
|
wrapper.var = this.getUniqueName(wrapper.var);
|
|
}
|
|
}
|
|
}
|
|
|
|
addDependencies(dependencies: Set<string>) {
|
|
dependencies.forEach(dependency => {
|
|
this.dependencies.add(dependency);
|
|
});
|
|
|
|
this.hasUpdateMethod = true;
|
|
}
|
|
|
|
addElement(
|
|
name: string,
|
|
renderStatement: string,
|
|
claimStatement: string,
|
|
parentNode: string,
|
|
noDetach?: boolean
|
|
) {
|
|
this.addVariable(name);
|
|
this.builders.create.addLine(`${name} = ${renderStatement};`);
|
|
|
|
if (this.renderer.options.hydratable) {
|
|
this.builders.claim.addLine(`${name} = ${claimStatement || renderStatement};`);
|
|
}
|
|
|
|
if (parentNode) {
|
|
this.builders.mount.addLine(`@append(${parentNode}, ${name});`);
|
|
if (parentNode === 'document.head') this.builders.destroy.addLine(`@detachNode(${name});`);
|
|
} else {
|
|
this.builders.mount.addLine(`@insert(#target, ${name}, anchor);`);
|
|
if (!noDetach) this.builders.destroy.addConditional('detach', `@detachNode(${name});`);
|
|
}
|
|
}
|
|
|
|
addIntro(local?: boolean) {
|
|
this.hasIntros = this.hasIntroMethod = this.renderer.hasIntroTransitions = true;
|
|
if (!local && this.parent) this.parent.addIntro();
|
|
}
|
|
|
|
addOutro(local?: boolean) {
|
|
this.hasOutros = this.hasOutroMethod = this.renderer.hasOutroTransitions = true;
|
|
this.outros += 1;
|
|
if (!local && this.parent) this.parent.addOutro();
|
|
}
|
|
|
|
addAnimation() {
|
|
this.hasAnimation = true;
|
|
}
|
|
|
|
addVariable(name: string, init?: string) {
|
|
if (name[0] === '#') {
|
|
name = this.alias(name.slice(1));
|
|
}
|
|
|
|
if (this.variables.has(name) && this.variables.get(name) !== init) {
|
|
throw new Error(
|
|
`Variable '${name}' already initialised with a different value`
|
|
);
|
|
}
|
|
|
|
this.variables.set(name, init);
|
|
}
|
|
|
|
alias(name: string) {
|
|
if (!this.aliases.has(name)) {
|
|
this.aliases.set(name, this.getUniqueName(name));
|
|
}
|
|
|
|
return this.aliases.get(name);
|
|
}
|
|
|
|
child(options: BlockOptions) {
|
|
return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
|
|
}
|
|
|
|
getContents(localKey?: string) {
|
|
const { dev } = this.renderer.options;
|
|
|
|
if (this.hasOutros) {
|
|
this.addVariable('#current');
|
|
|
|
if (!this.builders.intro.isEmpty()) {
|
|
this.builders.intro.addLine(`#current = true;`);
|
|
}
|
|
|
|
if (!this.builders.outro.isEmpty()) {
|
|
this.builders.outro.addLine(`#current = false;`);
|
|
}
|
|
}
|
|
|
|
if (this.autofocus) {
|
|
this.builders.mount.addLine(`${this.autofocus}.focus();`);
|
|
}
|
|
|
|
this.renderListeners();
|
|
|
|
const properties = new CodeBuilder();
|
|
|
|
if (localKey) {
|
|
properties.addBlock(`key: ${localKey},`);
|
|
}
|
|
|
|
if (this.first) {
|
|
properties.addBlock(`first: null,`);
|
|
this.builders.hydrate.addLine(`this.first = ${this.first};`);
|
|
}
|
|
|
|
if (this.builders.create.isEmpty() && this.builders.hydrate.isEmpty()) {
|
|
properties.addLine(`c: @noop,`);
|
|
} else {
|
|
const hydrate = !this.builders.hydrate.isEmpty() && (
|
|
this.renderer.options.hydratable
|
|
? `this.h()`
|
|
: this.builders.hydrate
|
|
);
|
|
|
|
properties.addBlock(deindent`
|
|
${dev ? 'c: function create' : 'c'}() {
|
|
${this.builders.create}
|
|
${hydrate}
|
|
},
|
|
`);
|
|
}
|
|
|
|
if (this.renderer.options.hydratable || !this.builders.claim.isEmpty()) {
|
|
if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) {
|
|
properties.addLine(`l: @noop,`);
|
|
} else {
|
|
properties.addBlock(deindent`
|
|
${dev ? 'l: function claim' : 'l'}(nodes) {
|
|
${this.builders.claim}
|
|
${this.renderer.options.hydratable && !this.builders.hydrate.isEmpty() && `this.h();`}
|
|
},
|
|
`);
|
|
}
|
|
}
|
|
|
|
if (this.renderer.options.hydratable && !this.builders.hydrate.isEmpty()) {
|
|
properties.addBlock(deindent`
|
|
${dev ? 'h: function hydrate' : 'h'}() {
|
|
${this.builders.hydrate}
|
|
},
|
|
`);
|
|
}
|
|
|
|
if (this.builders.mount.isEmpty()) {
|
|
properties.addLine(`m: @noop,`);
|
|
} else {
|
|
properties.addBlock(deindent`
|
|
${dev ? 'm: function mount' : 'm'}(#target, anchor) {
|
|
${this.builders.mount}
|
|
},
|
|
`);
|
|
}
|
|
|
|
if (this.hasUpdateMethod || this.maintainContext) {
|
|
if (this.builders.update.isEmpty() && !this.maintainContext) {
|
|
properties.addLine(`p: @noop,`);
|
|
} else {
|
|
properties.addBlock(deindent`
|
|
${dev ? 'p: function update' : 'p'}(changed, ${this.maintainContext ? 'new_ctx' : 'ctx'}) {
|
|
${this.maintainContext && `ctx = new_ctx;`}
|
|
${this.builders.update}
|
|
},
|
|
`);
|
|
}
|
|
}
|
|
|
|
if (this.hasAnimation) {
|
|
properties.addBlock(deindent`
|
|
${dev ? `r: function measure` : `r`}() {
|
|
${this.builders.measure}
|
|
},
|
|
|
|
${dev ? `f: function fix` : `f`}() {
|
|
${this.builders.fix}
|
|
},
|
|
|
|
${dev ? `a: function animate` : `a`}() {
|
|
${this.builders.animate}
|
|
},
|
|
`);
|
|
}
|
|
|
|
if (this.hasIntroMethod || this.hasOutroMethod) {
|
|
if (this.builders.intro.isEmpty()) {
|
|
properties.addLine(`i: @noop,`);
|
|
} else {
|
|
properties.addBlock(deindent`
|
|
${dev ? 'i: function intro' : 'i'}(#local) {
|
|
${this.hasOutros && `if (#current) return;`}
|
|
${this.builders.intro}
|
|
},
|
|
`);
|
|
}
|
|
|
|
if (this.builders.outro.isEmpty()) {
|
|
properties.addLine(`o: @noop,`);
|
|
} else {
|
|
properties.addBlock(deindent`
|
|
${dev ? 'o: function outro' : 'o'}(#local) {
|
|
${this.builders.outro}
|
|
},
|
|
`);
|
|
}
|
|
}
|
|
|
|
if (this.builders.destroy.isEmpty()) {
|
|
properties.addLine(`d: @noop`);
|
|
} else {
|
|
properties.addBlock(deindent`
|
|
${dev ? 'd: function destroy' : 'd'}(detach) {
|
|
${this.builders.destroy}
|
|
}
|
|
`);
|
|
}
|
|
|
|
return deindent`
|
|
${this.variables.size > 0 &&
|
|
`var ${Array.from(this.variables.keys())
|
|
.map(key => {
|
|
const init = this.variables.get(key);
|
|
return init !== undefined ? `${key} = ${init}` : key;
|
|
})
|
|
.join(', ')};`}
|
|
|
|
${!this.builders.init.isEmpty() && this.builders.init}
|
|
|
|
return {
|
|
${properties}
|
|
};
|
|
`.replace(/(#+)(\w*)/g, (match: string, sigil: string, name: string) => {
|
|
return sigil === '#' ? this.alias(name) : sigil.slice(1) + name;
|
|
});
|
|
}
|
|
|
|
renderListeners(chunk: string = '') {
|
|
if (this.event_listeners.length > 0) {
|
|
this.addVariable(`#dispose${chunk}`);
|
|
|
|
if (this.event_listeners.length === 1) {
|
|
this.builders.hydrate.addLine(
|
|
`#dispose${chunk} = ${this.event_listeners[0]};`
|
|
);
|
|
|
|
this.builders.destroy.addLine(
|
|
`#dispose${chunk}();`
|
|
)
|
|
} else {
|
|
this.builders.hydrate.addBlock(deindent`
|
|
#dispose${chunk} = [
|
|
${this.event_listeners.join(',\n')}
|
|
];
|
|
`);
|
|
|
|
this.builders.destroy.addLine(
|
|
`@run_all(#dispose${chunk});`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
toString() {
|
|
const localKey = this.key && this.getUniqueName('key');
|
|
|
|
return deindent`
|
|
${this.comment && `// ${this.comment}`}
|
|
function ${this.name}(${this.key ? `${localKey}, ` : ''}ctx) {
|
|
${this.getContents(localKey)}
|
|
}
|
|
`;
|
|
}
|
|
}
|