import { b, x } from 'code-red'; import { is_head } from './wrappers/shared/is_head.js'; import { regex_double_quotes } from '../../utils/patterns.js'; import { flatten } from '../../utils/flatten.js'; export default class Block { /** * @typedef {Object} Bindings * @property {import('estree').Identifier} object * @property {import('estree').Identifier} property * @property {import('estree').Node} snippet * @property {string} store * @property {(node:import('estree').Node) => import('estree').Node} modifier */ /** * @typedef {Object} BlockOptions * @property {Block} [parent] * @property {import('estree').Identifier} name * @property {string} type * @property {import('./Renderer.js').default} [renderer] * @property {string} [comment] * @property {import('estree').Identifier} [key] * @property {Map} [bindings] * @property {Set} [dependencies] */ /** @type {Block} */ parent; /** @type {import('./Renderer.js').default} */ renderer; /** @type {import('estree').Identifier} */ name; /** @type {string} */ type; /** @type {string} */ comment; /** @type {import('./wrappers/shared/Wrapper.js').default[]} */ wrappers; /** @type {import('estree').Identifier} */ key; /** @type {import('estree').Identifier} */ first; /** @type {Set} */ dependencies = new Set(); /** @type {Map} */ bindings; /** @type {Set} */ binding_group_initialised = new Set(); /** @type {Set} */ binding_groups = new Set(); /** * @type {{ * declarations: Array; * init: Array; * create: Array; * claim: Array; * hydrate: Array; * mount: Array; * measure: Array; * restore_measurements: Array; * fix: Array; * animate: Array; * intro: Array; * update: Array; * outro: Array; * destroy: Array; * }} */ chunks; /** @type {import('estree').Node[]} */ event_listeners = []; /** @type {boolean} */ maintain_context; /** @type {boolean} */ has_animation; /** @type {boolean} */ has_intros; /** @type {boolean} */ has_outros; /** @type {boolean} */ has_intro_method; // could have the method without the transition, due to siblings /** @type {boolean} */ has_outro_method; /** @type {number} */ outros; /** @type {Map} */ aliases; /** @type {Map} */ variables = new Map(); /** @type {(name: string) => import('estree').Identifier} */ get_unique_name; /** */ has_update_method = false; /** @type {{ element_var: string; condition_expression?: any }} */ autofocus; /** @param {BlockOptions} options */ constructor(options) { this.parent = options.parent; this.renderer = options.renderer; this.name = options.name; this.type = options.type; this.comment = options.comment; this.wrappers = []; // for keyed each blocks this.key = options.key; this.first = null; this.bindings = options.bindings; this.chunks = { declarations: [], init: [], create: [], claim: [], hydrate: [], mount: [], measure: [], restore_measurements: [], fix: [], animate: [], intro: [], update: [], outro: [], destroy: [] }; 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.aliases = new Map(); if (this.key) this.aliases.set('key', this.get_unique_name('key')); } assign_variable_names() { /** @type {Set} */ const seen = new Set(); /** @type {Set} */ const dupes = new Set(); let i = this.wrappers.length; while (i--) { const wrapper = this.wrappers[i]; if (!wrapper.var) continue; if (seen.has(wrapper.var.name)) { dupes.add(wrapper.var.name); } seen.add(wrapper.var.name); } const counts = new Map(); i = this.wrappers.length; while (i--) { const wrapper = this.wrappers[i]; if (!wrapper.var) continue; let suffix = ''; if (dupes.has(wrapper.var.name)) { const i = counts.get(wrapper.var.name) || 0; counts.set(wrapper.var.name, i + 1); suffix = i; } wrapper.var.name = this.get_unique_name(wrapper.var.name + suffix).name; } } /** @param {Set} dependencies */ add_dependencies(dependencies) { dependencies.forEach((dependency) => { this.dependencies.add(dependency); }); this.has_update_method = true; if (this.parent) { this.parent.add_dependencies(dependencies); } } /** * @param {import('estree').Identifier} id * @param {import('estree').Node} render_statement * @param {import('estree').Node} claim_statement * @param {import('estree').Node} parent_node * @param {boolean} [no_detach] */ add_element(id, render_statement, claim_statement, parent_node, no_detach) { this.add_variable(id); this.chunks.create.push(b`${id} = ${render_statement};`); if (this.renderer.options.hydratable) { this.chunks.claim.push(b`${id} = ${claim_statement || render_statement};`); } if (parent_node) { this.chunks.mount.push(b`@append(${parent_node}, ${id});`); if (is_head(parent_node) && !no_detach) this.chunks.destroy.push(b`@detach(${id});`); } else { this.chunks.mount.push(b`@insert(#target, ${id}, #anchor);`); if (!no_detach) this.chunks.destroy.push(b`if (detaching) @detach(${id});`); } } /** @param {boolean} [local] */ add_intro(local) { this.has_intros = this.has_intro_method = true; if (!local && this.parent) this.parent.add_intro(); } /** @param {boolean} [local] */ add_outro(local) { 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; } /** * @param {import('estree').Identifier} id * @param {import('estree').Node} [init] */ add_variable(id, init) { if (this.variables.has(id.name)) { throw new Error(`Variable '${id.name}' already initialised with a different value`); } this.variables.set(id.name, { id, init }); } /** @param {string} name */ alias(name) { if (!this.aliases.has(name)) { this.aliases.set(name, this.get_unique_name(name)); } return this.aliases.get(name); } /** @param {BlockOptions} options */ child(options) { return new Block(Object.assign({}, this, { key: null }, options, { parent: this })); } /** @param {any} [key] */ get_contents(key) { const { dev } = this.renderer.options; if (this.has_outros) { this.add_variable({ type: 'Identifier', name: '#current' }); if (this.chunks.intro.length > 0) { this.chunks.intro.push(b`#current = true;`); this.chunks.mount.push(b`#current = true;`); } if (this.chunks.outro.length > 0) { this.chunks.outro.push(b`#current = false;`); } } if (this.autofocus) { if (this.autofocus.condition_expression) { this.chunks.mount.push( b`if (${this.autofocus.condition_expression}) ${this.autofocus.element_var}.focus();` ); } else { this.chunks.mount.push(b`${this.autofocus.element_var}.focus();`); } } this.render_binding_groups(); this.render_listeners(); /** @type {Record} */ const properties = {}; const noop = x`@noop`; properties.key = key; if (this.first) { properties.first = x`null`; this.chunks.hydrate.push(b`this.first = ${this.first};`); } if (this.chunks.create.length === 0 && this.chunks.hydrate.length === 0) { properties.create = noop; } else { const hydrate = this.chunks.hydrate.length > 0 && (this.renderer.options.hydratable ? b`this.h();` : this.chunks.hydrate); properties.create = x`function #create() { ${this.chunks.create} ${hydrate} }`; } if (this.renderer.options.hydratable || this.chunks.claim.length > 0) { if (this.chunks.claim.length === 0 && this.chunks.hydrate.length === 0) { properties.claim = noop; } else { properties.claim = x`function #claim(#nodes) { ${this.chunks.claim} ${this.renderer.options.hydratable && this.chunks.hydrate.length > 0 && b`this.h();`} }`; } } if (this.renderer.options.hydratable && this.chunks.hydrate.length > 0) { properties.hydrate = x`function #hydrate() { ${this.chunks.hydrate} }`; } if (this.chunks.mount.length === 0) { properties.mount = noop; } else if (this.event_listeners.length === 0) { properties.mount = x`function #mount(#target, #anchor) { ${this.chunks.mount} }`; } else { properties.mount = x`function #mount(#target, #anchor) { ${this.chunks.mount} }`; } if (this.has_update_method || this.maintain_context) { if (this.chunks.update.length === 0 && !this.maintain_context) { properties.update = noop; } else { const ctx = this.maintain_context ? x`#new_ctx` : x`#ctx`; /** @type {import('estree').Identifier | import('estree').ArrayPattern} */ let dirty = { type: 'Identifier', name: '#dirty' }; if (!this.renderer.context_overflow && !this.parent) { dirty = { type: 'ArrayPattern', elements: [dirty] }; } properties.update = x`function #update(${ctx}, ${dirty}) { ${this.maintain_context && b`#ctx = ${ctx};`} ${this.chunks.update} }`; } } if (this.has_animation) { properties.measure = x`function #measure() { ${this.chunks.measure} }`; if (this.chunks.restore_measurements.length) { properties.restore_measurements = x`function #restore_measurements(#measurement) { ${this.chunks.restore_measurements} }`; } properties.fix = x`function #fix() { ${this.chunks.fix} }`; properties.animate = x`function #animate() { ${this.chunks.animate} }`; } if (this.has_intro_method || this.has_outro_method) { if (this.chunks.intro.length === 0) { properties.intro = noop; } else { properties.intro = x`function #intro(#local) { ${this.has_outros && b`if (#current) return;`} ${this.chunks.intro} }`; } if (this.chunks.outro.length === 0) { properties.outro = noop; } else { properties.outro = x`function #outro(#local) { ${this.chunks.outro} }`; } } if (this.chunks.destroy.length === 0) { properties.destroy = noop; } else { const dispose_elements = []; // Coalesce if blocks with the same condition const others = flatten(this.chunks.destroy).filter( /** @param {import('estree').Node} node */ (node) => { if ( node.type === 'IfStatement' && node.test.type === 'Identifier' && node.test.name === 'detaching' ) { dispose_elements.push(node.consequent); return false; } else { return true; } } ); properties.destroy = x`function #destroy(detaching) { ${dispose_elements.length ? b`if (detaching) { ${dispose_elements} }` : null} ${others} }`; } if (!this.renderer.component.compile_options.dev) { // allow shorthand names for (const name in properties) { const property = properties[name]; if (property) property.id = null; } } /** @type {any} */ const return_value = x`{ key: ${properties.key}, first: ${properties.first}, c: ${properties.create}, l: ${properties.claim}, h: ${properties.hydrate}, m: ${properties.mount}, p: ${properties.update}, r: ${properties.measure}, s: ${properties.restore_measurements}, f: ${properties.fix}, a: ${properties.animate}, i: ${properties.intro}, o: ${properties.outro}, d: ${properties.destroy} }`; const block = dev && this.get_unique_name('block'); const body = b` ${this.chunks.declarations} ${Array.from(this.variables.values()).map(({ id, init }) => { return init ? b`let ${id} = ${init}` : b`let ${id}`; })} ${this.chunks.init} ${ dev ? b` const ${block} = ${return_value}; @dispatch_dev("SvelteRegisterBlock", { block: ${block}, id: ${this.name || 'create_fragment'}.name, type: "${this.type}", source: "${this.comment ? this.comment.replace(regex_double_quotes, '\\"') : ''}", ctx: #ctx }); return ${block};` : b` return ${return_value};` } `; return body; } /** @returns {boolean} */ has_content() { return ( !!this.first || this.event_listeners.length > 0 || this.chunks.intro.length > 0 || this.chunks.outro.length > 0 || this.chunks.create.length > 0 || this.chunks.hydrate.length > 0 || this.chunks.claim.length > 0 || this.chunks.mount.length > 0 || this.chunks.update.length > 0 || this.chunks.destroy.length > 0 || this.has_animation ); } render() { const key = this.key && this.get_unique_name('key'); /** @type {any[]} */ const args = [x`#ctx`]; if (key) args.unshift(key); const fn = b`function ${this.name}(${args}) { ${this.get_contents(key)} }`; return this.comment ? b` // ${this.comment} ${fn}` : fn; } /** @param {string} chunk */ render_listeners(chunk = '') { if (this.event_listeners.length > 0) { this.add_variable({ type: 'Identifier', name: '#mounted' }); this.chunks.destroy.push(b`#mounted = false`); /** @type {import('estree').Identifier} */ const dispose = { type: 'Identifier', name: `#dispose${chunk}` }; this.add_variable(dispose); if (this.event_listeners.length === 1) { this.chunks.mount.push(b` if (!#mounted) { ${dispose} = ${this.event_listeners[0]}; #mounted = true; } `); this.chunks.destroy.push(b`${dispose}();`); } else { this.chunks.mount.push(b` if (!#mounted) { ${dispose} = [ ${this.event_listeners} ]; #mounted = true; } `); this.chunks.destroy.push(b`@run_all(${dispose});`); } } } render_binding_groups() { for (const binding_group of this.binding_groups) { binding_group.render(this); } } }