import CodeBuilder from '../utils/CodeBuilder'; import deindent from '../utils/deindent'; import Renderer from './Renderer'; import Wrapper from './wrappers/shared/Wrapper'; import { escape } from '../utils/stringify'; 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[] = []; maintain_context: boolean; has_animation: boolean; has_intros: boolean; has_outros: boolean; has_intro_method: boolean; // could have the method without the transition, due to siblings has_outro_method: boolean; outros: number; aliases: Map<string, string>; variables: Map<string, string>; get_unique_name: (name: string) => string; has_update_method = 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.has_animation = false; this.has_intro_method = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros this.has_outro_method = false; this.outros = 0; this.get_unique_name = this.renderer.component.get_unique_name_maker(); this.variables = new Map(); this.aliases = new Map().set('ctx', this.get_unique_name('ctx')); if (this.key) this.aliases.set('key', this.get_unique_name('key')); } assign_variable_names() { 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.can_use_innerhtml) 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.get_unique_name(wrapper.var + i); } else { wrapper.var = this.get_unique_name(wrapper.var); } } } add_dependencies(dependencies: Set<string>) { dependencies.forEach(dependency => { this.dependencies.add(dependency); }); this.has_update_method = true; } add_element( name: string, render_statement: string, claim_statement: string, parent_node: string, no_detach?: boolean ) { this.add_variable(name); this.builders.create.add_line(`${name} = ${render_statement};`); if (this.renderer.options.hydratable) { this.builders.claim.add_line(`${name} = ${claim_statement || render_statement};`); } if (parent_node) { this.builders.mount.add_line(`@append(${parent_node}, ${name});`); if (parent_node === '@_document.head' && !no_detach) this.builders.destroy.add_line(`@detach(${name});`); } else { this.builders.mount.add_line(`@insert(#target, ${name}, anchor);`); if (!no_detach) this.builders.destroy.add_conditional('detaching', `@detach(${name});`); } } add_intro(local?: boolean) { this.has_intros = this.has_intro_method = true; if (!local && this.parent) this.parent.add_intro(); } add_outro(local?: boolean) { this.has_outros = this.has_outro_method = true; this.outros += 1; if (!local && this.parent) this.parent.add_outro(); } add_animation() { this.has_animation = true; } add_variable(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.get_unique_name(name)); } return this.aliases.get(name); } child(options: BlockOptions) { return new Block(Object.assign({}, this, { key: null }, options, { parent: this })); } get_contents(local_key?: string) { const { dev } = this.renderer.options; if (this.has_outros) { this.add_variable('#current'); if (!this.builders.intro.is_empty()) { this.builders.intro.add_line(`#current = true;`); this.builders.mount.add_line(`#current = true;`); } if (!this.builders.outro.is_empty()) { this.builders.outro.add_line(`#current = false;`); } } if (this.autofocus) { this.builders.mount.add_line(`${this.autofocus}.focus();`); } this.render_listeners(); const properties = new CodeBuilder(); const method_name = (short: string, long: string) => dev ? `${short}: function ${this.get_unique_name(long)}` : short; if (local_key) { properties.add_block(`key: ${local_key},`); } if (this.first) { properties.add_block(`first: null,`); this.builders.hydrate.add_line(`this.first = ${this.first};`); } if (this.builders.create.is_empty() && this.builders.hydrate.is_empty()) { properties.add_line(`c: @noop,`); } else { const hydrate = !this.builders.hydrate.is_empty() && ( this.renderer.options.hydratable ? `this.h()` : this.builders.hydrate ); properties.add_block(deindent` ${method_name('c', 'create')}() { ${this.builders.create} ${hydrate} }, `); } if (this.renderer.options.hydratable || !this.builders.claim.is_empty()) { if (this.builders.claim.is_empty() && this.builders.hydrate.is_empty()) { properties.add_line(`l: @noop,`); } else { properties.add_block(deindent` ${method_name('l', 'claim')}(nodes) { ${this.builders.claim} ${this.renderer.options.hydratable && !this.builders.hydrate.is_empty() && `this.h();`} }, `); } } if (this.renderer.options.hydratable && !this.builders.hydrate.is_empty()) { properties.add_block(deindent` ${method_name('h', 'hydrate')}() { ${this.builders.hydrate} }, `); } if (this.builders.mount.is_empty()) { properties.add_line(`m: @noop,`); } else { properties.add_block(deindent` ${method_name('m', 'mount')}(#target, anchor) { ${this.builders.mount} }, `); } if (this.has_update_method || this.maintain_context) { if (this.builders.update.is_empty() && !this.maintain_context) { properties.add_line(`p: @noop,`); } else { properties.add_block(deindent` ${method_name('p', 'update')}(changed, ${this.maintain_context ? 'new_ctx' : 'ctx'}) { ${this.maintain_context && `ctx = new_ctx;`} ${this.builders.update} }, `); } } if (this.has_animation) { properties.add_block(deindent` ${method_name('r', 'measure')}() { ${this.builders.measure} }, ${method_name('f', 'fix')}() { ${this.builders.fix} }, ${method_name('a', 'animate')}() { ${this.builders.animate} }, `); } if (this.has_intro_method || this.has_outro_method) { if (this.builders.intro.is_empty()) { properties.add_line(`i: @noop,`); } else { properties.add_block(deindent` ${method_name('i', 'intro')}(#local) { ${this.has_outros && `if (#current) return;`} ${this.builders.intro} }, `); } if (this.builders.outro.is_empty()) { properties.add_line(`o: @noop,`); } else { properties.add_block(deindent` ${method_name('o', 'outro')}(#local) { ${this.builders.outro} }, `); } } if (this.builders.destroy.is_empty()) { properties.add_line(`d: @noop`); } else { properties.add_block(deindent` ${method_name('d', 'destroy')}(detaching) { ${this.builders.destroy} } `); } /* eslint-disable @typescript-eslint/indent,indent */ 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.is_empty() && this.builders.init} return { ${properties} }; `.replace(/(#+)(\w*)/g, (_match: string, sigil: string, name: string) => { return sigil === '#' ? this.alias(name) : sigil.slice(1) + name; }); /* eslint-enable @typescript-eslint/indent,indent */ } render_listeners(chunk: string = '') { if (this.event_listeners.length > 0) { this.add_variable(`#dispose${chunk}`); if (this.event_listeners.length === 1) { this.builders.hydrate.add_line( `#dispose${chunk} = ${this.event_listeners[0]};` ); this.builders.destroy.add_line( `#dispose${chunk}();` ); } else { this.builders.hydrate.add_block(deindent` #dispose${chunk} = [ ${this.event_listeners.join(',\n')} ]; `); this.builders.destroy.add_line( `@run_all(#dispose${chunk});` ); } } } toString() { const local_key = this.key && this.get_unique_name('key'); return deindent` ${this.comment && `// ${escape(this.comment, { only_escape_at_symbol: true })}`} function ${this.name}(${this.key ? `${local_key}, ` : ''}ctx) { ${this.get_contents(local_key)} } `; } }