Skip to content

Commit

Permalink
fix: [#474] Event listener properties prefixed with "on" should be th…
Browse files Browse the repository at this point in the history
…e evaluated value of the corresponding attribute (#1760)

* fix: [#474] Event listener properties should be the value of the corresponding attribute

* fix: [#474] Event listener properties should be the value of the corresponding attribute
  • Loading branch information
capricorn86 authored Mar 3, 2025
1 parent 73a3672 commit ee6fff7
Show file tree
Hide file tree
Showing 46 changed files with 3,969 additions and 405 deletions.
20 changes: 20 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "0.2",
"ignorePaths": [],
"dictionaryDefinitions": [],
"dictionaries": [],
"words": [
"clonable",
"ISVG",
"oncompositionend",
"oncompositionstart",
"oncompositionupdate",
"oncontentvisibilityautostatechange",
"onpointerrawupdate",
"onscrollsnapchange",
"onscrollsnapchanging",
"onsearch"
],
"ignoreWords": [],
"import": []
}
1 change: 1 addition & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,4 @@ export const moduleImportMap = Symbol('moduleImportMap');
export const dispatchError = Symbol('dispatchError');
export const supports = Symbol('supports');
export const reason = Symbol('reason');
export const propertyEventListeners = Symbol('propertyEventListeners');
139 changes: 69 additions & 70 deletions packages/happy-dom/src/event/EventTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,105 +234,104 @@ export default class EventTarget {
const browserSettings = window ? new WindowBrowserContext(window).getSettings() : null;
const eventPhase = event.eventPhase === EventPhaseEnum.capturing ? 'capturing' : 'bubbling';

if (event.eventPhase !== EventPhaseEnum.capturing) {
const onEventName = 'on' + event.type.toLowerCase();
// We need to clone the arrays because the listeners may remove themselves while we are iterating.
const listeners = this[PropertySymbol.listeners][eventPhase].get(event.type)?.slice();

if (listeners && listeners.length) {
const listenerOptions = this[PropertySymbol.listenerOptions][eventPhase]
.get(event.type)
?.slice();

for (let i = 0, max = listeners.length; i < max; i++) {
const listener = listeners[i];
const options = listenerOptions[i];

if (options?.passive) {
event[PropertySymbol.isInPassiveEventListener] = true;
}

if (typeof this[onEventName] === 'function') {
// We can end up in a never ending loop if the listener for the error event on Window also throws an error.
if (
window &&
(this !== <EventTarget>window || event.type !== 'error') &&
!browserSettings?.disableErrorCapturing &&
browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch
) {
let result: any;
try {
result = this[onEventName].call(this, event);
} catch (error) {
window[PropertySymbol.dispatchError](error);
}

if (result instanceof Promise) {
result.catch((error) => window[PropertySymbol.dispatchError](error));
if ((<TEventListenerObject>listener).handleEvent) {
let result: any;
try {
result = (<TEventListenerObject>listener).handleEvent.call(listener, event);
} catch (error) {
window[PropertySymbol.dispatchError](error);
}

if (result instanceof Promise) {
result.catch((error) => window[PropertySymbol.dispatchError](error));
}
} else {
let result: any;
try {
result = (<TEventListenerFunction>listener).call(this, event);
} catch (error) {
window[PropertySymbol.dispatchError](error);
}

if (result instanceof Promise) {
result.catch((error) => window[PropertySymbol.dispatchError](error));
}
}
} else {
this[onEventName].call(this, event);
if ((<TEventListenerObject>listener).handleEvent) {
(<TEventListenerObject>listener).handleEvent.call(this, event);
} else {
(<TEventListenerFunction>listener).call(this, event);
}
}
}
}

// We need to clone the arrays because the listeners may remove themselves while we are iterating.
const listeners = this[PropertySymbol.listeners][eventPhase].get(event.type)?.slice();

if (!listeners) {
return;
}

const listenerOptions = this[PropertySymbol.listenerOptions][eventPhase]
.get(event.type)
?.slice();
event[PropertySymbol.isInPassiveEventListener] = false;

for (let i = 0, max = listeners.length; i < max; i++) {
const listener = listeners[i];
const options = listenerOptions[i];
if (options?.once) {
// At this time, listeners and listenersOptions are cloned arrays. When the original value is deleted,
// The value corresponding to the cloned array is not deleted. So we need to delete the value in the cloned array.
listeners.splice(i, 1);
listenerOptions.splice(i, 1);
this.removeEventListener(event.type, listener);
i--;
max--;
}

if (options?.passive) {
event[PropertySymbol.isInPassiveEventListener] = true;
if (event[PropertySymbol.immediatePropagationStopped]) {
return;
}
}
}

// We can end up in a never ending loop if the listener for the error event on Window also throws an error.
if (
window &&
(this !== <EventTarget>window || event.type !== 'error') &&
!browserSettings?.disableErrorCapturing &&
browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch
) {
if ((<TEventListenerObject>listener).handleEvent) {
let result: any;
try {
result = (<TEventListenerObject>listener).handleEvent.call(listener, event);
} catch (error) {
window[PropertySymbol.dispatchError](error);
}
if (event.eventPhase !== EventPhaseEnum.capturing) {
const onEventName = 'on' + event.type.toLowerCase();
const eventListener = this[onEventName];

if (result instanceof Promise) {
result.catch((error) => window[PropertySymbol.dispatchError](error));
}
} else {
if (typeof eventListener === 'function') {
// We can end up in a never ending loop if the listener for the error event on Window also throws an error.
if (
window &&
(this !== <EventTarget>window || event.type !== 'error') &&
!browserSettings?.disableErrorCapturing &&
browserSettings?.errorCapture === BrowserErrorCaptureEnum.tryAndCatch
) {
let result: any;
try {
result = (<TEventListenerFunction>listener).call(this, event);
result = eventListener(event);
} catch (error) {
window[PropertySymbol.dispatchError](error);
}

if (result instanceof Promise) {
result.catch((error) => window[PropertySymbol.dispatchError](error));
}
}
} else {
if ((<TEventListenerObject>listener).handleEvent) {
(<TEventListenerObject>listener).handleEvent.call(this, event);
} else {
(<TEventListenerFunction>listener).call(this, event);
eventListener(event);
}
}

event[PropertySymbol.isInPassiveEventListener] = false;

if (options?.once) {
// At this time, listeners and listenersOptions are cloned arrays. When the original value is deleted,
// The value corresponding to the cloned array is not deleted. So we need to delete the value in the cloned array.
listeners.splice(i, 1);
listenerOptions.splice(i, 1);
this.removeEventListener(event.type, listener);
i--;
max--;
}

if (event[PropertySymbol.immediatePropagationStopped]) {
return;
}
}
}
}
Loading

0 comments on commit ee6fff7

Please sign in to comment.