mirror of https://github.com/sveltejs/svelte
				
				
				
			
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							848 lines
						
					
					
						
							24 KiB
						
					
					
				
			
		
		
	
	
							848 lines
						
					
					
						
							24 KiB
						
					
					
				| 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 { is_void, quote_prop_if_necessary, quote_name_if_necessary, sanitize } from '../../../../utils/names';
 | |
| import FragmentWrapper from '../Fragment';
 | |
| import { stringify, escape_html, escape } from '../../../utils/stringify';
 | |
| import TextWrapper from '../Text';
 | |
| import fix_attribute_casing from './fix_attribute_casing';
 | |
| import deindent from '../../../utils/deindent';
 | |
| import { namespaces } from '../../../../utils/namespaces';
 | |
| import AttributeWrapper from './Attribute';
 | |
| import StyleAttributeWrapper from './StyleAttribute';
 | |
| import { dimensions } from '../../../../utils/patterns';
 | |
| import Binding from './Binding';
 | |
| import InlineComponentWrapper from '../InlineComponent';
 | |
| import add_to_set from '../../../utils/add_to_set';
 | |
| import add_event_handlers from '../shared/add_event_handlers';
 | |
| import add_actions from '../shared/add_actions';
 | |
| import create_debugging_comment from '../shared/create_debugging_comment';
 | |
| import { get_context_merger } from '../shared/get_context_merger';
 | |
| 
 | |
