svelte
Version:
Cybernetically enhanced web apps
198 lines (168 loc) • 5.36 kB
JavaScript
/** @import { Expression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import { dev, locator } from '../../../../../state.js';
import * as b from '../../../../../utils/builders.js';
/**
* @param {AST.Attribute} node
* @param {ComponentContext} context
*/
export function visit_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 {AST.ExpressionTag} */ (node.value[0])
: /** @type {AST.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.hoisted) {
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 hoisted_params = /** @type {Expression[]} */ (
node.metadata.delegated.function.metadata.hoisted_params
);
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
if (hoisted_params) {
const args = [handler, ...hoisted_params];
delegated_assignment = b.array(args);
} else {
delegated_assignment = handler;
}
} else {
delegated_assignment = handler;
}
context.state.init.push(
b.stmt(
b.assignment('=', b.member(context.state.node, '__' + event_name), delegated_assignment)
)
);
} else {
const statement = b.stmt(
build_event(
event_name,
context.state.node,
handler,
capture,
is_passive_event(event_name) ? true : undefined
)
);
const type = /** @type {AST.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);
}
}
}
/**
* Creates a `$.event(...)` call for non-delegated event handlers
* @param {string} event_name
* @param {Expression} node
* @param {Expression} handler
* @param {boolean} capture
* @param {boolean | undefined} passive
*/
export function build_event(event_name, node, handler, capture, passive) {
return b.call(
'$.event',
b.literal(event_name),
node,
handler,
capture && b.true,
passive === undefined ? undefined : b.literal(passive)
);
}
/**
* Creates an event handler
* @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')))])
);
}
let handler = /** @type {Expression} */ (context.visit(node));
// inline handler
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
return handler;
}
// function declared in the script
if (handler.type === 'Identifier') {
const binding = context.state.scope.get(handler.name);
if (binding?.is_function()) {
return handler;
}
// local variable can be assigned directly
// except in dev mode where when need $.apply()
// in order to handle warnings.
if (!dev && binding?.declaration_kind !== 'import') {
return handler;
}
}
if (metadata.has_call) {
// memoize where necessary
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);
}
// wrap the handler in a function, so the expression is re-evaluated for each event
let call = b.call(b.member(handler, 'apply', false, true), b.this, b.id('$$args'));
if (dev) {
const loc = locator(/** @type {number} */ (node.start));
const remove_parens =
node.type === 'CallExpression' &&
node.arguments.length === 0 &&
node.callee.type === 'Identifier';
call = b.call(
'$.apply',
b.thunk(handler),
b.this,
b.id('$$args'),
b.id(context.state.analysis.name),
loc && b.array([b.literal(loc.line), b.literal(loc.column)]),
has_side_effects(node) && b.true,
remove_parens && b.true
);
}
return b.function(null, [b.rest(b.id('$$args'))], b.block([b.stmt(call)]));
}
/**
* @param {Expression} node
*/
function has_side_effects(node) {
if (
node.type === 'CallExpression' ||
node.type === 'NewExpression' ||
node.type === 'AssignmentExpression' ||
node.type === 'UpdateExpression'
) {
return true;
}
if (node.type === 'SequenceExpression') {
return node.expressions.some(has_side_effects);
}
return false;
}