|
9 | 9 | * - A `-` (not at the beginning) makes next character uppercase: `dbl-click` => `dblClick`
|
10 | 10 | */
|
11 | 11 |
|
| 12 | +export const enum EventNameJSXScope { |
| 13 | + on = 'on', |
| 14 | + window = 'window:on', |
| 15 | + document = 'document:on', |
| 16 | +} |
| 17 | + |
| 18 | +const enum EventNameHtmlScope { |
| 19 | + on = 'on:', |
| 20 | + window = 'on-window:', |
| 21 | + document = 'on-document:', |
| 22 | +} |
| 23 | + |
| 24 | +export const EVENT_SUFFIX = '$'; |
| 25 | +export const DOMContentLoadedEvent = 'DOMContentLoaded'; |
| 26 | + |
12 | 27 | export const isJsxPropertyAnEventName = (name: string): boolean => {
|
13 | 28 | return (
|
14 |
| - (name.startsWith('on') || name.startsWith('window:on') || name.startsWith('document:on')) && |
15 |
| - name.endsWith('$') |
| 29 | + (name.startsWith(EventNameJSXScope.on) || |
| 30 | + name.startsWith(EventNameJSXScope.window) || |
| 31 | + name.startsWith(EventNameJSXScope.document)) && |
| 32 | + name.endsWith(EVENT_SUFFIX) |
16 | 33 | );
|
17 | 34 | };
|
18 | 35 |
|
19 | 36 | export const isHtmlAttributeAnEventName = (name: string): boolean => {
|
20 |
| - return name.startsWith('on:') || name.startsWith('on-window:') || name.startsWith('on-document:'); |
| 37 | + return ( |
| 38 | + name.startsWith(EventNameHtmlScope.on) || |
| 39 | + name.startsWith(EventNameHtmlScope.window) || |
| 40 | + name.startsWith(EventNameHtmlScope.document) |
| 41 | + ); |
21 | 42 | };
|
22 | 43 |
|
23 |
| -export const getEventNameFromJsxProp = (name: string): string | null => { |
24 |
| - if (name.endsWith('$')) { |
25 |
| - let idx = -1; |
26 |
| - if (name.startsWith('on')) { |
27 |
| - idx = 2; |
28 |
| - } else if (name.startsWith('window:on')) { |
29 |
| - idx = 9; |
30 |
| - } else if (name.startsWith('document:on')) { |
31 |
| - idx = 11; |
32 |
| - } |
33 |
| - if (idx != -1) { |
34 |
| - const isCaseSensitive = isDashAt(name, idx) && !isDashAt(name, idx + 1); |
35 |
| - if (isCaseSensitive) { |
36 |
| - idx++; |
37 |
| - } |
38 |
| - let lastIdx = idx; |
39 |
| - let eventName = ''; |
40 |
| - while (true as boolean) { |
41 |
| - idx = name.indexOf('-', lastIdx); |
42 |
| - const chunk = name.substring( |
43 |
| - lastIdx, |
44 |
| - idx === -1 ? name.length - 1 /* don't include `$` */ : idx |
45 |
| - ); |
46 |
| - eventName += isCaseSensitive ? chunk : chunk.toLowerCase(); |
47 |
| - if (idx == -1) { |
48 |
| - return eventName; |
49 |
| - } |
50 |
| - if (isDashAt(name, idx + 1)) { |
51 |
| - eventName += '-'; |
52 |
| - idx++; |
53 |
| - } else { |
54 |
| - eventName += name.charAt(idx + 1).toUpperCase(); |
55 |
| - idx++; |
56 |
| - } |
57 |
| - lastIdx = idx + 1; |
58 |
| - } |
| 44 | +/** |
| 45 | + * Converts a JSX event property to an HTML attribute. Examples: |
| 46 | + * |
| 47 | + * - OnClick$ -> on:click |
| 48 | + * - On-DOMContentLoaded$ -> on:-d-o-m-content-loaded |
| 49 | + * - On-CustomEvent$ -> on:-custom-event |
| 50 | + */ |
| 51 | +export function jsxEventToHtmlAttribute(jsxEvent: string): string | null { |
| 52 | + if (jsxEvent.endsWith(EVENT_SUFFIX)) { |
| 53 | + const [prefix, idx] = getEventScopeDataFromJsxEvent(jsxEvent); |
| 54 | + |
| 55 | + if (idx !== -1) { |
| 56 | + const eventName = getEventNameFromJsxEvent(jsxEvent)!; |
| 57 | + return prefix + fromCamelToKebabCase(eventName); |
59 | 58 | }
|
60 | 59 | }
|
61 |
| - return null; |
62 |
| -}; |
| 60 | + return null; // Return null if not matching expected format |
| 61 | +} |
63 | 62 |
|
64 |
| -export const getEventNameScopeFromJsxProp = (name: string): string => { |
65 |
| - const index = name.indexOf(':'); |
66 |
| - return index !== -1 ? name.substring(0, index) : ''; |
67 |
| -}; |
| 63 | +/** |
| 64 | + * Converts an HTML attribute back to JSX event property. Examples: |
| 65 | + * |
| 66 | + * - On:click -> onClick$ |
| 67 | + * - On:-d-o-m-content-loaded -> onDOMContentLoaded$ |
| 68 | + * - On:-custom-event -> on-CustomEvent$ |
| 69 | + */ |
| 70 | +export function htmlAttributeToJsxEvent(htmlAttr: string): string | null { |
| 71 | + const eventScopeData = getEventScopeDataFromHtmlEvent(htmlAttr); |
| 72 | + let prefix = eventScopeData[0]; |
| 73 | + const idx = eventScopeData[1]; |
68 | 74 |
|
69 |
| -export const getEventNameFromHtmlAttr = (name: string): string | null => { |
70 |
| - let idx = -1; |
71 |
| - if (name.startsWith('on:')) { |
72 |
| - idx = 3; // 'on:'.length |
73 |
| - } else if (name.startsWith('on-window:')) { |
74 |
| - idx = 10; // 'on-window:'.length |
75 |
| - } else if (name.startsWith('on-document:')) { |
76 |
| - idx = 12; // 'on-document:'.length |
| 75 | + if (idx !== -1) { |
| 76 | + const isCaseSensitive = isDash(htmlAttr.charCodeAt(idx)); |
| 77 | + const eventName = htmlAttrToEventName(htmlAttr, idx); |
| 78 | + if (isCaseSensitive && eventName !== DOMContentLoadedEvent) { |
| 79 | + prefix += '-'; // Add hyphen at the start if case-sensitive |
| 80 | + } |
| 81 | + return eventNameToJsxEvent(eventName, prefix, idx); |
77 | 82 | }
|
78 |
| - if (idx != -1) { |
79 |
| - let lastIdx = idx; |
80 |
| - let eventName = ''; |
81 |
| - while (true as boolean) { |
82 |
| - idx = name.indexOf('-', lastIdx); |
83 |
| - const chunk = name.substring(lastIdx, idx === -1 ? name.length : idx); |
84 |
| - eventName += chunk; |
85 |
| - if (idx == -1) { |
86 |
| - return eventName; |
87 |
| - } |
88 |
| - eventName += name.charAt(idx + 1).toUpperCase(); |
89 |
| - idx++; |
90 |
| - lastIdx = idx + 1; |
| 83 | + return null; // Return null if not matching expected format |
| 84 | +} |
| 85 | + |
| 86 | +export function eventNameToJsxEvent(eventName: string, prefix: string | null, startIdx = 0) { |
| 87 | + eventName = eventName.charAt(0).toUpperCase() + eventName.substring(1); |
| 88 | + return prefix + eventName + EVENT_SUFFIX; |
| 89 | +} |
| 90 | + |
| 91 | +/** |
| 92 | + * Gets the event name from a JSX event property. Examples: |
| 93 | + * |
| 94 | + * - OnClick$ -> click |
| 95 | + * - OnDOMContentLoaded$ -> DOMContentLoaded |
| 96 | + * - On-CustomEvent$ -> CustomEvent |
| 97 | + */ |
| 98 | +export function getEventNameFromJsxEvent(jsxEvent: string): string | null { |
| 99 | + if (jsxEvent.endsWith(EVENT_SUFFIX)) { |
| 100 | + const [, idx] = getEventScopeDataFromJsxEvent(jsxEvent); |
| 101 | + if (idx != -1) { |
| 102 | + return jsxEventToEventName(jsxEvent, idx); |
91 | 103 | }
|
92 | 104 | }
|
93 | 105 | return null;
|
94 |
| -}; |
| 106 | +} |
95 | 107 |
|
96 |
| -const isDashAt = (name: string, idx: number): boolean => name.charCodeAt(idx) === 45; /* - */ |
| 108 | +function jsxEventToEventName(jsxEvent: string, startIdx: number = 0): string { |
| 109 | + const idx = startIdx; |
| 110 | + let lastIdx = idx; |
| 111 | + const isCaseSensitive = isDash(jsxEvent.charCodeAt(idx)); |
| 112 | + if (isCaseSensitive) { |
| 113 | + lastIdx++; |
| 114 | + } |
| 115 | + let eventName = ''; |
| 116 | + const chunk = jsxEvent.substring(lastIdx, jsxEvent.length - 1 /* don't include `$` */); |
| 117 | + if (chunk === DOMContentLoadedEvent) { |
| 118 | + return DOMContentLoadedEvent; |
| 119 | + } |
| 120 | + eventName += isCaseSensitive ? chunk : chunk.toLowerCase(); |
| 121 | + return eventName; |
| 122 | +} |
97 | 123 |
|
98 |
| -export const convertEventNameFromHtmlAttrToJsxProp = (name: string): string | null => { |
99 |
| - let prefix: string | null = null; |
100 |
| - if (name.startsWith('on:')) { |
101 |
| - prefix = 'on'; |
102 |
| - } else if (name.startsWith('on-window:')) { |
103 |
| - prefix = 'window:on'; |
104 |
| - } else if (name.startsWith('on-document:')) { |
105 |
| - prefix = 'document:on'; |
| 124 | +/** |
| 125 | + * Gets the event name from an HTML attribute. Examples: |
| 126 | + * |
| 127 | + * - On:click -> click |
| 128 | + * - On:-d-o-m-content-loaded -> DOMContentLoaded |
| 129 | + * - On:-custom-event -> CustomEvent |
| 130 | + */ |
| 131 | +export function getEventNameFromHtmlAttribute(htmlAttr: string): string { |
| 132 | + const [, idx] = getEventScopeDataFromHtmlEvent(htmlAttr); |
| 133 | + if (idx !== -1) { |
| 134 | + return htmlAttrToEventName(htmlAttr, idx); |
106 | 135 | }
|
107 |
| - if (prefix !== null) { |
108 |
| - const eventName = getEventNameFromHtmlAttr(name)!; |
109 |
| - let kebabCase = fromCamelToKebabCase(eventName); |
110 |
| - if (isDashAt(kebabCase, 0) && !isDashAt(kebabCase, 1)) { |
111 |
| - // special case for events which start with a `-` |
112 |
| - // if we would just append it would be interpreted as a case sensitive event |
113 |
| - kebabCase = '-' + kebabCase.charAt(1).toUpperCase() + kebabCase.substring(2); |
114 |
| - } |
115 |
| - return prefix + kebabCase + '$'; |
| 136 | + return htmlAttr; // Return as is if not matching expected format |
| 137 | +} |
| 138 | + |
| 139 | +/** Helper function to convert HTML attribute name to event name. */ |
| 140 | +function htmlAttrToEventName(htmlAttr: string, startIdx: number = 0): string { |
| 141 | + let idx = startIdx; |
| 142 | + let lastIdx = idx; |
| 143 | + let eventName = ''; |
| 144 | + const isCaseSensitive = isDash(htmlAttr.charCodeAt(lastIdx)); |
| 145 | + if (isCaseSensitive) { |
| 146 | + lastIdx++; // Skip the hyphen |
| 147 | + eventName += htmlAttr.charAt(lastIdx).toUpperCase(); // Capitalize the first letter |
| 148 | + lastIdx++; // Skip the first letter |
116 | 149 | }
|
117 |
| - return null; |
118 |
| -}; |
119 | 150 |
|
120 |
| -export const convertEventNameFromJsxPropToHtmlAttr = (name: string): string | null => { |
121 |
| - if (name.endsWith('$')) { |
122 |
| - let prefix: string | null = null; |
123 |
| - // let idx = -1; |
124 |
| - if (name.startsWith('on')) { |
125 |
| - prefix = 'on:'; |
126 |
| - // idx = 2; // 'on'.length |
127 |
| - } else if (name.startsWith('window:on')) { |
128 |
| - prefix = 'on-window:'; |
129 |
| - // idx = 9; // 'window:on'.length |
130 |
| - } else if (name.startsWith('document:on')) { |
131 |
| - prefix = 'on-document:'; |
132 |
| - // idx = 11; // 'document:on'.length |
133 |
| - } |
134 |
| - if (prefix !== null) { |
135 |
| - const eventName = getEventNameFromJsxProp(name)!; |
136 |
| - return prefix + fromCamelToKebabCase(eventName); |
| 151 | + while (true as boolean) { |
| 152 | + idx = htmlAttr.indexOf('-', lastIdx); // Find the next hyphen |
| 153 | + const chunk = htmlAttr.substring(lastIdx, idx === -1 ? htmlAttr.length : idx); // Get the chunk |
| 154 | + eventName += chunk; // Add the chunk to the event name |
| 155 | + if (idx == -1) { |
| 156 | + return eventName; // Return the event name if no more hyphens |
137 | 157 | }
|
| 158 | + idx++; // Move to the next character after the hyphen |
| 159 | + eventName += htmlAttr.charAt(idx).toUpperCase(); // Capitalize the next letter if previous character is hyphen |
| 160 | + lastIdx = idx + 1; // Move to the next character |
138 | 161 | }
|
139 |
| - return null; |
140 |
| -}; |
| 162 | + return eventName; |
| 163 | +} |
141 | 164 |
|
142 |
| -export const fromCamelToKebabCase = (text: string): string => { |
143 |
| - return text.replace(/([A-Z-])/g, '-$1').toLowerCase(); |
| 165 | +export function getEventScopeDataFromJsxEvent(eventName: string): [string | null, number] { |
| 166 | + let prefix: EventNameHtmlScope | null = null; |
| 167 | + let idx = -1; |
| 168 | + // set prefix and idx based on the scope |
| 169 | + if (eventName.startsWith(EventNameJSXScope.on)) { |
| 170 | + prefix = EventNameHtmlScope.on; |
| 171 | + idx = 2; |
| 172 | + } else if (eventName.startsWith(EventNameJSXScope.window)) { |
| 173 | + prefix = EventNameHtmlScope.window; |
| 174 | + idx = 9; |
| 175 | + } else if (eventName.startsWith(EventNameJSXScope.document)) { |
| 176 | + prefix = EventNameHtmlScope.document; |
| 177 | + idx = 11; |
| 178 | + } |
| 179 | + return [prefix, idx]; |
| 180 | +} |
| 181 | + |
| 182 | +function getEventScopeDataFromHtmlEvent(htmlAttr: string): [string | null, number] { |
| 183 | + let prefix: EventNameJSXScope | null = null; |
| 184 | + let idx = -1; |
| 185 | + if (htmlAttr.startsWith(EventNameHtmlScope.on)) { |
| 186 | + prefix = EventNameJSXScope.on; |
| 187 | + idx = 3; |
| 188 | + } else if (htmlAttr.startsWith(EventNameHtmlScope.window)) { |
| 189 | + prefix = EventNameJSXScope.window; |
| 190 | + idx = 10; |
| 191 | + } else if (htmlAttr.startsWith(EventNameHtmlScope.document)) { |
| 192 | + prefix = EventNameJSXScope.document; |
| 193 | + idx = 12; |
| 194 | + } |
| 195 | + return [prefix, idx]; |
| 196 | +} |
| 197 | + |
| 198 | +export const isDash = (charCode: number): boolean => charCode === 45; /* - */ |
| 199 | + |
| 200 | +export const getEventNameScopeFromJsxEvent = (name: string): string => { |
| 201 | + const index = name.indexOf(':'); |
| 202 | + return index !== -1 ? name.substring(0, index) : ''; |
144 | 203 | };
|
145 | 204 |
|
146 | 205 | export function isPreventDefault(key: string): boolean {
|
147 | 206 | return key.startsWith('preventdefault:');
|
148 | 207 | }
|
| 208 | + |
| 209 | +export const fromCamelToKebabCase = (text: string): string => { |
| 210 | + return text.replace(/([A-Z-])/g, '-$1').toLowerCase(); |
| 211 | +}; |
0 commit comments