Skip to content

Commit 203462a

Browse files
committed
add event delegation to spread_attributes, add event attributes to spread
also fixes an edge case bug with event hoistability
1 parent 3808bab commit 203462a

File tree

14 files changed

+221
-127
lines changed

14 files changed

+221
-127
lines changed

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ export default function tag(parser) {
127127
attributes: [],
128128
fragment: create_fragment(true),
129129
metadata: {
130-
svg: false
130+
svg: false,
131+
has_spread: false,
132+
can_delegate_events: null
131133
},
132134
parent: null
133135
}

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
object
1212
} from '../../utils/ast.js';
1313
import * as b from '../../utils/builders.js';
14-
import { DelegatedEvents, ReservedKeywords, Runes, SVGElements } from '../constants.js';
14+
import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
1515
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
1616
import { merge } from '../visitors.js';
1717
import Stylesheet from './css/Stylesheet.js';
@@ -20,6 +20,7 @@ import { warn } from '../../warnings.js';
2020
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
2121
import { regex_starts_with_newline } from '../patterns.js';
2222
import { create_attribute, is_element_node } from '../nodes.js';
23+
import { DelegatedEvents } from '../../../constants.js';
2324

2425
/**
2526
* @param {import('#compiler').Script | null} script
@@ -58,7 +59,7 @@ function get_component_name(filename) {
5859
}
5960

6061
/**
61-
* @param {Pick<import('#compiler').OnDirective, 'expression'| 'name' | 'modifiers'>} node
62+
* @param {Pick<import('#compiler').OnDirective, 'expression'| 'name' | 'modifiers'> & { type: string }} node
6263
* @param {import('./types').Context} context
6364
* @returns {null | import('#compiler').DelegatedEvent}
6465
*/
@@ -70,16 +71,13 @@ function get_delegated_event(node, context) {
7071
if (!handler || node.modifiers.includes('capture') || !DelegatedEvents.includes(event_name)) {
7172
return null;
7273
}
73-
// If we are not working with a RegularElement/SlotElement, then bail-out.
74+
// If we are not working with a RegularElement, then bail-out.
7475
const element = context.path.at(-1);
75-
if (element == null || (element.type !== 'RegularElement' && element.type !== 'SlotElement')) {
76+
if (element?.type !== 'RegularElement') {
7677
return null;
7778
}
78-
// If we have multiple OnDirectives of the same type, bail-out.
79-
if (
80-
element.attributes.filter((attr) => attr.type === 'OnDirective' && attr.name === event_name)
81-
.length > 1
82-
) {
79+
// If element says we can't delegate because we have multiple OnDirectives of the same type, bail-out.
80+
if (!element.metadata.can_delegate_events) {
8381
return null;
8482
}
8583

@@ -89,6 +87,11 @@ function get_delegated_event(node, context) {
8987
let target_function = null;
9088
let binding = null;
9189

90+
if (node.type === 'Attribute' && element.metadata.has_spread) {
91+
// event attribute becomes part of the dynamic spread array
92+
return non_hoistable;
93+
}
94+
9295
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
9396
target_function = handler;
9497
} else if (handler.type === 'Identifier') {
@@ -110,7 +113,11 @@ function get_delegated_event(node, context) {
110113
: null;
111114

112115
if (element) {
113-
if (element.type !== 'RegularElement' && element.type !== 'SlotElement') {
116+
if (
117+
element.type !== 'RegularElement' ||
118+
!determine_element_spread_and_delegatable(element).metadata.can_delegate_events ||
119+
(element.metadata.has_spread && node.type === 'Attribute')
120+
) {
114121
return non_hoistable;
115122
}
116123
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
@@ -772,16 +779,15 @@ const common_visitors = {
772779

773780
let name = node.name.slice(2);
774781

775-
if (
776-
name.endsWith('capture') &&
777-
name !== 'ongotpointercapture' &&
778-
name !== 'onlostpointercapture'
779-
) {
782+
if (is_capture_event(name)) {
780783
name = name.slice(0, -7);
781784
modifiers.push('capture');
782785
}
783786

784-
const delegated_event = get_delegated_event({ name, expression, modifiers }, context);
787+
const delegated_event = get_delegated_event(
788+
{ type: node.type, name, expression, modifiers },
789+
context
790+
);
785791

786792
if (delegated_event !== null) {
787793
if (delegated_event.type === 'hoistable') {
@@ -950,6 +956,8 @@ const common_visitors = {
950956
node.metadata.svg = true;
951957
}
952958

959+
determine_element_spread_and_delegatable(node);
960+
953961
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
954962
if (
955963
context.state.options.namespace !== 'foreign' &&
@@ -1005,6 +1013,58 @@ const common_visitors = {
10051013
}
10061014
};
10071015

1016+
/**
1017+
* Check if events on this element can theoretically be delegated. They can if there's no
1018+
* possibility of an OnDirective and an event attribute on the same element, and if there's
1019+
* no OnDirectives of the same type (the latter is a bit too strict because `on:click on:click on:keyup`
1020+
* means that `on:keyup` can be delegated but we gloss over this edge case).
1021+
* @param {import('#compiler').RegularElement} node
1022+
*/
1023+
function determine_element_spread_and_delegatable(node) {
1024+
if (typeof node.metadata.can_delegate_events === 'boolean') {
1025+
return node; // did this already
1026+
}
1027+
1028+
let events = new Map();
1029+
let has_spread = false;
1030+
let has_on = false;
1031+
for (const attribute of node.attributes) {
1032+
if (
1033+
attribute.type === 'OnDirective' ||
1034+
(attribute.type === 'Attribute' && is_event_attribute(attribute))
1035+
) {
1036+
let event_name = attribute.name;
1037+
if (attribute.type === 'Attribute') {
1038+
if (is_capture_event(event_name)) {
1039+
event_name = event_name.slice(0, -7);
1040+
}
1041+
event_name = event_name.slice(2);
1042+
}
1043+
events.set(event_name, (events.get(event_name) || 0) + 1);
1044+
if (!has_on && attribute.type === 'OnDirective') {
1045+
has_on = true;
1046+
}
1047+
} else if (!has_spread && attribute.type === 'SpreadAttribute') {
1048+
has_spread = true;
1049+
}
1050+
}
1051+
node.metadata.can_delegate_events =
1052+
!(has_spread && has_on) && ![...events.values()].some((count) => count > 1);
1053+
node.metadata.has_spread = has_spread;
1054+
1055+
return node;
1056+
}
1057+
1058+
/**
1059+
* @param {string} name
1060+
* @returns boolean
1061+
*/
1062+
function is_capture_event(name) {
1063+
return (
1064+
name.endsWith('capture') && name !== 'ongotpointercapture' && name !== 'onlostpointercapture'
1065+
);
1066+
}
1067+
10081068
/**
10091069
* @param {Map<import('estree').LabeledStatement, import('../types.js').ReactiveStatement>} unsorted_reactive_declarations
10101070
*/

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
4242
/** Used if condition for singular prop is false (see comment above) */
4343
grouped: Statement;
4444
}[];
45-
/** Stuff that happens just before the render effect (events) */
46-
readonly before_update: Statement[];
4745
/** Stuff that happens after the render effect (bindings, actions) */
4846
readonly after_update: Statement[];
4947
/** The HTML template string */

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,6 @@ function create_block(parent, name, nodes, context) {
954954
init: [],
955955
update: [],
956956
update_effects: [],
957-
before_update: [],
958957
after_update: [],
959958
template: [],
960959
metadata: {
@@ -1036,8 +1035,6 @@ function create_block(parent, name, nodes, context) {
10361035
}
10371036
}
10381037

1039-
body.push(...state.before_update);
1040-
10411038
if (state.update.length > 0 || state.update_effects.length > 0) {
10421039
/** @type {import('estree').Statement | undefined} */
10431040
let update;
@@ -1223,7 +1220,7 @@ function serialize_event(node, context) {
12231220
delegated_assignment = handler;
12241221
}
12251222

1226-
state.before_update.push(
1223+
state.after_update.push(
12271224
b.stmt(
12281225
b.assignment(
12291226
'=',
@@ -1731,7 +1728,6 @@ export const template_visitors = {
17311728
const is_custom_element = is_custom_element_node(node);
17321729
let needs_input_reset = false;
17331730
let needs_content_reset = false;
1734-
let has_spread = false;
17351731

17361732
/** @type {import('#compiler').BindDirective | null} */
17371733
let value_binding = null;
@@ -1751,27 +1747,22 @@ export const template_visitors = {
17511747

17521748
for (const attribute of node.attributes) {
17531749
if (attribute.type === 'Attribute') {
1754-
if (is_event_attribute(attribute)) {
1755-
serialize_event_attribute(attribute, context);
1756-
} else {
1757-
attributes.push(attribute);
1758-
if (
1759-
(attribute.name === 'value' || attribute.name === 'checked') &&
1760-
!is_text_attribute(attribute)
1761-
) {
1762-
needs_input_reset = true;
1763-
needs_content_reset = true;
1764-
} else if (
1765-
attribute.name === 'contenteditable' &&
1766-
(attribute.value === true ||
1767-
(is_text_attribute(attribute) && attribute.value[0].data === 'true'))
1768-
) {
1769-
is_content_editable = true;
1770-
}
1750+
attributes.push(attribute);
1751+
if (
1752+
(attribute.name === 'value' || attribute.name === 'checked') &&
1753+
!is_text_attribute(attribute)
1754+
) {
1755+
needs_input_reset = true;
1756+
needs_content_reset = true;
1757+
} else if (
1758+
attribute.name === 'contenteditable' &&
1759+
(attribute.value === true ||
1760+
(is_text_attribute(attribute) && attribute.value[0].data === 'true'))
1761+
) {
1762+
is_content_editable = true;
17711763
}
17721764
} else if (attribute.type === 'SpreadAttribute') {
17731765
attributes.push(attribute);
1774-
has_spread = true;
17751766
needs_input_reset = true;
17761767
needs_content_reset = true;
17771768
} else if (attribute.type === 'ClassDirective') {
@@ -1832,14 +1823,19 @@ export const template_visitors = {
18321823

18331824
// Then do attributes
18341825
let is_attributes_reactive = false;
1835-
if (has_spread) {
1826+
if (node.metadata.has_spread) {
18361827
const spread_id = serialize_element_spread_attributes(attributes, context, node_id);
18371828
if (child_metadata.namespace !== 'foreign') {
18381829
add_select_to_spread_update(spread_id, node, context, node_id);
18391830
}
18401831
is_attributes_reactive = spread_id !== null;
18411832
} else {
18421833
for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) {
1834+
if (is_event_attribute(attribute)) {
1835+
serialize_event_attribute(attribute, context);
1836+
continue;
1837+
}
1838+
18431839
if (needs_special_value_handling && attribute.name === 'value') {
18441840
serialize_element_special_value_attribute(node.name, node_id, attribute, context);
18451841
continue;
@@ -1954,7 +1950,6 @@ export const template_visitors = {
19541950
init: [],
19551951
update: [],
19561952
update_effects: [],
1957-
before_update: [],
19581953
after_update: []
19591954
}
19601955
};
@@ -2002,7 +1997,6 @@ export const template_visitors = {
20021997

20031998
/** @type {import('estree').Statement[]} */
20041999
const inner = inner_context.state.init;
2005-
inner.push(...inner_context.state.before_update);
20062000
if (inner_context.state.update.length > 0 || inner_context.state.update_effects.length > 0) {
20072001
if (inner_context.state.update_effects.length > 0) {
20082002
for (const render of inner_context.state.update_effects) {

packages/svelte/src/compiler/phases/constants.js

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,32 +66,6 @@ export const VoidElements = [
6666
'wbr'
6767
];
6868

69-
// list of Element events that will be delegated
70-
export const DelegatedEvents = [
71-
'beforeinput',
72-
'click',
73-
'dblclick',
74-
'contextmenu',
75-
'focusin',
76-
'focusout',
77-
// 'input', This conflicts with bind:input
78-
'keydown',
79-
'keyup',
80-
'mousedown',
81-
'mousemove',
82-
'mouseout',
83-
'mouseover',
84-
'mouseup',
85-
'pointerdown',
86-
'pointermove',
87-
'pointerout',
88-
'pointerover',
89-
'pointerup',
90-
'touchend',
91-
'touchmove',
92-
'touchstart'
93-
];
94-
9569
export const PassiveEvents = ['wheel', 'touchstart', 'touchmove', 'touchend', 'touchcancel'];
9670

9771
// TODO this is currently unused

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,14 @@ export interface RegularElement extends BaseElement {
283283
metadata: {
284284
/** `true` if this is an svg element */
285285
svg: boolean;
286+
/** `true` if contains a SpreadAttribute */
287+
has_spread: boolean;
288+
/**
289+
* `true` if events on this element can theoretically be delegated. This doesn't necessarily mean that
290+
* a specific event will be delegated, as there are other factors which affect the final outcome.
291+
* `null` only until it was determined whether this element can be delegated or not.
292+
*/
293+
can_delegate_events: boolean | null;
286294
};
287295
}
288296

packages/svelte/src/constants.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,32 @@ export const EACH_INDEX_REACTIVE = 1 << 1;
33
export const EACH_KEYED = 1 << 2;
44
export const EACH_IS_CONTROLLED = 1 << 3;
55
export const EACH_IS_ANIMATED = 1 << 4;
6+
7+
/** List of Element events that will be delegated */
8+
export const DelegatedEvents = [
9+
'beforeinput',
10+
'click',
11+
'dblclick',
12+
'contextmenu',
13+
'focusin',
14+
'focusout',
15+
// 'input', This conflicts with bind:input
16+
'keydown',
17+
'keyup',
18+
'mousedown',
19+
'mousemove',
20+
'mouseout',
21+
'mouseover',
22+
'mouseup',
23+
'pointerdown',
24+
'pointermove',
25+
'pointerout',
26+
'pointerover',
27+
'pointerup',
28+
'touchend',
29+
'touchmove',
30+
'touchstart'
31+
];
32+
33+
/** List of Element events that will be delegated and are passive */
34+
export const PassiveDelegatedEvents = ['touchstart', 'touchmove', 'touchend'];

0 commit comments

Comments
 (0)