| const events = [
 | |
| 	{
 | |
| 		event_names: ['input'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.name === 'textarea' ||
 | |
| 			node.name === 'input' && !/radio|checkbox|range/.test(node.get_static_attribute_value('type'))
 | |
| 	},
 | |
| 	{
 | |
| 		event_names: ['change'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.name === 'select' ||
 | |
| 			node.name === 'input' && /radio|checkbox/.test(node.get_static_attribute_value('type'))
 | |
| 	},
 | |
| 	{
 | |
| 		event_names: ['change', 'input'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.name === 'input' && node.get_static_attribute_value('type') === 'range'
 | |
| 	},
 | |
| 
 | |
| 	{
 | |
| 		event_names: ['resize'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			dimensions.test(name)
 | |
| 	},
 | |
| 
 | |
| 	// media events
 | |
| 	{
 | |
| 		event_names: ['timeupdate'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.is_media_node() &&
 | |
| 			(name === 'currentTime' || name === 'played')
 | |
| 	},
 | |
| 	{
 | |
| 		event_names: ['durationchange'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.is_media_node() &&
 | |
| 			name === 'duration'
 | |
| 	},
 | |
| 	{
 | |
| 		event_names: ['play', 'pause'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.is_media_node() &&
 | |
| 			name === 'paused'
 | |
| 	},
 | |
| 	{
 | |
| 		event_names: ['progress'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.is_media_node() &&
 | |
| 			name === 'buffered'
 | |
| 	},
 | |
| 	{
 | |
| 		event_names: ['loadedmetadata'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.is_media_node() &&
 | |
| 			(name === 'buffered' || name === 'seekable')
 | |
| 	},
 | |
| 	{
 | |
| 		event_names: ['volumechange'],
 | |
| 		filter: (node: Element, name: string) =>
 | |
| 			node.is_media_node() &&
 | |
| 			name === 'volume'
 | |
| 	}
 | |
| ];
 | |
| 
 | |
| export default class ElementWrapper extends Wrapper {
 | |
| 	node: Element;
 | |
| 	fragment: FragmentWrapper;
 | |
| 	attributes: AttributeWrapper[];
 | |
| 	bindings: Binding[];
 | |
| 	class_dependencies: string[];
 | |
| 
 | |
| 	slot_block: Block;
 | |
| 	select_binding_dependencies?: Set<string>;
 | |
| 
 | |
| 	var: string;
 | |
| 
 | |
| 	constructor(
 | |
| 		renderer: Renderer,
 | |
| 		block: Block,
 | |
| 		parent: Wrapper,
 | |
| 		node: Element,
 | |
| 		strip_whitespace: boolean,
 | |
| 		next_sibling: Wrapper
 | |
| 	) {
 | |
| 		super(renderer, block, parent, node);
 | |
| 		this.var = node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
 | |
| 
 | |
| 		this.class_dependencies = [];
 | |
| 
 | |
| 		this.attributes = this.node.attributes.map(attribute => {
 | |
| 			if (attribute.name === 'slot') {
 | |
| 				// TODO make separate subclass for this?
 | |
| 				let owner = this.parent;
 | |
| 				while (owner) {
 | |
| 					if (owner.node.type === 'InlineComponent') {
 | |
| 						break;
 | |
| 					}
 | |
| 
 | |
| 					if (owner.node.type === 'Element' && /-/.test(owner.node.name)) {
 | |
| 						break;
 | |
| 					}
 | |
| 
 | |
| 					owner = owner.parent;
 | |
| 				}
 | |
| 
 | |
| 				if (owner && owner.node.type === 'InlineComponent') {
 | |
| 					const name = attribute.get_static_value();
 | |
| 
 | |
| 					if (!(owner as InlineComponentWrapper).slots.has(name)) {
 | |
| 						const child_block = block.child({
 | |
| 							comment: create_debugging_comment(node, this.renderer.component),
 | |
| 							name: this.renderer.component.get_unique_name(`create_${sanitize(name)}_slot`)
 | |
| 						});
 | |
| 
 | |
| 						const fn = get_context_merger(this.node.lets);
 | |
| 
 | |
| 						(owner as InlineComponentWrapper).slots.set(name, {
 | |
| 							block: child_block,
 | |
| 							scope: this.node.scope,
 | |
| 							fn
 | |
| 						});
 | |
| 						this.renderer.blocks.push(child_block);
 | |
| 					}
 | |
| 
 | |
| 					this.slot_block = (owner as InlineComponentWrapper).slots.get(name).block;
 | |
| 					block = this.slot_block;
 | |
| 				}
 | |
| 			}
 | |
| 			if (attribute.name === 'style') {
 | |
| 				return new StyleAttributeWrapper(this, block, attribute);
 | |
| 			}
 | |
| 			return new AttributeWrapper(this, block, attribute);
 | |
| 		});
 | |
| 
 | |
| 		// ordinarily, there'll only be one... but we need to handle
 | |
| 		// the rare case where an element can have multiple bindings,
 | |
| 		// e.g. <audio bind:paused bind:currentTime>
 | |
| 		this.bindings = this.node.bindings.map(binding => new Binding(block, binding, this));
 | |
| 
 | |
| 		if (node.intro || node.outro) {
 | |
| 			if (node.intro) block.add_intro(node.intro.is_local);
 | |
| 			if (node.outro) block.add_outro(node.outro.is_local);
 | |
| 		}
 | |
| 
 | |
| 		if (node.animation) {
 | |
| 			block.add_animation();
 | |
| 		}
 | |
| 
 | |
| 		// add directive and handler dependencies
 | |
| 		[node.animation, node.outro, ...node.actions, ...node.classes].forEach(directive => {
 | |
| 			if (directive && directive.expression) {
 | |
| 				block.add_dependencies(directive.expression.dependencies);
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		node.handlers.forEach(handler => {
 | |
| 			if (handler.expression) {
 | |
| 				block.add_dependencies(handler.expression.dependencies);
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		if (this.parent) {
 | |
| 			if (node.actions.length > 0) this.parent.cannot_use_innerhtml();
 | |
| 			if (node.animation) this.parent.cannot_use_innerhtml();
 | |
| 			if (node.bindings.length > 0) this.parent.cannot_use_innerhtml();
 | |
| 			if (node.classes.length > 0) this.parent.cannot_use_innerhtml();
 | |
| 			if (node.intro || node.outro) this.parent.cannot_use_innerhtml();
 | |
| 			if (node.handlers.length > 0) this.parent.cannot_use_innerhtml();
 | |
| 
 | |
| 			if (this.node.name === 'option') this.parent.cannot_use_innerhtml();
 | |
| 
 | |
| 			if (renderer.options.dev) {
 | |
| 				this.parent.cannot_use_innerhtml(); // need to use add_location
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);
 | |
| 
 | |
| 		if (this.slot_block) {
 | |
| 			block.parent.add_dependencies(block.dependencies);
 | |
| 
 | |
| 			// appalling hack
 | |
| 			const index = block.parent.wrappers.indexOf(this);
 | |
| 			block.parent.wrappers.splice(index, 1);
 | |
| 			block.wrappers.push(this);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	render(block: Block, parent_node: string, parent_nodes: string) {
 | |
| 		const { renderer } = this;
 | |
| 
 | |
| 		if (this.node.name === 'slot') {
 | |
| 			const slotName = this.get_static_attribute_value('name') || 'default';
 | |
| 			renderer.slots.add(slotName);
 | |
| 		}
 | |
| 
 | |
| 		if (this.node.name === 'noscript') return;
 | |
| 
 | |
| 		if (this.slot_block) {
 | |
| 			block = this.slot_block;
 | |
| 		}
 | |
| 
 | |
| 		const node = this.var;
 | |
| 		const nodes = parent_nodes && block.get_unique_name(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parent_nodes is null
 | |
| 
 | |
| 		block.add_variable(node);
 | |
| 		const render_statement = this.get_render_statement();
 | |
| 		block.builders.create.add_line(
 | |
| 			`${node} = ${render_statement};`
 | |
| 		);
 | |
| 
 | |
| 		if (renderer.options.hydratable) {
 | |
| 			if (parent_nodes) {
 | |
| 				block.builders.claim.add_block(deindent`
 | |
| 					${node} = ${this.get_claim_statement(parent_nodes)};
 | |
| 					var ${nodes} = @children(${this.node.name === 'template' ? `${node}.content` : node});
 | |
| 				`);
 | |
| 			} else {
 | |
| 				block.builders.claim.add_line(
 | |
| 					`${node} = ${render_statement};`
 | |
| 				);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if (parent_node) {
 | |
| 			block.builders.mount.add_line(
 | |
| 				`@append(${parent_node}, ${node});`
 | |
| 			);
 | |
| 
 | |
| 			if (parent_node === 'document.head') {
 | |
| 				block.builders.destroy.add_line(`@detach(${node});`);
 | |
| 			}
 | |
| 		} else {
 | |
| 			block.builders.mount.add_line(`@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.add_conditional('detaching', `@detach(${node});`);
 | |
| 		}
 | |
| 
 | |
| 		// insert static children with textContent or innerHTML
 | |
| 		if (!this.node.namespace && this.can_use_innerhtml && this.fragment.nodes.length > 0) {
 | |
| 			if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
 | |
| 				block.builders.create.add_line(
 | |
| 					`${node}.textContent = ${stringify(this.fragment.nodes[0].data)};`
 | |
| 				);
 | |
| 			} else {
 | |
| 				const inner_html = escape(
 | |
| 					this.fragment.nodes
 | |
| 						.map(to_html)
 | |
| 						.join('')
 | |
| 				);
 | |
| 
 | |
| 				block.builders.create.add_line(
 | |
| 					`${node}.innerHTML = \`${inner_html}\`;`
 | |
| 				);
 | |
| 			}
 | |
| 		} else {
 | |
| 			this.fragment.nodes.forEach((child: Wrapper) => {
 | |
| 				child.render(
 | |
| 					block,
 | |
| 					this.node.name === 'template' ? `${node}.content` : node,
 | |
| 					nodes
 | |
| 				);
 | |
| 			});
 | |
| 		}
 | |
| 
 | |
| 		const event_handler_or_binding_uses_context = (
 | |
| 			this.bindings.some(binding => binding.handler.uses_context) ||
 | |
| 			this.node.handlers.some(handler => handler.uses_context) ||
 | |
| 			this.node.actions.some(action => action.uses_context)
 | |
| 		);
 | |
| 
 | |
| 		if (event_handler_or_binding_uses_context) {
 | |
| 			block.maintain_context = true;
 | |
| 		}
 | |
| 
 | |
| 		this.add_bindings(block);
 | |
| 		this.add_event_handlers(block);
 | |
| 		this.add_attributes(block);
 | |
| 		this.add_transitions(block);
 | |
| 		this.add_animation(block);
 | |
| 		this.add_actions(block);
 | |
| 		this.add_classes(block);
 | |
| 
 | |
| 		if (nodes && this.renderer.options.hydratable) {
 | |
| 			block.builders.claim.add_line(
 | |
| 				`${nodes}.forEach(@detach);`
 | |
| 			);
 | |
| 		}
 | |
| 
 | |
| 		function to_html(wrapper: ElementWrapper | TextWrapper) {
 | |
| 			if (wrapper.node.type === 'Text') {
 | |
| 				const { parent } = wrapper.node;
 | |
| 
 | |
| 				const raw = parent && (
 | |
| 					parent.name === 'script' ||
 | |
| 					parent.name === 'style'
 | |
| 				);
 | |
| 
 | |
| 				return raw
 | |
| 					? wrapper.node.data
 | |
| 					: escape_html(wrapper.node.data)
 | |
| 						.replace(/\\/g, '\\\\')
 | |
| 						.replace(/`/g, '\\`')
 | |
| 						.replace(/\$/g, '\\$');
 | |
| 			}
 | |
| 
 | |
| 			if (wrapper.node.name === 'noscript') return '';
 | |
| 
 | |
| 			let open = `<${wrapper.node.name}`;
 | |
| 
 | |
| 			(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
 | |
| 				open += ` ${fix_attribute_casing(attr.node.name)}${attr.stringify()}`
 | |
| 			});
 | |
| 
 | |
| 			if (is_void(wrapper.node.name)) return open + '>';
 | |
| 
 | |
| 			return `${open}>${wrapper.fragment.nodes.map(to_html).join('')}</${wrapper.node.name}>`;
 | |
| 		}
 | |
| 
 | |
| 		if (renderer.options.dev) {
 | |
| 			const loc = renderer.locate(this.node.start);
 | |
| 			block.builders.hydrate.add_line(
 | |
| 				`@add_location(${this.var}, ${renderer.file_var}, ${loc.line}, ${loc.column}, ${this.node.start});`
 | |
| 			);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	get_render_statement() {
 | |
| 		const { name, namespace } = this.node;
 | |
| 
 | |
| 		if (namespace === 'http://www.w3.org/2000/svg') {
 | |
| 			return `@svg_element("${name}")`;
 | |
| 		}
 | |
| 
 | |
| 		if (namespace) {
 | |
| 			return `document.createElementNS("${namespace}", "${name}")`;
 | |
| 		}
 | |
| 
 | |
| 		return `@element("${name}")`;
 | |
| 	}
 | |
| 
 | |
| 	get_claim_statement(nodes: string) {
 | |
| 		const attributes = this.node.attributes
 | |
| 			.filter((attr: Node) => attr.type === 'Attribute')
 | |
| 			.map((attr: Node) => `${quote_name_if_necessary(attr.name)}: true`)
 | |
| 			.join(', ');
 | |
| 
 | |
| 		const name = this.node.namespace
 | |
| 			? this.node.name
 | |
| 			: this.node.name.toUpperCase();
 | |
| 
 | |
| 		return `@claim_element(${nodes}, "${name}", ${attributes
 | |
| 			? `{ ${attributes} }`
 | |
| 			: `{}`}, ${this.node.namespace === namespaces.svg ? true : false})`;
 | |
| 	}
 | |
| 
 | |
| 	add_bindings(block: Block) {
 | |
| 		const { renderer } = this;
 | |
| 
 | |
| 		if (this.bindings.length === 0) return;
 | |
| 
 | |
| 		renderer.component.has_reactive_assignments = true;
 | |
| 
 | |
| 		const lock = this.bindings.some(binding => binding.needs_lock) ?
 | |
| 			block.get_unique_name(`${this.var}_updating`) :
 | |
| 			null;
 | |
| 
 | |
| 		if (lock) block.add_variable(lock, 'false');
 | |
| 
 | |
| 		const groups = events
 | |
| 			.map(event => ({
 | |
| 				events: event.event_names,
 | |
| 				bindings: this.bindings
 | |
| 					.filter(binding => binding.node.name !== 'this')
 | |
| 					.filter(binding => event.filter(this.node, binding.node.name))
 | |
| 			}))
 | |
| 			.filter(group => group.bindings.length);
 | |
| 
 | |
| 		groups.forEach(group => {
 | |
| 			const handler = renderer.component.get_unique_name(`${this.var}_${group.events.join('_')}_handler`);
 | |
| 
 | |
| 			renderer.component.add_var({
 | |
| 				name: handler,
 | |
| 				internal: true,
 | |
| 				referenced: true
 | |
| 			});
 | |
| 
 | |
| 			// TODO figure out how to handle locks
 | |
| 			const needs_lock = group.bindings.some(binding => binding.needs_lock);
 | |
| 
 | |
| 			const dependencies = new Set();
 | |
| 			const contextual_dependencies = new Set();
 | |
| 
 | |
| 			group.bindings.forEach(binding => {
 | |
| 				// TODO this is a mess
 | |
| 				add_to_set(dependencies, binding.get_dependencies());
 | |
| 				add_to_set(contextual_dependencies, binding.node.expression.contextual_dependencies);
 | |
| 				add_to_set(contextual_dependencies, binding.handler.contextual_dependencies);
 | |
| 
 | |
| 				binding.render(block, lock);
 | |
| 			});
 | |
| 
 | |
| 			// 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.get_unique_name(`${this.var}_animationframe`);
 | |
| 				block.add_variable(animation_frame);
 | |
| 			}
 | |
| 
 | |
| 			const has_local_function = contextual_dependencies.size > 0 || needs_lock || animation_frame;
 | |
| 
 | |
| 			let callee;
 | |
| 
 | |
| 			// TODO dry this out — similar code for event handlers and component bindings
 | |
| 			if (has_local_function) {
 | |
| 				// need to create a block-local function that calls an instance-level function
 | |
| 				block.builders.init.add_block(deindent`
 | |
| 					function ${handler}() {
 | |
| 						${animation_frame && deindent`
 | |
| 						cancelAnimationFrame(${animation_frame});
 | |
| 						if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`}
 | |
| 						${needs_lock && `${lock} = true;`}
 | |
| 						ctx.${handler}.call(${this.var}${contextual_dependencies.size > 0 ? ', ctx' : ''});
 | |
| 					}
 | |
| 				`);
 | |
| 
 | |
| 				callee = handler;
 | |
| 			} else {
 | |
| 				callee = `ctx.${handler}`;
 | |
| 			}
 | |
| 
 | |
| 			this.renderer.component.partly_hoisted.push(deindent`
 | |
| 				function ${handler}(${contextual_dependencies.size > 0 ? `{ ${Array.from(contextual_dependencies).join(', ')} }` : ``}) {
 | |
| 					${group.bindings.map(b => b.handler.mutation)}
 | |
| 					${Array.from(dependencies).filter(dep => dep[0] !== '$').map(dep => `${this.renderer.component.invalidate(dep)};`)}
 | |
| 				}
 | |
| 			`);
 | |
| 
 | |
| 			group.events.forEach(name => {
 | |
| 				if (name === 'resize') {
 | |
| 					// special case
 | |
| 					const resize_listener = block.get_unique_name(`${this.var}_resize_listener`);
 | |
| 					block.add_variable(resize_listener);
 | |
| 
 | |
| 					block.builders.mount.add_line(
 | |
| 						`${resize_listener} = @add_resize_listener(${this.var}, ${callee}.bind(${this.var}));`
 | |
| 					);
 | |
| 
 | |
| 					block.builders.destroy.add_line(
 | |
| 						`${resize_listener}.cancel();`
 | |
| 					);
 | |
| 				} else {
 | |
| 					block.event_listeners.push(
 | |
| 						`@listen(${this.var}, "${name}", ${callee})`
 | |
| 					);
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			const some_initial_state_is_undefined = group.bindings
 | |
| 				.map(binding => `${binding.snippet} === void 0`)
 | |
| 				.join(' || ');
 | |
| 
 | |
| 			if (this.node.name === 'select' || group.bindings.find(binding => binding.node.name === 'indeterminate' || binding.is_readonly_media_attribute())) {
 | |
| 				const callback = has_local_function ? handler : `() => ${callee}.call(${this.var})`;
 | |
| 				block.builders.hydrate.add_line(
 | |
| 					`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`
 | |
| 				);
 | |
| 			}
 | |
| 
 | |
| 			if (group.events[0] === 'resize') {
 | |
| 				block.builders.hydrate.add_line(
 | |
| 					`@add_render_callback(() => ${callee}.call(${this.var}));`
 | |
| 				);
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		if (lock) {
 | |
| 			block.builders.update.add_line(`${lock} = false;`);
 | |
| 		}
 | |
| 
 | |
| 		const this_binding = this.bindings.find(b => b.node.name === 'this');
 | |
| 		if (this_binding) {
 | |
| 			const name = renderer.component.get_unique_name(`${this.var}_binding`);
 | |
| 
 | |
| 			renderer.component.add_var({
 | |
| 				name,
 | |
| 				internal: true,
 | |
| 				referenced: true
 | |
| 			});
 | |
| 
 | |
| 			const { handler, object } = this_binding;
 | |
| 
 | |
| 			const args = [];
 | |
| 			for (const arg of handler.contextual_dependencies) {
 | |
| 				args.push(arg);
 | |
| 				block.add_variable(arg, `ctx.${arg}`);
 | |
| 			}
 | |
| 
 | |
| 			renderer.component.partly_hoisted.push(deindent`
 | |
| 				function ${name}(${['$$node', 'check'].concat(args).join(', ')}) {
 | |
| 					${handler.snippet ? `if ($$node || (!$$node && ${handler.snippet} === check)) ` : ''}${handler.mutation}
 | |
| 					${renderer.component.invalidate(object)};
 | |
| 				}
 | |
| 			`);
 | |
| 
 | |
| 			block.builders.mount.add_line(`@add_binding_callback(() => ctx.${name}(${[this.var, 'null'].concat(args).join(', ')}));`);
 | |
| 			block.builders.destroy.add_line(`ctx.${name}(${['null', this.var].concat(args).join(', ')});`);
 | |
| 			block.builders.update.add_line(deindent`
 | |
| 				if (changed.items) {
 | |
| 					ctx.${name}(${['null', this.var].concat(args).join(', ')});
 | |
| 					${args.map(a => `${a} = ctx.${a}`).join(', ')};
 | |
| 					ctx.${name}(${[this.var, 'null'].concat(args).join(', ')});
 | |
| 				}`
 | |
| 			);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	add_attributes(block: Block) {
 | |
| 		if (this.node.attributes.find(attr => attr.type === 'Spread')) {
 | |
| 			this.add_spread_attributes(block);
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		this.attributes.forEach((attribute: Attribute) => {
 | |
| 			if (attribute.node.name === 'class' && attribute.node.is_dynamic) {
 | |
| 				this.class_dependencies.push(...attribute.node.dependencies);
 | |
| 			}
 | |
| 			attribute.render(block);
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	add_spread_attributes(block: Block) {
 | |
| 		const levels = block.get_unique_name(`${this.var}_levels`);
 | |
| 		const data = block.get_unique_name(`${this.var}_data`);
 | |
| 
 | |
| 		const initial_props = [];
 | |
| 		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.is_spread) {
 | |
| 					const snippet = attr.expression.render(block);
 | |
| 
 | |
| 					initial_props.push(snippet);
 | |
| 
 | |
| 					updates.push(condition ? `${condition} && ${snippet}` : snippet);
 | |
| 				} else {
 | |
| 					const snippet = `{ ${quote_name_if_necessary(attr.name)}: ${attr.get_value(block)} }`;
 | |
| 					initial_props.push(snippet);
 | |
| 
 | |
| 					updates.push(condition ? `${condition} && ${snippet}` : snippet);
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 		block.builders.init.add_block(deindent`
 | |
| 			var ${levels} = [
 | |
| 				${initial_props.join(',\n')}
 | |
| 			];
 | |
| 
 | |
| 			var ${data} = {};
 | |
| 			for (var #i = 0; #i < ${levels}.length; #i += 1) {
 | |
| 				${data} = @assign(${data}, ${levels}[#i]);
 | |
| 			}
 | |
| 		`);
 | |
| 
 | |
| 		block.builders.hydrate.add_line(
 | |
| 			`@set_attributes(${this.var}, ${data});`
 | |
| 		);
 | |
| 
 | |
| 		block.builders.update.add_block(deindent`
 | |
| 			@set_attributes(${this.var}, @get_spread_update(${levels}, [
 | |
| 				${updates.join(',\n')}
 | |
| 			]));
 | |
| 		`);
 | |
| 	}
 | |
| 
 | |
| 	add_event_handlers(block: Block) {
 | |
| 		add_event_handlers(block, this.var, this.node.handlers);
 | |
| 	}
 | |
| 
 | |
| 	add_transitions(
 | |
| 		block: Block
 | |
| 	) {
 | |
| 		const { intro, outro } = this.node;
 | |
| 		if (!intro && !outro) return;
 | |
| 
 | |
| 		const { component } = this.renderer;
 | |
| 
 | |
| 		if (intro === outro) {
 | |
| 			// bidirectional transition
 | |
| 			const name = block.get_unique_name(`${this.var}_transition`);
 | |
| 			const snippet = intro.expression
 | |
| 				? intro.expression.render(block)
 | |
| 				: '{}';
 | |
| 
 | |
| 			block.add_variable(name);
 | |
| 
 | |
| 			const fn = component.qualify(intro.name);
 | |
| 
 | |
| 			const intro_block = deindent`
 | |
| 				@add_render_callback(() => {
 | |
| 					if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true);
 | |
| 					${name}.run(1);
 | |
| 				});
 | |
| 			`;
 | |
| 
 | |
| 			const outro_block = deindent`
 | |
| 				if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, false);
 | |
| 				${name}.run(0);
 | |
| 			`;
 | |
| 
 | |
| 			if (intro.is_local) {
 | |
| 				block.builders.intro.add_block(deindent`
 | |
| 					if (#local) {
 | |
| 						${intro_block}
 | |
| 					}
 | |
| 				`);
 | |
| 
 | |
| 				block.builders.outro.add_block(deindent`
 | |
| 					if (#local) {
 | |
| 						${outro_block}
 | |
| 					}
 | |
| 				`);
 | |
| 			} else {
 | |
| 				block.builders.intro.add_block(intro_block);
 | |
| 				block.builders.outro.add_block(outro_block);
 | |
| 			}
 | |
| 
 | |
| 			block.builders.destroy.add_conditional('detaching', `if (${name}) ${name}.end();`);
 | |
| 		}
 | |
| 
 | |
| 		else {
 | |
| 			const intro_name = intro && block.get_unique_name(`${this.var}_intro`);
 | |
| 			const outro_name = outro && block.get_unique_name(`${this.var}_outro`);
 | |
| 
 | |
| 			if (intro) {
 | |
| 				block.add_variable(intro_name);
 | |
| 				const snippet = intro.expression
 | |
| 					? intro.expression.render(block)
 | |
| 					: '{}';
 | |
| 
 | |
| 				const fn = component.qualify(intro.name);
 | |
| 
 | |
| 				let intro_block;
 | |
| 
 | |
| 				if (outro) {
 | |
| 					intro_block = deindent`
 | |
| 						@add_render_callback(() => {
 | |
| 							if (${outro_name}) ${outro_name}.end(1);
 | |
| 							if (!${intro_name}) ${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
 | |
| 							${intro_name}.start();
 | |
| 						});
 | |
| 					`;
 | |
| 
 | |
| 					block.builders.outro.add_line(`if (${intro_name}) ${intro_name}.invalidate();`);
 | |
| 				} else {
 | |
| 					intro_block = deindent`
 | |
| 						if (!${intro_name}) {
 | |
| 							@add_render_callback(() => {
 | |
| 								${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
 | |
| 								${intro_name}.start();
 | |
| 							});
 | |
| 						}
 | |
| 					`;
 | |
| 				}
 | |
| 
 | |
| 				if (intro.is_local) {
 | |
| 					intro_block = deindent`
 | |
| 						if (#local) {
 | |
| 							${intro_block}
 | |
| 						}
 | |
| 					`;
 | |
| 				}
 | |
| 
 | |
| 				block.builders.intro.add_block(intro_block);
 | |
| 			}
 | |
| 
 | |
| 			if (outro) {
 | |
| 				block.add_variable(outro_name);
 | |
| 				const snippet = outro.expression
 | |
| 					? outro.expression.render(block)
 | |
| 					: '{}';
 | |
| 
 | |
| 				const fn = component.qualify(outro.name);
 | |
| 
 | |
| 				if (!intro) {
 | |
| 					block.builders.intro.add_block(deindent`
 | |
| 						if (${outro_name}) ${outro_name}.end(1);
 | |
| 					`);
 | |
| 				}
 | |
| 
 | |
| 				// TODO hide elements that have outro'd (unless they belong to a still-outroing
 | |
| 				// group) prior to their removal from the DOM
 | |
| 				let outro_block = deindent`
 | |
| 					${outro_name} = @create_out_transition(${this.var}, ${fn}, ${snippet});
 | |
| 				`;
 | |
| 
 | |
| 				if (outro_block) {
 | |
| 					outro_block = deindent`
 | |
| 						if (#local) {
 | |
| 							${outro_block}
 | |
| 						}
 | |
| 					`;
 | |
| 				}
 | |
| 
 | |
| 				block.builders.outro.add_block(outro_block);
 | |
| 
 | |
| 				block.builders.destroy.add_conditional('detaching', `if (${outro_name}) ${outro_name}.end();`);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	add_animation(block: Block) {
 | |
| 		if (!this.node.animation) return;
 | |
| 
 | |
| 		const { component } = this.renderer;
 | |
| 
 | |
| 		const rect = block.get_unique_name('rect');
 | |
| 		const stop_animation = block.get_unique_name('stop_animation');
 | |
| 
 | |
| 		block.add_variable(rect);
 | |
| 		block.add_variable(stop_animation, '@noop');
 | |
| 
 | |
| 		block.builders.measure.add_block(deindent`
 | |
| 			${rect} = ${this.var}.getBoundingClientRect();
 | |
| 		`);
 | |
| 
 | |
| 		block.builders.fix.add_block(deindent`
 | |
| 			@fix_position(${this.var});
 | |
| 			${stop_animation}();
 | |
| 		`);
 | |
| 
 | |
| 		const params = this.node.animation.expression ? this.node.animation.expression.render(block) : '{}';
 | |
| 
 | |
| 		const name = component.qualify(this.node.animation.name);
 | |
| 
 | |
| 		block.builders.animate.add_block(deindent`
 | |
| 			${stop_animation}();
 | |
| 			${stop_animation} = @create_animation(${this.var}, ${rect}, ${name}, ${params});
 | |
| 		`);
 | |
| 	}
 | |
| 
 | |
| 	add_actions(block: Block) {
 | |
| 		add_actions(this.renderer.component, block, this.var, this.node.actions);
 | |
| 	}
 | |
| 
 | |
| 	add_classes(block: Block) {
 | |
| 		this.node.classes.forEach(class_directive => {
 | |
| 			const { expression, name } = class_directive;
 | |
| 			let snippet, dependencies;
 | |
| 			if (expression) {
 | |
| 				snippet = expression.render(block);
 | |
| 				dependencies = expression.dependencies;
 | |
| 			} else {
 | |
| 				snippet = `${quote_prop_if_necessary(name)}`;
 | |
| 				dependencies = new Set([name]);
 | |
| 			}
 | |
| 			const updater = `@toggle_class(${this.var}, "${name}", ${snippet});`;
 | |
| 
 | |
| 			block.builders.hydrate.add_line(updater);
 | |
| 
 | |
| 			if ((dependencies && dependencies.size > 0) || this.class_dependencies.length) {
 | |
| 				const all_dependencies = this.class_dependencies.concat(...dependencies);
 | |
| 				const deps = all_dependencies.map(dependency => `changed${quote_prop_if_necessary(dependency)}`).join(' || ');
 | |
| 				const condition = all_dependencies.length > 1 ? `(${deps})` : deps;
 | |
| 
 | |
| 				block.builders.update.add_conditional(
 | |
| 					condition,
 | |
| 					updater
 | |
| 				);
 | |
| 			}
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	get_static_attribute_value(name: string) {
 | |
| 		const attribute = this.node.attributes.find(
 | |
| 			(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
 | |
| 		);
 | |
| 
 | |
| 		if (!attribute) return null;
 | |
| 
 | |
| 		if (attribute.is_true) 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;
 | |
| 	}
 | |
| 
 | |
| 	add_css_class(class_name = this.component.stylesheet.id) {
 | |
| 		const class_attribute = this.attributes.find(a => a.name === 'class');
 | |
| 		if (class_attribute && !class_attribute.is_true) {
 | |
| 			if (class_attribute.chunks.length === 1 && class_attribute.chunks[0].type === 'Text') {
 | |
| 				(class_attribute.chunks[0] as Text).data += ` ${class_name}`;
 | |
| 			} else {
 | |
| 				(class_attribute.chunks as Node[]).push(
 | |
| 					new Text(this.component, this, this.scope, {
 | |
| 						type: 'Text',
 | |
| 						data: ` ${class_name}`
 | |
| 					})
 | |
| 				);
 | |
| 			}
 | |
| 		} else {
 | |
| 			this.attributes.push(
 | |
| 				new Attribute(this.component, this, this.scope, {
 | |
| 					type: 'Attribute',
 | |
| 					name: 'class',
 | |
| 					value: [{ type: 'Text', data: class_name }]
 | |
| 				})
 | |
| 			);
 | |
| 		}
 | |
| 	}
 | |
| }
 |