happy-dom 9.8.0 → 9.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of happy-dom might be problematic. Click here for more details.

Files changed (37) hide show
  1. package/lib/event/Event.d.ts.map +1 -1
  2. package/lib/event/Event.js +15 -14
  3. package/lib/event/Event.js.map +1 -1
  4. package/lib/event/EventTarget.d.ts +9 -0
  5. package/lib/event/EventTarget.d.ts.map +1 -1
  6. package/lib/event/EventTarget.js +73 -6
  7. package/lib/event/EventTarget.js.map +1 -1
  8. package/lib/nodes/document/Document.d.ts +0 -7
  9. package/lib/nodes/document/Document.d.ts.map +1 -1
  10. package/lib/nodes/document/Document.js +0 -13
  11. package/lib/nodes/document/Document.js.map +1 -1
  12. package/lib/nodes/element/Element.d.ts.map +1 -1
  13. package/lib/nodes/element/Element.js +8 -3
  14. package/lib/nodes/element/Element.js.map +1 -1
  15. package/lib/nodes/html-button-element/HTMLButtonElement.d.ts.map +1 -1
  16. package/lib/nodes/html-button-element/HTMLButtonElement.js +7 -2
  17. package/lib/nodes/html-button-element/HTMLButtonElement.js.map +1 -1
  18. package/lib/nodes/html-input-element/HTMLInputElement.d.ts.map +1 -1
  19. package/lib/nodes/html-input-element/HTMLInputElement.js +11 -2
  20. package/lib/nodes/html-input-element/HTMLInputElement.js.map +1 -1
  21. package/lib/nodes/node/Node.d.ts +0 -18
  22. package/lib/nodes/node/Node.d.ts.map +1 -1
  23. package/lib/nodes/node/Node.js +0 -75
  24. package/lib/nodes/node/Node.js.map +1 -1
  25. package/lib/window/Window.d.ts +3 -0
  26. package/lib/window/Window.d.ts.map +1 -1
  27. package/lib/window/Window.js +4 -0
  28. package/lib/window/Window.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/event/Event.ts +15 -14
  31. package/src/event/EventTarget.ts +92 -8
  32. package/src/nodes/document/Document.ts +0 -19
  33. package/src/nodes/element/Element.ts +11 -3
  34. package/src/nodes/html-button-element/HTMLButtonElement.ts +9 -2
  35. package/src/nodes/html-input-element/HTMLInputElement.ts +14 -2
  36. package/src/nodes/node/Node.ts +0 -105
  37. package/src/window/Window.ts +6 -0
@@ -6,6 +6,7 @@ import IEventTarget from './IEventTarget';
6
6
  import NodeTypeEnum from '../nodes/node/NodeTypeEnum';
7
7
  import { performance } from 'perf_hooks';
8
8
  import EventPhaseEnum from './EventPhaseEnum';
9
+ import IDocument from '../nodes/document/IDocument';
9
10
 
10
11
  /**
11
12
  * Event.
@@ -77,28 +78,28 @@ export default class Event {
77
78
  * @returns Composed path.
78
79
  */
