happy-dom 7.6.4 → 7.6.6

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 (30) hide show
  1. package/lib/nodes/element/Element.d.ts +4 -2
  2. package/lib/nodes/element/Element.js +8 -1
  3. package/lib/nodes/element/Element.js.map +1 -1
  4. package/lib/nodes/element/IElement.d.ts +4 -2
  5. package/lib/nodes/html-element/HTMLElement.d.ts +1 -12
  6. package/lib/nodes/html-element/HTMLElement.js +1 -11
  7. package/lib/nodes/html-element/HTMLElement.js.map +1 -1
  8. package/lib/nodes/html-option-element/HTMLOptionElement.d.ts +17 -0
  9. package/lib/nodes/html-option-element/HTMLOptionElement.js +55 -32
  10. package/lib/nodes/html-option-element/HTMLOptionElement.js.map +1 -1
  11. package/lib/nodes/html-option-element/HTMLOptionsCollection.d.ts +0 -1
  12. package/lib/nodes/html-option-element/HTMLOptionsCollection.js +2 -10
  13. package/lib/nodes/html-option-element/HTMLOptionsCollection.js.map +1 -1
  14. package/lib/nodes/html-select-element/HTMLSelectElement.d.ts +18 -2
  15. package/lib/nodes/html-select-element/HTMLSelectElement.js +99 -22
  16. package/lib/nodes/html-select-element/HTMLSelectElement.js.map +1 -1
  17. package/lib/nodes/svg-element/SVGElement.d.ts +1 -8
  18. package/lib/nodes/svg-element/SVGElement.js +1 -7
  19. package/lib/nodes/svg-element/SVGElement.js.map +1 -1
  20. package/lib/xml-parser/XMLParser.js +57 -54
  21. package/lib/xml-parser/XMLParser.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/nodes/element/Element.ts +15 -3
  24. package/src/nodes/element/IElement.ts +4 -2
  25. package/src/nodes/html-element/HTMLElement.ts +3 -12
  26. package/src/nodes/html-option-element/HTMLOptionElement.ts +69 -35
  27. package/src/nodes/html-option-element/HTMLOptionsCollection.ts +2 -9
  28. package/src/nodes/html-select-element/HTMLSelectElement.ts +109 -27
  29. package/src/nodes/svg-element/SVGElement.ts +3 -8
  30. package/src/xml-parser/XMLParser.ts +62 -58
@@ -1,7 +1,8 @@
1
+ import IAttr from '../attr/IAttr';
1
2
  import HTMLElement from '../html-element/HTMLElement';
2
3
  import IHTMLElement from '../html-element/IHTMLElement';
3
4
  import IHTMLFormElement from '../html-form-element/IHTMLFormElement';
4
- import IHTMLSelectElement from '../html-select-element/IHTMLSelectElement';
5
+ import HTMLSelectElement from '../html-select-element/HTMLSelectElement';
5
6
  import IHTMLOptionElement from './IHTMLOptionElement';
6
7
 
7
8
  /**
@@ -12,6 +13,8 @@ import IHTMLOptionElement from './IHTMLOptionElement';
12
13
  */
