All files / src/compiler/phases/3-transform/client/visitors/shared events.js

100% Statements 143/143
96.77% Branches 30/31
100% Functions 3/3
100% Lines 141/141

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 1422x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 270x 270x 270x 270x 2x 2x 2x 270x 270x 270x 270x 270x 270x 270x 270x 270x 255x 255x 255x 168x 168x 255x 255x 255x 202x 172x 172x 172x 172x 202x 202x 202x 202x 202x 202x 202x 202x 255x 53x 53x 255x 255x 255x 255x 255x 255x 255x 255x 255x 255x 270x 15x 15x 15x 15x 11x 11x 15x 4x 4x 15x 270x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 452x 452x 452x 452x 452x 452x 452x 452x 452x 2x 2x 2x 2x 2x 2x 2x 2x 2x 734x 21x 21x 21x 21x 21x 21x 21x 713x 734x 196x 196x 196x 162x 162x 196x 551x 551x 551x 551x 733x 147x 147x 138x 147x 734x 96x 96x 96x 96x 96x 96x 96x 551x 551x 551x 551x 551x 551x 551x  
/** @import { Expression } from 'estree' */
/** @import { Attribute, ExpressionMetadata, ExpressionTag, OnDirective, SvelteNode } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import * as b from '../../../../../utils/builders.js';
 
/**
 * @param {Attribute} node
 * @param {ComponentContext} context
 */
export function build_event_attribute(node, context) {
	let capture = false;
 
	let event_name = node.name.slice(2);
	if (is_capture_event(event_name)) {
		event_name = event_name.slice(0, -7);
		capture = true;
	}
 
	// we still need to support the weird `onclick="{() => {...}}" form
	const tag = Array.isArray(node.value)
		? /** @type {ExpressionTag} */ (node.value[0])
		: /** @type {ExpressionTag} */ (node.value);
 
	let handler = build_event_handler(tag.expression, tag.metadata.expression, context);
 
	if (node.metadata.delegated) {
		let delegated_assignment;
 
		if (!context.state.events.has(event_name)) {
			context.state.events.add(event_name);
		}
 
		// Hoist function if we can, otherwise we leave the function as is
		if (node.metadata.delegated.hoistable) {
			if (node.metadata.delegated.function === tag.expression) {
				const func_name = context.state.scope.root.unique('on_' + event_name);
				context.state.hoisted.push(b.var(func_name, handler));
				handler = func_name;
			}
 
			const hoistable_params = /** @type {Expression[]} */ (
				node.metadata.delegated.function.metadata.hoistable_params
			);
			// When we hoist a function we assign an array with the function and all
			// hoisted closure params.
			const args = [handler, ...hoistable_params];
			delegated_assignment = b.array(args);
		} else {
			delegated_assignment = handler;
		}
 
		context.state.init.push(
			b.stmt(
				b.assignment(
					'=',
					b.member(context.state.node, b.id('__' + event_name)),
					delegated_assignment
				)
			)
		);
	} else {
		const statement = b.stmt(build_event(event_name, handler, capture, undefined, context));
		const type = /** @type {SvelteNode} */ (context.path.at(-1)).type;
 
		if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') {
			// These nodes are above the component tree, and its events should run parent first
			context.state.init.push(statement);
		} else {
			context.state.after_update.push(statement);
		}
	}
}
 
/**
 * Serializes an event handler function of the `on:` directive or an attribute starting with `on`
 * @param {string} event_name
 * @param {Expression} handler
 * @param {boolean} capture
 * @param {boolean | undefined} passive
 * @param {ComponentContext} context
 */
export function build_event(event_name, handler, capture, passive, context) {
	return b.call(
		'$.event',
		b.literal(event_name),
		context.state.node,
		handler,
		capture && b.true,
		passive === undefined ? undefined : b.literal(passive)
	);
}
 
/**
 * Serializes the event handler function of the `on:` directive
 * @param {Expression | null} node
 * @param {ExpressionMetadata} metadata
 * @param {ComponentContext} context
 * @returns {Expression}
 */
export function build_event_handler(node, metadata, context) {
	if (node === null) {
		// bubble event
		return b.function(
			null,
			[b.id('$$arg')],
			b.block([b.stmt(b.call('$.bubble_event.call', b.this, b.id('$$props'), b.id('$$arg')))])
		);
	}
 
	if (node.type === 'Identifier') {
		// common case — function declared in the script
		const binding = context.state.scope.get(node.name);
		if (!binding || (binding.kind === 'normal' && binding.declaration_kind !== 'import')) {
			return node;
		}
	}
 
	let handler = /** @type {Expression} */ (context.visit(node));
 
	if (
		metadata.has_call &&
		!(
			(node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') &&
			node.metadata.hoistable
		)
	) {
		// Create a derived dynamic event handler
		const id = b.id(context.state.scope.generate('event_handler'));
 
		context.state.init.push(b.var(id, b.call('$.derived', b.thunk(handler))));
 
		handler = b.call('$.get', id);
	}
 
	return b.function(
		null,
		[b.rest(b.id('$$args'))],
		b.block([b.stmt(b.call(b.member(handler, b.id('apply'), false, true), b.this, b.id('$$args')))])
	);
}