79
80
  public composedPath(): IEventTarget[] {
80
- if (!this.target) {
81
+ if (!this._target) {
81
82
  return [];
82
83
  }
83
84
 
84
85
  const composedPath = [];
85
- let eventTarget: INode | IShadowRoot | IWindow = <INode | IShadowRoot>(<unknown>this.target);
86
+ let eventTarget: INode | IShadowRoot | IWindow = <INode | IShadowRoot>(<unknown>this._target);
86
87
 
87
88
  while (eventTarget) {
88
89
  composedPath.push(eventTarget);
89
90
 
90
- if (this.bubbles) {
91
- if (
92
- this.composed &&
93
- (<INode>eventTarget).nodeType === NodeTypeEnum.documentFragmentNode &&
94
- (<IShadowRoot>eventTarget).host
95
- ) {
96
- eventTarget = (<IShadowRoot>eventTarget).host;
97
- } else if ((<INode>(<unknown>this.target)).ownerDocument === eventTarget) {
98
- eventTarget = (<INode>(<unknown>this.target)).ownerDocument.defaultView;
99
- } else {
100
- eventTarget = (<INode>(<unknown>eventTarget)).parentNode || null;
101
- }
91
+ if ((<INode>(<unknown>eventTarget)).parentNode) {
92
+ eventTarget = (<INode>(<unknown>eventTarget)).parentNode;
93
+ } else if (
94
+ this.composed &&
95
+ (<INode>eventTarget).nodeType === NodeTypeEnum.documentFragmentNode &&
96
+ (<IShadowRoot>eventTarget).host
97
+ ) {
98
+ eventTarget = (<IShadowRoot>eventTarget).host;
99
+ } else if ((<INode>eventTarget).nodeType === NodeTypeEnum.documentNode) {
100
+ eventTarget = (<IDocument>(<unknown>eventTarget)).defaultView;
101
+ } else {
102
+ break;
102
103
  }
103
104
  }
104
105
 
@@ -3,6 +3,9 @@ import Event from './Event';
3
3
  import IEventTarget from './IEventTarget';
4
4
  import IEventListenerOptions from './IEventListenerOptions';
5
5
  import EventPhaseEnum from './EventPhaseEnum';
6
+ import INode from '../nodes/node/INode';
7
+ import IDocument from '../nodes/document/IDocument';
8
+ import IWindow from '../window/IWindow';
6
9
 
7
10
  /**
8
11
  * Handles events.
@@ -28,13 +31,23 @@ export default abstract class EventTarget implements IEventTarget {
28
31
  listener: ((event: Event) => void) | IEventListener,
29
32
  options?: boolean | IEventListenerOptions
30
33
  ): void {
34
+ const listenerOptions = typeof options === 'boolean' ? { capture: options } : options || null;
35
+
31
36
  this._listeners[type] = this._listeners[type] || [];
32
37
  this._listenerOptions[type] = this._listenerOptions[type] || [];
33
38
 
34
39
  this._listeners[type].push(listener);
35
- this._listenerOptions[type].push(
36
- typeof options === 'boolean' ? { capture: options } : options || null
37
- );
40
+ this._listenerOptions[type].push(listenerOptions);
41
+
42
+ // Tracks the amount of capture event listeners to improve performance when they are not used.
43
+ if (listenerOptions && listenerOptions.capture) {
44
+ const window = this._getWindow();
45
+ if (window) {
46
+ window['_captureEventListenerCount'][type] =
47
+ window['_captureEventListenerCount'][type] ?? 0;
48
+ window['_captureEventListenerCount'][type]++;
49
+ }
50
+ }
38
51
  }
39
52
 
40
53
  /**
@@ -50,6 +63,14 @@ export default abstract class EventTarget implements IEventTarget {
50
63
  if (this._listeners[type]) {
51
64
  const index = this._listeners[type].indexOf(listener);
52
65
  if (index !== -1) {
66
+ // Tracks the amount of capture event listeners to improve performance when they are not used.
67
+ if (this._listenerOptions[type][index] && this._listenerOptions[type][index].capture) {
68
+ const window = this._getWindow();
69
+ if (window && window['_captureEventListenerCount'][type]) {
70
+ window['_captureEventListenerCount'][type]--;
71
+ }
72
+ }
73
+
53
74
  this._listeners[type].splice(index, 1);
54
75
  this._listenerOptions[type].splice(index, 1);
55
76
  }
@@ -59,21 +80,63 @@ export default abstract class EventTarget implements IEventTarget {
59
80
  /**
60
81
  * Dispatches an event.
61
82
  *
83
+ * @see https://www.w3.org/TR/DOM-Level-3-Events/#event-flow
84
+ * @see https://www.quirksmode.org/js/events_order.html#link4
62
85
  * @param event Event.
63
86
  * @returns The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault().
64
87
  */
65
88
  public dispatchEvent(event: Event): boolean {
66
- if (!event._target) {
89
+ if (event.eventPhase === EventPhaseEnum.none) {
67
90
  event._target = this;
91
+
92
+ const composedPath = event.composedPath();
93
+ const window = this._getWindow();
94
+
95
+ // Capturing phase
96
+
97
+ // We only need to iterate over the composed path if there are capture event listeners.
98
+ if (window && window['_captureEventListenerCount'][event.type]) {
99
+ event.eventPhase = EventPhaseEnum.capturing;
100
+
101
+ for (let i = composedPath.length - 1; i >= 0; i--) {
102
+ composedPath[i].dispatchEvent(event);
103
+ if (event._propagationStopped || event._immediatePropagationStopped) {
104
+ break;
105
+ }
106
+ }
107
+ }
108
+
109
+ // At target phase
68
110
  event.eventPhase = EventPhaseEnum.atTarget;
111
+
112
+ this.dispatchEvent(event);
113
+
114
+ // Bubbling phase
115
+ if (event.bubbles && !event._propagationStopped && !event._immediatePropagationStopped) {
116
+ event.eventPhase = EventPhaseEnum.bubbling;
117
+
118
+ for (let i = 1; i < composedPath.length; i++) {
119
+ composedPath[i].dispatchEvent(event);
120
+ if (event._propagationStopped || event._immediatePropagationStopped) {
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ // None phase (completed)
127
+ event.eventPhase = EventPhaseEnum.none;
128
+
129
+ return !(event.cancelable && event.defaultPrevented);
69
130
  }
70
131
 
71
132
  event._currentTarget = this;
72
133
 
73
- const onEventName = 'on' + event.type.toLowerCase();
134
+ if (event.eventPhase !== EventPhaseEnum.capturing) {
135
+ const onEventName = 'on' + event.type.toLowerCase();
74
136
 
75
- if (typeof this[onEventName] === 'function') {
76
- this[onEventName].call(this, event);
137
+ if (typeof this[onEventName] === 'function') {
138
+ this[onEventName].call(this, event);
139
+ }
77
140
  }
78
141
 
79
142
  if (this._listeners[event.type]) {
@@ -85,7 +148,10 @@ export default abstract class EventTarget implements IEventTarget {
85
148
  const listener = listeners[i];
86
149
  const options = listenerOptions[i];
87
150
 
88
- if (options?.capture && event.eventPhase !== EventPhaseEnum.capturing) {
151
+ if (
152
+ (options?.capture && event.eventPhase !== EventPhaseEnum.capturing) ||
153
+ (!options?.capture && event.eventPhase === EventPhaseEnum.capturing)
154
+ ) {
89
155
  continue;
90
156
  }
91
157
 
@@ -141,4 +207,22 @@ export default abstract class EventTarget implements IEventTarget {
141
207
  public detachEvent(type: string, listener: ((event: Event) => void) | IEventListener): void {
142
208
  this.removeEventListener(type.replace('on', ''), listener);
143
209
  }
210
+
211
+ /**
212
+ * Finds and returns window if possible.
213
+ *
214
+ * @returns Window.
215
+ */
216
+ public _getWindow(): IWindow | null {
217
+ if ((<INode>(<unknown>this)).ownerDocument) {
218
+ return (<INode>(<unknown>this)).ownerDocument.defaultView;
219
+ }
220
+ if ((<IDocument>(<unknown>this)).defaultView) {
221
+ return (<IDocument>(<unknown>this)).defaultView;
222
+ }
223
+ if ((<IWindow>(<unknown>this)).document) {
224
+ return <IWindow>(<unknown>this);
225
+ }
226
+ return null;
227
+ }
144
228
  }
@@ -70,12 +70,6 @@ export default class Document extends Node implements IDocument {
70
70
  // Public in order to be accessible by the fetch and xhr.
71
71
  public _cookie = new CookieJar();
72
72
 
73
- // List of all nodes that has capture listeners.
74
- // We need to keep track of them globally as capture listeners are called before other listeners.
75
- public readonly _captureEventListenerNodes: {
76
- [eventType: string]: INode[];
77
- } = {};
78
-
79
73
  protected _isFirstWrite = true;
80
74
  protected _isFirstWriteAfterOpen = false;
81
75
 
@@ -969,19 +963,6 @@ export default class Document extends Node implements IDocument {
969
963
  return !!this.activeElement;
970
964
  }
971
965
 
972
- /**
973
- * @override
974
- */
975
- public dispatchEvent(event: Event): boolean {
976
- const returnValue = super.dispatchEvent(event);
977
-
978
- if (event.bubbles && !event._propagationStopped) {
979
- return this.defaultView.dispatchEvent(event);
980
- }
981
-
982
- return returnValue;
983
- }
984
-
985
966
  /**
986
967
  * Triggered by window when it is ready.
987
968
  */
@@ -31,6 +31,7 @@ import Event from '../../event/Event';
31
31
  import ElementUtility from './ElementUtility';
32
32
  import HTMLCollection from './HTMLCollection';
33
33
  import CharacterDataUtility from '../character-data/CharacterDataUtility';
34
+ import EventPhaseEnum from '../../event/EventPhaseEnum';
34
35
 
35
36
  /**
36
37
  * Element.
@@ -1041,10 +1042,17 @@ export default class Element extends Node implements IElement {
1041
1042
  */
1042
1043
  public override dispatchEvent(event: Event): boolean {
1043
1044
  const returnValue = super.dispatchEvent(event);
1044
- const attribute = this.getAttribute('on' + event.type);
1045
1045
 
1046
- if (attribute && !event._immediatePropagationStopped) {
1047
- this.ownerDocument.defaultView.eval(attribute);
1046
+ if (
1047
+ (event.eventPhase === EventPhaseEnum.atTarget ||
1048
+ event.eventPhase === EventPhaseEnum.bubbling) &&
1049
+ !event._immediatePropagationStopped
1050
+ ) {
1051
+ const attribute = this.getAttribute('on' + event.type);
1052
+
1053
+ if (attribute && !event._immediatePropagationStopped) {
1054
+ this.ownerDocument.defaultView.eval(attribute);
1055
+ }
1048
1056
  }
1049
1057
 
1050
1058
  return returnValue;
@@ -1,4 +1,5 @@
1
1
  import Event from '../../event/Event';
2
+ import EventPhaseEnum from '../../event/EventPhaseEnum';
2
3
  import ValidityState from '../../validity-state/ValidityState';
3
4
  import IAttr from '../attr/IAttr';
4
5
  import IDocument from '../document/IDocument';
@@ -210,13 +211,19 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto
210
211
  * @override
211
212
  */
212
213
  public override dispatchEvent(event: Event): boolean {
213
- if (event.type === 'click' && this.disabled) {
214
+ if (event.type === 'click' && event.eventPhase === EventPhaseEnum.none && this.disabled) {
214
215
  return false;
215
216
  }
216
217
 
217
218
  const returnValue = super.dispatchEvent(event);
218
219
 
219
- if (event.type === 'click' && this._formNode && this.isConnected) {
220
+ if (
221
+ event.type === 'click' &&
222
+ (event.eventPhase === EventPhaseEnum.atTarget ||
223
+ event.eventPhase === EventPhaseEnum.bubbling) &&
224
+ this._formNode &&
225
+ this.isConnected
226
+ ) {
220
227
  const form = <IHTMLFormElement>this._formNode;
221
228
  switch (this.type) {
222
229
  case 'submit':
@@ -21,6 +21,7 @@ import IHTMLLabelElement from '../html-label-element/IHTMLLabelElement';
21
21
  import IDocument from '../document/IDocument';
22
22
  import IShadowRoot from '../shadow-root/IShadowRoot';
23
23
  import NodeList from '../node/NodeList';
24
+ import EventPhaseEnum from '../../event/EventPhaseEnum';
24
25
 
25
26
  /**
26
27
  * HTML Input Element.
@@ -1039,19 +1040,30 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE
1039
1040
  * @override
1040
1041
  */
1041
1042
  public override dispatchEvent(event: Event): boolean {
1042
- if (event.type === 'click' && this.disabled) {
1043
+ if (event.type === 'click' && event.eventPhase === EventPhaseEnum.none && this.disabled) {
1043
1044
  return false;
1044
1045
  }
1045
1046
 
1047
+ if (
1048
+ event.type === 'click' &&
1049
+ (event.eventPhase === EventPhaseEnum.atTarget ||
1050
+ event.eventPhase === EventPhaseEnum.bubbling) &&
1051
+ this.isConnected &&
1052
+ (this.type === 'checkbox' || this.type === 'radio')
1053
+ ) {
1054
+ this.checked = this.type === 'checkbox' ? !this.checked : true;
1055
+ }
1056
+
1046
1057
  const returnValue = super.dispatchEvent(event);
1047
1058
 
1048
1059
  if (
1049
1060
  event.type === 'click' &&
1061
+ (event.eventPhase === EventPhaseEnum.atTarget ||
1062
+ event.eventPhase === EventPhaseEnum.bubbling) &&
1050
1063
  this.isConnected &&
1051
1064
  (!this.readOnly || this.type === 'checkbox' || this.type === 'radio')
1052
1065
  ) {
1053
1066
  if (this.type === 'checkbox' || this.type === 'radio') {
1054
- this.checked = this.type === 'checkbox' ? !this.checked : true;
1055
1067
  this.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
1056
1068
  this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
1057
1069
  } else if (this.type === 'submit') {
@@ -2,7 +2,6 @@ import EventTarget from '../../event/EventTarget';
2
2
  import MutationRecord from '../../mutation-observer/MutationRecord';
3
3
  import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum';
4
4
  import MutationListener from '../../mutation-observer/MutationListener';
5
- import Event from '../../event/Event';
6
5
  import INode from './INode';
7
6
  import DOMException from '../../exception/DOMException';
8
7
  import IDocument from '../document/IDocument';
@@ -14,10 +13,6 @@ import NodeUtility from './NodeUtility';
14
13
  import IAttr from '../attr/IAttr';
15
14
  import NodeList from './NodeList';
16
15
  import INodeList from './INodeList';
17
- import IShadowRoot from '../shadow-root/IShadowRoot';
18
- import IEventListener from '../../event/IEventListener';
19
- import IEventListenerOptions from '../../event/IEventListenerOptions';
20
- import EventPhaseEnum from '../../event/EventPhaseEnum';
21
16
 
22
17
  const JSON_CIRCULAR_PROPERTIES = [
23
18
  'ownerDocument',
@@ -481,106 +476,6 @@ export default class Node extends EventTarget implements INode {
481
476
  return oldChild;
482
477
  }
483
478
 
484
- /**
485
- * @override
486
- */
487
- public override addEventListener(
488
- type: string,
489
- listener: ((event: Event) => void) | IEventListener,
490
- options?: boolean | IEventListenerOptions
491
- ): void {
492
- super.addEventListener(type, listener, options);
493
-
494
- if (options === true || (options && options.capture)) {
495
- const captureEventListenerNodes = this.ownerDocument
496
- ? <{ [eventType: string]: INode[] }>this.ownerDocument['_captureEventListenerNodes']
497
- : <{ [eventType: string]: INode[] }>this['_captureEventListenerNodes'];
498
-
499
- captureEventListenerNodes[type] = captureEventListenerNodes[type] || [];
500
-
501
- if (!captureEventListenerNodes[type].includes(this)) {
502
- captureEventListenerNodes[type].push(this);
503
- }
504
- }
505
- }
506
-
507
- /**
508
- * Adds an event listener.
509
- *
510
- * @param type Event type.
511
- * @param listener Listener.
512
- */
513
- public override removeEventListener(
514
- type: string,
515
- listener: ((event: Event) => void) | IEventListener
516
- ): void {
517
- const index = this._listeners[type]?.indexOf(listener) || -1;
518
- const options = index !== -1 ? this._listenerOptions[type][index] : null;
519
-
520
- if (options?.capture) {
521
- const captureEventListenerNodes = this.ownerDocument
522
- ? <{ [eventType: string]: INode[] }>this.ownerDocument['_captureEventListenerNodes']
523
- : <{ [eventType: string]: INode[] }>this['_captureEventListenerNodes'];
524
-
525
- if (captureEventListenerNodes[type]) {
526
- const index = captureEventListenerNodes[type].indexOf(this);
527
- if (index !== -1) {
528
- captureEventListenerNodes[type].splice(index, 1);
529
- }
530
- }
531
- }
532
-
533
- super.removeEventListener(type, listener);
534
- }
535
-
536
- /**
537
- * @override
538
- */
539
- public override dispatchEvent(event: Event): boolean {
540
- // Capture phase
541
- if (!event._target) {
542
- const captureEventListenerNodes = this.ownerDocument
543
- ? <{ [eventType: string]: INode[] }>this.ownerDocument['_captureEventListenerNodes']
544
- : <{ [eventType: string]: INode[] }>this['_captureEventListenerNodes'];
545
-
546
- if (captureEventListenerNodes[event.type]) {
547
- event._target = this;
548
- event.eventPhase = EventPhaseEnum.capturing;
549
-
550
- for (const node of captureEventListenerNodes[event.type]) {
551
- if (node !== this && NodeUtility.contains(node, this, event.composed)) {
552
- node.dispatchEvent(event);
553
- }
554
- }
555
-
556
- event.eventPhase = EventPhaseEnum.atTarget;
557
- }
558
- }
559
-
560
- const returnValue = super.dispatchEvent(event);
561
-
562
- // Bubbling phase
563
- if (event.bubbles && !event._propagationStopped && !event._immediatePropagationStopped) {
564
- event.eventPhase = EventPhaseEnum.bubbling;
565
-
566
- if (this.parentNode) {
567
- return this.parentNode.dispatchEvent(event);
568
- }
569
-
570
- // eslint-disable-next-line
571
- if (
572
- event.composed &&
573
- this.nodeType === NodeTypeEnum.documentFragmentNode &&
574
- (<IShadowRoot>(<unknown>this)).host
575
- ) {
576
- // eslint-disable-next-line
577
- return (<IShadowRoot>(<unknown>this)).host.dispatchEvent(event);
578
- }
579
- }
580
-
581
- return returnValue;
582
- }
583
-
584
479
  /**
585
480
  * Converts the node to a string.
586
481
  *
@@ -393,6 +393,12 @@ export default class Window extends EventTarget implements IWindow {
393
393
  public Object;
394
394
  public Function;
395
395
 
396
+ // Public internal properties
397
+
398
+ // Used for tracking capture event listeners to improve performance when they are not used.
399
+ // See EventTarget class.
400
+ public _captureEventListenerCount: { [eventType: string]: number } = {};
401
+
396
402
  // Private properties
397
403
  private _setTimeout;
398
404
  private _clearTimeout;