@@ -11,7 +11,7 @@ import {
11
11
object
12
12
} from '../../utils/ast.js' ;
13
13
import * as b from '../../utils/builders.js' ;
14
- import { DelegatedEvents , ReservedKeywords , Runes , SVGElements } from '../constants.js' ;
14
+ import { ReservedKeywords , Runes , SVGElements } from '../constants.js' ;
15
15
import { Scope , ScopeRoot , create_scopes , get_rune , set_scope } from '../scope.js' ;
16
16
import { merge } from '../visitors.js' ;
17
17
import Stylesheet from './css/Stylesheet.js' ;
@@ -20,6 +20,7 @@ import { warn } from '../../warnings.js';
20
20
import check_graph_for_cycles from './utils/check_graph_for_cycles.js' ;
21
21
import { regex_starts_with_newline } from '../patterns.js' ;
22
22
import { create_attribute , is_element_node } from '../nodes.js' ;
23
+ import { DelegatedEvents } from '../../../constants.js' ;
23
24
24
25
/**
25
26
* @param {import('#compiler').Script | null } script
@@ -58,7 +59,7 @@ function get_component_name(filename) {
58
59
}
59
60
60
61
/**
61
- * @param {Pick<import('#compiler').OnDirective, 'expression'| 'name' | 'modifiers'> } node
62
+ * @param {Pick<import('#compiler').OnDirective, 'expression'| 'name' | 'modifiers'> & { type: string } } node
62
63
* @param {import('./types').Context } context
63
64
* @returns {null | import('#compiler').DelegatedEvent }
64
65
*/
@@ -70,16 +71,13 @@ function get_delegated_event(node, context) {
70
71
if ( ! handler || node . modifiers . includes ( 'capture' ) || ! DelegatedEvents . includes ( event_name ) ) {
71
72
return null ;
72
73
}
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.
74
75
const element = context . path . at ( - 1 ) ;
75
- if ( element == null || ( element . type !== 'RegularElement' && element . type !== 'SlotElement' ) ) {
76
+ if ( element ? .type !== 'RegularElement' ) {
76
77
return null ;
77
78
}
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 ) {
83
81
return null ;
84
82
}
85
83
@@ -89,6 +87,11 @@ function get_delegated_event(node, context) {
89
87
let target_function = null ;
90
88
let binding = null ;
91
89
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
+
92
95
if ( handler . type === 'ArrowFunctionExpression' || handler . type === 'FunctionExpression' ) {
93
96
target_function = handler ;
94
97
} else if ( handler . type === 'Identifier' ) {
@@ -101,16 +104,29 @@ function get_delegated_event(node, context) {
101
104
return non_hoistable ;
102
105
}
103
106
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
+ }
111
122
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
+ ) {
114
130
return non_hoistable ;
115
131
}
116
132
} else if ( parent . type !== 'FunctionDeclaration' && parent . type !== 'VariableDeclarator' ) {
@@ -772,16 +788,15 @@ const common_visitors = {
772
788
773
789
let name = node . name . slice ( 2 ) ;
774
790
775
- if (
776
- name . endsWith ( 'capture' ) &&
777
- name !== 'ongotpointercapture' &&
778
- name !== 'onlostpointercapture'
779
- ) {
791
+ if ( is_capture_event ( name ) ) {
780
792
name = name . slice ( 0 , - 7 ) ;
781
793
modifiers . push ( 'capture' ) ;
782
794
}
783
795
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
+ ) ;
785
800
786
801
if ( delegated_event !== null ) {
787
802
if ( delegated_event . type === 'hoistable' ) {
@@ -950,6 +965,8 @@ const common_visitors = {
950
965
node . metadata . svg = true ;
951
966
}
952
967
968
+ determine_element_spread_and_delegatable ( node ) ;
969
+
953
970
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
954
971
if (
955
972
context . state . options . namespace !== 'foreign' &&
@@ -1005,6 +1022,77 @@ const common_visitors = {
1005
1022
}
1006
1023
} ;
1007
1024
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
+
1008
1096
/**
1009
1097
* @param {Map<import('estree').LabeledStatement, import('../types.js').ReactiveStatement> } unsorted_reactive_declarations
1010
1098
*/
0 commit comments