13
14
  export default class HTMLOptionElement extends HTMLElement implements IHTMLOptionElement {
14
15
  public _index: number;
16
+ public _selectedness = false;
17
+ public _dirtyness = false;
15
18
 
16
19
  /**
17
20
  * Returns inner text, which is the rendered appearance of text.
@@ -59,20 +62,7 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio
59
62
  * @returns Selected.
60
63
  */
61
64
  public get selected(): boolean {
62
- const parentNode = <IHTMLSelectElement>this.parentNode;
63
-
64
- if (parentNode?.tagName === 'SELECT') {
65
- let index = -1;
66
- for (let i = 0; i < parentNode.options.length; i++) {
67
- if (parentNode.options[i] === this) {
68
- index = i;
69
- break;
70
- }
71
- }
72
- return index !== -1 && parentNode.options.selectedIndex === index;
73
- }
74
-
75
- return false;
65
+ return this._selectedness;
76
66
  }
77
67
 
78
68
  /**
@@ -81,26 +71,13 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio
81
71
  * @param selected Selected.
82
72
  */
83
73
  public set selected(selected: boolean) {
84
- const parentNode = <IHTMLSelectElement>this.parentNode;
85
- if (parentNode?.tagName === 'SELECT') {
86
- if (selected) {
87
- let index = -1;
88
-
89
- for (let i = 0; i < parentNode.options.length; i++) {
90
- if (parentNode.options[i] === this) {
91
- index = i;
92
- break;
93
- }
94
- }
95
-
96
- if (index !== -1) {
97
- parentNode.options.selectedIndex = index;
98
- }
99
- } else if (parentNode.options.length) {
100
- parentNode.options.selectedIndex = 0;
101
- } else {
102
- parentNode.options.selectedIndex = -1;
103
- }
74
+ const selectElement = this._getSelectElement();
75
+
76
+ this._dirtyness = true;
77
+ this._selectedness = Boolean(selected);
78
+
79
+ if (selectElement) {
80
+ selectElement._resetOptionSelectednes(this._selectedness ? this : null);
104
81
  }
105
82
  }
106
83
 
@@ -143,4 +120,61 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio
143
120
  public set value(value: string) {
144
121
  this.setAttributeNS(null, 'value', value);
145
122
  }
123
+
124
+ /**
125
+ * @override
126
+ */
127
+ public setAttributeNode(attribute: IAttr): IAttr {
128
+ const replacedAttribute = super.setAttributeNode(attribute);
129
+
130
+ if (
131
+ !this._dirtyness &&
132
+ attribute.name === 'selected' &&
133
+ replacedAttribute?.value !== attribute.value
134
+ ) {
135
+ const selectElement = this._getSelectElement();
136
+
137
+ this._selectedness = true;
138
+
139
+ if (selectElement) {
140
+ selectElement._resetOptionSelectednes(this);
141
+ }
142
+ }
143
+
144
+ return replacedAttribute;
145
+ }
146
+
147
+ /**
148
+ * @override
149
+ */
150
+ public removeAttributeNode(attribute: IAttr): IAttr {
151
+ super.removeAttributeNode(attribute);
152
+
153
+ if (!this._dirtyness && attribute.name === 'selected') {
154
+ const selectElement = this._getSelectElement();
155
+
156
+ this._selectedness = false;
157
+
158
+ if (selectElement) {
159
+ selectElement._resetOptionSelectednes();
160
+ }
161
+ }
162
+
163
+ return attribute;
164
+ }
165
+
166
+ /**
167
+ * Returns select element.
168
+ *
169
+ * @returns Select element.
170
+ */
171
+ private _getSelectElement(): HTMLSelectElement {
172
+ const parentNode = <HTMLSelectElement>this.parentNode;
173
+ if (parentNode?.tagName === 'SELECT') {
174
+ return <HTMLSelectElement>parentNode;
175
+ }
176
+ if ((<HTMLSelectElement>parentNode?.parentNode)?.tagName === 'SELECT') {
177
+ return <HTMLSelectElement>parentNode.parentNode;
178
+ }
179
+ }
146
180
  }
@@ -16,7 +16,6 @@ export default class HTMLOptionsCollection
16
16
  implements IHTMLOptionsCollection
17
17
  {
18
18
  private _selectElement: IHTMLSelectElement;
19
- private _selectedIndex = -1;
20
19
 
21
20
  /**
22
21
  *
@@ -34,7 +33,7 @@ export default class HTMLOptionsCollection
34
33
  * @returns SelectedIndex.
35
34
  */
36
35
  public get selectedIndex(): number {
37
- return this._selectedIndex;
36
+ return this._selectElement.selectedIndex;
38
37
  }
39
38
 
40
39
  /**
@@ -43,13 +42,7 @@ export default class HTMLOptionsCollection
43
42
  * @param selectedIndex SelectedIndex.
44
43
  */
45
44
  public set selectedIndex(selectedIndex: number) {
46
- if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) {
47
- if (selectedIndex >= 0 && selectedIndex < this.length) {
48
- this._selectedIndex = selectedIndex;
49
- } else {
50
- this._selectedIndex = -1;
51
- }
52
- }
45
+ this._selectElement.selectedIndex = selectedIndex;
53
46
  }
54
47
 
55
48
  /**
@@ -167,13 +167,14 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec
167
167
  * @returns Value.
168
168
  */
169
169
  public get value(): string {
170
- if (this.options.selectedIndex === -1) {
171
- return '';
170
+ for (let i = 0, max = this.options.length; i < max; i++) {
171
+ const option = <HTMLOptionElement>this.options[i];
172
+ if (option._selectedness) {
173
+ return option.value;
174
+ }
172
175
  }
173
176
 
174
- const option = this.options[this.options.selectedIndex];
175
-
176
- return option instanceof HTMLOptionElement ? option.value : '';
177
+ return '';
177
178
  }
178
179
 
179
180
  /**
@@ -182,9 +183,15 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec
182
183
  * @param value Value.
183
184
  */
184
185
  public set value(value: string) {
185
- this.options.selectedIndex = this.options.findIndex(
186
- (o) => o instanceof HTMLOptionElement && o.value === value
187
- );
186
+ for (let i = 0, max = this.options.length; i < max; i++) {
187
+ const option = <HTMLOptionElement>this.options[i];
188
+ if (option.value === value) {
189
+ option._selectedness = true;
190
+ option._dirtyness = true;
191
+ } else {
192
+ option._selectedness = false;
193
+ }
194
+ }
188
195
  }
189
196
 
190
197
  /**
@@ -193,16 +200,31 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec
193
200
  * @returns Value.
194
201
  */
195
202
  public get selectedIndex(): number {
196
- return this.options.selectedIndex;
203
+ for (let i = 0, max = this.options.length; i < max; i++) {
204
+ if ((<HTMLOptionElement>this.options[i])._selectedness) {
205
+ return i;
206
+ }
207
+ }
208
+ return -1;
197
209
  }
198
210
 
199
211
  /**
200
212
  * Sets value.
201
213
  *
202
- * @param value Value.
214
+ * @param selectedIndex Selected index.
203
215
  */
204
- public set selectedIndex(value: number) {
205
- this.options.selectedIndex = value;
216
+ public set selectedIndex(selectedIndex: number) {
217
+ if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) {
218
+ for (let i = 0, max = this.options.length; i < max; i++) {
219
+ (<HTMLOptionElement>this.options[i])._selectedness = false;
220
+ }
221
+
222
+ const selectedOption = <HTMLOptionElement>this.options[selectedIndex];
223
+ if (selectedOption) {
224
+ selectedOption._selectedness = true;
225
+ selectedOption._dirtyness = true;
226
+ }
227
+ }
206
228
  }
207
229
 
208
230
  /**
@@ -287,13 +309,10 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec
287
309
 
288
310
  if (element.tagName === 'OPTION' || element.tagName === 'OPTGROUP') {
289
311
  this.options.push(<IHTMLOptionElement | IHTMLOptGroupElement>element);
290
-
291
- if (this.options.length === 1) {
292
- this.options.selectedIndex = 0;
293
- }
294
312
  }
295
313
 
296
314
  this._updateIndexProperties(previousLength, this.options.length);
315
+ this._resetOptionSelectednes();
297
316
  }
298
317
 
299
318
  return super.appendChild(node);
@@ -332,15 +351,15 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec
332
351
  } else {
333
352
  this.options.push(<IHTMLOptionElement | IHTMLOptGroupElement>newElement);
334
353
  }
335
-
336
- if (this.options.length === 1) {
337
- this.options.selectedIndex = 0;
338
- }
339
354
  }
340
355
 
341
356
  this._updateIndexProperties(previousLength, this.options.length);
342
357
  }
343
358
 
359
+ if (newNode.nodeType === NodeTypeEnum.elementNode) {
360
+ this._resetOptionSelectednes();
361
+ }
362
+
344
363
  return returnValue;
345
364
  }
346
365
 
@@ -358,20 +377,83 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec
358
377
  if (index !== -1) {
359
378
  this.options.splice(index, 1);
360
379
  }
380
+ }
361
381
 
362
- if (this.options.selectedIndex >= this.options.length) {
363
- this.options.selectedIndex = this.options.length - 1;
382
+ this._updateIndexProperties(previousLength, this.options.length);
383
+ this._resetOptionSelectednes();
384
+ }
385
+
386
+ return super.removeChild(node);
387
+ }
388
+
389
+ /**
390
+ * Resets the option selectedness.
391
+ *
392
+ * Based on:
393
+ * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLSelectElement-impl.js
394
+ *
395
+ * @param [newOption] Optional new option element to be selected.
396
+ * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm
397
+ */
398
+ public _resetOptionSelectednes(newOption?: IHTMLOptionElement): void {
399
+ if (this.hasAttributeNS(null, 'multiple')) {
400
+ return;
401
+ }
402
+
403
+ const selected: HTMLOptionElement[] = [];
404
+
405
+ for (let i = 0, max = this.options.length; i < max; i++) {
406
+ if (newOption) {
407
+ (<HTMLOptionElement>this.options[i])._selectedness = this.options[i] === newOption;
408
+ }
409
+
410
+ if ((<HTMLOptionElement>this.options[i])._selectedness) {
411
+ selected.push(<HTMLOptionElement>this.options[i]);
412
+ }
413
+ }
414
+
415
+ const size = this._getDisplaySize();
416
+
417
+ if (size === 1 && !selected.length) {
418
+ for (let i = 0, max = this.options.length; i < max; i++) {
419
+ const option = <HTMLOptionElement>this.options[i];
420
+
421
+ let disabled = option.hasAttributeNS(null, 'disabled');
422
+ const parentNode = <IHTMLElement>option.parentNode;
423
+ if (
424
+ parentNode &&
425
+ parentNode.nodeType === NodeTypeEnum.elementNode &&
426
+ parentNode.tagName === 'OPTGROUP' &&
427
+ parentNode.hasAttributeNS(null, 'disabled')
428
+ ) {
429
+ disabled = true;
364
430
  }
365
431
 
366
- if (!this.options.length) {
367
- this.options.selectedIndex = -1;
432
+ if (!disabled) {
433
+ option._selectedness = true;
434
+ break;
368
435
  }
369
436
  }
370
-
371
- this._updateIndexProperties(previousLength, this.options.length);
437
+ } else if (selected.length >= 2) {
438
+ for (let i = 0, max = this.options.length; i < max; i++) {
439
+ (<HTMLOptionElement>this.options[i])._selectedness = i === selected.length - 1;
440
+ }
372
441
  }
442
+ }
373
443
 
374
- return super.removeChild(node);
444
+ /**
445
+ * Returns display size.
446
+ *
447
+ * @returns Display size.
448
+ */
449
+ protected _getDisplaySize(): number {
450
+ if (this.hasAttributeNS(null, 'size')) {
451
+ const size = parseInt(this.getAttributeNS(null, 'size'));
452
+ if (!isNaN(size) && size >= 0) {
453
+ return size;
454
+ }
455
+ }
456
+ return this.hasAttributeNS(null, 'multiple') ? 4 : 1;
375
457
  }
376
458
 
377
459
  /**
@@ -74,11 +74,7 @@ export default class SVGElement extends Element implements ISVGElement {
74
74
  }
75
75
 
76
76
  /**
77
- * The setAttributeNode() method adds a new Attr node to the specified element.
78
- *
79
77
  * @override
80
- * @param attribute Attribute.
81
- * @returns Replaced attribute.
82
78
  */
83
79
  public setAttributeNode(attribute: IAttr): IAttr {
84
80
  const replacedAttribute = super.setAttributeNode(attribute);
@@ -91,16 +87,15 @@ export default class SVGElement extends Element implements ISVGElement {
91
87
  }
92
88
 
93
89
  /**
94
- * Removes an Attr node.
95
- *
96
90
  * @override
97
- * @param attribute Attribute.
98
91
  */
99
- public removeAttributeNode(attribute: IAttr): void {
92
+ public removeAttributeNode(attribute: IAttr): IAttr {
100
93
  super.removeAttributeNode(attribute);
101
94
 
102
95
  if (attribute.name === 'style' && this._style) {
103
96
  this._style.cssText = '';
104
97
  }
98
+
99
+ return attribute;
105
100
  }
106
101
  }
@@ -42,76 +42,80 @@ export default class XMLParser {
42
42
  let lastTextIndex = 0;
43
43
  let match: RegExpExecArray;
44
44
 
45
- while ((match = markupRegexp.exec(data))) {
46
- const tagName = match[2].toLowerCase();
47
- const isStartTag = !match[1];
45
+ if (data !== null && data !== undefined) {
46
+ data = String(data);
48
47
 
49
- if (parent && match.index !== lastTextIndex) {
50
- const text = data.substring(lastTextIndex, match.index);
51
- this.appendTextAndCommentNodes(document, parent, text);
52
- }
53
-
54
- if (isStartTag) {
55
- const namespaceURI =
56
- tagName === 'svg'
57
- ? NamespaceURI.svg
58
- : (<IElement>parent).namespaceURI || NamespaceURI.html;
59
- const newElement = document.createElementNS(namespaceURI, tagName);
60
-
61
- // Scripts are not allowed to be executed when they are parsed using innerHTML, outerHTML, replaceWith() etc.
62
- // However, they are allowed to be executed when document.write() is used.
63
- // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement
64
- if (tagName === 'script') {
65
- (<HTMLScriptElement>newElement)._evaluateScript = evaluateScripts;
66
- }
48
+ while ((match = markupRegexp.exec(data))) {
49
+ const tagName = match[2].toLowerCase();
50
+ const isStartTag = !match[1];
67
51
 
68
- // An assumption that the same rule should be applied for the HTMLLinkElement is made here.
69
- if (tagName === 'link') {
70
- (<HTMLLinkElement>newElement)._evaluateCSS = evaluateScripts;
52
+ if (parent && match.index !== lastTextIndex) {
53
+ const text = data.substring(lastTextIndex, match.index);
54
+ this.appendTextAndCommentNodes(document, parent, text);
71
55
  }
72
56
 
73
- this.setAttributes(newElement, match[3]);
57
+ if (isStartTag) {
58
+ const namespaceURI =
59
+ tagName === 'svg'
60
+ ? NamespaceURI.svg
61
+ : (<IElement>parent).namespaceURI || NamespaceURI.html;
62
+ const newElement = document.createElementNS(namespaceURI, tagName);
63
+
64
+ // Scripts are not allowed to be executed when they are parsed using innerHTML, outerHTML, replaceWith() etc.
65
+ // However, they are allowed to be executed when document.write() is used.
66
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement
67
+ if (tagName === 'script') {
68
+ (<HTMLScriptElement>newElement)._evaluateScript = evaluateScripts;
69
+ }
74
70
 
75
- if (!match[4] && !VoidElements.includes(tagName)) {
76
- // Some elements are not allowed to be nested (e.g. "<a><a></a></a>" is not allowed.).
77
- // Therefore we will auto-close the tag.
78
- if (parentUnnestableTagName === tagName) {
79
- stack.pop();
80
- parent = <Element>parent.parentNode || root;
71
+ // An assumption that the same rule should be applied for the HTMLLinkElement is made here.
72
+ if (tagName === 'link') {
73
+ (<HTMLLinkElement>newElement)._evaluateCSS = evaluateScripts;
81
74
  }
82
75
 
83
- parent = <Element>parent.appendChild(newElement);
84
- parentUnnestableTagName = this.getUnnestableTagName(parent);
85
- stack.push(parent);
86
- } else {
87
- parent.appendChild(newElement);
88
- }
89
- lastTextIndex = markupRegexp.lastIndex;
90
-
91
- // Tags which contain non-parsed content
92
- // For example: <script> JavaScript should not be parsed
93
- if (ChildLessElements.includes(tagName)) {
94
- let childLessMatch = null;
95
- while ((childLessMatch = markupRegexp.exec(data))) {
96
- if (childLessMatch[2].toLowerCase() === tagName && childLessMatch[1]) {
97
- markupRegexp.lastIndex -= childLessMatch[0].length;
98
- break;
76
+ this.setAttributes(newElement, match[3]);
77
+
78
+ if (!match[4] && !VoidElements.includes(tagName)) {
79
+ // Some elements are not allowed to be nested (e.g. "<a><a></a></a>" is not allowed.).
80
+ // Therefore we will auto-close the tag.
81
+ if (parentUnnestableTagName === tagName) {
82
+ stack.pop();
83
+ parent = <Element>parent.parentNode || root;
84
+ }
85
+
86
+ parent = <Element>parent.appendChild(newElement);
87
+ parentUnnestableTagName = this.getUnnestableTagName(parent);
88
+ stack.push(parent);
89
+ } else {
90
+ parent.appendChild(newElement);
91
+ }
92
+ lastTextIndex = markupRegexp.lastIndex;
93
+
94
+ // Tags which contain non-parsed content
95
+ // For example: <script> JavaScript should not be parsed
96
+ if (ChildLessElements.includes(tagName)) {
97
+ let childLessMatch = null;
98
+ while ((childLessMatch = markupRegexp.exec(data))) {
99
+ if (childLessMatch[2].toLowerCase() === tagName && childLessMatch[1]) {
100
+ markupRegexp.lastIndex -= childLessMatch[0].length;
101
+ break;
102
+ }
99
103
  }
100
104
  }
101
- }
102
- } else {
103
- stack.pop();
104
- parent = stack[stack.length - 1] || root;
105
- parentUnnestableTagName = this.getUnnestableTagName(parent);
105
+ } else {
106
+ stack.pop();
107
+ parent = stack[stack.length - 1] || root;
108
+ parentUnnestableTagName = this.getUnnestableTagName(parent);
106
109
 
107
- lastTextIndex = markupRegexp.lastIndex;
110
+ lastTextIndex = markupRegexp.lastIndex;
111
+ }
108
112
  }
109
- }
110
113
 
111
- // Text after last element
112
- if ((!match && data.length > 0) || (match && lastTextIndex !== match.index)) {
113
- const text = data.substring(lastTextIndex);
114
- this.appendTextAndCommentNodes(document, parent || root, text);
114
+ // Text after last element
115
+ if ((!match && data.length > 0) || (match && lastTextIndex !== match.index)) {
116
+ const text = data.substring(lastTextIndex);
117
+ this.appendTextAndCommentNodes(document, parent || root, text);
118
+ }
115
119
  }
116
120
 
117
121
  return root;