@@ -11,7 +11,7 @@ import {
1111 object
1212} from '../../utils/ast.js' ;
1313import * as b from '../../utils/builders.js' ;
14- import { DelegatedEvents , ReservedKeywords , Runes , SVGElements } from '../constants.js' ;
14+ import { ReservedKeywords , Runes , SVGElements } from '../constants.js' ;
1515import { Scope , ScopeRoot , create_scopes , get_rune , set_scope } from '../scope.js' ;
1616import { merge } from '../visitors.js' ;
1717import Stylesheet from './css/Stylesheet.js' ;
@@ -20,6 +20,7 @@ import { warn } from '../../warnings.js';
2020import check_graph_for_cycles from './utils/check_graph_for_cycles.js' ;
2121import { regex_starts_with_newline } from '../patterns.js' ;
2222import { 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' ) {
@@ -101,16 +104,29 @@ function get_delegated_event(node, context) {
101104 return non_hoistable ;
102105 }
103106
104- const element =
105- parent . type === 'OnDirective'
106- ? path . at ( - 2 )
107- : parent . type === 'ExpressionTag' &&
108- is_event_attribute ( /** @type {import('#compiler').Attribute } */ ( path . at ( - 2 ) ) )
109- ? path . at ( - 3 )
110- : null ;
107+ /** @type {import('#compiler').RegularElement | null } */
108+ let element = null ;
109+ /** @type {string | null } */
110+ let event_name = null ;
111+ if ( parent . type === 'OnDirective' ) {
112+ element = /** @type {import('#compiler').RegularElement } */ ( path . at ( - 2 ) ) ;
113+ event_name = parent . name ;
114+ } else if (
115+ parent . type === 'ExpressionTag' &&
116+ is_event_attribute ( /** @type {import('#compiler').Attribute } */ ( path . at ( - 2 ) ) )
117+ ) {
118+ element = /** @type {import('#compiler').RegularElement } */ ( path . at ( - 3 ) ) ;
119+ const attribute = /** @type {import('#compiler').Attribute } */ ( path . at ( - 2 ) ) ;
120+ event_name = get_attribute_event_name ( attribute . name ) ;
121+ }
111122
112- if ( element ) {
113- if ( element . type !== 'RegularElement' && element . type !== 'SlotElement' ) {
123+ if ( element && event_name ) {
124+ if (
125+ element . type !== 'RegularElement' ||
126+ ! determine_element_spread_and_delegatable ( element ) . metadata . can_delegate_events ||
127+ ( element . metadata . has_spread && node . type === 'Attribute' ) ||
128+ ! DelegatedEvents . includes ( event_name )
129+ ) {
114130 return non_hoistable ;
115131 }
116132 } else if ( parent . type !== 'FunctionDeclaration' && parent . type !== 'VariableDeclarator' ) {
@@ -772,16 +788,15 @@ const common_visitors = {
772788
773789 let name = node . name . slice ( 2 ) ;
774790
775- if (
776- name . endsWith ( 'capture' ) &&
777- name !== 'ongotpointercapture' &&
778- name !== 'onlostpointercapture'
779- ) {
791+ if ( is_capture_event ( name ) ) {
780792 name = name . slice ( 0 , - 7 ) ;
781793 modifiers . push ( 'capture' ) ;
782794 }
783795
784- const delegated_event = get_delegated_event ( { name, expression, modifiers } , context ) ;
796+ const delegated_event = get_delegated_event (
797+ { type : node . type , name, expression, modifiers } ,
798+ context
799+ ) ;
785800
786801 if ( delegated_event !== null ) {
787802 if ( delegated_event . type === 'hoistable' ) {
@@ -950,6 +965,8 @@ const common_visitors = {
950965 node . metadata . svg = true ;
951966 }
952967
968+ determine_element_spread_and_delegatable ( node ) ;
969+
953970 // Special case: Move the children of <textarea> into a value attribute if they are dynamic
954971 if (
955972 context . state . options . namespace !== 'foreign' &&
@@ -1005,6 +1022,77 @@ const common_visitors = {
10051022 }
10061023} ;
10071024
1025+ /**
1026+ * Check if events on this element can theoretically be delegated. They can if there's no
1027+ * possibility of an OnDirective and an event attribute on the same element, and if there's
1028+ * no OnDirectives of the same type (the latter is a bit too strict because `on:click on:click on:keyup`
1029+ * means that `on:keyup` can be delegated but we gloss over this edge case).
1030+ * @param {import('#compiler').RegularElement } node
1031+ */
1032+ function determine_element_spread_and_delegatable ( node ) {
1033+ if ( typeof node . metadata . can_delegate_events === 'boolean' ) {
1034+ return node ; // did this already
1035+ }
1036+
1037+ let events = new Map ( ) ;
1038+ let has_spread = false ;
1039+ let has_on = false ;
1040+ let has_action_or_bind = false ;
1041+ for ( const attribute of node . attributes ) {
1042+ if (
1043+ attribute . type === 'OnDirective' ||
1044+ ( attribute . type === 'Attribute' && is_event_attribute ( attribute ) )
1045+ ) {
1046+ let event_name = attribute . name ;
1047+ if ( attribute . type === 'Attribute' ) {
1048+ event_name = get_attribute_event_name ( event_name ) ;
1049+ }
1050+ events . set ( event_name , ( events . get ( event_name ) || 0 ) + 1 ) ;
1051+ if ( ! has_on && attribute . type === 'OnDirective' ) {
1052+ has_on = true ;
1053+ }
1054+ } else if ( ! has_spread && attribute . type === 'SpreadAttribute' ) {
1055+ has_spread = true ;
1056+ } else if (
1057+ ! has_action_or_bind &&
1058+ ( attribute . type === 'BindDirective' || attribute . type === 'UseDirective' )
1059+ ) {
1060+ has_action_or_bind = true ;
1061+ }
1062+ }
1063+ node . metadata . can_delegate_events =
1064+ // Actions/bindings need the old on:-events to fire in order
1065+ ! has_action_or_bind &&
1066+ // spreading events means we don't know if there's an event attribute with the same name as an on:-event
1067+ ! ( has_spread && has_on ) &&
1068+ // multiple on:-events/event attributes with the same name
1069+ ! [ ...events . values ( ) ] . some ( ( count ) => count > 1 ) ;
1070+ node . metadata . has_spread = has_spread ;
1071+
1072+ return node ;
1073+ }
1074+
1075+ /**
1076+ * @param {string } event_name
1077+ */
1078+ function get_attribute_event_name ( event_name ) {
1079+ if ( is_capture_event ( event_name ) ) {
1080+ event_name = event_name . slice ( 0 , - 7 ) ;
1081+ }
1082+ event_name = event_name . slice ( 2 ) ;
1083+ return event_name ;
1084+ }
1085+
1086+ /**
1087+ * @param {string } name
1088+ * @returns boolean
1089+ */
1090+ function is_capture_event ( name ) {
1091+ return (
1092+ name . endsWith ( 'capture' ) && name !== 'ongotpointercapture' && name !== 'onlostpointercapture'
1093+ ) ;
1094+ }
1095+
10081096/**
10091097 * @param {Map<import('estree').LabeledStatement, import('../types.js').ReactiveStatement> } unsorted_reactive_declarations
10101098 */
0 commit comments