From 63fbf5b060276f7625f74916185bebef43474855 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sun, 24 Mar 2024 18:52:19 +0100 Subject: [PATCH 01/51] feat: [#1332] Adds basic implementation for remaining HTML elements --- .../happy-dom/src/config/HTMLElementConfig.ts | 124 ++++++-------- .../src/config/IHTMLElementTagNameMap.ts | 162 ++++++++++++------ packages/happy-dom/src/index.ts | 134 ++++++++++----- .../html-area-element/HTMLAreaElement.ts | 8 + .../html-body-element/HTMLBodyElement.ts | 8 + .../nodes/html-br-element/HTMLBRElement.ts | 8 + .../html-canvas-element/HTMLCanvasElement.ts | 8 + .../html-d-list-element/HTMLDListElement.ts | 8 + .../html-data-element/HTMLDataElement.ts | 8 + .../HTMLDataListElement.ts | 8 + .../HTMLDetailsElement.ts | 8 + .../nodes/html-div-element/HTMLDivElement.ts | 8 + .../html-embed-element/HTMLEmbedElement.ts | 8 + .../HTMLFieldSetElement.ts | 8 + .../html-head-element/HTMLHeadElement.ts | 8 + .../HTMLHeadingElement.ts | 8 + .../nodes/html-hr-element/HTMLHRElement.ts | 8 + .../html-html-element/HTMLHtmlElement.ts | 8 + .../html-legend-element/HTMLLegendElement.ts | 8 + .../nodes/html-li-element/HTMLLIElement.ts | 8 + .../nodes/html-map-element/HTMLMapElement.ts | 8 + .../html-menu-element/HTMLMenuElement.ts | 8 + .../html-meter-element/HTMLMeterElement.ts | 8 + .../nodes/html-mod-element/HTMLModElement.ts | 8 + .../html-o-list-element/HTMLOListElement.ts | 8 + .../html-object-element/HTMLObjectElement.ts | 8 + .../html-output-element/HTMLOutputElement.ts | 8 + .../HTMLParagraphElement.ts | 8 + .../html-param-element/HTMLParamElement.ts | 8 + .../HTMLPictureElement.ts | 8 + .../nodes/html-pre-element/HTMLPreElement.ts | 8 + .../HTMLProgressElement.ts | 8 + .../html-quote-element/HTMLQuoteElement.ts | 8 + .../html-source-element/HTMLSourceElement.ts | 8 + .../html-span-element/HTMLSpanElement.ts | 8 + .../HTMLTableCaptionElement.ts | 8 + .../HTMLTableCellElement.ts | 8 + .../HTMLTableColElement.ts | 8 + .../html-table-element/HTMLTableElement.ts | 8 + .../HTMLTableRowElement.ts | 8 + .../HTMLTableSectionElement.ts | 8 + .../html-time-element/HTMLTimeElement.ts | 8 + .../html-title-element/HTMLTitleElement.ts | 8 + .../html-track-element/HTMLTrackElement.ts | 8 + .../html-u-list-element/HTMLUListElement.ts | 8 + .../happy-dom/src/window/BrowserWindow.ts | 141 +++++++++------ packages/happy-dom/test/index.test.ts | 13 ++ .../html-area-element/HTMLAreaElement.test.ts | 24 +++ .../html-body-element/HTMLBodyElement.test.ts | 24 +++ .../html-br-element/HTMLBRElement.test.ts | 24 +++ .../HTMLCanvasElement.test.ts | 24 +++ .../HTMLDListElement.test.ts | 24 +++ .../html-data-element/HTMLDataElement.test.ts | 24 +++ .../HTMLDataListElement.test.ts | 24 +++ .../HTMLDetailsElement.test.ts | 24 +++ .../html-div-element/HTMLDivElement.test.ts | 24 +++ .../nodes/html-element/HTMLElement.test.ts | 4 +- .../HTMLEmbedElement.test.ts | 24 +++ .../HTMLFieldSetElement.test.ts | 24 +++ .../html-head-element/HTMLHeadElement.test.ts | 24 +++ .../HTMLHeadingElement.test.ts | 24 +++ .../html-hr-element/HTMLHRElement.test.ts | 24 +++ .../html-html-element/HTMLHtmlElement.test.ts | 24 +++ .../HTMLLegendElement.test.ts | 24 +++ .../html-li-element/HTMLLIElement.test.ts | 24 +++ .../html-map-element/HTMLMapElement.test.ts | 24 +++ .../html-menu-element/HTMLMenuElement.test.ts | 24 +++ .../HTMLMeterElement.test.ts | 24 +++ .../html-mod-element/HTMLModElement.test.ts | 24 +++ .../HTMLOListElement.test.ts | 24 +++ .../HTMLObjectElement.test.ts | 24 +++ .../HTMLOutputElement.test.ts | 24 +++ .../HTMLParagraphElement.test.ts | 24 +++ .../HTMLParamElement.test.ts | 24 +++ .../HTMLPictureElement.test.ts | 24 +++ .../html-pre-element/HTMLPreElement.test.ts | 24 +++ .../HTMLProgressElement.test.ts | 24 +++ .../HTMLQuoteElement.test.ts | 24 +++ .../HTMLSourceElement.test.ts | 24 +++ .../html-span-element/HTMLSpanElement.test.ts | 24 +++ .../HTMLTableCaptionElement.test.ts | 24 +++ .../HTMLTableCellElement.test.ts | 23 +++ .../HTMLTableColElement.test.ts | 24 +++ .../HTMLTableElement.test.ts | 24 +++ .../HTMLTableRowElement.test.ts | 24 +++ .../HTMLTableSectionElement.test.ts | 24 +++ .../html-time-element/HTMLTimeElement.test.ts | 24 +++ .../HTMLTitleElement.test.ts | 24 +++ .../HTMLTrackElement.test.ts | 24 +++ .../HTMLUListElement.test.ts | 24 +++ .../test/query-selector/QuerySelector.test.ts | 32 ++-- .../test/window/BrowserWindow.test.ts | 11 ++ packages/happy-dom/vitest.config.ts | 3 +- 93 files changed, 1726 insertions(+), 241 deletions(-) create mode 100644 packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts create mode 100644 packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts create mode 100644 packages/happy-dom/src/nodes/html-br-element/HTMLBRElement.ts create mode 100644 packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts create mode 100644 packages/happy-dom/src/nodes/html-d-list-element/HTMLDListElement.ts create mode 100644 packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts create mode 100644 packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts create mode 100644 packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts create mode 100644 packages/happy-dom/src/nodes/html-div-element/HTMLDivElement.ts create mode 100644 packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts create mode 100644 packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts create mode 100644 packages/happy-dom/src/nodes/html-head-element/HTMLHeadElement.ts create mode 100644 packages/happy-dom/src/nodes/html-heading-element/HTMLHeadingElement.ts create mode 100644 packages/happy-dom/src/nodes/html-hr-element/HTMLHRElement.ts create mode 100644 packages/happy-dom/src/nodes/html-html-element/HTMLHtmlElement.ts create mode 100644 packages/happy-dom/src/nodes/html-legend-element/HTMLLegendElement.ts create mode 100644 packages/happy-dom/src/nodes/html-li-element/HTMLLIElement.ts create mode 100644 packages/happy-dom/src/nodes/html-map-element/HTMLMapElement.ts create mode 100644 packages/happy-dom/src/nodes/html-menu-element/HTMLMenuElement.ts create mode 100644 packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts create mode 100644 packages/happy-dom/src/nodes/html-mod-element/HTMLModElement.ts create mode 100644 packages/happy-dom/src/nodes/html-o-list-element/HTMLOListElement.ts create mode 100644 packages/happy-dom/src/nodes/html-object-element/HTMLObjectElement.ts create mode 100644 packages/happy-dom/src/nodes/html-output-element/HTMLOutputElement.ts create mode 100644 packages/happy-dom/src/nodes/html-paragraph-element/HTMLParagraphElement.ts create mode 100644 packages/happy-dom/src/nodes/html-param-element/HTMLParamElement.ts create mode 100644 packages/happy-dom/src/nodes/html-picture-element/HTMLPictureElement.ts create mode 100644 packages/happy-dom/src/nodes/html-pre-element/HTMLPreElement.ts create mode 100644 packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts create mode 100644 packages/happy-dom/src/nodes/html-quote-element/HTMLQuoteElement.ts create mode 100644 packages/happy-dom/src/nodes/html-source-element/HTMLSourceElement.ts create mode 100644 packages/happy-dom/src/nodes/html-span-element/HTMLSpanElement.ts create mode 100644 packages/happy-dom/src/nodes/html-table-caption-element/HTMLTableCaptionElement.ts create mode 100644 packages/happy-dom/src/nodes/html-table-cell-element/HTMLTableCellElement.ts create mode 100644 packages/happy-dom/src/nodes/html-table-col-element/HTMLTableColElement.ts create mode 100644 packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts create mode 100644 packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts create mode 100644 packages/happy-dom/src/nodes/html-table-section-element/HTMLTableSectionElement.ts create mode 100644 packages/happy-dom/src/nodes/html-time-element/HTMLTimeElement.ts create mode 100644 packages/happy-dom/src/nodes/html-title-element/HTMLTitleElement.ts create mode 100644 packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts create mode 100644 packages/happy-dom/src/nodes/html-u-list-element/HTMLUListElement.ts create mode 100644 packages/happy-dom/test/index.test.ts create mode 100644 packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-br-element/HTMLBRElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-d-list-element/HTMLDListElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-div-element/HTMLDivElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-head-element/HTMLHeadElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-heading-element/HTMLHeadingElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-hr-element/HTMLHRElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-html-element/HTMLHtmlElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-legend-element/HTMLLegendElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-li-element/HTMLLIElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-map-element/HTMLMapElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-menu-element/HTMLMenuElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-meter-element/HTMLMeterElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-mod-element/HTMLModElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-o-list-element/HTMLOListElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-object-element/HTMLObjectElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-output-element/HTMLOutputElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-paragraph-element/HTMLParagraphElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-param-element/HTMLParamElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-picture-element/HTMLPictureElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-pre-element/HTMLPreElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-progress-element/HTMLProgressElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-quote-element/HTMLQuoteElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-source-element/HTMLSourceElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-span-element/HTMLSpanElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-table-caption-element/HTMLTableCaptionElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-table-cell-element/HTMLTableCellElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-table-col-element/HTMLTableColElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-table-element/HTMLTableElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-table-row-element/HTMLTableRowElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-table-section-element/HTMLTableSectionElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-time-element/HTMLTimeElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-title-element/HTMLTitleElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts create mode 100644 packages/happy-dom/test/nodes/html-u-list-element/HTMLUListElement.test.ts diff --git a/packages/happy-dom/src/config/HTMLElementConfig.ts b/packages/happy-dom/src/config/HTMLElementConfig.ts index 2bf98409e..bb88ca205 100644 --- a/packages/happy-dom/src/config/HTMLElementConfig.ts +++ b/packages/happy-dom/src/config/HTMLElementConfig.ts @@ -24,7 +24,7 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, area: { - className: 'HTMLElement', + className: 'HTMLAreaElement', localName: 'area', tagName: 'AREA', contentModel: HTMLElementConfigContentModelEnum.noDescendants @@ -71,14 +71,8 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ tagName: 'BDO', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, - blockquaote: { - className: 'HTMLElement', - localName: 'blockquaote', - tagName: 'BLOCKQUAOTE', - contentModel: HTMLElementConfigContentModelEnum.anyDescendants - }, body: { - className: 'HTMLElement', + className: 'HTMLBodyElement', localName: 'body', tagName: 'BODY', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -150,13 +144,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.noDescendants }, blockquote: { - className: 'HTMLElement', + className: 'HTMLQuoteElement', localName: 'blockquote', tagName: 'BLOCKQUOTE', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, br: { - className: 'HTMLElement', + className: 'HTMLBRElement', localName: 'br', tagName: 'BR', contentModel: HTMLElementConfigContentModelEnum.noDescendants @@ -168,13 +162,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, canvas: { - className: 'HTMLElement', + className: 'HTMLCanvasElement', localName: 'canvas', tagName: 'CANVAS', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, caption: { - className: 'HTMLElement', + className: 'HTMLTableCaptionElement', localName: 'caption', tagName: 'CAPTION', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -192,25 +186,25 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, col: { - className: 'HTMLElement', + className: 'HTMLTableColElement', localName: 'col', tagName: 'COL', contentModel: HTMLElementConfigContentModelEnum.noDescendants }, colgroup: { - className: 'HTMLElement', + className: 'HTMLTableColElement', localName: 'colgroup', tagName: 'COLGROUP', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, data: { - className: 'HTMLElement', + className: 'HTMLDataElement', localName: 'data', tagName: 'DATA', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, datalist: { - className: 'HTMLElement', + className: 'HTMLDataListElement', localName: 'datalist', tagName: 'DATALIST', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -222,13 +216,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, del: { - className: 'HTMLElement', + className: 'HTMLModElement', localName: 'del', tagName: 'DEL', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, details: { - className: 'HTMLElement', + className: 'HTMLDetailsElement', localName: 'details', tagName: 'DETAILS', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -246,13 +240,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, div: { - className: 'HTMLElement', + className: 'HTMLDivElement', localName: 'div', tagName: 'DIV', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, dl: { - className: 'HTMLElement', + className: 'HTMLDListElement', localName: 'dl', tagName: 'DL', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -270,13 +264,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, embed: { - className: 'HTMLElement', + className: 'HTMLEmbedElement', localName: 'embed', tagName: 'EMBED', contentModel: HTMLElementConfigContentModelEnum.noDescendants }, fieldset: { - className: 'HTMLElement', + className: 'HTMLFieldSetElement', localName: 'fieldset', tagName: 'FIELDSET', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -300,43 +294,43 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, h1: { - className: 'HTMLElement', + className: 'HTMLHeadingElement', localName: 'h1', tagName: 'H1', contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, h2: { - className: 'HTMLElement', + className: 'HTMLHeadingElement', localName: 'h2', tagName: 'H2', contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, h3: { - className: 'HTMLElement', + className: 'HTMLHeadingElement', localName: 'h3', tagName: 'H3', contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, h4: { - className: 'HTMLElement', + className: 'HTMLHeadingElement', localName: 'h4', tagName: 'H4', contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, h5: { - className: 'HTMLElement', + className: 'HTMLHeadingElement', localName: 'h5', tagName: 'H5', contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, h6: { - className: 'HTMLElement', + className: 'HTMLHeadingElement', localName: 'h6', tagName: 'H6', contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, head: { - className: 'HTMLElement', + className: 'HTMLHeadElement', localName: 'head', tagName: 'HEAD', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -354,13 +348,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, hr: { - className: 'HTMLElement', + className: 'HTMLHRElement', localName: 'hr', tagName: 'HR', contentModel: HTMLElementConfigContentModelEnum.noDescendants }, html: { - className: 'HTMLElement', + className: 'HTMLHtmlElement', localName: 'html', tagName: 'HTML', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -378,7 +372,7 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, ins: { - className: 'HTMLElement', + className: 'HTMLModElement', localName: 'ins', tagName: 'INS', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -390,13 +384,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, legend: { - className: 'HTMLElement', + className: 'HTMLLegendElement', localName: 'legend', tagName: 'LEGEND', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, li: { - className: 'HTMLElement', + className: 'HTMLLIElement', localName: 'li', tagName: 'LI', contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants @@ -408,7 +402,7 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, map: { - className: 'HTMLElement', + className: 'HTMLMapElement', localName: 'map', tagName: 'MAP', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -419,26 +413,14 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ tagName: 'MARK', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, - math: { - className: 'HTMLElement', - localName: 'math', - tagName: 'MATH', - contentModel: HTMLElementConfigContentModelEnum.anyDescendants - }, menu: { - className: 'HTMLElement', + className: 'HTMLMenuElement', localName: 'menu', tagName: 'MENU', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, - menuitem: { - className: 'HTMLElement', - localName: 'menuitem', - tagName: 'MENUITEM', - contentModel: HTMLElementConfigContentModelEnum.anyDescendants - }, meter: { - className: 'HTMLElement', + className: 'HTMLMeterElement', localName: 'meter', tagName: 'METER', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -456,13 +438,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, object: { - className: 'HTMLElement', + className: 'HTMLObjectElement', localName: 'object', tagName: 'OBJECT', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, ol: { - className: 'HTMLElement', + className: 'HTMLOListElement', localName: 'ol', tagName: 'OL', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -480,43 +462,43 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, output: { - className: 'HTMLElement', + className: 'HTMLOutputElement', localName: 'output', tagName: 'OUTPUT', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, p: { - className: 'HTMLElement', + className: 'HTMLParagraphElement', localName: 'p', tagName: 'P', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, param: { - className: 'HTMLElement', + className: 'HTMLParamElement', localName: 'param', tagName: 'PARAM', contentModel: HTMLElementConfigContentModelEnum.noDescendants }, picture: { - className: 'HTMLElement', + className: 'HTMLPictureElement', localName: 'picture', tagName: 'PICTURE', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, pre: { - className: 'HTMLElement', + className: 'HTMLPreElement', localName: 'pre', tagName: 'PRE', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, progress: { - className: 'HTMLElement', + className: 'HTMLProgressElement', localName: 'progress', tagName: 'PROGRESS', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, q: { - className: 'HTMLElement', + className: 'HTMLQuoteElement', localName: 'q', tagName: 'Q', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -582,13 +564,13 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, source: { - className: 'HTMLElement', + className: 'HTMLSourceElement', localName: 'source', tagName: 'SOURCE', contentModel: HTMLElementConfigContentModelEnum.noDescendants }, span: { - className: 'HTMLElement', + className: 'HTMLSpanElement', localName: 'span', tagName: 'SPAN', contentModel: HTMLElementConfigContentModelEnum.anyDescendants @@ -618,61 +600,61 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, table: { - className: 'HTMLElement', + className: 'HTMLTableElement', localName: 'table', tagName: 'TABLE', contentModel: HTMLElementConfigContentModelEnum.noFirstLevelSelfDescendants }, tbody: { - className: 'HTMLElement', + className: 'HTMLTableSectionElement', localName: 'tbody', tagName: 'TBODY', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, td: { - className: 'HTMLElement', + className: 'HTMLTableCellElement', localName: 'td', tagName: 'TD', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, tfoot: { - className: 'HTMLElement', + className: 'HTMLTableSectionElement', localName: 'tfoot', tagName: 'TFOOT', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, th: { - className: 'HTMLElement', + className: 'HTMLTableCellElement', localName: 'th', tagName: 'TH', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, thead: { - className: 'HTMLElement', + className: 'HTMLTableSectionElement', localName: 'thead', tagName: 'THEAD', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, time: { - className: 'HTMLElement', + className: 'HTMLTimeElement', localName: 'time', tagName: 'TIME', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, title: { - className: 'HTMLElement', + className: 'HTMLTitleElement', localName: 'title', tagName: 'TITLE', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, tr: { - className: 'HTMLElement', + className: 'HTMLTableRowElement', localName: 'tr', tagName: 'TR', contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, track: { - className: 'HTMLElement', + className: 'HTMLTrackElement', localName: 'track', tagName: 'TRACK', contentModel: HTMLElementConfigContentModelEnum.noDescendants @@ -684,7 +666,7 @@ export default <{ [key: string]: IHTMLElementConfigEntity }>{ contentModel: HTMLElementConfigContentModelEnum.anyDescendants }, ul: { - className: 'HTMLElement', + className: 'HTMLUListElement', localName: 'ul', tagName: 'UL', contentModel: HTMLElementConfigContentModelEnum.anyDescendants diff --git a/packages/happy-dom/src/config/IHTMLElementTagNameMap.ts b/packages/happy-dom/src/config/IHTMLElementTagNameMap.ts index 6c3ab496a..29e8c9699 100644 --- a/packages/happy-dom/src/config/IHTMLElementTagNameMap.ts +++ b/packages/happy-dom/src/config/IHTMLElementTagNameMap.ts @@ -1,3 +1,56 @@ +import HTMLUListElement from '../nodes/html-u-list-element/HTMLUListElement.js'; +import HTMLTrackElement from '../nodes/html-track-element/HTMLTrackElement.js'; +import HTMLTableRowElement from '../nodes/html-table-row-element/HTMLTableRowElement.js'; +import HTMLTitleElement from '../nodes/html-title-element/HTMLTitleElement.js'; +import HTMLTimeElement from '../nodes/html-time-element/HTMLTimeElement.js'; +import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; +import HTMLTableCellElement from '../nodes/html-table-cell-element/HTMLTableCellElement.js'; +import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; +import HTMLTableCellElement from '../nodes/html-table-cell-element/HTMLTableCellElement.js'; +import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; +import HTMLTableElement from '../nodes/html-table-element/HTMLTableElement.js'; +import HTMLSpanElement from '../nodes/html-span-element/HTMLSpanElement.js'; +import HTMLSourceElement from '../nodes/html-source-element/HTMLSourceElement.js'; +import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; +import HTMLProgressElement from '../nodes/html-progress-element/HTMLProgressElement.js'; +import HTMLPreElement from '../nodes/html-pre-element/HTMLPreElement.js'; +import HTMLPictureElement from '../nodes/html-picture-element/HTMLPictureElement.js'; +import HTMLParamElement from '../nodes/html-param-element/HTMLParamElement.js'; +import HTMLParagraphElement from '../nodes/html-paragraph-element/HTMLParagraphElement.js'; +import HTMLOutputElement from '../nodes/html-output-element/HTMLOutputElement.js'; +import HTMLOListElement from '../nodes/html-o-list-element/HTMLOListElement.js'; +import HTMLObjectElement from '../nodes/html-object-element/HTMLObjectElement.js'; +import HTMLMeterElement from '../nodes/html-meter-element/HTMLMeterElement.js'; +import HTMLMenuElement from '../nodes/html-menu-element/HTMLMenuElement.js'; +import HTMLMapElement from '../nodes/html-map-element/HTMLMapElement.js'; +import HTMLLIElement from '../nodes/html-li-element/HTMLLIElement.js'; +import HTMLLegendElement from '../nodes/html-legend-element/HTMLLegendElement.js'; +import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; +import HTMLHtmlElement from '../nodes/html-html-element/HTMLHtmlElement.js'; +import HTMLHRElement from '../nodes/html-hr-element/HTMLHRElement.js'; +import HTMLHeadElement from '../nodes/html-head-element/HTMLHeadElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLFieldSetElement from '../nodes/html-field-set-element/HTMLFieldSetElement.js'; +import HTMLEmbedElement from '../nodes/html-embed-element/HTMLEmbedElement.js'; +import HTMLDListElement from '../nodes/html-d-list-element/HTMLDListElement.js'; +import HTMLDivElement from '../nodes/html-div-element/HTMLDivElement.js'; +import HTMLDetailsElement from '../nodes/html-details-element/HTMLDetailsElement.js'; +import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; +import HTMLDataListElement from '../nodes/html-data-list-element/HTMLDataListElement.js'; +import HTMLDataElement from '../nodes/html-data-element/HTMLDataElement.js'; +import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableCaptionElement from '../nodes/html-table-caption-element/HTMLTableCaptionElement.js'; +import HTMLCanvasElement from '../nodes/html-canvas-element/HTMLCanvasElement.js'; +import HTMLBRElement from '../nodes/html-br-element/HTMLBRElement.js'; +import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; +import HTMLBodyElement from '../nodes/html-body-element/HTMLBodyElement.js'; +import HTMLAreaElement from '../nodes/html-area-element/HTMLAreaElement.js'; import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; import HTMLElement from '../nodes/html-element/HTMLElement.js'; import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; @@ -34,7 +87,7 @@ export default interface IHTMLElementTagNameMap extends HTMLElementTagNameMap { a: HTMLAnchorElement; abbr: HTMLElement; address: HTMLElement; - area: HTMLElement; + area: HTMLAreaElement; article: HTMLElement; aside: HTMLElement; audio: HTMLAudioElement; @@ -42,8 +95,7 @@ export default interface IHTMLElementTagNameMap extends HTMLElementTagNameMap { base: HTMLBaseElement; bdi: HTMLElement; bdo: HTMLElement; - blockquaote: HTMLElement; - body: HTMLElement; + body: HTMLBodyElement; template: HTMLTemplateElement; form: HTMLFormElement; input: HTMLInputElement; @@ -55,68 +107,66 @@ export default interface IHTMLElementTagNameMap extends HTMLElementTagNameMap { label: HTMLLabelElement; slot: HTMLSlotElement; meta: HTMLMetaElement; - blockquote: HTMLElement; - br: HTMLElement; + blockquote: HTMLQuoteElement; + br: HTMLBRElement; button: HTMLButtonElement; - canvas: HTMLElement; - caption: HTMLElement; + canvas: HTMLCanvasElement; + caption: HTMLTableCaptionElement; cite: HTMLElement; code: HTMLElement; - col: HTMLElement; - colgroup: HTMLElement; - data: HTMLElement; - datalist: HTMLElement; + col: HTMLTableColElement; + colgroup: HTMLTableColElement; + data: HTMLDataElement; + datalist: HTMLDataListElement; dd: HTMLElement; - del: HTMLElement; - details: HTMLElement; + del: HTMLModElement; + details: HTMLDetailsElement; dfn: HTMLElement; dialog: HTMLDialogElement; - div: HTMLElement; - dl: HTMLElement; + div: HTMLDivElement; + dl: HTMLDListElement; dt: HTMLElement; em: HTMLElement; - embed: HTMLElement; - fieldset: HTMLElement; + embed: HTMLEmbedElement; + fieldset: HTMLFieldSetElement; figcaption: HTMLElement; figure: HTMLElement; footer: HTMLElement; - h1: HTMLElement; - h2: HTMLElement; - h3: HTMLElement; - h4: HTMLElement; - h5: HTMLElement; - h6: HTMLElement; - head: HTMLElement; + h1: HTMLHeadingElement; + h2: HTMLHeadingElement; + h3: HTMLHeadingElement; + h4: HTMLHeadingElement; + h5: HTMLHeadingElement; + h6: HTMLHeadingElement; + head: HTMLHeadElement; header: HTMLElement; hgroup: HTMLElement; - hr: HTMLElement; - html: HTMLElement; + hr: HTMLHRElement; + html: HTMLHtmlElement; i: HTMLElement; iframe: HTMLIFrameElement; - ins: HTMLElement; + ins: HTMLModElement; kbd: HTMLElement; - legend: HTMLElement; - li: HTMLElement; + legend: HTMLLegendElement; + li: HTMLLIElement; main: HTMLElement; - map: HTMLElement; + map: HTMLMapElement; mark: HTMLElement; - math: HTMLElement; - menu: HTMLElement; - menuitem: HTMLElement; - meter: HTMLElement; + menu: HTMLMenuElement; + meter: HTMLMeterElement; nav: HTMLElement; noscript: HTMLElement; - object: HTMLElement; - ol: HTMLElement; + object: HTMLObjectElement; + ol: HTMLOListElement; optgroup: HTMLOptGroupElement; option: HTMLOptionElement; - output: HTMLElement; - p: HTMLElement; - param: HTMLElement; - picture: HTMLElement; - pre: HTMLElement; - progress: HTMLElement; - q: HTMLElement; + output: HTMLOutputElement; + p: HTMLParagraphElement; + param: HTMLParamElement; + picture: HTMLPictureElement; + pre: HTMLPreElement; + progress: HTMLProgressElement; + q: HTMLQuoteElement; rb: HTMLElement; rp: HTMLElement; rt: HTMLElement; @@ -127,24 +177,24 @@ export default interface IHTMLElementTagNameMap extends HTMLElementTagNameMap { section: HTMLElement; select: HTMLSelectElement; small: HTMLElement; - source: HTMLElement; - span: HTMLElement; + source: HTMLSourceElement; + span: HTMLSpanElement; strong: HTMLElement; sub: HTMLElement; summary: HTMLElement; sup: HTMLElement; - table: HTMLElement; - tbody: HTMLElement; - td: HTMLElement; - tfoot: HTMLElement; - th: HTMLElement; - thead: HTMLElement; - time: HTMLElement; - title: HTMLElement; - tr: HTMLElement; - track: HTMLElement; + table: HTMLTableElement; + tbody: HTMLTableSectionElement; + td: HTMLTableCellElement; + tfoot: HTMLTableSectionElement; + th: HTMLTableCellElement; + thead: HTMLTableSectionElement; + time: HTMLTimeElement; + title: HTMLTitleElement; + tr: HTMLTableRowElement; + track: HTMLTrackElement; u: HTMLElement; - ul: HTMLElement; + ul: HTMLUListElement; var: HTMLElement; video: HTMLVideoElement; wbr: HTMLElement; diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index 491a9a761..a0d507ae5 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -1,3 +1,48 @@ +import HTMLUListElement from './nodes/html-u-list-element/HTMLUListElement.js'; +import HTMLTrackElement from './nodes/html-track-element/HTMLTrackElement.js'; +import HTMLTableRowElement from './nodes/html-table-row-element/HTMLTableRowElement.js'; +import HTMLTitleElement from './nodes/html-title-element/HTMLTitleElement.js'; +import HTMLTimeElement from './nodes/html-time-element/HTMLTimeElement.js'; +import HTMLTableSectionElement from './nodes/html-table-section-element/HTMLTableSectionElement.js'; +import HTMLTableCellElement from './nodes/html-table-cell-element/HTMLTableCellElement.js'; +import HTMLTableElement from './nodes/html-table-element/HTMLTableElement.js'; +import HTMLSpanElement from './nodes/html-span-element/HTMLSpanElement.js'; +import HTMLSourceElement from './nodes/html-source-element/HTMLSourceElement.js'; +import HTMLQuoteElement from './nodes/html-quote-element/HTMLQuoteElement.js'; +import HTMLProgressElement from './nodes/html-progress-element/HTMLProgressElement.js'; +import HTMLPreElement from './nodes/html-pre-element/HTMLPreElement.js'; +import HTMLPictureElement from './nodes/html-picture-element/HTMLPictureElement.js'; +import HTMLParamElement from './nodes/html-param-element/HTMLParamElement.js'; +import HTMLParagraphElement from './nodes/html-paragraph-element/HTMLParagraphElement.js'; +import HTMLOutputElement from './nodes/html-output-element/HTMLOutputElement.js'; +import HTMLOListElement from './nodes/html-o-list-element/HTMLOListElement.js'; +import HTMLObjectElement from './nodes/html-object-element/HTMLObjectElement.js'; +import HTMLMeterElement from './nodes/html-meter-element/HTMLMeterElement.js'; +import HTMLMenuElement from './nodes/html-menu-element/HTMLMenuElement.js'; +import HTMLMapElement from './nodes/html-map-element/HTMLMapElement.js'; +import HTMLLIElement from './nodes/html-li-element/HTMLLIElement.js'; +import HTMLLegendElement from './nodes/html-legend-element/HTMLLegendElement.js'; +import HTMLModElement from './nodes/html-mod-element/HTMLModElement.js'; +import HTMLHtmlElement from './nodes/html-html-element/HTMLHtmlElement.js'; +import HTMLHRElement from './nodes/html-hr-element/HTMLHRElement.js'; +import HTMLHeadElement from './nodes/html-head-element/HTMLHeadElement.js'; +import HTMLHeadingElement from './nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLFieldSetElement from './nodes/html-field-set-element/HTMLFieldSetElement.js'; +import HTMLEmbedElement from './nodes/html-embed-element/HTMLEmbedElement.js'; +import HTMLDListElement from './nodes/html-d-list-element/HTMLDListElement.js'; +import HTMLDivElement from './nodes/html-div-element/HTMLDivElement.js'; +import HTMLDetailsElement from './nodes/html-details-element/HTMLDetailsElement.js'; +import HTMLModElement from './nodes/html-mod-element/HTMLModElement.js'; +import HTMLDataListElement from './nodes/html-data-list-element/HTMLDataListElement.js'; +import HTMLDataElement from './nodes/html-data-element/HTMLDataElement.js'; +import HTMLTableColElement from './nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableColElement from './nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableCaptionElement from './nodes/html-table-caption-element/HTMLTableCaptionElement.js'; +import HTMLCanvasElement from './nodes/html-canvas-element/HTMLCanvasElement.js'; +import HTMLBRElement from './nodes/html-br-element/HTMLBRElement.js'; +import HTMLQuoteElement from './nodes/html-quote-element/HTMLQuoteElement.js'; +import HTMLBodyElement from './nodes/html-body-element/HTMLBodyElement.js'; +import HTMLAreaElement from './nodes/html-area-element/HTMLAreaElement.js'; import { URLSearchParams } from 'url'; import Browser from './browser/Browser.js'; import BrowserContext from './browser/BrowserContext.js'; @@ -178,6 +223,48 @@ export type { }; export { + HTMLUListElement, + HTMLTrackElement, + HTMLTableRowElement, + HTMLTitleElement, + HTMLTimeElement, + HTMLTableSectionElement, + HTMLTableCellElement, + HTMLTableElement, + HTMLSpanElement, + HTMLSourceElement, + HTMLQuoteElement, + HTMLProgressElement, + HTMLPreElement, + HTMLPictureElement, + HTMLParamElement, + HTMLParagraphElement, + HTMLOutputElement, + HTMLOListElement, + HTMLObjectElement, + HTMLMeterElement, + HTMLMenuElement, + HTMLMapElement, + HTMLLIElement, + HTMLLegendElement, + HTMLModElement, + HTMLHtmlElement, + HTMLHRElement, + HTMLHeadElement, + HTMLHeadingElement, + HTMLFieldSetElement, + HTMLEmbedElement, + HTMLDListElement, + HTMLDivElement, + HTMLDetailsElement, + HTMLDataListElement, + HTMLDataElement, + HTMLTableColElement, + HTMLTableCaptionElement, + HTMLCanvasElement, + HTMLBRElement, + HTMLBodyElement, + HTMLAreaElement, AbortController, AbortSignal, AnimationEvent, @@ -231,77 +318,30 @@ export { FormData, GlobalWindow, HTMLAnchorElement, - HTMLElement as HTMLAreaElement, HTMLAudioElement, - HTMLElement as HTMLBRElement, HTMLBaseElement, - HTMLElement as HTMLBodyElement, HTMLButtonElement, - HTMLElement as HTMLCanvasElement, HTMLCollection, - HTMLElement as HTMLDListElement, - HTMLElement as HTMLDataElement, - HTMLElement as HTMLDataListElement, - HTMLElement as HTMLDetailsElement, HTMLDialogElement, - HTMLElement as HTMLDirectoryElement, - HTMLElement as HTMLDivElement, HTMLDocument, HTMLElement, - HTMLElement as HTMLEmbedElement, - HTMLElement as HTMLFieldSetElement, - HTMLElement as HTMLFontElement, HTMLFormControlsCollection, HTMLFormElement, - HTMLElement as HTMLFrameElement, - HTMLElement as HTMLFrameSetElement, - HTMLElement as HTMLHRElement, - HTMLElement as HTMLHeadElement, - HTMLElement as HTMLHeadingElement, - HTMLElement as HTMLHtmlElement, HTMLIFrameElement, HTMLImageElement, HTMLInputElement, - HTMLElement as HTMLLElement, HTMLLabelElement, - HTMLElement as HTMLLegendElement, HTMLLinkElement, - HTMLElement as HTMLMapElement, - HTMLElement as HTMLMarqueeElement, HTMLMediaElement, - HTMLElement as HTMLMenuElement, HTMLMetaElement, - HTMLElement as HTMLMeterElement, - HTMLElement as HTMLModElement, - HTMLElement as HTMLOListElement, - HTMLElement as HTMLObjectElement, HTMLOptGroupElement, HTMLOptionElement, - HTMLElement as HTMLOutputElement, - HTMLElement as HTMLParagraphElement, - HTMLElement as HTMLParamElement, - HTMLElement as HTMLPictureElement, - HTMLElement as HTMLPreElement, - HTMLElement as HTMLProgressElement, - HTMLElement as HTMLQuoteElement, HTMLScriptElement, HTMLSelectElement, HTMLSlotElement, - HTMLElement as HTMLSourceElement, - HTMLElement as HTMLSpanElement, HTMLStyleElement, - HTMLElement as HTMLTableCaptionElement, - HTMLElement as HTMLTableCellElement, - HTMLElement as HTMLTableColElement, - HTMLElement as HTMLTableElement, - HTMLElement as HTMLTableRowElement, - HTMLElement as HTMLTableSectionElement, HTMLTemplateElement, HTMLTextAreaElement, - HTMLElement as HTMLTimeElement, - HTMLElement as HTMLTitleElement, - HTMLElement as HTMLTrackElement, - HTMLElement as HTMLUListElement, HTMLUnknownElement, HTMLVideoElement, HashChangeEvent, diff --git a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts new file mode 100644 index 000000000..a8ce78daf --- /dev/null +++ b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLAreaElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLAreaElement + */ + export default class HTMLAreaElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts b/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts new file mode 100644 index 000000000..3faaee4d6 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLBodyElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLBodyElement + */ + export default class HTMLBodyElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-br-element/HTMLBRElement.ts b/packages/happy-dom/src/nodes/html-br-element/HTMLBRElement.ts new file mode 100644 index 000000000..23b6b7089 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-br-element/HTMLBRElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLBRElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLBRElement + */ + export default class HTMLBRElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts new file mode 100644 index 000000000..53c25f9ee --- /dev/null +++ b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLCanvasElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement + */ + export default class HTMLCanvasElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-d-list-element/HTMLDListElement.ts b/packages/happy-dom/src/nodes/html-d-list-element/HTMLDListElement.ts new file mode 100644 index 000000000..ecb73c149 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-d-list-element/HTMLDListElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLDListElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDListElement + */ + export default class HTMLDListElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts b/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts new file mode 100644 index 000000000..b24ba46a2 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLDataElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataElement + */ + export default class HTMLDataElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts new file mode 100644 index 000000000..081e1b99f --- /dev/null +++ b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLDataListElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataListElement + */ + export default class HTMLDataListElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts new file mode 100644 index 000000000..1b0709da9 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLDetailsElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDetailsElement + */ + export default class HTMLDetailsElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-div-element/HTMLDivElement.ts b/packages/happy-dom/src/nodes/html-div-element/HTMLDivElement.ts new file mode 100644 index 000000000..1a16ea7d3 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-div-element/HTMLDivElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLDivElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement + */ + export default class HTMLDivElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts b/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts new file mode 100644 index 000000000..7dcf1f3b9 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLEmbedElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLEmbedElement + */ + export default class HTMLEmbedElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts new file mode 100644 index 000000000..eb4943c2e --- /dev/null +++ b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLFieldSetElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFieldSetElement + */ + export default class HTMLFieldSetElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-head-element/HTMLHeadElement.ts b/packages/happy-dom/src/nodes/html-head-element/HTMLHeadElement.ts new file mode 100644 index 000000000..98bba84ab --- /dev/null +++ b/packages/happy-dom/src/nodes/html-head-element/HTMLHeadElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLHeadElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHeadElement + */ + export default class HTMLHeadElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-heading-element/HTMLHeadingElement.ts b/packages/happy-dom/src/nodes/html-heading-element/HTMLHeadingElement.ts new file mode 100644 index 000000000..92ebfdad0 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-heading-element/HTMLHeadingElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLHeadingElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHeadingElement + */ + export default class HTMLHeadingElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-hr-element/HTMLHRElement.ts b/packages/happy-dom/src/nodes/html-hr-element/HTMLHRElement.ts new file mode 100644 index 000000000..aa4e408bd --- /dev/null +++ b/packages/happy-dom/src/nodes/html-hr-element/HTMLHRElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLHRElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHRElement + */ + export default class HTMLHRElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-html-element/HTMLHtmlElement.ts b/packages/happy-dom/src/nodes/html-html-element/HTMLHtmlElement.ts new file mode 100644 index 000000000..65375ba3a --- /dev/null +++ b/packages/happy-dom/src/nodes/html-html-element/HTMLHtmlElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLHtmlElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHtmlElement + */ + export default class HTMLHtmlElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-legend-element/HTMLLegendElement.ts b/packages/happy-dom/src/nodes/html-legend-element/HTMLLegendElement.ts new file mode 100644 index 000000000..900167ae4 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-legend-element/HTMLLegendElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLLegendElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLLegendElement + */ + export default class HTMLLegendElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-li-element/HTMLLIElement.ts b/packages/happy-dom/src/nodes/html-li-element/HTMLLIElement.ts new file mode 100644 index 000000000..bc9615f31 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-li-element/HTMLLIElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLLIElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLLIElement + */ + export default class HTMLLIElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-map-element/HTMLMapElement.ts b/packages/happy-dom/src/nodes/html-map-element/HTMLMapElement.ts new file mode 100644 index 000000000..4f3be88bd --- /dev/null +++ b/packages/happy-dom/src/nodes/html-map-element/HTMLMapElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLMapElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMapElement + */ + export default class HTMLMapElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-menu-element/HTMLMenuElement.ts b/packages/happy-dom/src/nodes/html-menu-element/HTMLMenuElement.ts new file mode 100644 index 000000000..620443f64 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-menu-element/HTMLMenuElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLMenuElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMenuElement + */ + export default class HTMLMenuElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts b/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts new file mode 100644 index 000000000..446cd29c0 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLMeterElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMeterElement + */ + export default class HTMLMeterElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-mod-element/HTMLModElement.ts b/packages/happy-dom/src/nodes/html-mod-element/HTMLModElement.ts new file mode 100644 index 000000000..0cf32b46f --- /dev/null +++ b/packages/happy-dom/src/nodes/html-mod-element/HTMLModElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLModElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLModElement + */ + export default class HTMLModElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-o-list-element/HTMLOListElement.ts b/packages/happy-dom/src/nodes/html-o-list-element/HTMLOListElement.ts new file mode 100644 index 000000000..323e3a398 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-o-list-element/HTMLOListElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLOListElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOListElement + */ + export default class HTMLOListElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-object-element/HTMLObjectElement.ts b/packages/happy-dom/src/nodes/html-object-element/HTMLObjectElement.ts new file mode 100644 index 000000000..65130b0ef --- /dev/null +++ b/packages/happy-dom/src/nodes/html-object-element/HTMLObjectElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLObjectElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement + */ + export default class HTMLObjectElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-output-element/HTMLOutputElement.ts b/packages/happy-dom/src/nodes/html-output-element/HTMLOutputElement.ts new file mode 100644 index 000000000..f7608a8d2 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-output-element/HTMLOutputElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLOutputElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOutputElement + */ + export default class HTMLOutputElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-paragraph-element/HTMLParagraphElement.ts b/packages/happy-dom/src/nodes/html-paragraph-element/HTMLParagraphElement.ts new file mode 100644 index 000000000..7c63008e9 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-paragraph-element/HTMLParagraphElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLParagraphElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLParagraphElement + */ + export default class HTMLParagraphElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-param-element/HTMLParamElement.ts b/packages/happy-dom/src/nodes/html-param-element/HTMLParamElement.ts new file mode 100644 index 000000000..26d7e41d8 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-param-element/HTMLParamElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLParamElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLParamElement + */ + export default class HTMLParamElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-picture-element/HTMLPictureElement.ts b/packages/happy-dom/src/nodes/html-picture-element/HTMLPictureElement.ts new file mode 100644 index 000000000..57386026b --- /dev/null +++ b/packages/happy-dom/src/nodes/html-picture-element/HTMLPictureElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLPictureElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLPictureElement + */ + export default class HTMLPictureElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-pre-element/HTMLPreElement.ts b/packages/happy-dom/src/nodes/html-pre-element/HTMLPreElement.ts new file mode 100644 index 000000000..74ee9f17e --- /dev/null +++ b/packages/happy-dom/src/nodes/html-pre-element/HTMLPreElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLPreElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLPreElement + */ + export default class HTMLPreElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts b/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts new file mode 100644 index 000000000..8d1ce968d --- /dev/null +++ b/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLProgressElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLProgressElement + */ + export default class HTMLProgressElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-quote-element/HTMLQuoteElement.ts b/packages/happy-dom/src/nodes/html-quote-element/HTMLQuoteElement.ts new file mode 100644 index 000000000..eeb527e7c --- /dev/null +++ b/packages/happy-dom/src/nodes/html-quote-element/HTMLQuoteElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLQuoteElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLQuoteElement + */ + export default class HTMLQuoteElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-source-element/HTMLSourceElement.ts b/packages/happy-dom/src/nodes/html-source-element/HTMLSourceElement.ts new file mode 100644 index 000000000..268330597 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-source-element/HTMLSourceElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLSourceElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLSourceElement + */ + export default class HTMLSourceElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-span-element/HTMLSpanElement.ts b/packages/happy-dom/src/nodes/html-span-element/HTMLSpanElement.ts new file mode 100644 index 000000000..47769862f --- /dev/null +++ b/packages/happy-dom/src/nodes/html-span-element/HTMLSpanElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLSpanElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLSpanElement + */ + export default class HTMLSpanElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-table-caption-element/HTMLTableCaptionElement.ts b/packages/happy-dom/src/nodes/html-table-caption-element/HTMLTableCaptionElement.ts new file mode 100644 index 000000000..15e6ea78f --- /dev/null +++ b/packages/happy-dom/src/nodes/html-table-caption-element/HTMLTableCaptionElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTableCaptionElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCaptionElement + */ + export default class HTMLTableCaptionElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-table-cell-element/HTMLTableCellElement.ts b/packages/happy-dom/src/nodes/html-table-cell-element/HTMLTableCellElement.ts new file mode 100644 index 000000000..a1e66f5e0 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-table-cell-element/HTMLTableCellElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTableCellElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement + */ + export default class HTMLTableCellElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-table-col-element/HTMLTableColElement.ts b/packages/happy-dom/src/nodes/html-table-col-element/HTMLTableColElement.ts new file mode 100644 index 000000000..d858d4d92 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-table-col-element/HTMLTableColElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTableColElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableColElement + */ + export default class HTMLTableColElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts b/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts new file mode 100644 index 000000000..2b8ad9b6a --- /dev/null +++ b/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTableElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableElement + */ + export default class HTMLTableElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts b/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts new file mode 100644 index 000000000..3daec5120 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTableRowElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableRowElement + */ + export default class HTMLTableRowElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-table-section-element/HTMLTableSectionElement.ts b/packages/happy-dom/src/nodes/html-table-section-element/HTMLTableSectionElement.ts new file mode 100644 index 000000000..02c786e17 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-table-section-element/HTMLTableSectionElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTableSectionElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableSectionElement + */ + export default class HTMLTableSectionElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-time-element/HTMLTimeElement.ts b/packages/happy-dom/src/nodes/html-time-element/HTMLTimeElement.ts new file mode 100644 index 000000000..ff96e5a81 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-time-element/HTMLTimeElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTimeElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTimeElement + */ + export default class HTMLTimeElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-title-element/HTMLTitleElement.ts b/packages/happy-dom/src/nodes/html-title-element/HTMLTitleElement.ts new file mode 100644 index 000000000..092eb83fd --- /dev/null +++ b/packages/happy-dom/src/nodes/html-title-element/HTMLTitleElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTitleElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement + */ + export default class HTMLTitleElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts b/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts new file mode 100644 index 000000000..d557e8db1 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLTrackElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTrackElement + */ + export default class HTMLTrackElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/html-u-list-element/HTMLUListElement.ts b/packages/happy-dom/src/nodes/html-u-list-element/HTMLUListElement.ts new file mode 100644 index 000000000..5ccd2d6e8 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-u-list-element/HTMLUListElement.ts @@ -0,0 +1,8 @@ + + import HTMLElement from '../html-element/HTMLElement.js'; + /** + * HTMLUListElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLUListElement + */ + export default class HTMLUListElement extends HTMLElement {} \ No newline at end of file diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index d9fd217e1..719be6d1b 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -1,3 +1,49 @@ +import HTMLUListElement from '../nodes/html-u-list-element/HTMLUListElement.js'; +import HTMLTrackElement from '../nodes/html-track-element/HTMLTrackElement.js'; +import HTMLTableRowElement from '../nodes/html-table-row-element/HTMLTableRowElement.js'; +import HTMLTitleElement from '../nodes/html-title-element/HTMLTitleElement.js'; +import HTMLTimeElement from '../nodes/html-time-element/HTMLTimeElement.js'; +import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; +import HTMLTableCellElement from '../nodes/html-table-cell-element/HTMLTableCellElement.js'; +import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; +import HTMLTableElement from '../nodes/html-table-element/HTMLTableElement.js'; +import HTMLSpanElement from '../nodes/html-span-element/HTMLSpanElement.js'; +import HTMLSourceElement from '../nodes/html-source-element/HTMLSourceElement.js'; +import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; +import HTMLProgressElement from '../nodes/html-progress-element/HTMLProgressElement.js'; +import HTMLPreElement from '../nodes/html-pre-element/HTMLPreElement.js'; +import HTMLPictureElement from '../nodes/html-picture-element/HTMLPictureElement.js'; +import HTMLParamElement from '../nodes/html-param-element/HTMLParamElement.js'; +import HTMLParagraphElement from '../nodes/html-paragraph-element/HTMLParagraphElement.js'; +import HTMLOutputElement from '../nodes/html-output-element/HTMLOutputElement.js'; +import HTMLOListElement from '../nodes/html-o-list-element/HTMLOListElement.js'; +import HTMLObjectElement from '../nodes/html-object-element/HTMLObjectElement.js'; +import HTMLMeterElement from '../nodes/html-meter-element/HTMLMeterElement.js'; +import HTMLMenuElement from '../nodes/html-menu-element/HTMLMenuElement.js'; +import HTMLMapElement from '../nodes/html-map-element/HTMLMapElement.js'; +import HTMLLIElement from '../nodes/html-li-element/HTMLLIElement.js'; +import HTMLLegendElement from '../nodes/html-legend-element/HTMLLegendElement.js'; +import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; +import HTMLHtmlElement from '../nodes/html-html-element/HTMLHtmlElement.js'; +import HTMLHRElement from '../nodes/html-hr-element/HTMLHRElement.js'; +import HTMLHeadElement from '../nodes/html-head-element/HTMLHeadElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLFieldSetElement from '../nodes/html-field-set-element/HTMLFieldSetElement.js'; +import HTMLEmbedElement from '../nodes/html-embed-element/HTMLEmbedElement.js'; +import HTMLDListElement from '../nodes/html-d-list-element/HTMLDListElement.js'; +import HTMLDivElement from '../nodes/html-div-element/HTMLDivElement.js'; +import HTMLDetailsElement from '../nodes/html-details-element/HTMLDetailsElement.js'; +import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; +import HTMLDataListElement from '../nodes/html-data-list-element/HTMLDataListElement.js'; +import HTMLDataElement from '../nodes/html-data-element/HTMLDataElement.js'; +import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableCaptionElement from '../nodes/html-table-caption-element/HTMLTableCaptionElement.js'; +import HTMLCanvasElement from '../nodes/html-canvas-element/HTMLCanvasElement.js'; +import HTMLBRElement from '../nodes/html-br-element/HTMLBRElement.js'; +import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; +import HTMLBodyElement from '../nodes/html-body-element/HTMLBodyElement.js'; +import HTMLAreaElement from '../nodes/html-area-element/HTMLAreaElement.js'; import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; import * as PropertySymbol from '../PropertySymbol.js'; import DocumentImplementation from '../nodes/document/Document.js'; @@ -213,55 +259,52 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public readonly HTMLLinkElement: typeof HTMLLinkElementImplementation; public readonly HTMLIFrameElement: typeof HTMLIFrameElementImplementation; public readonly HTMLFormElement: typeof HTMLFormElementImplementation; - - // Non-implemented element classes - public readonly HTMLHeadElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTitleElement: typeof HTMLElement = HTMLElement; - public readonly HTMLBodyElement: typeof HTMLElement = HTMLElement; - public readonly HTMLHeadingElement: typeof HTMLElement = HTMLElement; - public readonly HTMLParagraphElement: typeof HTMLElement = HTMLElement; - public readonly HTMLHRElement: typeof HTMLElement = HTMLElement; - public readonly HTMLPreElement: typeof HTMLElement = HTMLElement; - public readonly HTMLUListElement: typeof HTMLElement = HTMLElement; - public readonly HTMLOListElement: typeof HTMLElement = HTMLElement; - public readonly HTMLLElement: typeof HTMLElement = HTMLElement; - public readonly HTMLMenuElement: typeof HTMLElement = HTMLElement; - public readonly HTMLDListElement: typeof HTMLElement = HTMLElement; - public readonly HTMLDivElement: typeof HTMLElement = HTMLElement; - public readonly HTMLAreaElement: typeof HTMLElement = HTMLElement; - public readonly HTMLBRElement: typeof HTMLElement = HTMLElement; - public readonly HTMLCanvasElement: typeof HTMLElement = HTMLElement; - public readonly HTMLDataElement: typeof HTMLElement = HTMLElement; - public readonly HTMLDataListElement: typeof HTMLElement = HTMLElement; - public readonly HTMLDetailsElement: typeof HTMLElement = HTMLElement; - public readonly HTMLDirectoryElement: typeof HTMLElement = HTMLElement; - public readonly HTMLFieldSetElement: typeof HTMLElement = HTMLElement; - public readonly HTMLFontElement: typeof HTMLElement = HTMLElement; - public readonly HTMLHtmlElement: typeof HTMLElement = HTMLElement; - public readonly HTMLLegendElement: typeof HTMLElement = HTMLElement; - public readonly HTMLMapElement: typeof HTMLElement = HTMLElement; - public readonly HTMLMarqueeElement: typeof HTMLElement = HTMLElement; - public readonly HTMLMeterElement: typeof HTMLElement = HTMLElement; - public readonly HTMLModElement: typeof HTMLElement = HTMLElement; - public readonly HTMLOutputElement: typeof HTMLElement = HTMLElement; - public readonly HTMLPictureElement: typeof HTMLElement = HTMLElement; - public readonly HTMLProgressElement: typeof HTMLElement = HTMLElement; - public readonly HTMLQuoteElement: typeof HTMLElement = HTMLElement; - public readonly HTMLSourceElement: typeof HTMLElement = HTMLElement; - public readonly HTMLSpanElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTableCaptionElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTableCellElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTableColElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTableElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTimeElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTableRowElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTableSectionElement: typeof HTMLElement = HTMLElement; - public readonly HTMLFrameElement: typeof HTMLElement = HTMLElement; - public readonly HTMLFrameSetElement: typeof HTMLElement = HTMLElement; - public readonly HTMLEmbedElement: typeof HTMLElement = HTMLElement; - public readonly HTMLObjectElement: typeof HTMLElement = HTMLElement; - public readonly HTMLParamElement: typeof HTMLElement = HTMLElement; - public readonly HTMLTrackElement: typeof HTMLElement = HTMLElement; + public readonly HTMLUListElement: typeof HTMLUListElement = HTMLUListElement; + public readonly HTMLTrackElement: typeof HTMLTrackElement = HTMLTrackElement; + public readonly HTMLTableRowElement: typeof HTMLTableRowElement = HTMLTableRowElement; + public readonly HTMLTitleElement: typeof HTMLTitleElement = HTMLTitleElement; + public readonly HTMLTimeElement: typeof HTMLTimeElement = HTMLTimeElement; + public readonly HTMLTableSectionElement: typeof HTMLTableSectionElement = HTMLTableSectionElement; + public readonly HTMLTableCellElement: typeof HTMLTableCellElement = HTMLTableCellElement; + public readonly HTMLTableSectionElement: typeof HTMLTableSectionElement = HTMLTableSectionElement; + public readonly HTMLTableElement: typeof HTMLTableElement = HTMLTableElement; + public readonly HTMLSpanElement: typeof HTMLSpanElement = HTMLSpanElement; + public readonly HTMLSourceElement: typeof HTMLSourceElement = HTMLSourceElement; + public readonly HTMLQuoteElement: typeof HTMLQuoteElement = HTMLQuoteElement; + public readonly HTMLProgressElement: typeof HTMLProgressElement = HTMLProgressElement; + public readonly HTMLPreElement: typeof HTMLPreElement = HTMLPreElement; + public readonly HTMLPictureElement: typeof HTMLPictureElement = HTMLPictureElement; + public readonly HTMLParamElement: typeof HTMLParamElement = HTMLParamElement; + public readonly HTMLParagraphElement: typeof HTMLParagraphElement = HTMLParagraphElement; + public readonly HTMLOutputElement: typeof HTMLOutputElement = HTMLOutputElement; + public readonly HTMLOListElement: typeof HTMLOListElement = HTMLOListElement; + public readonly HTMLObjectElement: typeof HTMLObjectElement = HTMLObjectElement; + public readonly HTMLMeterElement: typeof HTMLMeterElement = HTMLMeterElement; + public readonly HTMLMenuElement: typeof HTMLMenuElement = HTMLMenuElement; + public readonly HTMLMapElement: typeof HTMLMapElement = HTMLMapElement; + public readonly HTMLLIElement: typeof HTMLLIElement = HTMLLIElement; + public readonly HTMLLegendElement: typeof HTMLLegendElement = HTMLLegendElement; + public readonly HTMLModElement: typeof HTMLModElement = HTMLModElement; + public readonly HTMLHtmlElement: typeof HTMLHtmlElement = HTMLHtmlElement; + public readonly HTMLHRElement: typeof HTMLHRElement = HTMLHRElement; + public readonly HTMLHeadElement: typeof HTMLHeadElement = HTMLHeadElement; + public readonly HTMLHeadingElement: typeof HTMLHeadingElement = HTMLHeadingElement; + public readonly HTMLFieldSetElement: typeof HTMLFieldSetElement = HTMLFieldSetElement; + public readonly HTMLEmbedElement: typeof HTMLEmbedElement = HTMLEmbedElement; + public readonly HTMLDListElement: typeof HTMLDListElement = HTMLDListElement; + public readonly HTMLDivElement: typeof HTMLDivElement = HTMLDivElement; + public readonly HTMLDetailsElement: typeof HTMLDetailsElement = HTMLDetailsElement; + public readonly HTMLModElement: typeof HTMLModElement = HTMLModElement; + public readonly HTMLDataListElement: typeof HTMLDataListElement = HTMLDataListElement; + public readonly HTMLDataElement: typeof HTMLDataElement = HTMLDataElement; + public readonly HTMLTableColElement: typeof HTMLTableColElement = HTMLTableColElement; + public readonly HTMLTableColElement: typeof HTMLTableColElement = HTMLTableColElement; + public readonly HTMLTableCaptionElement: typeof HTMLTableCaptionElement = HTMLTableCaptionElement; + public readonly HTMLCanvasElement: typeof HTMLCanvasElement = HTMLCanvasElement; + public readonly HTMLBRElement: typeof HTMLBRElement = HTMLBRElement; + public readonly HTMLQuoteElement: typeof HTMLQuoteElement = HTMLQuoteElement; + public readonly HTMLBodyElement: typeof HTMLBodyElement = HTMLBodyElement; + public readonly HTMLAreaElement: typeof HTMLAreaElement = HTMLAreaElement; // Event classes public readonly Event = Event; diff --git a/packages/happy-dom/test/index.test.ts b/packages/happy-dom/test/index.test.ts new file mode 100644 index 000000000..967a438c0 --- /dev/null +++ b/packages/happy-dom/test/index.test.ts @@ -0,0 +1,13 @@ +import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; +import HTMLElementConfig from '../src/config/HTMLElementConfig.js'; +import * as Index from '../src/index.js'; + +describe('Index', () => { + for (const tagName of Object.keys(HTMLElementConfig)) { + it(`Exposes the element class "${HTMLElementConfig[tagName].className}" for tag name "${tagName}"`, () => { + expect(Index[HTMLElementConfig[tagName].className].name).toBe( + HTMLElementConfig[tagName].className + ); + }); + } +}); diff --git a/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts b/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts new file mode 100644 index 000000000..fd4b99811 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLAreaElement from '../../../src/nodes/html-area-element/HTMLAreaElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLAreaElement', () => { + let window: Window; + let document: Document; + let element: HTMLAreaElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('area'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLAreaElement', () => { + expect(element instanceof HTMLAreaElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts b/packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts new file mode 100644 index 000000000..786c3c931 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLBodyElement from '../../../src/nodes/html-body-element/HTMLBodyElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLBodyElement', () => { + let window: Window; + let document: Document; + let element: HTMLBodyElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('body'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLBodyElement', () => { + expect(element instanceof HTMLBodyElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-br-element/HTMLBRElement.test.ts b/packages/happy-dom/test/nodes/html-br-element/HTMLBRElement.test.ts new file mode 100644 index 000000000..ba41d28f7 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-br-element/HTMLBRElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLBRElement from '../../../src/nodes/html-br-element/HTMLBRElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLBRElement', () => { + let window: Window; + let document: Document; + let element: HTMLBRElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('br'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLBRElement', () => { + expect(element instanceof HTMLBRElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts new file mode 100644 index 000000000..199258827 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLCanvasElement from '../../../src/nodes/html-canvas-element/HTMLCanvasElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLCanvasElement', () => { + let window: Window; + let document: Document; + let element: HTMLCanvasElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('canvas'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLCanvasElement', () => { + expect(element instanceof HTMLCanvasElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-d-list-element/HTMLDListElement.test.ts b/packages/happy-dom/test/nodes/html-d-list-element/HTMLDListElement.test.ts new file mode 100644 index 000000000..66601e80b --- /dev/null +++ b/packages/happy-dom/test/nodes/html-d-list-element/HTMLDListElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLDListElement from '../../../src/nodes/html-d-list-element/HTMLDListElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLDListElement', () => { + let window: Window; + let document: Document; + let element: HTMLDListElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('dl'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDListElement', () => { + expect(element instanceof HTMLDListElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts b/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts new file mode 100644 index 000000000..9564452a1 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLDataElement from '../../../src/nodes/html-data-element/HTMLDataElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLDataElement', () => { + let window: Window; + let document: Document; + let element: HTMLDataElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('data'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDataElement', () => { + expect(element instanceof HTMLDataElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts b/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts new file mode 100644 index 000000000..f1d925242 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLDataListElement from '../../../src/nodes/html-data-list-element/HTMLDataListElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLDataListElement', () => { + let window: Window; + let document: Document; + let element: HTMLDataListElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('datalist'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDataListElement', () => { + expect(element instanceof HTMLDataListElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts b/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts new file mode 100644 index 000000000..5e1533683 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLDetailsElement from '../../../src/nodes/html-details-element/HTMLDetailsElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLDetailsElement', () => { + let window: Window; + let document: Document; + let element: HTMLDetailsElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('details'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDetailsElement', () => { + expect(element instanceof HTMLDetailsElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-div-element/HTMLDivElement.test.ts b/packages/happy-dom/test/nodes/html-div-element/HTMLDivElement.test.ts new file mode 100644 index 000000000..52abd5248 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-div-element/HTMLDivElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLDivElement from '../../../src/nodes/html-div-element/HTMLDivElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLDivElement', () => { + let window: Window; + let document: Document; + let element: HTMLDivElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('div'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDivElement', () => { + expect(element instanceof HTMLDivElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts index 6e59ccb60..03e336fbf 100644 --- a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts +++ b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts @@ -25,8 +25,8 @@ describe('HTMLElement', () => { }); describe('Object.prototype.toString', () => { - it('Returns `[object HTMLElement]`', () => { - expect(Object.prototype.toString.call(element)).toBe('[object HTMLElement]'); + it('Returns `[object HTMLDivElement]`', () => { + expect(Object.prototype.toString.call(element)).toBe('[object HTMLDivElement]'); }); }); diff --git a/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts b/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts new file mode 100644 index 000000000..817bb65f2 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLEmbedElement from '../../../src/nodes/html-embed-element/HTMLEmbedElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLEmbedElement', () => { + let window: Window; + let document: Document; + let element: HTMLEmbedElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('embed'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLEmbedElement', () => { + expect(element instanceof HTMLEmbedElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts new file mode 100644 index 000000000..e9aae2a80 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLFieldSetElement from '../../../src/nodes/html-field-set-element/HTMLFieldSetElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLFieldSetElement', () => { + let window: Window; + let document: Document; + let element: HTMLFieldSetElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('fieldset'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLFieldSetElement', () => { + expect(element instanceof HTMLFieldSetElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-head-element/HTMLHeadElement.test.ts b/packages/happy-dom/test/nodes/html-head-element/HTMLHeadElement.test.ts new file mode 100644 index 000000000..7ae7ff6f0 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-head-element/HTMLHeadElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLHeadElement from '../../../src/nodes/html-head-element/HTMLHeadElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLHeadElement', () => { + let window: Window; + let document: Document; + let element: HTMLHeadElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('head'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLHeadElement', () => { + expect(element instanceof HTMLHeadElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-heading-element/HTMLHeadingElement.test.ts b/packages/happy-dom/test/nodes/html-heading-element/HTMLHeadingElement.test.ts new file mode 100644 index 000000000..4dfe2d09d --- /dev/null +++ b/packages/happy-dom/test/nodes/html-heading-element/HTMLHeadingElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLHeadingElement from '../../../src/nodes/html-heading-element/HTMLHeadingElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLHeadingElement', () => { + let window: Window; + let document: Document; + let element: HTMLHeadingElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('h6'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLHeadingElement', () => { + expect(element instanceof HTMLHeadingElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-hr-element/HTMLHRElement.test.ts b/packages/happy-dom/test/nodes/html-hr-element/HTMLHRElement.test.ts new file mode 100644 index 000000000..0e08d24ec --- /dev/null +++ b/packages/happy-dom/test/nodes/html-hr-element/HTMLHRElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLHRElement from '../../../src/nodes/html-hr-element/HTMLHRElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLHRElement', () => { + let window: Window; + let document: Document; + let element: HTMLHRElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('hr'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLHRElement', () => { + expect(element instanceof HTMLHRElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-html-element/HTMLHtmlElement.test.ts b/packages/happy-dom/test/nodes/html-html-element/HTMLHtmlElement.test.ts new file mode 100644 index 000000000..83443a7eb --- /dev/null +++ b/packages/happy-dom/test/nodes/html-html-element/HTMLHtmlElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLHtmlElement from '../../../src/nodes/html-html-element/HTMLHtmlElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLHtmlElement', () => { + let window: Window; + let document: Document; + let element: HTMLHtmlElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('html'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLHtmlElement', () => { + expect(element instanceof HTMLHtmlElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-legend-element/HTMLLegendElement.test.ts b/packages/happy-dom/test/nodes/html-legend-element/HTMLLegendElement.test.ts new file mode 100644 index 000000000..fa71c12f1 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-legend-element/HTMLLegendElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLLegendElement from '../../../src/nodes/html-legend-element/HTMLLegendElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLLegendElement', () => { + let window: Window; + let document: Document; + let element: HTMLLegendElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('legend'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLLegendElement', () => { + expect(element instanceof HTMLLegendElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-li-element/HTMLLIElement.test.ts b/packages/happy-dom/test/nodes/html-li-element/HTMLLIElement.test.ts new file mode 100644 index 000000000..0ea6c8b0b --- /dev/null +++ b/packages/happy-dom/test/nodes/html-li-element/HTMLLIElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLLIElement from '../../../src/nodes/html-li-element/HTMLLIElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLLIElement', () => { + let window: Window; + let document: Document; + let element: HTMLLIElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('li'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLLIElement', () => { + expect(element instanceof HTMLLIElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-map-element/HTMLMapElement.test.ts b/packages/happy-dom/test/nodes/html-map-element/HTMLMapElement.test.ts new file mode 100644 index 000000000..674d23128 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-map-element/HTMLMapElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLMapElement from '../../../src/nodes/html-map-element/HTMLMapElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLMapElement', () => { + let window: Window; + let document: Document; + let element: HTMLMapElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('map'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLMapElement', () => { + expect(element instanceof HTMLMapElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-menu-element/HTMLMenuElement.test.ts b/packages/happy-dom/test/nodes/html-menu-element/HTMLMenuElement.test.ts new file mode 100644 index 000000000..525f44492 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-menu-element/HTMLMenuElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLMenuElement from '../../../src/nodes/html-menu-element/HTMLMenuElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLMenuElement', () => { + let window: Window; + let document: Document; + let element: HTMLMenuElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('menu'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLMenuElement', () => { + expect(element instanceof HTMLMenuElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-meter-element/HTMLMeterElement.test.ts b/packages/happy-dom/test/nodes/html-meter-element/HTMLMeterElement.test.ts new file mode 100644 index 000000000..28f8f5148 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-meter-element/HTMLMeterElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLMeterElement from '../../../src/nodes/html-meter-element/HTMLMeterElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLMeterElement', () => { + let window: Window; + let document: Document; + let element: HTMLMeterElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('meter'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLMeterElement', () => { + expect(element instanceof HTMLMeterElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-mod-element/HTMLModElement.test.ts b/packages/happy-dom/test/nodes/html-mod-element/HTMLModElement.test.ts new file mode 100644 index 000000000..517eb7893 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-mod-element/HTMLModElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLModElement from '../../../src/nodes/html-mod-element/HTMLModElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLModElement', () => { + let window: Window; + let document: Document; + let element: HTMLModElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('ins'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLModElement', () => { + expect(element instanceof HTMLModElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-o-list-element/HTMLOListElement.test.ts b/packages/happy-dom/test/nodes/html-o-list-element/HTMLOListElement.test.ts new file mode 100644 index 000000000..576fb524c --- /dev/null +++ b/packages/happy-dom/test/nodes/html-o-list-element/HTMLOListElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLOListElement from '../../../src/nodes/html-o-list-element/HTMLOListElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLOListElement', () => { + let window: Window; + let document: Document; + let element: HTMLOListElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('ol'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLOListElement', () => { + expect(element instanceof HTMLOListElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-object-element/HTMLObjectElement.test.ts b/packages/happy-dom/test/nodes/html-object-element/HTMLObjectElement.test.ts new file mode 100644 index 000000000..7c68caedb --- /dev/null +++ b/packages/happy-dom/test/nodes/html-object-element/HTMLObjectElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLObjectElement from '../../../src/nodes/html-object-element/HTMLObjectElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLObjectElement', () => { + let window: Window; + let document: Document; + let element: HTMLObjectElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('object'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLObjectElement', () => { + expect(element instanceof HTMLObjectElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-output-element/HTMLOutputElement.test.ts b/packages/happy-dom/test/nodes/html-output-element/HTMLOutputElement.test.ts new file mode 100644 index 000000000..135c78491 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-output-element/HTMLOutputElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLOutputElement from '../../../src/nodes/html-output-element/HTMLOutputElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLOutputElement', () => { + let window: Window; + let document: Document; + let element: HTMLOutputElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('output'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLOutputElement', () => { + expect(element instanceof HTMLOutputElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-paragraph-element/HTMLParagraphElement.test.ts b/packages/happy-dom/test/nodes/html-paragraph-element/HTMLParagraphElement.test.ts new file mode 100644 index 000000000..a115a9734 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-paragraph-element/HTMLParagraphElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLParagraphElement from '../../../src/nodes/html-paragraph-element/HTMLParagraphElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLParagraphElement', () => { + let window: Window; + let document: Document; + let element: HTMLParagraphElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('p'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLParagraphElement', () => { + expect(element instanceof HTMLParagraphElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-param-element/HTMLParamElement.test.ts b/packages/happy-dom/test/nodes/html-param-element/HTMLParamElement.test.ts new file mode 100644 index 000000000..cc756f061 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-param-element/HTMLParamElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLParamElement from '../../../src/nodes/html-param-element/HTMLParamElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLParamElement', () => { + let window: Window; + let document: Document; + let element: HTMLParamElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('param'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLParamElement', () => { + expect(element instanceof HTMLParamElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-picture-element/HTMLPictureElement.test.ts b/packages/happy-dom/test/nodes/html-picture-element/HTMLPictureElement.test.ts new file mode 100644 index 000000000..648191483 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-picture-element/HTMLPictureElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLPictureElement from '../../../src/nodes/html-picture-element/HTMLPictureElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLPictureElement', () => { + let window: Window; + let document: Document; + let element: HTMLPictureElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('picture'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLPictureElement', () => { + expect(element instanceof HTMLPictureElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-pre-element/HTMLPreElement.test.ts b/packages/happy-dom/test/nodes/html-pre-element/HTMLPreElement.test.ts new file mode 100644 index 000000000..d6ed2643e --- /dev/null +++ b/packages/happy-dom/test/nodes/html-pre-element/HTMLPreElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLPreElement from '../../../src/nodes/html-pre-element/HTMLPreElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLPreElement', () => { + let window: Window; + let document: Document; + let element: HTMLPreElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('pre'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLPreElement', () => { + expect(element instanceof HTMLPreElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-progress-element/HTMLProgressElement.test.ts b/packages/happy-dom/test/nodes/html-progress-element/HTMLProgressElement.test.ts new file mode 100644 index 000000000..7fbbc0fea --- /dev/null +++ b/packages/happy-dom/test/nodes/html-progress-element/HTMLProgressElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLProgressElement from '../../../src/nodes/html-progress-element/HTMLProgressElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLProgressElement', () => { + let window: Window; + let document: Document; + let element: HTMLProgressElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('progress'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLProgressElement', () => { + expect(element instanceof HTMLProgressElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-quote-element/HTMLQuoteElement.test.ts b/packages/happy-dom/test/nodes/html-quote-element/HTMLQuoteElement.test.ts new file mode 100644 index 000000000..8ec62c8f2 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-quote-element/HTMLQuoteElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLQuoteElement from '../../../src/nodes/html-quote-element/HTMLQuoteElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLQuoteElement', () => { + let window: Window; + let document: Document; + let element: HTMLQuoteElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('q'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLQuoteElement', () => { + expect(element instanceof HTMLQuoteElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-source-element/HTMLSourceElement.test.ts b/packages/happy-dom/test/nodes/html-source-element/HTMLSourceElement.test.ts new file mode 100644 index 000000000..a35787fb2 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-source-element/HTMLSourceElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLSourceElement from '../../../src/nodes/html-source-element/HTMLSourceElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLSourceElement', () => { + let window: Window; + let document: Document; + let element: HTMLSourceElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('source'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLSourceElement', () => { + expect(element instanceof HTMLSourceElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-span-element/HTMLSpanElement.test.ts b/packages/happy-dom/test/nodes/html-span-element/HTMLSpanElement.test.ts new file mode 100644 index 000000000..f9b24568b --- /dev/null +++ b/packages/happy-dom/test/nodes/html-span-element/HTMLSpanElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLSpanElement from '../../../src/nodes/html-span-element/HTMLSpanElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLSpanElement', () => { + let window: Window; + let document: Document; + let element: HTMLSpanElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('span'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLSpanElement', () => { + expect(element instanceof HTMLSpanElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-table-caption-element/HTMLTableCaptionElement.test.ts b/packages/happy-dom/test/nodes/html-table-caption-element/HTMLTableCaptionElement.test.ts new file mode 100644 index 000000000..378f1e915 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-table-caption-element/HTMLTableCaptionElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLTableCaptionElement from '../../../src/nodes/html-table-caption-element/HTMLTableCaptionElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLTableCaptionElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableCaptionElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('caption'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableCaptionElement', () => { + expect(element instanceof HTMLTableCaptionElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-table-cell-element/HTMLTableCellElement.test.ts b/packages/happy-dom/test/nodes/html-table-cell-element/HTMLTableCellElement.test.ts new file mode 100644 index 000000000..180a2db8f --- /dev/null +++ b/packages/happy-dom/test/nodes/html-table-cell-element/HTMLTableCellElement.test.ts @@ -0,0 +1,23 @@ +import HTMLTableCellElement from '../../../src/nodes/html-table-cell-element/HTMLTableCellElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; + +describe('HTMLTableCellElement', () => { + let window: Window; + let document: Document; + + beforeEach(() => { + window = new Window(); + document = window.document; + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableCellElement', () => { + const element = document.createElement('th'); + expect(element instanceof HTMLTableCellElement).toBe(true); + const element2 = document.createElement('td'); + expect(element2 instanceof HTMLTableCellElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-table-col-element/HTMLTableColElement.test.ts b/packages/happy-dom/test/nodes/html-table-col-element/HTMLTableColElement.test.ts new file mode 100644 index 000000000..55378df9d --- /dev/null +++ b/packages/happy-dom/test/nodes/html-table-col-element/HTMLTableColElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLTableColElement from '../../../src/nodes/html-table-col-element/HTMLTableColElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLTableColElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableColElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('colgroup'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableColElement', () => { + expect(element instanceof HTMLTableColElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-table-element/HTMLTableElement.test.ts b/packages/happy-dom/test/nodes/html-table-element/HTMLTableElement.test.ts new file mode 100644 index 000000000..41df97468 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-table-element/HTMLTableElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLTableElement from '../../../src/nodes/html-table-element/HTMLTableElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLTableElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('table'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableElement', () => { + expect(element instanceof HTMLTableElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-table-row-element/HTMLTableRowElement.test.ts b/packages/happy-dom/test/nodes/html-table-row-element/HTMLTableRowElement.test.ts new file mode 100644 index 000000000..1ea93956c --- /dev/null +++ b/packages/happy-dom/test/nodes/html-table-row-element/HTMLTableRowElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLTableRowElement from '../../../src/nodes/html-table-row-element/HTMLTableRowElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLTableRowElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableRowElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('tr'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableRowElement', () => { + expect(element instanceof HTMLTableRowElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-table-section-element/HTMLTableSectionElement.test.ts b/packages/happy-dom/test/nodes/html-table-section-element/HTMLTableSectionElement.test.ts new file mode 100644 index 000000000..cbc23c960 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-table-section-element/HTMLTableSectionElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLTableSectionElement from '../../../src/nodes/html-table-section-element/HTMLTableSectionElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLTableSectionElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableSectionElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('thead'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableSectionElement', () => { + expect(element instanceof HTMLTableSectionElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-time-element/HTMLTimeElement.test.ts b/packages/happy-dom/test/nodes/html-time-element/HTMLTimeElement.test.ts new file mode 100644 index 000000000..04280dfad --- /dev/null +++ b/packages/happy-dom/test/nodes/html-time-element/HTMLTimeElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLTimeElement from '../../../src/nodes/html-time-element/HTMLTimeElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLTimeElement', () => { + let window: Window; + let document: Document; + let element: HTMLTimeElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('time'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTimeElement', () => { + expect(element instanceof HTMLTimeElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-title-element/HTMLTitleElement.test.ts b/packages/happy-dom/test/nodes/html-title-element/HTMLTitleElement.test.ts new file mode 100644 index 000000000..90ab7ea83 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-title-element/HTMLTitleElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLTitleElement from '../../../src/nodes/html-title-element/HTMLTitleElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLTitleElement', () => { + let window: Window; + let document: Document; + let element: HTMLTitleElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('title'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTitleElement', () => { + expect(element instanceof HTMLTitleElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts b/packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts new file mode 100644 index 000000000..2bf3d2bf9 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLTrackElement from '../../../src/nodes/html-track-element/HTMLTrackElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLTrackElement', () => { + let window: Window; + let document: Document; + let element: HTMLTrackElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('track'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTrackElement', () => { + expect(element instanceof HTMLTrackElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/nodes/html-u-list-element/HTMLUListElement.test.ts b/packages/happy-dom/test/nodes/html-u-list-element/HTMLUListElement.test.ts new file mode 100644 index 000000000..402fd2820 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-u-list-element/HTMLUListElement.test.ts @@ -0,0 +1,24 @@ + + import HTMLUListElement from '../../../src/nodes/html-u-list-element/HTMLUListElement.js'; + import Window from '../../../src/window/Window.js'; + import Document from '../../../src/nodes/document/Document.js'; + import { beforeEach, describe, it, expect } from 'vitest'; + + describe('HTMLUListElement', () => { + let window: Window; + let document: Document; + let element: HTMLUListElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('ul'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLUListElement', () => { + expect(element instanceof HTMLUListElement).toBe(true); + }); + }); + }); + \ No newline at end of file diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index 4805be222..619652b4e 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -989,22 +989,22 @@ describe('QuerySelector', () => { it('Throws an error when providing an invalid selector', () => { const div = document.createElement('div'); expect(() => div.querySelectorAll('1')).toThrowError( - "Failed to execute 'querySelectorAll' on 'HTMLElement': '1' is not a valid selector." + "Failed to execute 'querySelectorAll' on 'HTMLDivElement': '1' is not a valid selector." ); expect(() => div.querySelectorAll('[1')).toThrowError( - "Failed to execute 'querySelectorAll' on 'HTMLElement': '[1' is not a valid selector." + "Failed to execute 'querySelectorAll' on 'HTMLDivElement': '[1' is not a valid selector." ); expect(() => div.querySelectorAll('.1')).toThrowError( - "Failed to execute 'querySelectorAll' on 'HTMLElement': '.1' is not a valid selector." + "Failed to execute 'querySelectorAll' on 'HTMLDivElement': '.1' is not a valid selector." ); expect(() => div.querySelectorAll('#1')).toThrowError( - "Failed to execute 'querySelectorAll' on 'HTMLElement': '#1' is not a valid selector." + "Failed to execute 'querySelectorAll' on 'HTMLDivElement': '#1' is not a valid selector." ); expect(() => div.querySelectorAll('a.')).toThrowError( - "Failed to execute 'querySelectorAll' on 'HTMLElement': 'a.' is not a valid selector." + "Failed to execute 'querySelectorAll' on 'HTMLDivElement': 'a.' is not a valid selector." ); expect(() => div.querySelectorAll('a#')).toThrowError( - "Failed to execute 'querySelectorAll' on 'HTMLElement': 'a#' is not a valid selector." + "Failed to execute 'querySelectorAll' on 'HTMLDivElement': 'a#' is not a valid selector." ); }); }); @@ -1208,22 +1208,22 @@ describe('QuerySelector', () => { it('Throws an error when providing an invalid selector', () => { const div = document.createElement('div'); expect(() => div.querySelector('1')).toThrowError( - "Failed to execute 'querySelector' on 'HTMLElement': '1' is not a valid selector." + "Failed to execute 'querySelector' on 'HTMLDivElement': '1' is not a valid selector." ); expect(() => div.querySelector('[1')).toThrowError( - "Failed to execute 'querySelector' on 'HTMLElement': '[1' is not a valid selector." + "Failed to execute 'querySelector' on 'HTMLDivElement': '[1' is not a valid selector." ); expect(() => div.querySelector('.1')).toThrowError( - "Failed to execute 'querySelector' on 'HTMLElement': '.1' is not a valid selector." + "Failed to execute 'querySelector' on 'HTMLDivElement': '.1' is not a valid selector." ); expect(() => div.querySelector('#1')).toThrowError( - "Failed to execute 'querySelector' on 'HTMLElement': '#1' is not a valid selector." + "Failed to execute 'querySelector' on 'HTMLDivElement': '#1' is not a valid selector." ); expect(() => div.querySelector('a.')).toThrowError( - "Failed to execute 'querySelector' on 'HTMLElement': 'a.' is not a valid selector." + "Failed to execute 'querySelector' on 'HTMLDivElement': 'a.' is not a valid selector." ); expect(() => div.querySelector('a#')).toThrowError( - "Failed to execute 'querySelector' on 'HTMLElement': 'a#' is not a valid selector." + "Failed to execute 'querySelector' on 'HTMLDivElement': 'a#' is not a valid selector." ); }); @@ -1371,14 +1371,16 @@ describe('QuerySelector', () => { div.innerHTML = '
'; const element = div.children[0]; expect(() => element.matches('1')).toThrow( - new Error(`Failed to execute 'matches' on 'HTMLElement': '1' is not a valid selector.`) + new Error(`Failed to execute 'matches' on 'HTMLDivElement': '1' is not a valid selector.`) ); expect(() => element.matches(':not')).toThrow( - new Error(`Failed to execute 'matches' on 'HTMLElement': ':not' is not a valid selector.`) + new Error( + `Failed to execute 'matches' on 'HTMLDivElement': ':not' is not a valid selector.` + ) ); expect(() => element.matches('div:not')).toThrow( new Error( - `Failed to execute 'matches' on 'HTMLElement': 'div:not' is not a valid selector.` + `Failed to execute 'matches' on 'HTMLDivElement': 'div:not' is not a valid selector.` ) ); }); diff --git a/packages/happy-dom/test/window/BrowserWindow.test.ts b/packages/happy-dom/test/window/BrowserWindow.test.ts index 2fe889f05..3a8e4891e 100644 --- a/packages/happy-dom/test/window/BrowserWindow.test.ts +++ b/packages/happy-dom/test/window/BrowserWindow.test.ts @@ -32,6 +32,7 @@ import IBrowserPage from '../../src/browser/types/IBrowserPage.js'; import AdoptedStyleSheetCustomElement from '../AdoptedStyleSheetCustomElement.js'; import CSSStyleSheet from '../../src/css/CSSStyleSheet.js'; import Location from '../../src/location/Location.js'; +import HTMLElementConfig from '../../src/config/HTMLElementConfig.js'; import '../types.d.js'; @@ -138,6 +139,16 @@ describe('BrowserWindow', () => { }); }); + describe('get {ElementClass}()', () => { + for (const tagName of Object.keys(HTMLElementConfig)) { + it(`Exposes the element class "${HTMLElementConfig[tagName].className}" for tag name "${tagName}"`, () => { + expect(window[HTMLElementConfig[tagName].className].name).toBe( + HTMLElementConfig[tagName].className + ); + }); + } + }); + describe('get performance()', () => { it('Exposes "performance" from NodeJS.', () => { expect(typeof window.performance.now()).toBe('number'); diff --git a/packages/happy-dom/vitest.config.ts b/packages/happy-dom/vitest.config.ts index 554c98429..4d5ca84b9 100644 --- a/packages/happy-dom/vitest.config.ts +++ b/packages/happy-dom/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ environment: 'node', include: ['./test/**/*.test.ts'], setupFiles: ['./test/setup.ts'], - testTimeout: 500 + testTimeout: 500, + restoreMocks: true } }); From e4e24413894b7ed46fcfbf43f1e18a2f558ec3ca Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sun, 24 Mar 2024 19:02:07 +0100 Subject: [PATCH 02/51] chore: [#1332] Fixes multiple imports of same module --- .../src/config/IHTMLElementTagNameMap.ts | 117 +++--- packages/happy-dom/src/index.ts | 173 +++++---- .../html-area-element/HTMLAreaElement.ts | 15 +- .../html-body-element/HTMLBodyElement.ts | 15 +- .../nodes/html-br-element/HTMLBRElement.ts | 15 +- .../html-canvas-element/HTMLCanvasElement.ts | 15 +- .../html-d-list-element/HTMLDListElement.ts | 15 +- .../html-data-element/HTMLDataElement.ts | 15 +- .../HTMLDataListElement.ts | 15 +- .../HTMLDetailsElement.ts | 15 +- .../nodes/html-div-element/HTMLDivElement.ts | 15 +- .../html-embed-element/HTMLEmbedElement.ts | 15 +- .../HTMLFieldSetElement.ts | 15 +- .../html-head-element/HTMLHeadElement.ts | 15 +- .../HTMLHeadingElement.ts | 15 +- .../nodes/html-hr-element/HTMLHRElement.ts | 15 +- .../html-html-element/HTMLHtmlElement.ts | 15 +- .../html-legend-element/HTMLLegendElement.ts | 15 +- .../nodes/html-li-element/HTMLLIElement.ts | 15 +- .../nodes/html-map-element/HTMLMapElement.ts | 15 +- .../html-menu-element/HTMLMenuElement.ts | 15 +- .../html-meter-element/HTMLMeterElement.ts | 15 +- .../nodes/html-mod-element/HTMLModElement.ts | 15 +- .../html-o-list-element/HTMLOListElement.ts | 15 +- .../html-object-element/HTMLObjectElement.ts | 15 +- .../html-output-element/HTMLOutputElement.ts | 15 +- .../HTMLParagraphElement.ts | 15 +- .../html-param-element/HTMLParamElement.ts | 15 +- .../HTMLPictureElement.ts | 15 +- .../nodes/html-pre-element/HTMLPreElement.ts | 15 +- .../HTMLProgressElement.ts | 15 +- .../html-quote-element/HTMLQuoteElement.ts | 15 +- .../html-source-element/HTMLSourceElement.ts | 15 +- .../html-span-element/HTMLSpanElement.ts | 15 +- .../HTMLTableCaptionElement.ts | 15 +- .../HTMLTableCellElement.ts | 15 +- .../HTMLTableColElement.ts | 15 +- .../html-table-element/HTMLTableElement.ts | 15 +- .../HTMLTableRowElement.ts | 15 +- .../HTMLTableSectionElement.ts | 15 +- .../html-time-element/HTMLTimeElement.ts | 15 +- .../html-title-element/HTMLTitleElement.ts | 15 +- .../html-track-element/HTMLTrackElement.ts | 15 +- .../html-u-list-element/HTMLUListElement.ts | 15 +- .../happy-dom/src/window/BrowserWindow.ts | 337 +++++++++--------- .../html-area-element/HTMLAreaElement.test.ts | 42 ++- .../html-body-element/HTMLBodyElement.test.ts | 42 ++- .../html-br-element/HTMLBRElement.test.ts | 42 ++- .../HTMLCanvasElement.test.ts | 42 ++- .../HTMLDListElement.test.ts | 42 ++- .../html-data-element/HTMLDataElement.test.ts | 42 ++- .../HTMLDataListElement.test.ts | 42 ++- .../HTMLDetailsElement.test.ts | 42 ++- .../html-div-element/HTMLDivElement.test.ts | 42 ++- .../HTMLEmbedElement.test.ts | 42 ++- .../HTMLFieldSetElement.test.ts | 42 ++- .../html-head-element/HTMLHeadElement.test.ts | 42 ++- .../HTMLHeadingElement.test.ts | 42 ++- .../html-hr-element/HTMLHRElement.test.ts | 42 ++- .../html-html-element/HTMLHtmlElement.test.ts | 42 ++- .../HTMLLegendElement.test.ts | 42 ++- .../html-li-element/HTMLLIElement.test.ts | 42 ++- .../html-map-element/HTMLMapElement.test.ts | 42 ++- .../html-menu-element/HTMLMenuElement.test.ts | 42 ++- .../HTMLMeterElement.test.ts | 42 ++- .../html-mod-element/HTMLModElement.test.ts | 42 ++- .../HTMLOListElement.test.ts | 42 ++- .../HTMLObjectElement.test.ts | 42 ++- .../HTMLOutputElement.test.ts | 42 ++- .../HTMLParagraphElement.test.ts | 42 ++- .../HTMLParamElement.test.ts | 42 ++- .../HTMLPictureElement.test.ts | 42 ++- .../html-pre-element/HTMLPreElement.test.ts | 42 ++- .../HTMLProgressElement.test.ts | 42 ++- .../HTMLQuoteElement.test.ts | 42 ++- .../HTMLSourceElement.test.ts | 42 ++- .../html-span-element/HTMLSpanElement.test.ts | 42 ++- .../HTMLTableCaptionElement.test.ts | 42 ++- .../HTMLTableColElement.test.ts | 42 ++- .../HTMLTableElement.test.ts | 42 ++- .../HTMLTableRowElement.test.ts | 42 ++- .../HTMLTableSectionElement.test.ts | 42 ++- .../html-time-element/HTMLTimeElement.test.ts | 42 ++- .../HTMLTitleElement.test.ts | 42 ++- .../HTMLTrackElement.test.ts | 42 ++- .../HTMLUListElement.test.ts | 42 ++- 86 files changed, 1416 insertions(+), 1563 deletions(-) diff --git a/packages/happy-dom/src/config/IHTMLElementTagNameMap.ts b/packages/happy-dom/src/config/IHTMLElementTagNameMap.ts index 29e8c9699..e5be90d07 100644 --- a/packages/happy-dom/src/config/IHTMLElementTagNameMap.ts +++ b/packages/happy-dom/src/config/IHTMLElementTagNameMap.ts @@ -1,77 +1,66 @@ -import HTMLUListElement from '../nodes/html-u-list-element/HTMLUListElement.js'; -import HTMLTrackElement from '../nodes/html-track-element/HTMLTrackElement.js'; -import HTMLTableRowElement from '../nodes/html-table-row-element/HTMLTableRowElement.js'; -import HTMLTitleElement from '../nodes/html-title-element/HTMLTitleElement.js'; -import HTMLTimeElement from '../nodes/html-time-element/HTMLTimeElement.js'; -import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; -import HTMLTableCellElement from '../nodes/html-table-cell-element/HTMLTableCellElement.js'; -import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; -import HTMLTableCellElement from '../nodes/html-table-cell-element/HTMLTableCellElement.js'; -import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; -import HTMLTableElement from '../nodes/html-table-element/HTMLTableElement.js'; -import HTMLSpanElement from '../nodes/html-span-element/HTMLSpanElement.js'; -import HTMLSourceElement from '../nodes/html-source-element/HTMLSourceElement.js'; -import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; -import HTMLProgressElement from '../nodes/html-progress-element/HTMLProgressElement.js'; -import HTMLPreElement from '../nodes/html-pre-element/HTMLPreElement.js'; -import HTMLPictureElement from '../nodes/html-picture-element/HTMLPictureElement.js'; -import HTMLParamElement from '../nodes/html-param-element/HTMLParamElement.js'; -import HTMLParagraphElement from '../nodes/html-paragraph-element/HTMLParagraphElement.js'; -import HTMLOutputElement from '../nodes/html-output-element/HTMLOutputElement.js'; -import HTMLOListElement from '../nodes/html-o-list-element/HTMLOListElement.js'; -import HTMLObjectElement from '../nodes/html-object-element/HTMLObjectElement.js'; -import HTMLMeterElement from '../nodes/html-meter-element/HTMLMeterElement.js'; -import HTMLMenuElement from '../nodes/html-menu-element/HTMLMenuElement.js'; -import HTMLMapElement from '../nodes/html-map-element/HTMLMapElement.js'; -import HTMLLIElement from '../nodes/html-li-element/HTMLLIElement.js'; -import HTMLLegendElement from '../nodes/html-legend-element/HTMLLegendElement.js'; -import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; -import HTMLHtmlElement from '../nodes/html-html-element/HTMLHtmlElement.js'; -import HTMLHRElement from '../nodes/html-hr-element/HTMLHRElement.js'; -import HTMLHeadElement from '../nodes/html-head-element/HTMLHeadElement.js'; -import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; -import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; -import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; -import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; -import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; -import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; -import HTMLFieldSetElement from '../nodes/html-field-set-element/HTMLFieldSetElement.js'; -import HTMLEmbedElement from '../nodes/html-embed-element/HTMLEmbedElement.js'; -import HTMLDListElement from '../nodes/html-d-list-element/HTMLDListElement.js'; -import HTMLDivElement from '../nodes/html-div-element/HTMLDivElement.js'; -import HTMLDetailsElement from '../nodes/html-details-element/HTMLDetailsElement.js'; -import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; -import HTMLDataListElement from '../nodes/html-data-list-element/HTMLDataListElement.js'; -import HTMLDataElement from '../nodes/html-data-element/HTMLDataElement.js'; -import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; -import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; -import HTMLTableCaptionElement from '../nodes/html-table-caption-element/HTMLTableCaptionElement.js'; -import HTMLCanvasElement from '../nodes/html-canvas-element/HTMLCanvasElement.js'; -import HTMLBRElement from '../nodes/html-br-element/HTMLBRElement.js'; -import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; -import HTMLBodyElement from '../nodes/html-body-element/HTMLBodyElement.js'; -import HTMLAreaElement from '../nodes/html-area-element/HTMLAreaElement.js'; import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; -import HTMLElement from '../nodes/html-element/HTMLElement.js'; +import HTMLAreaElement from '../nodes/html-area-element/HTMLAreaElement.js'; import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; -import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; +import HTMLBodyElement from '../nodes/html-body-element/HTMLBodyElement.js'; +import HTMLBRElement from '../nodes/html-br-element/HTMLBRElement.js'; +import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; +import HTMLCanvasElement from '../nodes/html-canvas-element/HTMLCanvasElement.js'; +import HTMLDListElement from '../nodes/html-d-list-element/HTMLDListElement.js'; +import HTMLDataElement from '../nodes/html-data-element/HTMLDataElement.js'; +import HTMLDataListElement from '../nodes/html-data-list-element/HTMLDataListElement.js'; +import HTMLDetailsElement from '../nodes/html-details-element/HTMLDetailsElement.js'; +import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; +import HTMLDivElement from '../nodes/html-div-element/HTMLDivElement.js'; +import HTMLElement from '../nodes/html-element/HTMLElement.js'; +import HTMLEmbedElement from '../nodes/html-embed-element/HTMLEmbedElement.js'; +import HTMLFieldSetElement from '../nodes/html-field-set-element/HTMLFieldSetElement.js'; import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; -import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; -import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; -import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; +import HTMLHeadElement from '../nodes/html-head-element/HTMLHeadElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLHRElement from '../nodes/html-hr-element/HTMLHRElement.js'; +import HTMLHtmlElement from '../nodes/html-html-element/HTMLHtmlElement.js'; +import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; -import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; -import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; +import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; -import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; +import HTMLLegendElement from '../nodes/html-legend-element/HTMLLegendElement.js'; +import HTMLLIElement from '../nodes/html-li-element/HTMLLIElement.js'; +import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; +import HTMLMapElement from '../nodes/html-map-element/HTMLMapElement.js'; +import HTMLMenuElement from '../nodes/html-menu-element/HTMLMenuElement.js'; import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; -import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; -import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; -import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; +import HTMLMeterElement from '../nodes/html-meter-element/HTMLMeterElement.js'; +import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; +import HTMLOListElement from '../nodes/html-o-list-element/HTMLOListElement.js'; +import HTMLObjectElement from '../nodes/html-object-element/HTMLObjectElement.js'; import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; +import HTMLOutputElement from '../nodes/html-output-element/HTMLOutputElement.js'; +import HTMLParagraphElement from '../nodes/html-paragraph-element/HTMLParagraphElement.js'; +import HTMLParamElement from '../nodes/html-param-element/HTMLParamElement.js'; +import HTMLPictureElement from '../nodes/html-picture-element/HTMLPictureElement.js'; +import HTMLPreElement from '../nodes/html-pre-element/HTMLPreElement.js'; +import HTMLProgressElement from '../nodes/html-progress-element/HTMLProgressElement.js'; +import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; +import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; +import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; +import HTMLSourceElement from '../nodes/html-source-element/HTMLSourceElement.js'; +import HTMLSpanElement from '../nodes/html-span-element/HTMLSpanElement.js'; +import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; +import HTMLTableCaptionElement from '../nodes/html-table-caption-element/HTMLTableCaptionElement.js'; +import HTMLTableCellElement from '../nodes/html-table-cell-element/HTMLTableCellElement.js'; +import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableElement from '../nodes/html-table-element/HTMLTableElement.js'; +import HTMLTableRowElement from '../nodes/html-table-row-element/HTMLTableRowElement.js'; +import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; +import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; +import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; +import HTMLTimeElement from '../nodes/html-time-element/HTMLTimeElement.js'; +import HTMLTitleElement from '../nodes/html-title-element/HTMLTitleElement.js'; +import HTMLTrackElement from '../nodes/html-track-element/HTMLTrackElement.js'; +import HTMLUListElement from '../nodes/html-u-list-element/HTMLUListElement.js'; import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; // Makes it work with custom elements when they declare their own interface. diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index a0d507ae5..9b1d4c28a 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -1,48 +1,3 @@ -import HTMLUListElement from './nodes/html-u-list-element/HTMLUListElement.js'; -import HTMLTrackElement from './nodes/html-track-element/HTMLTrackElement.js'; -import HTMLTableRowElement from './nodes/html-table-row-element/HTMLTableRowElement.js'; -import HTMLTitleElement from './nodes/html-title-element/HTMLTitleElement.js'; -import HTMLTimeElement from './nodes/html-time-element/HTMLTimeElement.js'; -import HTMLTableSectionElement from './nodes/html-table-section-element/HTMLTableSectionElement.js'; -import HTMLTableCellElement from './nodes/html-table-cell-element/HTMLTableCellElement.js'; -import HTMLTableElement from './nodes/html-table-element/HTMLTableElement.js'; -import HTMLSpanElement from './nodes/html-span-element/HTMLSpanElement.js'; -import HTMLSourceElement from './nodes/html-source-element/HTMLSourceElement.js'; -import HTMLQuoteElement from './nodes/html-quote-element/HTMLQuoteElement.js'; -import HTMLProgressElement from './nodes/html-progress-element/HTMLProgressElement.js'; -import HTMLPreElement from './nodes/html-pre-element/HTMLPreElement.js'; -import HTMLPictureElement from './nodes/html-picture-element/HTMLPictureElement.js'; -import HTMLParamElement from './nodes/html-param-element/HTMLParamElement.js'; -import HTMLParagraphElement from './nodes/html-paragraph-element/HTMLParagraphElement.js'; -import HTMLOutputElement from './nodes/html-output-element/HTMLOutputElement.js'; -import HTMLOListElement from './nodes/html-o-list-element/HTMLOListElement.js'; -import HTMLObjectElement from './nodes/html-object-element/HTMLObjectElement.js'; -import HTMLMeterElement from './nodes/html-meter-element/HTMLMeterElement.js'; -import HTMLMenuElement from './nodes/html-menu-element/HTMLMenuElement.js'; -import HTMLMapElement from './nodes/html-map-element/HTMLMapElement.js'; -import HTMLLIElement from './nodes/html-li-element/HTMLLIElement.js'; -import HTMLLegendElement from './nodes/html-legend-element/HTMLLegendElement.js'; -import HTMLModElement from './nodes/html-mod-element/HTMLModElement.js'; -import HTMLHtmlElement from './nodes/html-html-element/HTMLHtmlElement.js'; -import HTMLHRElement from './nodes/html-hr-element/HTMLHRElement.js'; -import HTMLHeadElement from './nodes/html-head-element/HTMLHeadElement.js'; -import HTMLHeadingElement from './nodes/html-heading-element/HTMLHeadingElement.js'; -import HTMLFieldSetElement from './nodes/html-field-set-element/HTMLFieldSetElement.js'; -import HTMLEmbedElement from './nodes/html-embed-element/HTMLEmbedElement.js'; -import HTMLDListElement from './nodes/html-d-list-element/HTMLDListElement.js'; -import HTMLDivElement from './nodes/html-div-element/HTMLDivElement.js'; -import HTMLDetailsElement from './nodes/html-details-element/HTMLDetailsElement.js'; -import HTMLModElement from './nodes/html-mod-element/HTMLModElement.js'; -import HTMLDataListElement from './nodes/html-data-list-element/HTMLDataListElement.js'; -import HTMLDataElement from './nodes/html-data-element/HTMLDataElement.js'; -import HTMLTableColElement from './nodes/html-table-col-element/HTMLTableColElement.js'; -import HTMLTableColElement from './nodes/html-table-col-element/HTMLTableColElement.js'; -import HTMLTableCaptionElement from './nodes/html-table-caption-element/HTMLTableCaptionElement.js'; -import HTMLCanvasElement from './nodes/html-canvas-element/HTMLCanvasElement.js'; -import HTMLBRElement from './nodes/html-br-element/HTMLBRElement.js'; -import HTMLQuoteElement from './nodes/html-quote-element/HTMLQuoteElement.js'; -import HTMLBodyElement from './nodes/html-body-element/HTMLBodyElement.js'; -import HTMLAreaElement from './nodes/html-area-element/HTMLAreaElement.js'; import { URLSearchParams } from 'url'; import Browser from './browser/Browser.js'; import BrowserContext from './browser/BrowserContext.js'; @@ -105,6 +60,7 @@ import File from './file/File.js'; import FileReader from './file/FileReader.js'; import FormData from './form-data/FormData.js'; import History from './history/History.js'; +import Location from './location/Location.js'; import MutationObserver from './mutation-observer/MutationObserver.js'; import MutationRecord from './mutation-observer/MutationRecord.js'; import Attr from './nodes/attr/Attr.js'; @@ -116,31 +72,73 @@ import DOMRect from './nodes/element/DOMRect.js'; import Element from './nodes/element/Element.js'; import HTMLCollection from './nodes/element/HTMLCollection.js'; import HTMLAnchorElement from './nodes/html-anchor-element/HTMLAnchorElement.js'; +import HTMLAreaElement from './nodes/html-area-element/HTMLAreaElement.js'; import HTMLAudioElement from './nodes/html-audio-element/HTMLAudioElement.js'; import HTMLBaseElement from './nodes/html-base-element/HTMLBaseElement.js'; +import HTMLBodyElement from './nodes/html-body-element/HTMLBodyElement.js'; +import HTMLBRElement from './nodes/html-br-element/HTMLBRElement.js'; import HTMLButtonElement from './nodes/html-button-element/HTMLButtonElement.js'; +import HTMLCanvasElement from './nodes/html-canvas-element/HTMLCanvasElement.js'; +import HTMLDListElement from './nodes/html-d-list-element/HTMLDListElement.js'; +import HTMLDataElement from './nodes/html-data-element/HTMLDataElement.js'; +import HTMLDataListElement from './nodes/html-data-list-element/HTMLDataListElement.js'; +import HTMLDetailsElement from './nodes/html-details-element/HTMLDetailsElement.js'; import HTMLDialogElement from './nodes/html-dialog-element/HTMLDialogElement.js'; +import HTMLDivElement from './nodes/html-div-element/HTMLDivElement.js'; import HTMLDocument from './nodes/html-document/HTMLDocument.js'; import HTMLElement from './nodes/html-element/HTMLElement.js'; +import HTMLEmbedElement from './nodes/html-embed-element/HTMLEmbedElement.js'; +import HTMLFieldSetElement from './nodes/html-field-set-element/HTMLFieldSetElement.js'; import HTMLFormControlsCollection from './nodes/html-form-element/HTMLFormControlsCollection.js'; import HTMLFormElement from './nodes/html-form-element/HTMLFormElement.js'; +import HTMLHeadElement from './nodes/html-head-element/HTMLHeadElement.js'; +import HTMLHeadingElement from './nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLHRElement from './nodes/html-hr-element/HTMLHRElement.js'; +import HTMLHtmlElement from './nodes/html-html-element/HTMLHtmlElement.js'; import HTMLIFrameElement from './nodes/html-iframe-element/HTMLIFrameElement.js'; import HTMLImageElement from './nodes/html-image-element/HTMLImageElement.js'; import Image from './nodes/html-image-element/Image.js'; import FileList from './nodes/html-input-element/FileList.js'; import HTMLInputElement from './nodes/html-input-element/HTMLInputElement.js'; import HTMLLabelElement from './nodes/html-label-element/HTMLLabelElement.js'; +import HTMLLegendElement from './nodes/html-legend-element/HTMLLegendElement.js'; +import HTMLLIElement from './nodes/html-li-element/HTMLLIElement.js'; import HTMLLinkElement from './nodes/html-link-element/HTMLLinkElement.js'; +import HTMLMapElement from './nodes/html-map-element/HTMLMapElement.js'; import HTMLMediaElement from './nodes/html-media-element/HTMLMediaElement.js'; +import HTMLMenuElement from './nodes/html-menu-element/HTMLMenuElement.js'; import HTMLMetaElement from './nodes/html-meta-element/HTMLMetaElement.js'; +import HTMLMeterElement from './nodes/html-meter-element/HTMLMeterElement.js'; +import HTMLModElement from './nodes/html-mod-element/HTMLModElement.js'; +import HTMLOListElement from './nodes/html-o-list-element/HTMLOListElement.js'; +import HTMLObjectElement from './nodes/html-object-element/HTMLObjectElement.js'; import HTMLOptGroupElement from './nodes/html-opt-group-element/HTMLOptGroupElement.js'; import HTMLOptionElement from './nodes/html-option-element/HTMLOptionElement.js'; +import HTMLOutputElement from './nodes/html-output-element/HTMLOutputElement.js'; +import HTMLParagraphElement from './nodes/html-paragraph-element/HTMLParagraphElement.js'; +import HTMLParamElement from './nodes/html-param-element/HTMLParamElement.js'; +import HTMLPictureElement from './nodes/html-picture-element/HTMLPictureElement.js'; +import HTMLPreElement from './nodes/html-pre-element/HTMLPreElement.js'; +import HTMLProgressElement from './nodes/html-progress-element/HTMLProgressElement.js'; +import HTMLQuoteElement from './nodes/html-quote-element/HTMLQuoteElement.js'; import HTMLScriptElement from './nodes/html-script-element/HTMLScriptElement.js'; import HTMLSelectElement from './nodes/html-select-element/HTMLSelectElement.js'; import HTMLSlotElement from './nodes/html-slot-element/HTMLSlotElement.js'; +import HTMLSourceElement from './nodes/html-source-element/HTMLSourceElement.js'; +import HTMLSpanElement from './nodes/html-span-element/HTMLSpanElement.js'; import HTMLStyleElement from './nodes/html-style-element/HTMLStyleElement.js'; +import HTMLTableCaptionElement from './nodes/html-table-caption-element/HTMLTableCaptionElement.js'; +import HTMLTableCellElement from './nodes/html-table-cell-element/HTMLTableCellElement.js'; +import HTMLTableColElement from './nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableElement from './nodes/html-table-element/HTMLTableElement.js'; +import HTMLTableRowElement from './nodes/html-table-row-element/HTMLTableRowElement.js'; +import HTMLTableSectionElement from './nodes/html-table-section-element/HTMLTableSectionElement.js'; import HTMLTemplateElement from './nodes/html-template-element/HTMLTemplateElement.js'; import HTMLTextAreaElement from './nodes/html-text-area-element/HTMLTextAreaElement.js'; +import HTMLTimeElement from './nodes/html-time-element/HTMLTimeElement.js'; +import HTMLTitleElement from './nodes/html-title-element/HTMLTitleElement.js'; +import HTMLTrackElement from './nodes/html-track-element/HTMLTrackElement.js'; +import HTMLUListElement from './nodes/html-u-list-element/HTMLUListElement.js'; import HTMLUnknownElement from './nodes/html-unknown-element/HTMLUnknownElement.js'; import HTMLVideoElement from './nodes/html-video-element/HTMLVideoElement.js'; import Node from './nodes/node/Node.js'; @@ -162,7 +160,6 @@ import Storage from './storage/Storage.js'; import NodeFilter from './tree-walker/NodeFilter.js'; import NodeIterator from './tree-walker/NodeIterator.js'; import TreeWalker from './tree-walker/TreeWalker.js'; -import Location from './location/Location.js'; import URL from './url/URL.js'; import BrowserWindow from './window/BrowserWindow.js'; import GlobalWindow from './window/GlobalWindow.js'; @@ -223,48 +220,6 @@ export type { }; export { - HTMLUListElement, - HTMLTrackElement, - HTMLTableRowElement, - HTMLTitleElement, - HTMLTimeElement, - HTMLTableSectionElement, - HTMLTableCellElement, - HTMLTableElement, - HTMLSpanElement, - HTMLSourceElement, - HTMLQuoteElement, - HTMLProgressElement, - HTMLPreElement, - HTMLPictureElement, - HTMLParamElement, - HTMLParagraphElement, - HTMLOutputElement, - HTMLOListElement, - HTMLObjectElement, - HTMLMeterElement, - HTMLMenuElement, - HTMLMapElement, - HTMLLIElement, - HTMLLegendElement, - HTMLModElement, - HTMLHtmlElement, - HTMLHRElement, - HTMLHeadElement, - HTMLHeadingElement, - HTMLFieldSetElement, - HTMLEmbedElement, - HTMLDListElement, - HTMLDivElement, - HTMLDetailsElement, - HTMLDataListElement, - HTMLDataElement, - HTMLTableColElement, - HTMLTableCaptionElement, - HTMLCanvasElement, - HTMLBRElement, - HTMLBodyElement, - HTMLAreaElement, AbortController, AbortSignal, AnimationEvent, @@ -318,30 +273,72 @@ export { FormData, GlobalWindow, HTMLAnchorElement, + HTMLAreaElement, HTMLAudioElement, + HTMLBRElement, HTMLBaseElement, + HTMLBodyElement, HTMLButtonElement, + HTMLCanvasElement, HTMLCollection, + HTMLDListElement, + HTMLDataElement, + HTMLDataListElement, + HTMLDetailsElement, HTMLDialogElement, + HTMLDivElement, HTMLDocument, HTMLElement, + HTMLEmbedElement, + HTMLFieldSetElement, HTMLFormControlsCollection, HTMLFormElement, + HTMLHRElement, + HTMLHeadElement, + HTMLHeadingElement, + HTMLHtmlElement, HTMLIFrameElement, HTMLImageElement, HTMLInputElement, + HTMLLIElement, HTMLLabelElement, + HTMLLegendElement, HTMLLinkElement, + HTMLMapElement, HTMLMediaElement, + HTMLMenuElement, HTMLMetaElement, + HTMLMeterElement, + HTMLModElement, + HTMLOListElement, + HTMLObjectElement, HTMLOptGroupElement, HTMLOptionElement, + HTMLOutputElement, + HTMLParagraphElement, + HTMLParamElement, + HTMLPictureElement, + HTMLPreElement, + HTMLProgressElement, + HTMLQuoteElement, HTMLScriptElement, HTMLSelectElement, HTMLSlotElement, + HTMLSourceElement, + HTMLSpanElement, HTMLStyleElement, + HTMLTableCaptionElement, + HTMLTableCellElement, + HTMLTableColElement, + HTMLTableElement, + HTMLTableRowElement, + HTMLTableSectionElement, HTMLTemplateElement, HTMLTextAreaElement, + HTMLTimeElement, + HTMLTitleElement, + HTMLTrackElement, + HTMLUListElement, HTMLUnknownElement, HTMLVideoElement, HashChangeEvent, diff --git a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts index a8ce78daf..b810ee97c 100644 --- a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLAreaElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLAreaElement - */ - export default class HTMLAreaElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLAreaElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLAreaElement + */ +export default class HTMLAreaElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts b/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts index 3faaee4d6..55f4159f0 100644 --- a/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts +++ b/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLBodyElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLBodyElement - */ - export default class HTMLBodyElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLBodyElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLBodyElement + */ +export default class HTMLBodyElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-br-element/HTMLBRElement.ts b/packages/happy-dom/src/nodes/html-br-element/HTMLBRElement.ts index 23b6b7089..4d0d5ca6d 100644 --- a/packages/happy-dom/src/nodes/html-br-element/HTMLBRElement.ts +++ b/packages/happy-dom/src/nodes/html-br-element/HTMLBRElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLBRElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLBRElement - */ - export default class HTMLBRElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLBRElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLBRElement + */ +export default class HTMLBRElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts index 53c25f9ee..328082e3f 100644 --- a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts +++ b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLCanvasElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement - */ - export default class HTMLCanvasElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLCanvasElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement + */ +export default class HTMLCanvasElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-d-list-element/HTMLDListElement.ts b/packages/happy-dom/src/nodes/html-d-list-element/HTMLDListElement.ts index ecb73c149..f91058b26 100644 --- a/packages/happy-dom/src/nodes/html-d-list-element/HTMLDListElement.ts +++ b/packages/happy-dom/src/nodes/html-d-list-element/HTMLDListElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLDListElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDListElement - */ - export default class HTMLDListElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLDListElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDListElement + */ +export default class HTMLDListElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts b/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts index b24ba46a2..5854d9bd7 100644 --- a/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts +++ b/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLDataElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataElement - */ - export default class HTMLDataElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLDataElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataElement + */ +export default class HTMLDataElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts index 081e1b99f..d9090ec42 100644 --- a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts +++ b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLDataListElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataListElement - */ - export default class HTMLDataListElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLDataListElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataListElement + */ +export default class HTMLDataListElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts index 1b0709da9..3bf7e0954 100644 --- a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts +++ b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLDetailsElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDetailsElement - */ - export default class HTMLDetailsElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLDetailsElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDetailsElement + */ +export default class HTMLDetailsElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-div-element/HTMLDivElement.ts b/packages/happy-dom/src/nodes/html-div-element/HTMLDivElement.ts index 1a16ea7d3..adb66c807 100644 --- a/packages/happy-dom/src/nodes/html-div-element/HTMLDivElement.ts +++ b/packages/happy-dom/src/nodes/html-div-element/HTMLDivElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLDivElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement - */ - export default class HTMLDivElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLDivElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement + */ +export default class HTMLDivElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts b/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts index 7dcf1f3b9..93bf31f01 100644 --- a/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts +++ b/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLEmbedElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLEmbedElement - */ - export default class HTMLEmbedElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLEmbedElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLEmbedElement + */ +export default class HTMLEmbedElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts index eb4943c2e..007850854 100644 --- a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts +++ b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLFieldSetElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFieldSetElement - */ - export default class HTMLFieldSetElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLFieldSetElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFieldSetElement + */ +export default class HTMLFieldSetElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-head-element/HTMLHeadElement.ts b/packages/happy-dom/src/nodes/html-head-element/HTMLHeadElement.ts index 98bba84ab..fc41838b2 100644 --- a/packages/happy-dom/src/nodes/html-head-element/HTMLHeadElement.ts +++ b/packages/happy-dom/src/nodes/html-head-element/HTMLHeadElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLHeadElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHeadElement - */ - export default class HTMLHeadElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLHeadElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHeadElement + */ +export default class HTMLHeadElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-heading-element/HTMLHeadingElement.ts b/packages/happy-dom/src/nodes/html-heading-element/HTMLHeadingElement.ts index 92ebfdad0..30587c6fb 100644 --- a/packages/happy-dom/src/nodes/html-heading-element/HTMLHeadingElement.ts +++ b/packages/happy-dom/src/nodes/html-heading-element/HTMLHeadingElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLHeadingElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHeadingElement - */ - export default class HTMLHeadingElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLHeadingElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHeadingElement + */ +export default class HTMLHeadingElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-hr-element/HTMLHRElement.ts b/packages/happy-dom/src/nodes/html-hr-element/HTMLHRElement.ts index aa4e408bd..2238f6004 100644 --- a/packages/happy-dom/src/nodes/html-hr-element/HTMLHRElement.ts +++ b/packages/happy-dom/src/nodes/html-hr-element/HTMLHRElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLHRElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHRElement - */ - export default class HTMLHRElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLHRElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHRElement + */ +export default class HTMLHRElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-html-element/HTMLHtmlElement.ts b/packages/happy-dom/src/nodes/html-html-element/HTMLHtmlElement.ts index 65375ba3a..44b00b656 100644 --- a/packages/happy-dom/src/nodes/html-html-element/HTMLHtmlElement.ts +++ b/packages/happy-dom/src/nodes/html-html-element/HTMLHtmlElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLHtmlElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHtmlElement - */ - export default class HTMLHtmlElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLHtmlElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLHtmlElement + */ +export default class HTMLHtmlElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-legend-element/HTMLLegendElement.ts b/packages/happy-dom/src/nodes/html-legend-element/HTMLLegendElement.ts index 900167ae4..acef4f03a 100644 --- a/packages/happy-dom/src/nodes/html-legend-element/HTMLLegendElement.ts +++ b/packages/happy-dom/src/nodes/html-legend-element/HTMLLegendElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLLegendElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLLegendElement - */ - export default class HTMLLegendElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLLegendElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLLegendElement + */ +export default class HTMLLegendElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-li-element/HTMLLIElement.ts b/packages/happy-dom/src/nodes/html-li-element/HTMLLIElement.ts index bc9615f31..8a1dd1709 100644 --- a/packages/happy-dom/src/nodes/html-li-element/HTMLLIElement.ts +++ b/packages/happy-dom/src/nodes/html-li-element/HTMLLIElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLLIElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLLIElement - */ - export default class HTMLLIElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLLIElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLLIElement + */ +export default class HTMLLIElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-map-element/HTMLMapElement.ts b/packages/happy-dom/src/nodes/html-map-element/HTMLMapElement.ts index 4f3be88bd..33a2b25d8 100644 --- a/packages/happy-dom/src/nodes/html-map-element/HTMLMapElement.ts +++ b/packages/happy-dom/src/nodes/html-map-element/HTMLMapElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLMapElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMapElement - */ - export default class HTMLMapElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLMapElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMapElement + */ +export default class HTMLMapElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-menu-element/HTMLMenuElement.ts b/packages/happy-dom/src/nodes/html-menu-element/HTMLMenuElement.ts index 620443f64..ad79e869b 100644 --- a/packages/happy-dom/src/nodes/html-menu-element/HTMLMenuElement.ts +++ b/packages/happy-dom/src/nodes/html-menu-element/HTMLMenuElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLMenuElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMenuElement - */ - export default class HTMLMenuElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLMenuElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMenuElement + */ +export default class HTMLMenuElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts b/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts index 446cd29c0..409e1b6d1 100644 --- a/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts +++ b/packages/happy-dom/src/nodes/html-meter-element/HTMLMeterElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLMeterElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMeterElement - */ - export default class HTMLMeterElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLMeterElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMeterElement + */ +export default class HTMLMeterElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-mod-element/HTMLModElement.ts b/packages/happy-dom/src/nodes/html-mod-element/HTMLModElement.ts index 0cf32b46f..ab6d25678 100644 --- a/packages/happy-dom/src/nodes/html-mod-element/HTMLModElement.ts +++ b/packages/happy-dom/src/nodes/html-mod-element/HTMLModElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLModElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLModElement - */ - export default class HTMLModElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLModElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLModElement + */ +export default class HTMLModElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-o-list-element/HTMLOListElement.ts b/packages/happy-dom/src/nodes/html-o-list-element/HTMLOListElement.ts index 323e3a398..1712f85d7 100644 --- a/packages/happy-dom/src/nodes/html-o-list-element/HTMLOListElement.ts +++ b/packages/happy-dom/src/nodes/html-o-list-element/HTMLOListElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLOListElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOListElement - */ - export default class HTMLOListElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLOListElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOListElement + */ +export default class HTMLOListElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-object-element/HTMLObjectElement.ts b/packages/happy-dom/src/nodes/html-object-element/HTMLObjectElement.ts index 65130b0ef..eb1eb86cb 100644 --- a/packages/happy-dom/src/nodes/html-object-element/HTMLObjectElement.ts +++ b/packages/happy-dom/src/nodes/html-object-element/HTMLObjectElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLObjectElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement - */ - export default class HTMLObjectElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLObjectElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement + */ +export default class HTMLObjectElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-output-element/HTMLOutputElement.ts b/packages/happy-dom/src/nodes/html-output-element/HTMLOutputElement.ts index f7608a8d2..90818a19c 100644 --- a/packages/happy-dom/src/nodes/html-output-element/HTMLOutputElement.ts +++ b/packages/happy-dom/src/nodes/html-output-element/HTMLOutputElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLOutputElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOutputElement - */ - export default class HTMLOutputElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLOutputElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOutputElement + */ +export default class HTMLOutputElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-paragraph-element/HTMLParagraphElement.ts b/packages/happy-dom/src/nodes/html-paragraph-element/HTMLParagraphElement.ts index 7c63008e9..9f7e1a460 100644 --- a/packages/happy-dom/src/nodes/html-paragraph-element/HTMLParagraphElement.ts +++ b/packages/happy-dom/src/nodes/html-paragraph-element/HTMLParagraphElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLParagraphElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLParagraphElement - */ - export default class HTMLParagraphElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLParagraphElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLParagraphElement + */ +export default class HTMLParagraphElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-param-element/HTMLParamElement.ts b/packages/happy-dom/src/nodes/html-param-element/HTMLParamElement.ts index 26d7e41d8..9b793d286 100644 --- a/packages/happy-dom/src/nodes/html-param-element/HTMLParamElement.ts +++ b/packages/happy-dom/src/nodes/html-param-element/HTMLParamElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLParamElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLParamElement - */ - export default class HTMLParamElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLParamElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLParamElement + */ +export default class HTMLParamElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-picture-element/HTMLPictureElement.ts b/packages/happy-dom/src/nodes/html-picture-element/HTMLPictureElement.ts index 57386026b..75469f935 100644 --- a/packages/happy-dom/src/nodes/html-picture-element/HTMLPictureElement.ts +++ b/packages/happy-dom/src/nodes/html-picture-element/HTMLPictureElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLPictureElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLPictureElement - */ - export default class HTMLPictureElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLPictureElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLPictureElement + */ +export default class HTMLPictureElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-pre-element/HTMLPreElement.ts b/packages/happy-dom/src/nodes/html-pre-element/HTMLPreElement.ts index 74ee9f17e..0131112ba 100644 --- a/packages/happy-dom/src/nodes/html-pre-element/HTMLPreElement.ts +++ b/packages/happy-dom/src/nodes/html-pre-element/HTMLPreElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLPreElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLPreElement - */ - export default class HTMLPreElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLPreElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLPreElement + */ +export default class HTMLPreElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts b/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts index 8d1ce968d..1f978d767 100644 --- a/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts +++ b/packages/happy-dom/src/nodes/html-progress-element/HTMLProgressElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLProgressElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLProgressElement - */ - export default class HTMLProgressElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLProgressElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLProgressElement + */ +export default class HTMLProgressElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-quote-element/HTMLQuoteElement.ts b/packages/happy-dom/src/nodes/html-quote-element/HTMLQuoteElement.ts index eeb527e7c..dd13465b3 100644 --- a/packages/happy-dom/src/nodes/html-quote-element/HTMLQuoteElement.ts +++ b/packages/happy-dom/src/nodes/html-quote-element/HTMLQuoteElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLQuoteElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLQuoteElement - */ - export default class HTMLQuoteElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLQuoteElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLQuoteElement + */ +export default class HTMLQuoteElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-source-element/HTMLSourceElement.ts b/packages/happy-dom/src/nodes/html-source-element/HTMLSourceElement.ts index 268330597..8da0da614 100644 --- a/packages/happy-dom/src/nodes/html-source-element/HTMLSourceElement.ts +++ b/packages/happy-dom/src/nodes/html-source-element/HTMLSourceElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLSourceElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLSourceElement - */ - export default class HTMLSourceElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLSourceElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLSourceElement + */ +export default class HTMLSourceElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-span-element/HTMLSpanElement.ts b/packages/happy-dom/src/nodes/html-span-element/HTMLSpanElement.ts index 47769862f..7f5cc9b32 100644 --- a/packages/happy-dom/src/nodes/html-span-element/HTMLSpanElement.ts +++ b/packages/happy-dom/src/nodes/html-span-element/HTMLSpanElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLSpanElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLSpanElement - */ - export default class HTMLSpanElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLSpanElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLSpanElement + */ +export default class HTMLSpanElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-table-caption-element/HTMLTableCaptionElement.ts b/packages/happy-dom/src/nodes/html-table-caption-element/HTMLTableCaptionElement.ts index 15e6ea78f..928d89a6c 100644 --- a/packages/happy-dom/src/nodes/html-table-caption-element/HTMLTableCaptionElement.ts +++ b/packages/happy-dom/src/nodes/html-table-caption-element/HTMLTableCaptionElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTableCaptionElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCaptionElement - */ - export default class HTMLTableCaptionElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTableCaptionElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCaptionElement + */ +export default class HTMLTableCaptionElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-table-cell-element/HTMLTableCellElement.ts b/packages/happy-dom/src/nodes/html-table-cell-element/HTMLTableCellElement.ts index a1e66f5e0..a4684bde2 100644 --- a/packages/happy-dom/src/nodes/html-table-cell-element/HTMLTableCellElement.ts +++ b/packages/happy-dom/src/nodes/html-table-cell-element/HTMLTableCellElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTableCellElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement - */ - export default class HTMLTableCellElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTableCellElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement + */ +export default class HTMLTableCellElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-table-col-element/HTMLTableColElement.ts b/packages/happy-dom/src/nodes/html-table-col-element/HTMLTableColElement.ts index d858d4d92..ef17b0216 100644 --- a/packages/happy-dom/src/nodes/html-table-col-element/HTMLTableColElement.ts +++ b/packages/happy-dom/src/nodes/html-table-col-element/HTMLTableColElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTableColElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableColElement - */ - export default class HTMLTableColElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTableColElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableColElement + */ +export default class HTMLTableColElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts b/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts index 2b8ad9b6a..dfc1b9584 100644 --- a/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts +++ b/packages/happy-dom/src/nodes/html-table-element/HTMLTableElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTableElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableElement - */ - export default class HTMLTableElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTableElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableElement + */ +export default class HTMLTableElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts b/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts index 3daec5120..a094cf0cc 100644 --- a/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts +++ b/packages/happy-dom/src/nodes/html-table-row-element/HTMLTableRowElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTableRowElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableRowElement - */ - export default class HTMLTableRowElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTableRowElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableRowElement + */ +export default class HTMLTableRowElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-table-section-element/HTMLTableSectionElement.ts b/packages/happy-dom/src/nodes/html-table-section-element/HTMLTableSectionElement.ts index 02c786e17..f58924a8c 100644 --- a/packages/happy-dom/src/nodes/html-table-section-element/HTMLTableSectionElement.ts +++ b/packages/happy-dom/src/nodes/html-table-section-element/HTMLTableSectionElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTableSectionElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableSectionElement - */ - export default class HTMLTableSectionElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTableSectionElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableSectionElement + */ +export default class HTMLTableSectionElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-time-element/HTMLTimeElement.ts b/packages/happy-dom/src/nodes/html-time-element/HTMLTimeElement.ts index ff96e5a81..413c2f54f 100644 --- a/packages/happy-dom/src/nodes/html-time-element/HTMLTimeElement.ts +++ b/packages/happy-dom/src/nodes/html-time-element/HTMLTimeElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTimeElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTimeElement - */ - export default class HTMLTimeElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTimeElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTimeElement + */ +export default class HTMLTimeElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-title-element/HTMLTitleElement.ts b/packages/happy-dom/src/nodes/html-title-element/HTMLTitleElement.ts index 092eb83fd..aa92ccf8b 100644 --- a/packages/happy-dom/src/nodes/html-title-element/HTMLTitleElement.ts +++ b/packages/happy-dom/src/nodes/html-title-element/HTMLTitleElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTitleElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement - */ - export default class HTMLTitleElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTitleElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement + */ +export default class HTMLTitleElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts b/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts index d557e8db1..5689d0694 100644 --- a/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts +++ b/packages/happy-dom/src/nodes/html-track-element/HTMLTrackElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLTrackElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTrackElement - */ - export default class HTMLTrackElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLTrackElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLTrackElement + */ +export default class HTMLTrackElement extends HTMLElement {} diff --git a/packages/happy-dom/src/nodes/html-u-list-element/HTMLUListElement.ts b/packages/happy-dom/src/nodes/html-u-list-element/HTMLUListElement.ts index 5ccd2d6e8..a81163cd9 100644 --- a/packages/happy-dom/src/nodes/html-u-list-element/HTMLUListElement.ts +++ b/packages/happy-dom/src/nodes/html-u-list-element/HTMLUListElement.ts @@ -1,8 +1,7 @@ - - import HTMLElement from '../html-element/HTMLElement.js'; - /** - * HTMLUListElement - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLUListElement - */ - export default class HTMLUListElement extends HTMLElement {} \ No newline at end of file +import HTMLElement from '../html-element/HTMLElement.js'; +/** + * HTMLUListElement + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLUListElement + */ +export default class HTMLUListElement extends HTMLElement {} diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 719be6d1b..386cea296 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -1,115 +1,20 @@ -import HTMLUListElement from '../nodes/html-u-list-element/HTMLUListElement.js'; -import HTMLTrackElement from '../nodes/html-track-element/HTMLTrackElement.js'; -import HTMLTableRowElement from '../nodes/html-table-row-element/HTMLTableRowElement.js'; -import HTMLTitleElement from '../nodes/html-title-element/HTMLTitleElement.js'; -import HTMLTimeElement from '../nodes/html-time-element/HTMLTimeElement.js'; -import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; -import HTMLTableCellElement from '../nodes/html-table-cell-element/HTMLTableCellElement.js'; -import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; -import HTMLTableElement from '../nodes/html-table-element/HTMLTableElement.js'; -import HTMLSpanElement from '../nodes/html-span-element/HTMLSpanElement.js'; -import HTMLSourceElement from '../nodes/html-source-element/HTMLSourceElement.js'; -import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; -import HTMLProgressElement from '../nodes/html-progress-element/HTMLProgressElement.js'; -import HTMLPreElement from '../nodes/html-pre-element/HTMLPreElement.js'; -import HTMLPictureElement from '../nodes/html-picture-element/HTMLPictureElement.js'; -import HTMLParamElement from '../nodes/html-param-element/HTMLParamElement.js'; -import HTMLParagraphElement from '../nodes/html-paragraph-element/HTMLParagraphElement.js'; -import HTMLOutputElement from '../nodes/html-output-element/HTMLOutputElement.js'; -import HTMLOListElement from '../nodes/html-o-list-element/HTMLOListElement.js'; -import HTMLObjectElement from '../nodes/html-object-element/HTMLObjectElement.js'; -import HTMLMeterElement from '../nodes/html-meter-element/HTMLMeterElement.js'; -import HTMLMenuElement from '../nodes/html-menu-element/HTMLMenuElement.js'; -import HTMLMapElement from '../nodes/html-map-element/HTMLMapElement.js'; -import HTMLLIElement from '../nodes/html-li-element/HTMLLIElement.js'; -import HTMLLegendElement from '../nodes/html-legend-element/HTMLLegendElement.js'; -import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; -import HTMLHtmlElement from '../nodes/html-html-element/HTMLHtmlElement.js'; -import HTMLHRElement from '../nodes/html-hr-element/HTMLHRElement.js'; -import HTMLHeadElement from '../nodes/html-head-element/HTMLHeadElement.js'; -import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; -import HTMLFieldSetElement from '../nodes/html-field-set-element/HTMLFieldSetElement.js'; -import HTMLEmbedElement from '../nodes/html-embed-element/HTMLEmbedElement.js'; -import HTMLDListElement from '../nodes/html-d-list-element/HTMLDListElement.js'; -import HTMLDivElement from '../nodes/html-div-element/HTMLDivElement.js'; -import HTMLDetailsElement from '../nodes/html-details-element/HTMLDetailsElement.js'; -import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; -import HTMLDataListElement from '../nodes/html-data-list-element/HTMLDataListElement.js'; -import HTMLDataElement from '../nodes/html-data-element/HTMLDataElement.js'; -import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; -import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; -import HTMLTableCaptionElement from '../nodes/html-table-caption-element/HTMLTableCaptionElement.js'; -import HTMLCanvasElement from '../nodes/html-canvas-element/HTMLCanvasElement.js'; -import HTMLBRElement from '../nodes/html-br-element/HTMLBRElement.js'; -import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; -import HTMLBodyElement from '../nodes/html-body-element/HTMLBodyElement.js'; -import HTMLAreaElement from '../nodes/html-area-element/HTMLAreaElement.js'; -import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; -import * as PropertySymbol from '../PropertySymbol.js'; -import DocumentImplementation from '../nodes/document/Document.js'; -import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; -import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; -import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; -import Node from '../nodes/node/Node.js'; -import NodeFilter from '../tree-walker/NodeFilter.js'; -import Text from '../nodes/text/Text.js'; -import Comment from '../nodes/comment/Comment.js'; -import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; -import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; -import HTMLFormElementImplementation from '../nodes/html-form-element/HTMLFormElement.js'; -import HTMLElement from '../nodes/html-element/HTMLElement.js'; -import HTMLUnknownElement from '../nodes/html-unknown-element/HTMLUnknownElement.js'; -import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; -import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; -import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; -import HTMLLinkElementImplementation from '../nodes/html-link-element/HTMLLinkElement.js'; -import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; -import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; -import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; -import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; -import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; -import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; -import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; -import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; -import HTMLIFrameElementImplementation from '../nodes/html-iframe-element/HTMLIFrameElement.js'; -import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; -import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; -import SVGElement from '../nodes/svg-element/SVGElement.js'; -import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; -import HTMLScriptElementImplementation from '../nodes/html-script-element/HTMLScriptElement.js'; -import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; -import CharacterData from '../nodes/character-data/CharacterData.js'; -import DocumentType from '../nodes/document-type/DocumentType.js'; -import NodeIterator from '../tree-walker/NodeIterator.js'; -import TreeWalker from '../tree-walker/TreeWalker.js'; -import Event from '../event/Event.js'; -import CustomEvent from '../event/events/CustomEvent.js'; -import AnimationEvent from '../event/events/AnimationEvent.js'; -import KeyboardEvent from '../event/events/KeyboardEvent.js'; -import MessageEvent from '../event/events/MessageEvent.js'; -import ProgressEvent from '../event/events/ProgressEvent.js'; -import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; -import HashChangeEvent from '../event/events/HashChangeEvent.js'; -import TouchEvent from '../event/events/TouchEvent.js'; -import Touch from '../event/Touch.js'; -import EventTarget from '../event/EventTarget.js'; -import MessagePort from '../event/MessagePort.js'; +import { Buffer } from 'buffer'; +import { webcrypto } from 'crypto'; +import Stream from 'stream'; +import { ReadableStream } from 'stream/web'; import { URLSearchParams } from 'url'; -import URL from '../url/URL.js'; -import Location from '../location/Location.js'; -import MutationObserver from '../mutation-observer/MutationObserver.js'; -import MutationRecord from '../mutation-observer/MutationRecord.js'; -import XMLSerializer from '../xml-serializer/XMLSerializer.js'; -import ResizeObserver from '../resize-observer/ResizeObserver.js'; -import Blob from '../file/Blob.js'; -import File from '../file/File.js'; -import DOMException from '../exception/DOMException.js'; -import History from '../history/History.js'; -import CSSStyleSheet from '../css/CSSStyleSheet.js'; -import CSSStyleDeclaration from '../css/declaration/CSSStyleDeclaration.js'; +import VM from 'vm'; +import * as PropertySymbol from '../PropertySymbol.js'; +import Base64 from '../base64/Base64.js'; +import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import Clipboard from '../clipboard/Clipboard.js'; +import ClipboardItem from '../clipboard/ClipboardItem.js'; import CSS from '../css/CSS.js'; -import CSSUnitValue from '../css/CSSUnitValue.js'; import CSSRule from '../css/CSSRule.js'; +import CSSStyleSheet from '../css/CSSStyleSheet.js'; +import CSSUnitValue from '../css/CSSUnitValue.js'; +import CSSStyleDeclaration from '../css/declaration/CSSStyleDeclaration.js'; import CSSContainerRule from '../css/rules/CSSContainerRule.js'; import CSSFontFaceRule from '../css/rules/CSSFontFaceRule.js'; import CSSKeyframeRule from '../css/rules/CSSKeyframeRule.js'; @@ -117,86 +22,176 @@ import CSSKeyframesRule from '../css/rules/CSSKeyframesRule.js'; import CSSMediaRule from '../css/rules/CSSMediaRule.js'; import CSSStyleRule from '../css/rules/CSSStyleRule.js'; import CSSSupportsRule from '../css/rules/CSSSupportsRule.js'; -import MouseEvent from '../event/events/MouseEvent.js'; -import PointerEvent from '../event/events/PointerEvent.js'; -import FocusEvent from '../event/events/FocusEvent.js'; -import WheelEvent from '../event/events/WheelEvent.js'; +import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; +import DOMParserImplementation from '../dom-parser/DOMParser.js'; import DataTransfer from '../event/DataTransfer.js'; import DataTransferItem from '../event/DataTransferItem.js'; import DataTransferItemList from '../event/DataTransferItemList.js'; -import InputEvent from '../event/events/InputEvent.js'; +import Event from '../event/Event.js'; +import EventTarget from '../event/EventTarget.js'; +import MessagePort from '../event/MessagePort.js'; +import Touch from '../event/Touch.js'; import UIEvent from '../event/UIEvent.js'; +import AnimationEvent from '../event/events/AnimationEvent.js'; +import ClipboardEvent from '../event/events/ClipboardEvent.js'; +import CustomEvent from '../event/events/CustomEvent.js'; import ErrorEvent from '../event/events/ErrorEvent.js'; +import FocusEvent from '../event/events/FocusEvent.js'; +import HashChangeEvent from '../event/events/HashChangeEvent.js'; +import InputEvent from '../event/events/InputEvent.js'; +import KeyboardEvent from '../event/events/KeyboardEvent.js'; +import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; +import MessageEvent from '../event/events/MessageEvent.js'; +import MouseEvent from '../event/events/MouseEvent.js'; +import PointerEvent from '../event/events/PointerEvent.js'; +import ProgressEvent from '../event/events/ProgressEvent.js'; import StorageEvent from '../event/events/StorageEvent.js'; import SubmitEvent from '../event/events/SubmitEvent.js'; -import Screen from '../screen/Screen.js'; +import TouchEvent from '../event/events/TouchEvent.js'; +import WheelEvent from '../event/events/WheelEvent.js'; +import DOMException from '../exception/DOMException.js'; +import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; +import AbortController from '../fetch/AbortController.js'; +import AbortSignal from '../fetch/AbortSignal.js'; +import Fetch from '../fetch/Fetch.js'; +import Headers from '../fetch/Headers.js'; +import RequestImplementation from '../fetch/Request.js'; +import { default as Response, default as ResponseImplementation } from '../fetch/Response.js'; +import IRequestInfo from '../fetch/types/IRequestInfo.js'; import IRequestInit from '../fetch/types/IRequestInit.js'; -import Storage from '../storage/Storage.js'; -import StorageFactory from '../storage/StorageFactory.js'; -import HTMLCollection from '../nodes/element/HTMLCollection.js'; -import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; -import NodeList from '../nodes/node/NodeList.js'; +import IResponseBody from '../fetch/types/IResponseBody.js'; +import IResponseInit from '../fetch/types/IResponseInit.js'; +import Blob from '../file/Blob.js'; +import File from '../file/File.js'; +import FileReaderImplementation from '../file/FileReader.js'; +import FormData from '../form-data/FormData.js'; +import History from '../history/History.js'; +import Location from '../location/Location.js'; import MediaQueryList from '../match-media/MediaQueryList.js'; -import Selection from '../selection/Selection.js'; -import Navigator from '../navigator/Navigator.js'; +import MutationObserver from '../mutation-observer/MutationObserver.js'; +import MutationRecord from '../mutation-observer/MutationRecord.js'; +import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; import MimeType from '../navigator/MimeType.js'; import MimeTypeArray from '../navigator/MimeTypeArray.js'; +import Navigator from '../navigator/Navigator.js'; import Plugin from '../navigator/Plugin.js'; import PluginArray from '../navigator/PluginArray.js'; -import Fetch from '../fetch/Fetch.js'; -import DOMRect from '../nodes/element/DOMRect.js'; -import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; -import VM from 'vm'; -import { Buffer } from 'buffer'; -import { webcrypto } from 'crypto'; -import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; -import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js'; -import Base64 from '../base64/Base64.js'; import Attr from '../nodes/attr/Attr.js'; -import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; -import Element from '../nodes/element/Element.js'; -import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; -import FileList from '../nodes/html-input-element/FileList.js'; -import Stream from 'stream'; -import { ReadableStream } from 'stream/web'; -import FormData from '../form-data/FormData.js'; -import AbortController from '../fetch/AbortController.js'; -import AbortSignal from '../fetch/AbortSignal.js'; -import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; -import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; -import ValidityState from '../validity-state/ValidityState.js'; -import WindowErrorUtility from './WindowErrorUtility.js'; -import Permissions from '../permissions/Permissions.js'; -import PermissionStatus from '../permissions/PermissionStatus.js'; -import Clipboard from '../clipboard/Clipboard.js'; -import ClipboardItem from '../clipboard/ClipboardItem.js'; -import ClipboardEvent from '../event/events/ClipboardEvent.js'; -import Headers from '../fetch/Headers.js'; -import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; -import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; -import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; +import CharacterData from '../nodes/character-data/CharacterData.js'; +import Comment from '../nodes/comment/Comment.js'; +import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; +import DocumentType from '../nodes/document-type/DocumentType.js'; +import DocumentImplementation from '../nodes/document/Document.js'; import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js'; -import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; +import DOMRect from '../nodes/element/DOMRect.js'; +import Element from '../nodes/element/Element.js'; +import HTMLCollection from '../nodes/element/HTMLCollection.js'; import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; -import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; -import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; -import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; -import WindowPageOpenUtility from './WindowPageOpenUtility.js'; -import IResponseBody from '../fetch/types/IResponseBody.js'; -import IResponseInit from '../fetch/types/IResponseInit.js'; -import IRequestInfo from '../fetch/types/IRequestInfo.js'; -import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js'; +import HTMLAreaElement from '../nodes/html-area-element/HTMLAreaElement.js'; import AudioImplementation from '../nodes/html-audio-element/Audio.js'; +import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; +import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; +import HTMLBodyElement from '../nodes/html-body-element/HTMLBodyElement.js'; +import HTMLBRElement from '../nodes/html-br-element/HTMLBRElement.js'; +import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; +import HTMLCanvasElement from '../nodes/html-canvas-element/HTMLCanvasElement.js'; +import HTMLDListElement from '../nodes/html-d-list-element/HTMLDListElement.js'; +import HTMLDataElement from '../nodes/html-data-element/HTMLDataElement.js'; +import HTMLDataListElement from '../nodes/html-data-list-element/HTMLDataListElement.js'; +import HTMLDetailsElement from '../nodes/html-details-element/HTMLDetailsElement.js'; +import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; +import HTMLDivElement from '../nodes/html-div-element/HTMLDivElement.js'; +import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; +import HTMLElement from '../nodes/html-element/HTMLElement.js'; +import HTMLEmbedElement from '../nodes/html-embed-element/HTMLEmbedElement.js'; +import HTMLFieldSetElement from '../nodes/html-field-set-element/HTMLFieldSetElement.js'; +import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; +import HTMLFormElementImplementation from '../nodes/html-form-element/HTMLFormElement.js'; +import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; +import HTMLHeadElement from '../nodes/html-head-element/HTMLHeadElement.js'; +import HTMLHeadingElement from '../nodes/html-heading-element/HTMLHeadingElement.js'; +import HTMLHRElement from '../nodes/html-hr-element/HTMLHRElement.js'; +import HTMLHtmlElement from '../nodes/html-html-element/HTMLHtmlElement.js'; +import HTMLIFrameElementImplementation from '../nodes/html-iframe-element/HTMLIFrameElement.js'; +import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; import ImageImplementation from '../nodes/html-image-element/Image.js'; -import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; -import DOMParserImplementation from '../dom-parser/DOMParser.js'; -import FileReaderImplementation from '../file/FileReader.js'; -import RequestImplementation from '../fetch/Request.js'; -import ResponseImplementation from '../fetch/Response.js'; +import FileList from '../nodes/html-input-element/FileList.js'; +import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; +import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; +import HTMLLegendElement from '../nodes/html-legend-element/HTMLLegendElement.js'; +import HTMLLIElement from '../nodes/html-li-element/HTMLLIElement.js'; +import HTMLLinkElementImplementation from '../nodes/html-link-element/HTMLLinkElement.js'; +import HTMLMapElement from '../nodes/html-map-element/HTMLMapElement.js'; +import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; +import HTMLMenuElement from '../nodes/html-menu-element/HTMLMenuElement.js'; +import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; +import HTMLMeterElement from '../nodes/html-meter-element/HTMLMeterElement.js'; +import HTMLModElement from '../nodes/html-mod-element/HTMLModElement.js'; +import HTMLOListElement from '../nodes/html-o-list-element/HTMLOListElement.js'; +import HTMLObjectElement from '../nodes/html-object-element/HTMLObjectElement.js'; +import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; +import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; +import HTMLOutputElement from '../nodes/html-output-element/HTMLOutputElement.js'; +import HTMLParagraphElement from '../nodes/html-paragraph-element/HTMLParagraphElement.js'; +import HTMLParamElement from '../nodes/html-param-element/HTMLParamElement.js'; +import HTMLPictureElement from '../nodes/html-picture-element/HTMLPictureElement.js'; +import HTMLPreElement from '../nodes/html-pre-element/HTMLPreElement.js'; +import HTMLProgressElement from '../nodes/html-progress-element/HTMLProgressElement.js'; +import HTMLQuoteElement from '../nodes/html-quote-element/HTMLQuoteElement.js'; +import HTMLScriptElementImplementation from '../nodes/html-script-element/HTMLScriptElement.js'; +import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; +import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; +import HTMLSourceElement from '../nodes/html-source-element/HTMLSourceElement.js'; +import HTMLSpanElement from '../nodes/html-span-element/HTMLSpanElement.js'; +import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; +import HTMLTableCaptionElement from '../nodes/html-table-caption-element/HTMLTableCaptionElement.js'; +import HTMLTableCellElement from '../nodes/html-table-cell-element/HTMLTableCellElement.js'; +import HTMLTableColElement from '../nodes/html-table-col-element/HTMLTableColElement.js'; +import HTMLTableElement from '../nodes/html-table-element/HTMLTableElement.js'; +import HTMLTableRowElement from '../nodes/html-table-row-element/HTMLTableRowElement.js'; +import HTMLTableSectionElement from '../nodes/html-table-section-element/HTMLTableSectionElement.js'; +import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; +import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; +import HTMLTimeElement from '../nodes/html-time-element/HTMLTimeElement.js'; +import HTMLTitleElement from '../nodes/html-title-element/HTMLTitleElement.js'; +import HTMLTrackElement from '../nodes/html-track-element/HTMLTrackElement.js'; +import HTMLUListElement from '../nodes/html-u-list-element/HTMLUListElement.js'; +import HTMLUnknownElement from '../nodes/html-unknown-element/HTMLUnknownElement.js'; +import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; +import Node from '../nodes/node/Node.js'; +import NodeList from '../nodes/node/NodeList.js'; +import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; +import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; +import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; +import SVGElement from '../nodes/svg-element/SVGElement.js'; +import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; +import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; +import Text from '../nodes/text/Text.js'; +import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; +import PermissionStatus from '../permissions/PermissionStatus.js'; +import Permissions from '../permissions/Permissions.js'; import RangeImplementation from '../range/Range.js'; -import INodeJSGlobal from './INodeJSGlobal.js'; +import ResizeObserver from '../resize-observer/ResizeObserver.js'; +import Screen from '../screen/Screen.js'; +import Selection from '../selection/Selection.js'; +import Storage from '../storage/Storage.js'; +import StorageFactory from '../storage/StorageFactory.js'; +import NodeFilter from '../tree-walker/NodeFilter.js'; +import NodeIterator from '../tree-walker/NodeIterator.js'; +import TreeWalker from '../tree-walker/TreeWalker.js'; +import URL from '../url/URL.js'; +import ValidityState from '../validity-state/ValidityState.js'; +import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; +import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js'; +import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; +import XMLSerializer from '../xml-serializer/XMLSerializer.js'; import CrossOriginBrowserWindow from './CrossOriginBrowserWindow.js'; -import Response from '../fetch/Response.js'; +import INodeJSGlobal from './INodeJSGlobal.js'; +import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; +import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; +import WindowErrorUtility from './WindowErrorUtility.js'; +import WindowPageOpenUtility from './WindowPageOpenUtility.js'; const TIMER = { setTimeout: globalThis.setTimeout.bind(globalThis), @@ -266,7 +261,6 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public readonly HTMLTimeElement: typeof HTMLTimeElement = HTMLTimeElement; public readonly HTMLTableSectionElement: typeof HTMLTableSectionElement = HTMLTableSectionElement; public readonly HTMLTableCellElement: typeof HTMLTableCellElement = HTMLTableCellElement; - public readonly HTMLTableSectionElement: typeof HTMLTableSectionElement = HTMLTableSectionElement; public readonly HTMLTableElement: typeof HTMLTableElement = HTMLTableElement; public readonly HTMLSpanElement: typeof HTMLSpanElement = HTMLSpanElement; public readonly HTMLSourceElement: typeof HTMLSourceElement = HTMLSourceElement; @@ -294,15 +288,12 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public readonly HTMLDListElement: typeof HTMLDListElement = HTMLDListElement; public readonly HTMLDivElement: typeof HTMLDivElement = HTMLDivElement; public readonly HTMLDetailsElement: typeof HTMLDetailsElement = HTMLDetailsElement; - public readonly HTMLModElement: typeof HTMLModElement = HTMLModElement; public readonly HTMLDataListElement: typeof HTMLDataListElement = HTMLDataListElement; public readonly HTMLDataElement: typeof HTMLDataElement = HTMLDataElement; public readonly HTMLTableColElement: typeof HTMLTableColElement = HTMLTableColElement; - public readonly HTMLTableColElement: typeof HTMLTableColElement = HTMLTableColElement; public readonly HTMLTableCaptionElement: typeof HTMLTableCaptionElement = HTMLTableCaptionElement; public readonly HTMLCanvasElement: typeof HTMLCanvasElement = HTMLCanvasElement; public readonly HTMLBRElement: typeof HTMLBRElement = HTMLBRElement; - public readonly HTMLQuoteElement: typeof HTMLQuoteElement = HTMLQuoteElement; public readonly HTMLBodyElement: typeof HTMLBodyElement = HTMLBodyElement; public readonly HTMLAreaElement: typeof HTMLAreaElement = HTMLAreaElement; diff --git a/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts b/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts index fd4b99811..0d6e1f4d3 100644 --- a/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts +++ b/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts @@ -1,24 +1,22 @@ +import HTMLAreaElement from '../../../src/nodes/html-area-element/HTMLAreaElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLAreaElement from '../../../src/nodes/html-area-element/HTMLAreaElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLAreaElement', () => { - let window: Window; - let document: Document; - let element: HTMLAreaElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('area'); - }); +describe('HTMLAreaElement', () => { + let window: Window; + let document: Document; + let element: HTMLAreaElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLAreaElement', () => { - expect(element instanceof HTMLAreaElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('area'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLAreaElement', () => { + expect(element instanceof HTMLAreaElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts b/packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts index 786c3c931..4ec1b59e9 100644 --- a/packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts +++ b/packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts @@ -1,24 +1,22 @@ +import HTMLBodyElement from '../../../src/nodes/html-body-element/HTMLBodyElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLBodyElement from '../../../src/nodes/html-body-element/HTMLBodyElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLBodyElement', () => { - let window: Window; - let document: Document; - let element: HTMLBodyElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('body'); - }); +describe('HTMLBodyElement', () => { + let window: Window; + let document: Document; + let element: HTMLBodyElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLBodyElement', () => { - expect(element instanceof HTMLBodyElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('body'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLBodyElement', () => { + expect(element instanceof HTMLBodyElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-br-element/HTMLBRElement.test.ts b/packages/happy-dom/test/nodes/html-br-element/HTMLBRElement.test.ts index ba41d28f7..38bc4b062 100644 --- a/packages/happy-dom/test/nodes/html-br-element/HTMLBRElement.test.ts +++ b/packages/happy-dom/test/nodes/html-br-element/HTMLBRElement.test.ts @@ -1,24 +1,22 @@ +import HTMLBRElement from '../../../src/nodes/html-br-element/HTMLBRElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLBRElement from '../../../src/nodes/html-br-element/HTMLBRElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLBRElement', () => { - let window: Window; - let document: Document; - let element: HTMLBRElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('br'); - }); +describe('HTMLBRElement', () => { + let window: Window; + let document: Document; + let element: HTMLBRElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLBRElement', () => { - expect(element instanceof HTMLBRElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('br'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLBRElement', () => { + expect(element instanceof HTMLBRElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts index 199258827..213ce5f91 100644 --- a/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts +++ b/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts @@ -1,24 +1,22 @@ +import HTMLCanvasElement from '../../../src/nodes/html-canvas-element/HTMLCanvasElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLCanvasElement from '../../../src/nodes/html-canvas-element/HTMLCanvasElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLCanvasElement', () => { - let window: Window; - let document: Document; - let element: HTMLCanvasElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('canvas'); - }); +describe('HTMLCanvasElement', () => { + let window: Window; + let document: Document; + let element: HTMLCanvasElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLCanvasElement', () => { - expect(element instanceof HTMLCanvasElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('canvas'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLCanvasElement', () => { + expect(element instanceof HTMLCanvasElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-d-list-element/HTMLDListElement.test.ts b/packages/happy-dom/test/nodes/html-d-list-element/HTMLDListElement.test.ts index 66601e80b..8645889c4 100644 --- a/packages/happy-dom/test/nodes/html-d-list-element/HTMLDListElement.test.ts +++ b/packages/happy-dom/test/nodes/html-d-list-element/HTMLDListElement.test.ts @@ -1,24 +1,22 @@ +import HTMLDListElement from '../../../src/nodes/html-d-list-element/HTMLDListElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLDListElement from '../../../src/nodes/html-d-list-element/HTMLDListElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLDListElement', () => { - let window: Window; - let document: Document; - let element: HTMLDListElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('dl'); - }); +describe('HTMLDListElement', () => { + let window: Window; + let document: Document; + let element: HTMLDListElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLDListElement', () => { - expect(element instanceof HTMLDListElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('dl'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDListElement', () => { + expect(element instanceof HTMLDListElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts b/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts index 9564452a1..926f367e4 100644 --- a/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts +++ b/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts @@ -1,24 +1,22 @@ +import HTMLDataElement from '../../../src/nodes/html-data-element/HTMLDataElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLDataElement from '../../../src/nodes/html-data-element/HTMLDataElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLDataElement', () => { - let window: Window; - let document: Document; - let element: HTMLDataElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('data'); - }); +describe('HTMLDataElement', () => { + let window: Window; + let document: Document; + let element: HTMLDataElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLDataElement', () => { - expect(element instanceof HTMLDataElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('data'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDataElement', () => { + expect(element instanceof HTMLDataElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts b/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts index f1d925242..42881f210 100644 --- a/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts +++ b/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts @@ -1,24 +1,22 @@ +import HTMLDataListElement from '../../../src/nodes/html-data-list-element/HTMLDataListElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLDataListElement from '../../../src/nodes/html-data-list-element/HTMLDataListElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLDataListElement', () => { - let window: Window; - let document: Document; - let element: HTMLDataListElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('datalist'); - }); +describe('HTMLDataListElement', () => { + let window: Window; + let document: Document; + let element: HTMLDataListElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLDataListElement', () => { - expect(element instanceof HTMLDataListElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('datalist'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDataListElement', () => { + expect(element instanceof HTMLDataListElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts b/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts index 5e1533683..2124697e5 100644 --- a/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts +++ b/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts @@ -1,24 +1,22 @@ +import HTMLDetailsElement from '../../../src/nodes/html-details-element/HTMLDetailsElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLDetailsElement from '../../../src/nodes/html-details-element/HTMLDetailsElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLDetailsElement', () => { - let window: Window; - let document: Document; - let element: HTMLDetailsElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('details'); - }); +describe('HTMLDetailsElement', () => { + let window: Window; + let document: Document; + let element: HTMLDetailsElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLDetailsElement', () => { - expect(element instanceof HTMLDetailsElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('details'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDetailsElement', () => { + expect(element instanceof HTMLDetailsElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-div-element/HTMLDivElement.test.ts b/packages/happy-dom/test/nodes/html-div-element/HTMLDivElement.test.ts index 52abd5248..8371a64a3 100644 --- a/packages/happy-dom/test/nodes/html-div-element/HTMLDivElement.test.ts +++ b/packages/happy-dom/test/nodes/html-div-element/HTMLDivElement.test.ts @@ -1,24 +1,22 @@ +import HTMLDivElement from '../../../src/nodes/html-div-element/HTMLDivElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLDivElement from '../../../src/nodes/html-div-element/HTMLDivElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLDivElement', () => { - let window: Window; - let document: Document; - let element: HTMLDivElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('div'); - }); +describe('HTMLDivElement', () => { + let window: Window; + let document: Document; + let element: HTMLDivElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLDivElement', () => { - expect(element instanceof HTMLDivElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('div'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLDivElement', () => { + expect(element instanceof HTMLDivElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts b/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts index 817bb65f2..fe6677a67 100644 --- a/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts +++ b/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts @@ -1,24 +1,22 @@ +import HTMLEmbedElement from '../../../src/nodes/html-embed-element/HTMLEmbedElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLEmbedElement from '../../../src/nodes/html-embed-element/HTMLEmbedElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLEmbedElement', () => { - let window: Window; - let document: Document; - let element: HTMLEmbedElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('embed'); - }); +describe('HTMLEmbedElement', () => { + let window: Window; + let document: Document; + let element: HTMLEmbedElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLEmbedElement', () => { - expect(element instanceof HTMLEmbedElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('embed'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLEmbedElement', () => { + expect(element instanceof HTMLEmbedElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts index e9aae2a80..9ae355ecc 100644 --- a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts +++ b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts @@ -1,24 +1,22 @@ +import HTMLFieldSetElement from '../../../src/nodes/html-field-set-element/HTMLFieldSetElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLFieldSetElement from '../../../src/nodes/html-field-set-element/HTMLFieldSetElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLFieldSetElement', () => { - let window: Window; - let document: Document; - let element: HTMLFieldSetElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('fieldset'); - }); +describe('HTMLFieldSetElement', () => { + let window: Window; + let document: Document; + let element: HTMLFieldSetElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLFieldSetElement', () => { - expect(element instanceof HTMLFieldSetElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('fieldset'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLFieldSetElement', () => { + expect(element instanceof HTMLFieldSetElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-head-element/HTMLHeadElement.test.ts b/packages/happy-dom/test/nodes/html-head-element/HTMLHeadElement.test.ts index 7ae7ff6f0..a01a4d6be 100644 --- a/packages/happy-dom/test/nodes/html-head-element/HTMLHeadElement.test.ts +++ b/packages/happy-dom/test/nodes/html-head-element/HTMLHeadElement.test.ts @@ -1,24 +1,22 @@ +import HTMLHeadElement from '../../../src/nodes/html-head-element/HTMLHeadElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLHeadElement from '../../../src/nodes/html-head-element/HTMLHeadElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLHeadElement', () => { - let window: Window; - let document: Document; - let element: HTMLHeadElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('head'); - }); +describe('HTMLHeadElement', () => { + let window: Window; + let document: Document; + let element: HTMLHeadElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLHeadElement', () => { - expect(element instanceof HTMLHeadElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('head'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLHeadElement', () => { + expect(element instanceof HTMLHeadElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-heading-element/HTMLHeadingElement.test.ts b/packages/happy-dom/test/nodes/html-heading-element/HTMLHeadingElement.test.ts index 4dfe2d09d..0c34c1d3d 100644 --- a/packages/happy-dom/test/nodes/html-heading-element/HTMLHeadingElement.test.ts +++ b/packages/happy-dom/test/nodes/html-heading-element/HTMLHeadingElement.test.ts @@ -1,24 +1,22 @@ +import HTMLHeadingElement from '../../../src/nodes/html-heading-element/HTMLHeadingElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLHeadingElement from '../../../src/nodes/html-heading-element/HTMLHeadingElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLHeadingElement', () => { - let window: Window; - let document: Document; - let element: HTMLHeadingElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('h6'); - }); +describe('HTMLHeadingElement', () => { + let window: Window; + let document: Document; + let element: HTMLHeadingElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLHeadingElement', () => { - expect(element instanceof HTMLHeadingElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('h6'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLHeadingElement', () => { + expect(element instanceof HTMLHeadingElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-hr-element/HTMLHRElement.test.ts b/packages/happy-dom/test/nodes/html-hr-element/HTMLHRElement.test.ts index 0e08d24ec..6a2c590cb 100644 --- a/packages/happy-dom/test/nodes/html-hr-element/HTMLHRElement.test.ts +++ b/packages/happy-dom/test/nodes/html-hr-element/HTMLHRElement.test.ts @@ -1,24 +1,22 @@ +import HTMLHRElement from '../../../src/nodes/html-hr-element/HTMLHRElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLHRElement from '../../../src/nodes/html-hr-element/HTMLHRElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLHRElement', () => { - let window: Window; - let document: Document; - let element: HTMLHRElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('hr'); - }); +describe('HTMLHRElement', () => { + let window: Window; + let document: Document; + let element: HTMLHRElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLHRElement', () => { - expect(element instanceof HTMLHRElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('hr'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLHRElement', () => { + expect(element instanceof HTMLHRElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-html-element/HTMLHtmlElement.test.ts b/packages/happy-dom/test/nodes/html-html-element/HTMLHtmlElement.test.ts index 83443a7eb..11c650536 100644 --- a/packages/happy-dom/test/nodes/html-html-element/HTMLHtmlElement.test.ts +++ b/packages/happy-dom/test/nodes/html-html-element/HTMLHtmlElement.test.ts @@ -1,24 +1,22 @@ +import HTMLHtmlElement from '../../../src/nodes/html-html-element/HTMLHtmlElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLHtmlElement from '../../../src/nodes/html-html-element/HTMLHtmlElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLHtmlElement', () => { - let window: Window; - let document: Document; - let element: HTMLHtmlElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('html'); - }); +describe('HTMLHtmlElement', () => { + let window: Window; + let document: Document; + let element: HTMLHtmlElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLHtmlElement', () => { - expect(element instanceof HTMLHtmlElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('html'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLHtmlElement', () => { + expect(element instanceof HTMLHtmlElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-legend-element/HTMLLegendElement.test.ts b/packages/happy-dom/test/nodes/html-legend-element/HTMLLegendElement.test.ts index fa71c12f1..68e0ea8b1 100644 --- a/packages/happy-dom/test/nodes/html-legend-element/HTMLLegendElement.test.ts +++ b/packages/happy-dom/test/nodes/html-legend-element/HTMLLegendElement.test.ts @@ -1,24 +1,22 @@ +import HTMLLegendElement from '../../../src/nodes/html-legend-element/HTMLLegendElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLLegendElement from '../../../src/nodes/html-legend-element/HTMLLegendElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLLegendElement', () => { - let window: Window; - let document: Document; - let element: HTMLLegendElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('legend'); - }); +describe('HTMLLegendElement', () => { + let window: Window; + let document: Document; + let element: HTMLLegendElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLLegendElement', () => { - expect(element instanceof HTMLLegendElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('legend'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLLegendElement', () => { + expect(element instanceof HTMLLegendElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-li-element/HTMLLIElement.test.ts b/packages/happy-dom/test/nodes/html-li-element/HTMLLIElement.test.ts index 0ea6c8b0b..a4925eaa7 100644 --- a/packages/happy-dom/test/nodes/html-li-element/HTMLLIElement.test.ts +++ b/packages/happy-dom/test/nodes/html-li-element/HTMLLIElement.test.ts @@ -1,24 +1,22 @@ +import HTMLLIElement from '../../../src/nodes/html-li-element/HTMLLIElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLLIElement from '../../../src/nodes/html-li-element/HTMLLIElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLLIElement', () => { - let window: Window; - let document: Document; - let element: HTMLLIElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('li'); - }); +describe('HTMLLIElement', () => { + let window: Window; + let document: Document; + let element: HTMLLIElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLLIElement', () => { - expect(element instanceof HTMLLIElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('li'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLLIElement', () => { + expect(element instanceof HTMLLIElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-map-element/HTMLMapElement.test.ts b/packages/happy-dom/test/nodes/html-map-element/HTMLMapElement.test.ts index 674d23128..87af84946 100644 --- a/packages/happy-dom/test/nodes/html-map-element/HTMLMapElement.test.ts +++ b/packages/happy-dom/test/nodes/html-map-element/HTMLMapElement.test.ts @@ -1,24 +1,22 @@ +import HTMLMapElement from '../../../src/nodes/html-map-element/HTMLMapElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLMapElement from '../../../src/nodes/html-map-element/HTMLMapElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLMapElement', () => { - let window: Window; - let document: Document; - let element: HTMLMapElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('map'); - }); +describe('HTMLMapElement', () => { + let window: Window; + let document: Document; + let element: HTMLMapElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLMapElement', () => { - expect(element instanceof HTMLMapElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('map'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLMapElement', () => { + expect(element instanceof HTMLMapElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-menu-element/HTMLMenuElement.test.ts b/packages/happy-dom/test/nodes/html-menu-element/HTMLMenuElement.test.ts index 525f44492..28db6781e 100644 --- a/packages/happy-dom/test/nodes/html-menu-element/HTMLMenuElement.test.ts +++ b/packages/happy-dom/test/nodes/html-menu-element/HTMLMenuElement.test.ts @@ -1,24 +1,22 @@ +import HTMLMenuElement from '../../../src/nodes/html-menu-element/HTMLMenuElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLMenuElement from '../../../src/nodes/html-menu-element/HTMLMenuElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLMenuElement', () => { - let window: Window; - let document: Document; - let element: HTMLMenuElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('menu'); - }); +describe('HTMLMenuElement', () => { + let window: Window; + let document: Document; + let element: HTMLMenuElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLMenuElement', () => { - expect(element instanceof HTMLMenuElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('menu'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLMenuElement', () => { + expect(element instanceof HTMLMenuElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-meter-element/HTMLMeterElement.test.ts b/packages/happy-dom/test/nodes/html-meter-element/HTMLMeterElement.test.ts index 28f8f5148..b58c4a21c 100644 --- a/packages/happy-dom/test/nodes/html-meter-element/HTMLMeterElement.test.ts +++ b/packages/happy-dom/test/nodes/html-meter-element/HTMLMeterElement.test.ts @@ -1,24 +1,22 @@ +import HTMLMeterElement from '../../../src/nodes/html-meter-element/HTMLMeterElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLMeterElement from '../../../src/nodes/html-meter-element/HTMLMeterElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLMeterElement', () => { - let window: Window; - let document: Document; - let element: HTMLMeterElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('meter'); - }); +describe('HTMLMeterElement', () => { + let window: Window; + let document: Document; + let element: HTMLMeterElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLMeterElement', () => { - expect(element instanceof HTMLMeterElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('meter'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLMeterElement', () => { + expect(element instanceof HTMLMeterElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-mod-element/HTMLModElement.test.ts b/packages/happy-dom/test/nodes/html-mod-element/HTMLModElement.test.ts index 517eb7893..a8faebcb3 100644 --- a/packages/happy-dom/test/nodes/html-mod-element/HTMLModElement.test.ts +++ b/packages/happy-dom/test/nodes/html-mod-element/HTMLModElement.test.ts @@ -1,24 +1,22 @@ +import HTMLModElement from '../../../src/nodes/html-mod-element/HTMLModElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLModElement from '../../../src/nodes/html-mod-element/HTMLModElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLModElement', () => { - let window: Window; - let document: Document; - let element: HTMLModElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('ins'); - }); +describe('HTMLModElement', () => { + let window: Window; + let document: Document; + let element: HTMLModElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLModElement', () => { - expect(element instanceof HTMLModElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('ins'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLModElement', () => { + expect(element instanceof HTMLModElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-o-list-element/HTMLOListElement.test.ts b/packages/happy-dom/test/nodes/html-o-list-element/HTMLOListElement.test.ts index 576fb524c..1af37ee63 100644 --- a/packages/happy-dom/test/nodes/html-o-list-element/HTMLOListElement.test.ts +++ b/packages/happy-dom/test/nodes/html-o-list-element/HTMLOListElement.test.ts @@ -1,24 +1,22 @@ +import HTMLOListElement from '../../../src/nodes/html-o-list-element/HTMLOListElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLOListElement from '../../../src/nodes/html-o-list-element/HTMLOListElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLOListElement', () => { - let window: Window; - let document: Document; - let element: HTMLOListElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('ol'); - }); +describe('HTMLOListElement', () => { + let window: Window; + let document: Document; + let element: HTMLOListElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLOListElement', () => { - expect(element instanceof HTMLOListElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('ol'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLOListElement', () => { + expect(element instanceof HTMLOListElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-object-element/HTMLObjectElement.test.ts b/packages/happy-dom/test/nodes/html-object-element/HTMLObjectElement.test.ts index 7c68caedb..6f55fd8a1 100644 --- a/packages/happy-dom/test/nodes/html-object-element/HTMLObjectElement.test.ts +++ b/packages/happy-dom/test/nodes/html-object-element/HTMLObjectElement.test.ts @@ -1,24 +1,22 @@ +import HTMLObjectElement from '../../../src/nodes/html-object-element/HTMLObjectElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLObjectElement from '../../../src/nodes/html-object-element/HTMLObjectElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLObjectElement', () => { - let window: Window; - let document: Document; - let element: HTMLObjectElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('object'); - }); +describe('HTMLObjectElement', () => { + let window: Window; + let document: Document; + let element: HTMLObjectElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLObjectElement', () => { - expect(element instanceof HTMLObjectElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('object'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLObjectElement', () => { + expect(element instanceof HTMLObjectElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-output-element/HTMLOutputElement.test.ts b/packages/happy-dom/test/nodes/html-output-element/HTMLOutputElement.test.ts index 135c78491..716f44566 100644 --- a/packages/happy-dom/test/nodes/html-output-element/HTMLOutputElement.test.ts +++ b/packages/happy-dom/test/nodes/html-output-element/HTMLOutputElement.test.ts @@ -1,24 +1,22 @@ +import HTMLOutputElement from '../../../src/nodes/html-output-element/HTMLOutputElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLOutputElement from '../../../src/nodes/html-output-element/HTMLOutputElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLOutputElement', () => { - let window: Window; - let document: Document; - let element: HTMLOutputElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('output'); - }); +describe('HTMLOutputElement', () => { + let window: Window; + let document: Document; + let element: HTMLOutputElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLOutputElement', () => { - expect(element instanceof HTMLOutputElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('output'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLOutputElement', () => { + expect(element instanceof HTMLOutputElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-paragraph-element/HTMLParagraphElement.test.ts b/packages/happy-dom/test/nodes/html-paragraph-element/HTMLParagraphElement.test.ts index a115a9734..f04a93dff 100644 --- a/packages/happy-dom/test/nodes/html-paragraph-element/HTMLParagraphElement.test.ts +++ b/packages/happy-dom/test/nodes/html-paragraph-element/HTMLParagraphElement.test.ts @@ -1,24 +1,22 @@ +import HTMLParagraphElement from '../../../src/nodes/html-paragraph-element/HTMLParagraphElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLParagraphElement from '../../../src/nodes/html-paragraph-element/HTMLParagraphElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLParagraphElement', () => { - let window: Window; - let document: Document; - let element: HTMLParagraphElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('p'); - }); +describe('HTMLParagraphElement', () => { + let window: Window; + let document: Document; + let element: HTMLParagraphElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLParagraphElement', () => { - expect(element instanceof HTMLParagraphElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('p'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLParagraphElement', () => { + expect(element instanceof HTMLParagraphElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-param-element/HTMLParamElement.test.ts b/packages/happy-dom/test/nodes/html-param-element/HTMLParamElement.test.ts index cc756f061..07b7ff5ff 100644 --- a/packages/happy-dom/test/nodes/html-param-element/HTMLParamElement.test.ts +++ b/packages/happy-dom/test/nodes/html-param-element/HTMLParamElement.test.ts @@ -1,24 +1,22 @@ +import HTMLParamElement from '../../../src/nodes/html-param-element/HTMLParamElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLParamElement from '../../../src/nodes/html-param-element/HTMLParamElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLParamElement', () => { - let window: Window; - let document: Document; - let element: HTMLParamElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('param'); - }); +describe('HTMLParamElement', () => { + let window: Window; + let document: Document; + let element: HTMLParamElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLParamElement', () => { - expect(element instanceof HTMLParamElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('param'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLParamElement', () => { + expect(element instanceof HTMLParamElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-picture-element/HTMLPictureElement.test.ts b/packages/happy-dom/test/nodes/html-picture-element/HTMLPictureElement.test.ts index 648191483..244230088 100644 --- a/packages/happy-dom/test/nodes/html-picture-element/HTMLPictureElement.test.ts +++ b/packages/happy-dom/test/nodes/html-picture-element/HTMLPictureElement.test.ts @@ -1,24 +1,22 @@ +import HTMLPictureElement from '../../../src/nodes/html-picture-element/HTMLPictureElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLPictureElement from '../../../src/nodes/html-picture-element/HTMLPictureElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLPictureElement', () => { - let window: Window; - let document: Document; - let element: HTMLPictureElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('picture'); - }); +describe('HTMLPictureElement', () => { + let window: Window; + let document: Document; + let element: HTMLPictureElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLPictureElement', () => { - expect(element instanceof HTMLPictureElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('picture'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLPictureElement', () => { + expect(element instanceof HTMLPictureElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-pre-element/HTMLPreElement.test.ts b/packages/happy-dom/test/nodes/html-pre-element/HTMLPreElement.test.ts index d6ed2643e..78566fa3c 100644 --- a/packages/happy-dom/test/nodes/html-pre-element/HTMLPreElement.test.ts +++ b/packages/happy-dom/test/nodes/html-pre-element/HTMLPreElement.test.ts @@ -1,24 +1,22 @@ +import HTMLPreElement from '../../../src/nodes/html-pre-element/HTMLPreElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLPreElement from '../../../src/nodes/html-pre-element/HTMLPreElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLPreElement', () => { - let window: Window; - let document: Document; - let element: HTMLPreElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('pre'); - }); +describe('HTMLPreElement', () => { + let window: Window; + let document: Document; + let element: HTMLPreElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLPreElement', () => { - expect(element instanceof HTMLPreElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('pre'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLPreElement', () => { + expect(element instanceof HTMLPreElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-progress-element/HTMLProgressElement.test.ts b/packages/happy-dom/test/nodes/html-progress-element/HTMLProgressElement.test.ts index 7fbbc0fea..fd2a1a86c 100644 --- a/packages/happy-dom/test/nodes/html-progress-element/HTMLProgressElement.test.ts +++ b/packages/happy-dom/test/nodes/html-progress-element/HTMLProgressElement.test.ts @@ -1,24 +1,22 @@ +import HTMLProgressElement from '../../../src/nodes/html-progress-element/HTMLProgressElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLProgressElement from '../../../src/nodes/html-progress-element/HTMLProgressElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLProgressElement', () => { - let window: Window; - let document: Document; - let element: HTMLProgressElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('progress'); - }); +describe('HTMLProgressElement', () => { + let window: Window; + let document: Document; + let element: HTMLProgressElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLProgressElement', () => { - expect(element instanceof HTMLProgressElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('progress'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLProgressElement', () => { + expect(element instanceof HTMLProgressElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-quote-element/HTMLQuoteElement.test.ts b/packages/happy-dom/test/nodes/html-quote-element/HTMLQuoteElement.test.ts index 8ec62c8f2..ee4dd35fc 100644 --- a/packages/happy-dom/test/nodes/html-quote-element/HTMLQuoteElement.test.ts +++ b/packages/happy-dom/test/nodes/html-quote-element/HTMLQuoteElement.test.ts @@ -1,24 +1,22 @@ +import HTMLQuoteElement from '../../../src/nodes/html-quote-element/HTMLQuoteElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLQuoteElement from '../../../src/nodes/html-quote-element/HTMLQuoteElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLQuoteElement', () => { - let window: Window; - let document: Document; - let element: HTMLQuoteElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('q'); - }); +describe('HTMLQuoteElement', () => { + let window: Window; + let document: Document; + let element: HTMLQuoteElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLQuoteElement', () => { - expect(element instanceof HTMLQuoteElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('q'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLQuoteElement', () => { + expect(element instanceof HTMLQuoteElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-source-element/HTMLSourceElement.test.ts b/packages/happy-dom/test/nodes/html-source-element/HTMLSourceElement.test.ts index a35787fb2..49975167f 100644 --- a/packages/happy-dom/test/nodes/html-source-element/HTMLSourceElement.test.ts +++ b/packages/happy-dom/test/nodes/html-source-element/HTMLSourceElement.test.ts @@ -1,24 +1,22 @@ +import HTMLSourceElement from '../../../src/nodes/html-source-element/HTMLSourceElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLSourceElement from '../../../src/nodes/html-source-element/HTMLSourceElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLSourceElement', () => { - let window: Window; - let document: Document; - let element: HTMLSourceElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('source'); - }); +describe('HTMLSourceElement', () => { + let window: Window; + let document: Document; + let element: HTMLSourceElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLSourceElement', () => { - expect(element instanceof HTMLSourceElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('source'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLSourceElement', () => { + expect(element instanceof HTMLSourceElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-span-element/HTMLSpanElement.test.ts b/packages/happy-dom/test/nodes/html-span-element/HTMLSpanElement.test.ts index f9b24568b..ecdd5dd50 100644 --- a/packages/happy-dom/test/nodes/html-span-element/HTMLSpanElement.test.ts +++ b/packages/happy-dom/test/nodes/html-span-element/HTMLSpanElement.test.ts @@ -1,24 +1,22 @@ +import HTMLSpanElement from '../../../src/nodes/html-span-element/HTMLSpanElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLSpanElement from '../../../src/nodes/html-span-element/HTMLSpanElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLSpanElement', () => { - let window: Window; - let document: Document; - let element: HTMLSpanElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('span'); - }); +describe('HTMLSpanElement', () => { + let window: Window; + let document: Document; + let element: HTMLSpanElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLSpanElement', () => { - expect(element instanceof HTMLSpanElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('span'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLSpanElement', () => { + expect(element instanceof HTMLSpanElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-table-caption-element/HTMLTableCaptionElement.test.ts b/packages/happy-dom/test/nodes/html-table-caption-element/HTMLTableCaptionElement.test.ts index 378f1e915..b97191214 100644 --- a/packages/happy-dom/test/nodes/html-table-caption-element/HTMLTableCaptionElement.test.ts +++ b/packages/happy-dom/test/nodes/html-table-caption-element/HTMLTableCaptionElement.test.ts @@ -1,24 +1,22 @@ +import HTMLTableCaptionElement from '../../../src/nodes/html-table-caption-element/HTMLTableCaptionElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLTableCaptionElement from '../../../src/nodes/html-table-caption-element/HTMLTableCaptionElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLTableCaptionElement', () => { - let window: Window; - let document: Document; - let element: HTMLTableCaptionElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('caption'); - }); +describe('HTMLTableCaptionElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableCaptionElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLTableCaptionElement', () => { - expect(element instanceof HTMLTableCaptionElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('caption'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableCaptionElement', () => { + expect(element instanceof HTMLTableCaptionElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-table-col-element/HTMLTableColElement.test.ts b/packages/happy-dom/test/nodes/html-table-col-element/HTMLTableColElement.test.ts index 55378df9d..98862a0a5 100644 --- a/packages/happy-dom/test/nodes/html-table-col-element/HTMLTableColElement.test.ts +++ b/packages/happy-dom/test/nodes/html-table-col-element/HTMLTableColElement.test.ts @@ -1,24 +1,22 @@ +import HTMLTableColElement from '../../../src/nodes/html-table-col-element/HTMLTableColElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLTableColElement from '../../../src/nodes/html-table-col-element/HTMLTableColElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLTableColElement', () => { - let window: Window; - let document: Document; - let element: HTMLTableColElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('colgroup'); - }); +describe('HTMLTableColElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableColElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLTableColElement', () => { - expect(element instanceof HTMLTableColElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('colgroup'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableColElement', () => { + expect(element instanceof HTMLTableColElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-table-element/HTMLTableElement.test.ts b/packages/happy-dom/test/nodes/html-table-element/HTMLTableElement.test.ts index 41df97468..0fc8f7775 100644 --- a/packages/happy-dom/test/nodes/html-table-element/HTMLTableElement.test.ts +++ b/packages/happy-dom/test/nodes/html-table-element/HTMLTableElement.test.ts @@ -1,24 +1,22 @@ +import HTMLTableElement from '../../../src/nodes/html-table-element/HTMLTableElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLTableElement from '../../../src/nodes/html-table-element/HTMLTableElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLTableElement', () => { - let window: Window; - let document: Document; - let element: HTMLTableElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('table'); - }); +describe('HTMLTableElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLTableElement', () => { - expect(element instanceof HTMLTableElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('table'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableElement', () => { + expect(element instanceof HTMLTableElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-table-row-element/HTMLTableRowElement.test.ts b/packages/happy-dom/test/nodes/html-table-row-element/HTMLTableRowElement.test.ts index 1ea93956c..e0ad01f4f 100644 --- a/packages/happy-dom/test/nodes/html-table-row-element/HTMLTableRowElement.test.ts +++ b/packages/happy-dom/test/nodes/html-table-row-element/HTMLTableRowElement.test.ts @@ -1,24 +1,22 @@ +import HTMLTableRowElement from '../../../src/nodes/html-table-row-element/HTMLTableRowElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLTableRowElement from '../../../src/nodes/html-table-row-element/HTMLTableRowElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLTableRowElement', () => { - let window: Window; - let document: Document; - let element: HTMLTableRowElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('tr'); - }); +describe('HTMLTableRowElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableRowElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLTableRowElement', () => { - expect(element instanceof HTMLTableRowElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('tr'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableRowElement', () => { + expect(element instanceof HTMLTableRowElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-table-section-element/HTMLTableSectionElement.test.ts b/packages/happy-dom/test/nodes/html-table-section-element/HTMLTableSectionElement.test.ts index cbc23c960..7e767cc49 100644 --- a/packages/happy-dom/test/nodes/html-table-section-element/HTMLTableSectionElement.test.ts +++ b/packages/happy-dom/test/nodes/html-table-section-element/HTMLTableSectionElement.test.ts @@ -1,24 +1,22 @@ +import HTMLTableSectionElement from '../../../src/nodes/html-table-section-element/HTMLTableSectionElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLTableSectionElement from '../../../src/nodes/html-table-section-element/HTMLTableSectionElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLTableSectionElement', () => { - let window: Window; - let document: Document; - let element: HTMLTableSectionElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('thead'); - }); +describe('HTMLTableSectionElement', () => { + let window: Window; + let document: Document; + let element: HTMLTableSectionElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLTableSectionElement', () => { - expect(element instanceof HTMLTableSectionElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('thead'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTableSectionElement', () => { + expect(element instanceof HTMLTableSectionElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-time-element/HTMLTimeElement.test.ts b/packages/happy-dom/test/nodes/html-time-element/HTMLTimeElement.test.ts index 04280dfad..f9874e7ee 100644 --- a/packages/happy-dom/test/nodes/html-time-element/HTMLTimeElement.test.ts +++ b/packages/happy-dom/test/nodes/html-time-element/HTMLTimeElement.test.ts @@ -1,24 +1,22 @@ +import HTMLTimeElement from '../../../src/nodes/html-time-element/HTMLTimeElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLTimeElement from '../../../src/nodes/html-time-element/HTMLTimeElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLTimeElement', () => { - let window: Window; - let document: Document; - let element: HTMLTimeElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('time'); - }); +describe('HTMLTimeElement', () => { + let window: Window; + let document: Document; + let element: HTMLTimeElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLTimeElement', () => { - expect(element instanceof HTMLTimeElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('time'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTimeElement', () => { + expect(element instanceof HTMLTimeElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-title-element/HTMLTitleElement.test.ts b/packages/happy-dom/test/nodes/html-title-element/HTMLTitleElement.test.ts index 90ab7ea83..416104fff 100644 --- a/packages/happy-dom/test/nodes/html-title-element/HTMLTitleElement.test.ts +++ b/packages/happy-dom/test/nodes/html-title-element/HTMLTitleElement.test.ts @@ -1,24 +1,22 @@ +import HTMLTitleElement from '../../../src/nodes/html-title-element/HTMLTitleElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLTitleElement from '../../../src/nodes/html-title-element/HTMLTitleElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLTitleElement', () => { - let window: Window; - let document: Document; - let element: HTMLTitleElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('title'); - }); +describe('HTMLTitleElement', () => { + let window: Window; + let document: Document; + let element: HTMLTitleElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLTitleElement', () => { - expect(element instanceof HTMLTitleElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('title'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTitleElement', () => { + expect(element instanceof HTMLTitleElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts b/packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts index 2bf3d2bf9..c8557a155 100644 --- a/packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts +++ b/packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts @@ -1,24 +1,22 @@ +import HTMLTrackElement from '../../../src/nodes/html-track-element/HTMLTrackElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLTrackElement from '../../../src/nodes/html-track-element/HTMLTrackElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLTrackElement', () => { - let window: Window; - let document: Document; - let element: HTMLTrackElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('track'); - }); +describe('HTMLTrackElement', () => { + let window: Window; + let document: Document; + let element: HTMLTrackElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLTrackElement', () => { - expect(element instanceof HTMLTrackElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('track'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLTrackElement', () => { + expect(element instanceof HTMLTrackElement).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-u-list-element/HTMLUListElement.test.ts b/packages/happy-dom/test/nodes/html-u-list-element/HTMLUListElement.test.ts index 402fd2820..329a5aeba 100644 --- a/packages/happy-dom/test/nodes/html-u-list-element/HTMLUListElement.test.ts +++ b/packages/happy-dom/test/nodes/html-u-list-element/HTMLUListElement.test.ts @@ -1,24 +1,22 @@ +import HTMLUListElement from '../../../src/nodes/html-u-list-element/HTMLUListElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; - import HTMLUListElement from '../../../src/nodes/html-u-list-element/HTMLUListElement.js'; - import Window from '../../../src/window/Window.js'; - import Document from '../../../src/nodes/document/Document.js'; - import { beforeEach, describe, it, expect } from 'vitest'; - - describe('HTMLUListElement', () => { - let window: Window; - let document: Document; - let element: HTMLUListElement; - - beforeEach(() => { - window = new Window(); - document = window.document; - element = document.createElement('ul'); - }); +describe('HTMLUListElement', () => { + let window: Window; + let document: Document; + let element: HTMLUListElement; - describe('constructor()', () => { - it('Should be an instanceof HTMLUListElement', () => { - expect(element instanceof HTMLUListElement).toBe(true); - }); - }); - }); - \ No newline at end of file + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('ul'); + }); + + describe('constructor()', () => { + it('Should be an instanceof HTMLUListElement', () => { + expect(element instanceof HTMLUListElement).toBe(true); + }); + }); +}); From c4c7fd348a7b373b6e4bdd5d4bfb2ee8e20b21ee Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 26 Mar 2024 00:58:51 +0100 Subject: [PATCH 03/51] chore: [#1332] Adds support for HTMLAreaElement --- packages/happy-dom/src/index.ts | 2 + .../html-anchor-element/HTMLAnchorElement.ts | 171 +----- .../html-area-element/HTMLAreaElement.ts | 398 ++++++++++++- .../HTMLHyperlinkElementNamedNodeMap.ts} | 7 +- .../HTMLHyperlinkElementUtility.ts | 329 +++++++++++ .../IHTMLHyperlinkElement.ts} | 2 +- .../HTMLAnchorElement.test.ts | 70 +-- .../html-area-element/HTMLAreaElement.test.ts | 521 +++++++++++++++++- 8 files changed, 1312 insertions(+), 188 deletions(-) rename packages/happy-dom/src/nodes/{html-anchor-element/HTMLAnchorElementNamedNodeMap.ts => html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts} (77%) create mode 100644 packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementUtility.ts rename packages/happy-dom/src/nodes/{html-anchor-element/IHTMLHyperlinkElementUtils.ts => html-hyperlink-element/IHTMLHyperlinkElement.ts} (86%) diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index 9b1d4c28a..ade3c8943 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -45,6 +45,7 @@ import InputEvent from './event/events/InputEvent.js'; import KeyboardEvent from './event/events/KeyboardEvent.js'; import MediaQueryListEvent from './event/events/MediaQueryListEvent.js'; import MouseEvent from './event/events/MouseEvent.js'; +import PointerEvent from './event/events/PointerEvent.js'; import ProgressEvent from './event/events/ProgressEvent.js'; import SubmitEvent from './event/events/SubmitEvent.js'; import TouchEvent from './event/events/TouchEvent.js'; @@ -357,6 +358,7 @@ export { NodeIterator, PermissionStatus, Permissions, + PointerEvent, ProcessingInstruction, ProgressEvent, Range, diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index aed0a7c18..5c5813888 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -1,13 +1,13 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; -import IHTMLHyperlinkElementUtils from './IHTMLHyperlinkElementUtils.js'; -import URL from '../../url/URL.js'; import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLAnchorElementNamedNodeMap from './HTMLAnchorElementNamedNodeMap.js'; +import HTMLHyperlinkElementNamedNodeMap from '../html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.js'; import Event from '../../event/Event.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import PointerEvent from '../../event/events/PointerEvent.js'; +import HTMLHyperlinkElementUtility from '../html-hyperlink-element/HTMLHyperlinkElementUtility.js'; +import IHTMLHyperlinkElement from '../html-hyperlink-element/IHTMLHyperlinkElement.js'; /** * HTML Anchor Element. @@ -15,11 +15,12 @@ import PointerEvent from '../../event/events/PointerEvent.js'; * Reference: * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement. */ -export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyperlinkElementUtils { - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLAnchorElementNamedNodeMap( +export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyperlinkElement { + public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLHyperlinkElementNamedNodeMap( this ); public [PropertySymbol.relList]: DOMTokenList = null; + #htmlHyperlinkElementUtility = new HTMLHyperlinkElementUtility(this); /** * Returns download. @@ -45,17 +46,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Hash. */ public get hash(): string { - const href = this.getAttribute('href'); - if (href.startsWith('#')) { - return href; - } - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return ''; - } - return url.hash; + return this.#htmlHyperlinkElementUtility.getHash(); } /** @@ -64,14 +55,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param hash Hash. */ public set hash(hash: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.hash = hash; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setHash(hash); } /** @@ -80,16 +64,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Href. */ public get href(): string { - if (!this.hasAttribute('href')) { - return ''; - } - - try { - return new URL(this.getAttribute('href'), this[PropertySymbol.ownerDocument].location.href) - .href; - } catch (e) { - return this.getAttribute('href'); - } + return this.#htmlHyperlinkElementUtility.getHref(); } /** @@ -98,7 +73,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param href Href. */ public set href(href: string) { - this.setAttribute('href', href); + this.#htmlHyperlinkElementUtility.setHref(href); } /** @@ -125,11 +100,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Origin. */ public get origin(): string { - try { - return new URL(this.href).origin; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getOrigin(); } /** @@ -156,11 +127,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Protocol. */ public get protocol(): string { - try { - return new URL(this.href).protocol; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getProtocol(); } /** @@ -169,14 +136,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param protocol Protocol. */ public set protocol(protocol: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.protocol = protocol; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setProtocol(protocol); } /** @@ -185,11 +145,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Username. */ public get username(): string { - try { - return new URL(this.href).username; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getUsername(); } /** @@ -198,14 +154,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param username Username. */ public set username(username: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.username = username; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setUsername(username); } /** @@ -214,11 +163,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Password. */ public get password(): string { - try { - return new URL(this.href).password; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getPassword(); } /** @@ -227,14 +172,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param password Password. */ public set password(password: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.password = password; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setPassword(password); } /** @@ -243,11 +181,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Pathname. */ public get pathname(): string { - try { - return new URL(this.href).pathname; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getPathname(); } /** @@ -256,14 +190,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param pathname Pathname. */ public set pathname(pathname: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.pathname = pathname; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setPathname(pathname); } /** @@ -272,11 +199,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Port. */ public get port(): string { - try { - return new URL(this.href).port; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getPort(); } /** @@ -285,14 +208,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param port Port. */ public set port(port: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.port = port; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setPort(port); } /** @@ -301,11 +217,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Host. */ public get host(): string { - try { - return new URL(this.href).host; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getHost(); } /** @@ -314,14 +226,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param host Host. */ public set host(host: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.host = host; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setHost(host); } /** @@ -330,11 +235,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Hostname. */ public get hostname(): string { - try { - return new URL(this.href).hostname; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getHostname(); } /** @@ -343,14 +244,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param hostname Hostname. */ public set hostname(hostname: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.hostname = hostname; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setHostname(hostname); } /** @@ -407,11 +301,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @returns Search. */ public get search(): string { - try { - return new URL(this.href).search; - } catch (e) { - return ''; - } + return this.#htmlHyperlinkElementUtility.getSearch(); } /** @@ -420,14 +310,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper * @param search Search. */ public set search(search: string) { - let url: URL; - try { - url = new URL(this.href); - } catch (e) { - return; - } - url.search = search; - this.href = url.href; + this.#htmlHyperlinkElementUtility.setSearch(search); } /** diff --git a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts index b810ee97c..2a3071803 100644 --- a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts @@ -1,7 +1,403 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import HTMLHyperlinkElementNamedNodeMap from '../html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.js'; +import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; +import HTMLHyperlinkElementUtility from '../html-hyperlink-element/HTMLHyperlinkElementUtility.js'; +import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; +import IHTMLHyperlinkElement from '../html-hyperlink-element/IHTMLHyperlinkElement.js'; +import PointerEvent from '../../event/events/PointerEvent.js'; +import Event from '../../event/Event.js'; +import EventPhaseEnum from '../../event/EventPhaseEnum.js'; + /** * HTMLAreaElement * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLAreaElement */ -export default class HTMLAreaElement extends HTMLElement {} +export default class HTMLAreaElement extends HTMLElement implements IHTMLHyperlinkElement { + public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLHyperlinkElementNamedNodeMap( + this + ); + public [PropertySymbol.relList]: DOMTokenList = null; + #htmlHyperlinkElementUtility = new HTMLHyperlinkElementUtility(this); + + /** + * Returns alt. + * + * @returns Alt. + */ + public get alt(): string { + return this.getAttribute('alt') || ''; + } + + /** + * Sets alt. + * + * @param alt Alt. + */ + public set alt(alt: string) { + this.setAttribute('alt', alt); + } + + /** + * Returns coords. + * + * @returns Coords. + */ + public get coords(): string { + return this.getAttribute('coords') || ''; + } + + /** + * Sets coords. + * + * @param coords Coords. + */ + public set coords(coords: string) { + this.setAttribute('coords', coords); + } + + /** + * Returns shape. + * + * @returns Shape. + */ + public get shape(): string { + return this.getAttribute('shape') || ''; + } + + /** + * Sets shape. + * + * @param shape Shape. + */ + public set shape(shape: string) { + this.setAttribute('shape', shape); + } + + /** + * Returns download. + * + * @returns download. + */ + public get download(): string { + return this.getAttribute('download') || ''; + } + + /** + * Sets download. + * + * @param download Download. + */ + public set download(download: string) { + this.setAttribute('download', download); + } + + /** + * Returns referrerPolicy. + * + * @returns Referrer Policy. + */ + public get referrerPolicy(): string { + return this.getAttribute('referrerPolicy') || ''; + } + + /** + * Sets referrerPolicy. + * + * @param referrerPolicy Referrer Policy. + */ + public set referrerPolicy(referrerPolicy: string) { + this.setAttribute('referrerPolicy', referrerPolicy); + } + + /** + * Returns ping. + * + * @returns Ping. + */ + public get ping(): string { + return this.getAttribute('ping') || ''; + } + + /** + * Sets ping. + * + * @param ping Ping. + */ + public set ping(ping: string) { + this.setAttribute('ping', ping); + } + + /** + * Returns rel. + * + * @returns Rel. + */ + public get rel(): string { + return this.getAttribute('rel') || ''; + } + + /** + * Sets rel. + * + * @param rel Rel. + */ + public set rel(rel: string) { + this.setAttribute('rel', rel); + } + + /** + * Returns rel list. + * + * @returns Rel list. + */ + public get relList(): DOMTokenList { + if (!this[PropertySymbol.relList]) { + this[PropertySymbol.relList] = new DOMTokenList(this, 'rel'); + } + return this[PropertySymbol.relList]; + } + + /** + * Returns target. + * + * @returns target. + */ + public get target(): string { + return this.getAttribute('target') || ''; + } + + /** + * Sets target. + * + * @param target Target. + */ + public set target(target: string) { + this.setAttribute('target', target); + } + + /** + * Returns the hyperlink's URL's origin. + * + * @returns Origin. + */ + public get origin(): string { + return this.#htmlHyperlinkElementUtility.getOrigin(); + } + + /** + * Returns href. + * + * @returns Href. + */ + public get href(): string { + return this.#htmlHyperlinkElementUtility.getHref(); + } + + /** + * Sets href. + * + * @param href Href. + */ + public set href(href: string) { + this.#htmlHyperlinkElementUtility.setHref(href); + } + + /** + * Returns protocol. + * + * @returns Protocol. + */ + public get protocol(): string { + return this.#htmlHyperlinkElementUtility.getProtocol(); + } + + /** + * Sets protocol. + * + * @param protocol Protocol. + */ + public set protocol(protocol: string) { + this.#htmlHyperlinkElementUtility.setProtocol(protocol); + } + + /** + * Returns username. + * + * @returns Username. + */ + public get username(): string { + return this.#htmlHyperlinkElementUtility.getUsername(); + } + + /** + * Sets username. + * + * @param username Username. + */ + public set username(username: string) { + this.#htmlHyperlinkElementUtility.setUsername(username); + } + + /** + * Returns password. + * + * @returns Password. + */ + public get password(): string { + return this.#htmlHyperlinkElementUtility.getPassword(); + } + + /** + * Sets password. + * + * @param password Password. + */ + public set password(password: string) { + this.#htmlHyperlinkElementUtility.setPassword(password); + } + + /** + * Returns host. + * + * @returns Host. + */ + public get host(): string { + return this.#htmlHyperlinkElementUtility.getHost(); + } + + /** + * Sets host. + * + * @param host Host. + */ + public set host(host: string) { + this.#htmlHyperlinkElementUtility.setHost(host); + } + + /** + * Returns hostname. + * + * @returns Hostname. + */ + public get hostname(): string { + return this.#htmlHyperlinkElementUtility.getHostname(); + } + + /** + * Sets hostname. + * + * @param hostname Hostname. + */ + public set hostname(hostname: string) { + this.#htmlHyperlinkElementUtility.setHostname(hostname); + } + + /** + * Returns port. + * + * @returns Port. + */ + public get port(): string { + return this.#htmlHyperlinkElementUtility.getPort(); + } + + /** + * Sets port. + * + * @param port Port. + */ + public set port(port: string) { + this.#htmlHyperlinkElementUtility.setPort(port); + } + + /** + * Returns pathname. + * + * @returns Pathname. + */ + public get pathname(): string { + return this.#htmlHyperlinkElementUtility.getPathname(); + } + + /** + * Sets pathname. + * + * @param pathname Pathname. + */ + public set pathname(pathname: string) { + this.#htmlHyperlinkElementUtility.setPathname(pathname); + } + + /** + * Returns search. + * + * @returns Search. + */ + public get search(): string { + return this.#htmlHyperlinkElementUtility.getSearch(); + } + + /** + * Sets search. + * + * @param search Search. + */ + public set search(search: string) { + this.#htmlHyperlinkElementUtility.setSearch(search); + } + + /** + * Returns hash. + * + * @returns Hash. + */ + public get hash(): string { + return this.#htmlHyperlinkElementUtility.getHash(); + } + + /** + * Sets hash. + * + * @param hash Hash. + */ + public set hash(hash: string) { + this.#htmlHyperlinkElementUtility.setHash(hash); + } + + /** + * @override + */ + public override toString(): string { + return this.href; + } + + /** + * @override + */ + public override dispatchEvent(event: Event): boolean { + const returnValue = super.dispatchEvent(event); + + if ( + event.type === 'click' && + event instanceof PointerEvent && + (event.eventPhase === EventPhaseEnum.atTarget || + event.eventPhase === EventPhaseEnum.bubbling) && + !event.defaultPrevented + ) { + const href = this.href; + if (href) { + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].open( + href, + this.target || '_self' + ); + if (this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].closed) { + event.stopImmediatePropagation(); + } + } + } + + return returnValue; + } +} diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts similarity index 77% rename from packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts rename to packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts index 4295a3337..5a884640a 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts @@ -1,15 +1,16 @@ import Attr from '../attr/Attr.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLAnchorElement from './HTMLAnchorElement.js'; +import HTMLAnchorElement from '../html-anchor-element/HTMLAnchorElement.js'; +import HTMLAreaElement from '../html-area-element/HTMLAreaElement.js'; /** * Named Node Map. * * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ -export default class HTMLAnchorElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLAnchorElement; +export default class HTMLHyperlinkElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected [PropertySymbol.ownerElement]: HTMLAnchorElement | HTMLAreaElement; /** * @override diff --git a/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementUtility.ts b/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementUtility.ts new file mode 100644 index 000000000..f5f85e3a4 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementUtility.ts @@ -0,0 +1,329 @@ +import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; + +/** + * HTML Hyperlink utility for HTMLAnchorElement and HTMLAreaElement. + * + * @see https://html.spec.whatwg.org/multipage/links.html#hyperlink + */ +export default class HTMLHyperlinkElementUtility { + private element: HTMLElement; + + /** + * Constructor. + * + * @param element Element. + */ + constructor(element: HTMLElement) { + this.element = element; + } + + /** + * Returns the hyperlink's URL's origin. + * + * @returns Origin. + */ + public getOrigin(): string { + try { + return new URL(this.getHref()).origin; + } catch (e) { + return ''; + } + } + + /** + * Returns href. + * + * @returns Href. + */ + public getHref(): string { + if (!this.element.hasAttribute('href')) { + return ''; + } + + try { + return new URL( + this.element.getAttribute('href'), + this.element[PropertySymbol.ownerDocument].location.href + ).href; + } catch (e) { + return this.element.getAttribute('href'); + } + } + + /** + * Sets href. + * + * @param href Href. + */ + public setHref(href: string): void { + this.element.setAttribute('href', href); + } + + /** + * Returns protocol. + * + * @returns Protocol. + */ + public getProtocol(): string { + try { + return new URL(this.getHref()).protocol; + } catch (e) { + return ''; + } + } + + /** + * Sets protocol. + * + * @param protocol Protocol. + */ + public setProtocol(protocol: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.protocol = protocol; + this.element.setAttribute('href', url.href); + } + + /** + * Returns username. + * + * @returns Username. + */ + public getUsername(): string { + try { + return new URL(this.getHref()).username; + } catch (e) { + return ''; + } + } + + /** + * Sets username. + * + * @param username Username. + */ + public setUsername(username: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.username = username; + this.element.setAttribute('href', url.href); + } + + /** + * Returns password. + * + * @returns Password. + */ + public getPassword(): string { + try { + return new URL(this.getHref()).password; + } catch (e) { + return ''; + } + } + + /** + * Sets password. + * + * @param password Password. + */ + public setPassword(password: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.password = password; + this.element.setAttribute('href', url.href); + } + + /** + * Returns host. + * + * @returns Host. + */ + public getHost(): string { + try { + return new URL(this.getHref()).host; + } catch (e) { + return ''; + } + } + + /** + * Sets host. + * + * @param host Host. + */ + public setHost(host: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.host = host; + this.element.setAttribute('href', url.href); + } + + /** + * Returns hostname. + * + * @returns Hostname. + */ + public getHostname(): string { + try { + return new URL(this.getHref()).hostname; + } catch (e) { + return ''; + } + } + + /** + * Sets hostname. + * + * @param hostname Hostname. + */ + public setHostname(hostname: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.hostname = hostname; + this.element.setAttribute('href', url.href); + } + + /** + * Returns port. + * + * @returns Port. + */ + public getPort(): string { + try { + return new URL(this.getHref()).port; + } catch (e) { + return ''; + } + } + + /** + * Sets port. + * + * @param port Port. + */ + public setPort(port: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.port = port; + this.element.setAttribute('href', url.href); + } + + /** + * Returns pathname. + * + * @returns Pathname. + */ + public getPathname(): string { + try { + return new URL(this.getHref()).pathname; + } catch (e) { + return ''; + } + } + + /** + * Sets pathname. + * + * @param pathname Pathname. + */ + public setPathname(pathname: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.pathname = pathname; + this.element.setAttribute('href', url.href); + } + + /** + * Returns search. + * + * @returns Search. + */ + public getSearch(): string { + try { + return new URL(this.getHref()).search; + } catch (e) { + return ''; + } + } + + /** + * Sets search. + * + * @param search Search. + */ + public setSearch(search: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.search = search; + this.element.setAttribute('href', url.href); + } + + /** + * Returns hash. + * + * @returns Hash. + */ + public getHash(): string { + const href = this.element.getAttribute('href'); + if (href.startsWith('#')) { + return href; + } + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return ''; + } + return url.hash; + } + + /** + * Sets hash. + * + * @param hash Hash. + */ + public setHash(hash: string): void { + let url: URL; + try { + url = new URL(this.getHref()); + } catch (e) { + return; + } + url.hash = hash; + this.element.setAttribute('href', url.href); + } +} diff --git a/packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts b/packages/happy-dom/src/nodes/html-hyperlink-element/IHTMLHyperlinkElement.ts similarity index 86% rename from packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts rename to packages/happy-dom/src/nodes/html-hyperlink-element/IHTMLHyperlinkElement.ts index 6d23f0564..967ea410e 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts +++ b/packages/happy-dom/src/nodes/html-hyperlink-element/IHTMLHyperlinkElement.ts @@ -4,7 +4,7 @@ * Reference: * https://html.spec.whatwg.org/multipage/links.html#htmlhyperlinkelementutils. */ -export default interface IHTMLHyperlinkElementUtils { +export default interface IHTMLHyperlinkElement { readonly origin: string; href: string; protocol: string; diff --git a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts index 859ed565d..4179927d7 100644 --- a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts +++ b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts @@ -52,69 +52,69 @@ describe('HTMLAnchorElement', () => { describe('get href()', () => { it('Returns the "href" attribute.', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'test'); expect(element.href).toBe('https://www.somesite.com/test'); }); it('Returns the "href" attribute when scheme is http.', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'http://www.example.com'); expect(element.href).toBe('http://www.example.com/'); }); it('Returns the "href" attribute when scheme is tel.', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'tel:+123456789'); expect(element.href).toBe('tel:+123456789'); }); it('Returns the "href" attribute when scheme-relative', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', '//example.com'); expect(element.href).toBe('https://example.com/'); }); it('Returns empty string if "href" attribute is empty.', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); expect(element.href).toBe(''); }); }); describe('toString()', () => { it('Returns the "href" attribute.', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'test'); expect(element.toString()).toBe('https://www.somesite.com/test'); }); it('Returns the "href" attribute when scheme is http.', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'http://www.example.com'); expect(element.toString()).toBe('http://www.example.com/'); }); it('Returns the "href" attribute when scheme is tel.', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'tel:+123456789'); expect(element.toString()).toBe('tel:+123456789'); }); it('Returns the "href" attribute when scheme-relative', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', '//example.com'); expect(element.toString()).toBe('https://example.com/'); }); it('Returns empty string if "href" attribute is empty.', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); expect(element.toString()).toBe(''); }); }); describe('set href()', () => { it('Sets the attribute "href".', () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.href = 'test'; expect(element.getAttribute('href')).toBe('test'); }); @@ -122,19 +122,19 @@ describe('HTMLAnchorElement', () => { describe('get origin()', () => { it("Returns the href URL's origin.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.origin).toBe('https://www.example.com'); }); it("Returns the href URL's origin with port when non-standard.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'http://www.example.com:8080/path?q1=a#xyz'); expect(element.origin).toBe('http://www.example.com:8080'); }); it("Returns the page's origin when href is relative.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', '/path?q1=a#xyz'); expect(element.origin).toBe('https://www.somesite.com'); }); @@ -142,7 +142,7 @@ describe('HTMLAnchorElement', () => { describe('get protocol()', () => { it("Returns the href URL's protocol.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.protocol).toBe('https:'); }); @@ -150,7 +150,7 @@ describe('HTMLAnchorElement', () => { describe('set protocol()', () => { it("Sets the href URL's protocol.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.protocol).toBe('https:'); @@ -163,7 +163,7 @@ describe('HTMLAnchorElement', () => { describe('get username()', () => { it("Returns the href URL's username.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); expect(element.username).toBe('user'); }); @@ -171,7 +171,7 @@ describe('HTMLAnchorElement', () => { describe('set username()', () => { it("Sets the href URL's username.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); expect(element.username).toBe('user'); @@ -184,7 +184,7 @@ describe('HTMLAnchorElement', () => { describe('get password()', () => { it("Returns the href URL's password.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); expect(element.password).toBe('pw'); }); @@ -192,7 +192,7 @@ describe('HTMLAnchorElement', () => { describe('set password()', () => { it("Sets the href URL's password.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); expect(element.password).toBe('pw'); @@ -205,7 +205,7 @@ describe('HTMLAnchorElement', () => { describe('get host()', () => { it("Returns the href URL's host.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.host).toBe('www.example.com'); }); @@ -213,7 +213,7 @@ describe('HTMLAnchorElement', () => { describe('set host()', () => { it("Sets the href URL's host.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.host).toBe('www.example.com'); @@ -226,7 +226,7 @@ describe('HTMLAnchorElement', () => { describe('get hostname()', () => { it("Returns the href URL's hostname.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.hostname).toBe('www.example.com'); }); @@ -234,7 +234,7 @@ describe('HTMLAnchorElement', () => { describe('set hostname()', () => { it("Sets the href URL's hostname.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.hostname).toBe('www.example.com'); @@ -247,7 +247,7 @@ describe('HTMLAnchorElement', () => { describe('get port()', () => { it("Returns the href URL's port.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.port).toBe(''); @@ -258,7 +258,7 @@ describe('HTMLAnchorElement', () => { describe('set port()', () => { it("Sets the href URL's port.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.port).toBe(''); @@ -271,7 +271,7 @@ describe('HTMLAnchorElement', () => { describe('get pathname()', () => { it("Returns the href URL's pathname.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.pathname).toBe('/path'); }); @@ -279,7 +279,7 @@ describe('HTMLAnchorElement', () => { describe('set pathname()', () => { it("Sets the href URL's pathname.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.pathname).toBe('/path'); @@ -292,7 +292,7 @@ describe('HTMLAnchorElement', () => { describe('get search()', () => { it("Returns the href URL's search.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.search).toBe('?q1=a'); }); @@ -300,7 +300,7 @@ describe('HTMLAnchorElement', () => { describe('set search()', () => { it("Sets the href URL's search.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.search).toBe('?q1=a'); @@ -313,7 +313,7 @@ describe('HTMLAnchorElement', () => { describe('get hash()', () => { it("Returns the href URL's hash.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.hash).toBe('#xyz'); }); @@ -321,7 +321,7 @@ describe('HTMLAnchorElement', () => { describe('set hash()', () => { it("Sets the href URL's hash.", () => { - const element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); expect(element.hash).toBe('#xyz'); @@ -433,7 +433,7 @@ describe('HTMLAnchorElement', () => { throw new Error('Fetch should not be called.'); }); - const element = document.createElement('a'); + const element = document.createElement('a'); element.href = 'https://www.example.com'; document.body.appendChild(element); element.dispatchEvent(new PointerEvent('click')); @@ -479,7 +479,7 @@ describe('HTMLAnchorElement', () => { throw new Error('Fetch should not be called.'); }); - const element = document.createElement('a'); + const element = document.createElement('a'); element.href = 'https://www.example.com'; document.body.appendChild(element); element.dispatchEvent(new PointerEvent('click')); @@ -496,7 +496,7 @@ describe('HTMLAnchorElement', () => { }); }); - const element = document.createElement('a'); + const element = document.createElement('a'); element.target = '_blank'; element.href = 'https://www.example.com'; document.body.appendChild(element); diff --git a/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts b/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts index 0d6e1f4d3..4d5fccb41 100644 --- a/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts +++ b/packages/happy-dom/test/nodes/html-area-element/HTMLAreaElement.test.ts @@ -1,22 +1,535 @@ import HTMLAreaElement from '../../../src/nodes/html-area-element/HTMLAreaElement.js'; import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; -import { beforeEach, describe, it, expect } from 'vitest'; +import { beforeEach, describe, it, expect, vi } from 'vitest'; +import PointerEvent from '../../../src/event/events/PointerEvent.js'; +import Request from '../../../src/fetch/Request.js'; +import Response from '../../../src/fetch/Response.js'; +import Browser from '../../../src/browser/Browser.js'; +import Fetch from '../../../src/fetch/Fetch.js'; describe('HTMLAreaElement', () => { let window: Window; let document: Document; - let element: HTMLAreaElement; beforeEach(() => { - window = new Window(); + window = new Window({ url: 'https://www.somesite.com/test.html' }); document = window.document; - element = document.createElement('area'); }); describe('constructor()', () => { it('Should be an instanceof HTMLAreaElement', () => { + const element = document.createElement('area'); expect(element instanceof HTMLAreaElement).toBe(true); }); }); + + describe('Object.prototype.toString', () => { + it('Returns `[object HTMLAreaElement]`', () => { + const element = document.createElement('area'); + expect(Object.prototype.toString.call(element)).toBe('[object HTMLAreaElement]'); + }); + }); + + for (const property of [ + 'download', + 'ping', + 'target', + 'referrerPolicy', + 'rel', + 'alt', + 'coords', + 'shape' + ]) { + describe(`get ${property}()`, () => { + it(`Returns the "${property}" attribute.`, () => { + const element = document.createElement('area'); + element.setAttribute(property, 'test'); + expect(element[property]).toBe('test'); + }); + }); + + describe(`set ${property}()`, () => { + it(`Sets the attribute "${property}".`, () => { + const element = document.createElement('area'); + element[property] = 'test'; + expect(element.getAttribute(property)).toBe('test'); + }); + }); + } + + describe('get href()', () => { + it('Returns the "href" attribute.', () => { + const element = document.createElement('area'); + element.setAttribute('href', 'test'); + expect(element.href).toBe('https://www.somesite.com/test'); + }); + + it('Returns the "href" attribute when scheme is http.', () => { + const element = document.createElement('area'); + element.setAttribute('href', 'http://www.example.com'); + expect(element.href).toBe('http://www.example.com/'); + }); + + it('Returns the "href" attribute when scheme is tel.', () => { + const element = document.createElement('area'); + element.setAttribute('href', 'tel:+123456789'); + expect(element.href).toBe('tel:+123456789'); + }); + + it('Returns the "href" attribute when scheme-relative', () => { + const element = document.createElement('area'); + element.setAttribute('href', '//example.com'); + expect(element.href).toBe('https://example.com/'); + }); + + it('Returns empty string if "href" attribute is empty.', () => { + const element = document.createElement('area'); + expect(element.href).toBe(''); + }); + }); + + describe('toString()', () => { + it('Returns the "href" attribute.', () => { + const element = document.createElement('area'); + element.setAttribute('href', 'test'); + expect(element.toString()).toBe('https://www.somesite.com/test'); + }); + + it('Returns the "href" attribute when scheme is http.', () => { + const element = document.createElement('area'); + element.setAttribute('href', 'http://www.example.com'); + expect(element.toString()).toBe('http://www.example.com/'); + }); + + it('Returns the "href" attribute when scheme is tel.', () => { + const element = document.createElement('area'); + element.setAttribute('href', 'tel:+123456789'); + expect(element.toString()).toBe('tel:+123456789'); + }); + + it('Returns the "href" attribute when scheme-relative', () => { + const element = document.createElement('area'); + element.setAttribute('href', '//example.com'); + expect(element.toString()).toBe('https://example.com/'); + }); + + it('Returns empty string if "href" attribute is empty.', () => { + const element = document.createElement('area'); + expect(element.toString()).toBe(''); + }); + }); + + describe('set href()', () => { + it('Sets the attribute "href".', () => { + const element = document.createElement('area'); + element.href = 'test'; + expect(element.getAttribute('href')).toBe('test'); + }); + }); + + describe('get origin()', () => { + it("Returns the href URL's origin.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.origin).toBe('https://www.example.com'); + }); + + it("Returns the href URL's origin with port when non-standard.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'http://www.example.com:8080/path?q1=a#xyz'); + expect(element.origin).toBe('http://www.example.com:8080'); + }); + + it("Returns the page's origin when href is relative.", () => { + const element = document.createElement('area'); + element.setAttribute('href', '/path?q1=a#xyz'); + expect(element.origin).toBe('https://www.somesite.com'); + }); + }); + + describe('get protocol()', () => { + it("Returns the href URL's protocol.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.protocol).toBe('https:'); + }); + }); + + describe('set protocol()', () => { + it("Sets the href URL's protocol.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.protocol).toBe('https:'); + + element.protocol = 'http'; + expect(element.protocol).toBe('http:'); + expect(element.href).toBe('http://www.example.com/path?q1=a#xyz'); + }); + }); + + describe('get username()', () => { + it("Returns the href URL's username.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); + expect(element.username).toBe('user'); + }); + }); + + describe('set username()', () => { + it("Sets the href URL's username.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); + + expect(element.username).toBe('user'); + + element.username = 'user2'; + expect(element.username).toBe('user2'); + expect(element.href).toBe('https://user2:pw@www.example.com/path?q1=a#xyz'); + }); + }); + + describe('get password()', () => { + it("Returns the href URL's password.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); + expect(element.password).toBe('pw'); + }); + }); + + describe('set password()', () => { + it("Sets the href URL's password.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); + + expect(element.password).toBe('pw'); + + element.password = 'pw2'; + expect(element.password).toBe('pw2'); + expect(element.href).toBe('https://user:pw2@www.example.com/path?q1=a#xyz'); + }); + }); + + describe('get host()', () => { + it("Returns the href URL's host.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.host).toBe('www.example.com'); + }); + }); + + describe('set host()', () => { + it("Sets the href URL's host.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.host).toBe('www.example.com'); + + element.host = 'abc.example2.com'; + expect(element.host).toBe('abc.example2.com'); + expect(element.href).toBe('https://abc.example2.com/path?q1=a#xyz'); + }); + }); + + describe('get hostname()', () => { + it("Returns the href URL's hostname.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.hostname).toBe('www.example.com'); + }); + }); + + describe('set hostname()', () => { + it("Sets the href URL's hostname.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.hostname).toBe('www.example.com'); + + element.hostname = 'abc.example2.com'; + expect(element.hostname).toBe('abc.example2.com'); + expect(element.href).toBe('https://abc.example2.com/path?q1=a#xyz'); + }); + }); + + describe('get port()', () => { + it("Returns the href URL's port.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.port).toBe(''); + + element.setAttribute('href', 'https://www.example.com:444/path?q1=a#xyz'); + expect(element.port).toBe('444'); + }); + }); + + describe('set port()', () => { + it("Sets the href URL's port.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.port).toBe(''); + + element.port = '8080'; + expect(element.port).toBe('8080'); + expect(element.href).toBe('https://www.example.com:8080/path?q1=a#xyz'); + }); + }); + + describe('get pathname()', () => { + it("Returns the href URL's pathname.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.pathname).toBe('/path'); + }); + }); + + describe('set pathname()', () => { + it("Sets the href URL's pathname.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.pathname).toBe('/path'); + + element.pathname = '/path2'; + expect(element.pathname).toBe('/path2'); + expect(element.href).toBe('https://www.example.com/path2?q1=a#xyz'); + }); + }); + + describe('get search()', () => { + it("Returns the href URL's search.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.search).toBe('?q1=a'); + }); + }); + + describe('set search()', () => { + it("Sets the href URL's search.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.search).toBe('?q1=a'); + + element.search = '?q1=b'; + expect(element.search).toBe('?q1=b'); + expect(element.href).toBe('https://www.example.com/path?q1=b#xyz'); + }); + }); + + describe('get hash()', () => { + it("Returns the href URL's hash.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.hash).toBe('#xyz'); + }); + }); + + describe('set hash()', () => { + it("Sets the href URL's hash.", () => { + const element = document.createElement('area'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.hash).toBe('#xyz'); + + element.hash = '#fgh'; + expect(element.hash).toBe('#fgh'); + expect(element.href).toBe('https://www.example.com/path?q1=a#fgh'); + }); + }); + + describe('dispatchEvent()', () => { + it('Navigates the browser when a "click" event is dispatched on an element.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const window = page.mainFrame.window; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const element = window.document.createElement('area'); + element.href = 'https://www.example.com'; + window.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + + const newWindow = page.mainFrame.window; + + expect(newWindow === window).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + + await browser.waitUntilComplete(); + + expect(newWindow.document.body.innerHTML).toBe('Test'); + + newWindow.close(); + + expect(newWindow.closed).toBe(true); + }); + + it('Navigates the browser when a "click" event is dispatched on an element with target "_blank".', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const window = page.mainFrame.window; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const element = window.document.createElement('area'); + element.href = 'https://www.example.com'; + element.target = '_blank'; + window.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + + const newWindow = browser.defaultContext.pages[1].mainFrame.window; + + expect(newWindow === window).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + + await browser.waitUntilComplete(); + + expect(newWindow.document.body.innerHTML).toBe('Test'); + + newWindow.close(); + + expect(newWindow.closed).toBe(true); + }); + + it('Navigates the browser when a "click" event is dispatched on an element, even if the element is not connected to DOM.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const window = page.mainFrame.window; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const element = window.document.createElement('area'); + element.href = 'https://www.example.com'; + element.dispatchEvent(new PointerEvent('click')); + + const newWindow = page.mainFrame.window; + + expect(newWindow === window).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + + await browser.waitUntilComplete(); + + expect(newWindow.document.body.innerHTML).toBe('Test'); + }); + + it(`Doesn't navigate or change the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "navigation.disableFallbackToSetURL" is set to "true".`, () => { + const window = new Window({ + settings: { + navigation: { + disableFallbackToSetURL: true + } + } + }); + document = window.document; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + throw new Error('Fetch should not be called.'); + }); + + const element = document.createElement('area'); + element.href = 'https://www.example.com'; + document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(window.location.href).toBe('about:blank'); + }); + + it(`Doesn't navigate, but changes the location of a new window when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "navigation.disableFallbackToSetURL" is set to "false" and "navigation.disableChildPageNavigation" is set to "true".`, () => { + const window = new Window({ + settings: { + navigation: { + disableFallbackToSetURL: false, + disableChildPageNavigation: true + } + } + }); + document = window.document; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + throw new Error('Fetch should not be called.'); + }); + + const newWindow = window.open(); + + const element = newWindow.document.createElement('area'); + element.href = 'https://www.example.com'; + newWindow.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(newWindow.closed).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + }); + + it('Changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "navigation.disableFallbackToSetURL" is set to "false".', () => { + const window = new Window({ + settings: { + navigation: { + disableFallbackToSetURL: false + } + } + }); + document = window.document; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + throw new Error('Fetch should not be called.'); + }); + + const element = document.createElement('area'); + element.href = 'https://www.example.com'; + document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(window.location.href).toBe('https://www.example.com/'); + }); + + it('Opens a window when a "click" event is dispatched on an element with target set to "_blank" inside the main frame of a detached browser.', () => { + let request: Request | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const element = document.createElement('area'); + element.target = '_blank'; + element.href = 'https://www.example.com'; + document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(((request)).url).toBe('https://www.example.com/'); + }); + + it('Navigates the browser when a "click" event is dispatched on an element inside a non-main frame of a detached browser.', () => { + let request: Request | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const newWindow = window.open(); + + const element = newWindow.document.createElement('area'); + element.href = 'https://www.example.com'; + newWindow.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(((request)).url).toBe('https://www.example.com/'); + expect(newWindow.closed).toBe(true); + }); + }); }); From 0af09bd6f25283926da764ef6621f592d94f7310 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Mon, 8 Apr 2024 01:00:37 +0200 Subject: [PATCH 04/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 4 + .../event/events/IMediaQueryListEventInit.ts | 6 + .../src/event/events/MediaStreamTrackEvent.ts | 22 +++ .../html-base-element/HTMLBaseElement.ts | 1 + .../html-body-element/HTMLBodyElement.ts | 23 +++- .../CanvasCaptureMediaStreamTrack.ts | 39 ++++++ .../html-canvas-element/HTMLCanvasElement.ts | 114 +++++++++++++++- .../IMediaTrackCapabilities.ts | 21 +++ .../IMediaTrackSettings.ts | 5 + .../nodes/html-canvas-element/ImageBitmap.ts | 25 ++++ .../nodes/html-canvas-element/MediaStream.ts | 108 +++++++++++++++ .../html-canvas-element/MediaStreamTrack.ts | 127 ++++++++++++++++++ .../html-canvas-element/OffscreenCanvas.ts | 58 ++++++++ 13 files changed, 551 insertions(+), 2 deletions(-) create mode 100644 packages/happy-dom/src/event/events/IMediaQueryListEventInit.ts create mode 100644 packages/happy-dom/src/event/events/MediaStreamTrackEvent.ts create mode 100644 packages/happy-dom/src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.ts create mode 100644 packages/happy-dom/src/nodes/html-canvas-element/IMediaTrackCapabilities.ts create mode 100644 packages/happy-dom/src/nodes/html-canvas-element/IMediaTrackSettings.ts create mode 100644 packages/happy-dom/src/nodes/html-canvas-element/ImageBitmap.ts create mode 100644 packages/happy-dom/src/nodes/html-canvas-element/MediaStream.ts create mode 100644 packages/happy-dom/src/nodes/html-canvas-element/MediaStreamTrack.ts create mode 100644 packages/happy-dom/src/nodes/html-canvas-element/OffscreenCanvas.ts diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 81e22cf21..85b2ccb0a 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -164,3 +164,7 @@ export const appendChild = Symbol('appendChild'); export const removeChild = Symbol('removeChild'); export const insertBefore = Symbol('insertBefore'); export const replaceChild = Symbol('replaceChild'); +export const tracks = Symbol('tracks'); +export const constraints = Symbol('constraints'); +export const capabilities = Symbol('capabilities'); +export const settings = Symbol('settings'); diff --git a/packages/happy-dom/src/event/events/IMediaQueryListEventInit.ts b/packages/happy-dom/src/event/events/IMediaQueryListEventInit.ts new file mode 100644 index 000000000..d734d9c93 --- /dev/null +++ b/packages/happy-dom/src/event/events/IMediaQueryListEventInit.ts @@ -0,0 +1,6 @@ +import MediaStreamTrack from '../../nodes/html-canvas-element/MediaStreamTrack.js'; +import IEventInit from '../IEventInit.js'; + +export default interface IMediaQueryListEventInit extends IEventInit { + track?: MediaStreamTrack; +} diff --git a/packages/happy-dom/src/event/events/MediaStreamTrackEvent.ts b/packages/happy-dom/src/event/events/MediaStreamTrackEvent.ts new file mode 100644 index 000000000..c974b0a71 --- /dev/null +++ b/packages/happy-dom/src/event/events/MediaStreamTrackEvent.ts @@ -0,0 +1,22 @@ +import MediaStreamTrack from '../../nodes/html-canvas-element/MediaStreamTrack.js'; +import Event from '../Event.js'; +import IMediaQueryListEventInit from './IMediaQueryListEventInit.js'; + +/** + * Media Stream Track Event. + */ +export default class MediaStreamTrackEvent extends Event { + public readonly track: MediaStreamTrack | null; + + /** + * Constructor. + * + * @param type Event type. + * @param [eventInit] Event init. + */ + constructor(type: string, eventInit: IMediaQueryListEventInit | null = null) { + super(type, eventInit); + + this.track = eventInit?.track ?? null; + } +} diff --git a/packages/happy-dom/src/nodes/html-base-element/HTMLBaseElement.ts b/packages/happy-dom/src/nodes/html-base-element/HTMLBaseElement.ts index b33b3c721..ee88f6146 100644 --- a/packages/happy-dom/src/nodes/html-base-element/HTMLBaseElement.ts +++ b/packages/happy-dom/src/nodes/html-base-element/HTMLBaseElement.ts @@ -9,6 +9,7 @@ import * as PropertySymbol from '../../PropertySymbol.js'; */ export default class HTMLBaseElement extends HTMLElement { public cloneNode: (deep?: boolean) => HTMLBaseElement; + /** * Returns href. * diff --git a/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts b/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts index 55f4159f0..d083489d6 100644 --- a/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts +++ b/packages/happy-dom/src/nodes/html-body-element/HTMLBodyElement.ts @@ -1,7 +1,28 @@ +import Event from '../../event/Event.js'; import HTMLElement from '../html-element/HTMLElement.js'; /** * HTMLBodyElement * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLBodyElement */ -export default class HTMLBodyElement extends HTMLElement {} +export default class HTMLBodyElement extends HTMLElement { + // Events + public onafterprint: (event: Event) => void | null = null; + public onbeforeprint: (event: Event) => void | null = null; + public onbeforeunload: (event: Event) => void | null = null; + public ongamepadconnected: (event: Event) => void | null = null; + public ongamepaddisconnected: (event: Event) => void | null = null; + public onhashchange: (event: Event) => void | null = null; + public onlanguagechange: (event: Event) => void | null = null; + public onmessage: (event: Event) => void | null = null; + public onmessageerror: (event: Event) => void | null = null; + public onoffline: (event: Event) => void | null = null; + public ononline: (event: Event) => void | null = null; + public onpagehide: (event: Event) => void | null = null; + public onpageshow: (event: Event) => void | null = null; + public onpopstate: (event: Event) => void | null = null; + public onrejectionhandled: (event: Event) => void | null = null; + public onstorage: (event: Event) => void | null = null; + public onunhandledrejection: (event: Event) => void | null = null; + public onunload: (event: Event) => void | null = null; +} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.ts b/packages/happy-dom/src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.ts new file mode 100644 index 000000000..441867367 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.ts @@ -0,0 +1,39 @@ +import HTMLCanvasElement from './HTMLCanvasElement.js'; +import MediaStreamTrack from './MediaStreamTrack.js'; + +/** + * Canvas Capture Media Stream Track. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasCaptureMediaStreamTrack + */ +export default class CanvasCaptureMediaStreamTrack extends MediaStreamTrack { + public canvas: HTMLCanvasElement; + + /** + * + * @param canvas + * @param frameRate + */ + constructor(canvas: HTMLCanvasElement) { + super(); + this.canvas = canvas; + } + + /** + * Requests a frame. + */ + public requestFrame(): void { + // Do nothing + } + + /** + * Clones the track. + * + * @returns Clone. + */ + public clone(): MediaStreamTrack { + const clone = super.clone(); + clone.canvas = this.canvas; + return clone; + } +} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts index 328082e3f..ff2c6e8d8 100644 --- a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts +++ b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts @@ -1,7 +1,119 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import CanvasCaptureMediaStreamTrack from './CanvasCaptureMediaStreamTrack.js'; +import MediaStream from './MediaStream.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import Blob from '../../file/Blob.js'; +import OffscreenCanvas from './OffscreenCanvas.js'; +import Event from '../../event/Event.js'; + /** * HTMLCanvasElement * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement */ -export default class HTMLCanvasElement extends HTMLElement {} +export default class HTMLCanvasElement extends HTMLElement { + // Events + public oncontextlost: (event: Event) => void | null = null; + public oncontextrestored: (event: Event) => void | null = null; + public onwebglcontextcreationerror: (event: Event) => void | null = null; + public onwebglcontextlost: (event: Event) => void | null = null; + public onwebglcontextrestored: (event: Event) => void | null = null; + + /** + * Returns width. + * + * @returns Width. + */ + public get width(): number { + const width = this.getAttribute('width'); + return width !== null ? Number(width) : 300; + } + + /** + * Sets width. + * + * @param width Width. + */ + public set width(width: number) { + this.setAttribute('width', String(width)); + } + + /** + * Returns height. + * + * @returns Height. + */ + public get height(): number { + const height = this.getAttribute('height'); + return height !== null ? Number(height) : 150; + } + + /** + * Sets height. + * + * @param height Height. + */ + public set height(height: number) { + this.setAttribute('height', String(height)); + } + + /** + * Returns capture stream. + * + * @param [_frameRate] Frame rate. + * @returns Capture stream. + */ + public captureStream(_frameRate?: number): MediaStream { + const stream = new MediaStream(); + stream.addTrack(new CanvasCaptureMediaStreamTrack(this)); + stream[PropertySymbol.capabilities].aspectRatio.max = this.width; + stream[PropertySymbol.capabilities].height.max = this.height; + stream[PropertySymbol.capabilities].width.max = this.width; + return stream; + } + + /** + * Returns context. + * + * @param _contextType Context type. + * @param [_contextAttributes] Context attributes. + * @returns Context. + */ + public getContext( + _contextType: '2d' | 'webgl' | 'webgl2' | 'webgpu' | 'bitmaprenderer', + _contextAttributes?: { [key: string]: any } + ): null { + return null; + } + + /** + * Returns to data URL. + * + * @param [_type] Type. + * @param [_encoderOptions] Quality. + * @returns Data URL. + */ + public toDataURL(_type?: string, _encoderOptions?: any): string { + return ''; + } + + /** + * Returns to blob. + * + * @param callback Callback. + * @param [_type] Type. + * @param [_quality] Quality. + */ + public toBlob(callback: (blob: Blob) => void, _type?: string, _quality?: any): void { + callback(new Blob([])); + } + + /** + * Transfers control to offscreen. + * + * @returns Offscreen canvas. + */ + public transferControlToOffscreen(): OffscreenCanvas { + return new OffscreenCanvas(this.width, this.height); + } +} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/IMediaTrackCapabilities.ts b/packages/happy-dom/src/nodes/html-canvas-element/IMediaTrackCapabilities.ts new file mode 100644 index 000000000..a3bb5cb3c --- /dev/null +++ b/packages/happy-dom/src/nodes/html-canvas-element/IMediaTrackCapabilities.ts @@ -0,0 +1,21 @@ +export default interface IMediaTrackCapabilities { + aspectRatio: { + max: number; + min: number; + }; + deviceId: string; + facingMode: []; + frameRate: { + max: number; + min: number; + }; + height: { + max: number; + min: number; + }; + resizeMode: string[]; + width: { + max: number; + min: number; + }; +} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/IMediaTrackSettings.ts b/packages/happy-dom/src/nodes/html-canvas-element/IMediaTrackSettings.ts new file mode 100644 index 000000000..4278256a2 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-canvas-element/IMediaTrackSettings.ts @@ -0,0 +1,5 @@ +export default interface IMediaTrackSettings { + deviceId: string; + frameRate: number; + resizeMode: string; +} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/ImageBitmap.ts b/packages/happy-dom/src/nodes/html-canvas-element/ImageBitmap.ts new file mode 100644 index 000000000..db82142b6 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-canvas-element/ImageBitmap.ts @@ -0,0 +1,25 @@ +/** + * + */ +export default class ImageBitmap { + public height: number; + public width: number; + + /** + * Constructor. + * + * @param width Width. + * @param height Height. + */ + constructor(width: number, height: number) { + this.width = width; + this.height = height; + } + + /** + * Disposes of all graphical resources associated with an ImageBitmap. + */ + public close(): void { + // TODO: Not implemented. + } +} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/MediaStream.ts b/packages/happy-dom/src/nodes/html-canvas-element/MediaStream.ts new file mode 100644 index 000000000..85171b5fe --- /dev/null +++ b/packages/happy-dom/src/nodes/html-canvas-element/MediaStream.ts @@ -0,0 +1,108 @@ +import * as PropertySymbol from '../../PropertySymbol.js'; +import Crypto from 'crypto'; +import EventTarget from '../../event/EventTarget.js'; +import MediaStreamTrackEvent from '../../event/events/MediaStreamTrackEvent.js'; +import MediaStreamTrack from './MediaStreamTrack.js'; + +/** + * MediaStream. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaStream + */ +export default class MediaStream extends EventTarget { + // Public properties + public active = true; + public id: string = Crypto.randomUUID(); + + // Events + public onaddtrack: (event: MediaStreamTrackEvent) => void | null = null; + public onremovetrack: (event: MediaStreamTrackEvent) => void | null = null; + + // Internal properties + public [PropertySymbol.tracks]: MediaStreamTrack[] = []; + + /** + * Constructor. + * + * @param [streamOrTracks] Stream or tracks. + */ + constructor(streamOrTracks?: MediaStream | MediaStreamTrack[]) { + super(); + + if (streamOrTracks !== undefined) { + this[PropertySymbol.tracks] = + streamOrTracks instanceof MediaStream + ? streamOrTracks[PropertySymbol.tracks].slice() + : streamOrTracks; + } + } + + /** + * Adds a track. + * + * @param track Track. + */ + public addTrack(track: MediaStreamTrack): void { + if (this[PropertySymbol.tracks].includes(track)) { + return; + } + this[PropertySymbol.tracks].push(track); + this.dispatchEvent(new MediaStreamTrackEvent('addtrack', { track })); + } + + /** + * Returns a clone. + * + * @returns Clone. + */ + public clone(): MediaStream { + return new (this.constructor)(this); + } + + /** + * Returns audio tracks. + * + * @returns Audio tracks. + */ + public getAudioTracks(): MediaStreamTrack[] { + return this[PropertySymbol.tracks].filter((track) => track.kind === 'audio'); + } + + /** + * Returns track by id. + * + * @param id Id. + * @returns Track. + */ + public getTrackById(id: string): MediaStreamTrack | null { + for (const track of this[PropertySymbol.tracks]) { + if (track.id === id) { + return track; + } + } + return null; + } + + /** + * Returns video tracks. + * + * @returns Video tracks. + */ + public getVideoTracks(): MediaStreamTrack[] { + return this[PropertySymbol.tracks].filter((track) => track.kind === 'video'); + } + + /** + * Removes a track. + * + * @param track Track. + */ + public removeTrack(track: MediaStreamTrack): void { + const index = this[PropertySymbol.tracks].indexOf(track); + if (index === -1) { + return; + } + this[PropertySymbol.tracks].splice(index, 1); + this.dispatchEvent(new MediaStreamTrackEvent('removetrack', { track })); + } +} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/MediaStreamTrack.ts b/packages/happy-dom/src/nodes/html-canvas-element/MediaStreamTrack.ts new file mode 100644 index 000000000..f653a54b8 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-canvas-element/MediaStreamTrack.ts @@ -0,0 +1,127 @@ +import Event from '../../event/Event.js'; +import EventTarget from '../../event/EventTarget.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import Crypto from 'crypto'; +import IMediaTrackCapabilities from './IMediaTrackCapabilities.js'; +import IMediaTrackSettings from './IMediaTrackSettings.js'; + +const DEVICE_ID = 'S3F/aBCdEfGHIjKlMnOpQRStUvWxYz1234567890+1AbC2DEf2GHi3jK34le+ab12C3+1aBCdEf=='; +const CAPABILITIES: IMediaTrackCapabilities = { + aspectRatio: { + max: 300, + min: 0.006666666666666667 + }, + deviceId: DEVICE_ID, + facingMode: [], + frameRate: { + max: 60, + min: 0 + }, + height: { + max: 150, + min: 1 + }, + resizeMode: ['none', 'crop-and-scale'], + width: { + max: 300, + min: 1 + } +}; +const SETTINGS: IMediaTrackSettings = { + deviceId: DEVICE_ID, + frameRate: 60, + resizeMode: 'none' +}; + +/** + * Canvas Capture Media Stream Track. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack + */ +export default class MediaStreamTrack extends EventTarget { + public contentHint: + | '' + | 'speech' + | 'speech-recognition' + | 'music' + | 'motion' + | 'detail' + | 'text' = ''; + public enabled = true; + public id: string = Crypto.randomUUID(); + public kind: 'audio' | 'video' = 'video'; + public label: string = DEVICE_ID; + public muted = false; + public readyState: 'live' | 'ended' = 'live'; + public [PropertySymbol.constraints]: object = {}; + public [PropertySymbol.capabilities]: IMediaTrackCapabilities = JSON.parse( + JSON.stringify(CAPABILITIES) + ); + public [PropertySymbol.settings]: IMediaTrackSettings = JSON.parse(JSON.stringify(SETTINGS)); + + // Events + public onended: (event: Event) => void | null = null; + public onmute: (event: Event) => void | null = null; + public onunmute: (event: Event) => void | null = null; + + /** + * Applies constraints. + * + * @param _constraints Constraints. + * @param constraints + */ + public async applyConstraints(constraints: object): Promise { + Object.apply(this[PropertySymbol.constraints], constraints); + } + + /** + * Returns capabilities. + * + * @returns Capabilities. + */ + public getCapabilities(): IMediaTrackCapabilities { + return this[PropertySymbol.capabilities]; + } + + /** + * Returns constraints. + * + * @returns Constraints. + */ + public getConstraints(): object { + return this[PropertySymbol.constraints]; + } + + /** + * Returns settings. + * + * @returns Settings. + */ + public getSettings(): IMediaTrackSettings { + return this[PropertySymbol.settings]; + } + + /** + * Clones the track. + * + * @returns Clone. + */ + public clone(): MediaStreamTrack { + const clone = new (this.constructor)(); + clone.contentHint = this.contentHint; + clone.enabled = this.enabled; + clone.id = this.id; + clone.kind = this.kind; + clone.label = this.label; + clone.muted = this.muted; + clone.readyState = this.readyState; + return clone; + } + + /** + * Stops the track. + */ + public stop(): void { + this.readyState = 'ended'; + } +} diff --git a/packages/happy-dom/src/nodes/html-canvas-element/OffscreenCanvas.ts b/packages/happy-dom/src/nodes/html-canvas-element/OffscreenCanvas.ts new file mode 100644 index 000000000..e95cd0fc2 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-canvas-element/OffscreenCanvas.ts @@ -0,0 +1,58 @@ +import Blob from '../../file/Blob.js'; +import ImageBitmap from './ImageBitmap.js'; + +/** + * OffscreenCanvas. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas/OffscreenCanvas + */ +export default class OffscreenCanvas { + public width: number; + public height: number; + + /** + * Constructor. + * + * @param width Width. + * @param height Height. + */ + constructor(width: number, height: number) { + this.width = width; + this.height = height; + } + + /** + * Returns context. + * + * @param _contextType Context type. + * @param [_contextAttributes] Context attributes. + * @returns Context. + */ + public getContext( + _contextType: '2d' | 'webgl' | 'webgl2' | 'webgpu' | 'bitmaprenderer', + _contextAttributes?: { [key: string]: any } + ): null { + return null; + } + + /** + * Converts the canvas to a Blob. + * + * @param [_options] Options. + * @param [_options.type] Type. + * @param [_options.quality] Quality. + * @returns Blob. + */ + public convertToBlob(_options?: { type?: string; quality?: any }): Promise { + return Promise.resolve(new Blob([])); + } + + /** + * Creates an ImageBitmap object from the most recently rendered image of the OffscreenCanvas. + * + * @returns ImageBitmap. + */ + public transferToImageBitmap(): ImageBitmap { + return new ImageBitmap(this.width, this.height); + } +} From 5f29bddf55f4bbb65721a524e4c09ed2cd507d85 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 9 Apr 2024 01:21:21 +0200 Subject: [PATCH 05/51] chore: [#1332] Continues on implementation --- .../CanvasCaptureMediaStreamTrack.ts | 14 +- .../html-canvas-element/HTMLCanvasElement.ts | 28 +++- .../nodes/html-canvas-element/ImageBitmap.ts | 2 + .../html-canvas-element/MediaStreamTrack.ts | 67 ++++++--- .../html-canvas-element/OffscreenCanvas.ts | 8 +- .../CanvasCaptureMediaStreamTrack.test.ts | 51 +++++++ .../HTMLCanvasElement.test.ts | 132 ++++++++++++++++++ .../html-canvas-element/ImageBitmap.test.ts | 24 ++++ .../html-canvas-element/MediaStream.test.ts | 103 ++++++++++++++ .../MediaStreamTrack.test.ts | 103 ++++++++++++++ .../OffscreenCanvas.test.ts | 49 +++++++ .../test/query-selector/QuerySelector.test.ts | 6 +- 12 files changed, 550 insertions(+), 37 deletions(-) create mode 100644 packages/happy-dom/test/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.test.ts create mode 100644 packages/happy-dom/test/nodes/html-canvas-element/ImageBitmap.test.ts create mode 100644 packages/happy-dom/test/nodes/html-canvas-element/MediaStream.test.ts create mode 100644 packages/happy-dom/test/nodes/html-canvas-element/MediaStreamTrack.test.ts create mode 100644 packages/happy-dom/test/nodes/html-canvas-element/OffscreenCanvas.test.ts diff --git a/packages/happy-dom/src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.ts b/packages/happy-dom/src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.ts index 441867367..0ed494797 100644 --- a/packages/happy-dom/src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.ts +++ b/packages/happy-dom/src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.ts @@ -10,13 +10,15 @@ export default class CanvasCaptureMediaStreamTrack extends MediaStreamTrack { public canvas: HTMLCanvasElement; /** + * Constructor. * - * @param canvas - * @param frameRate + * @param options Options. + * @param options.kind 'audio' or 'video'. + * @param options.canvas Canvas. */ - constructor(canvas: HTMLCanvasElement) { - super(); - this.canvas = canvas; + constructor(options: { kind: 'audio' | 'video'; canvas: HTMLCanvasElement }) { + super(options); + this.canvas = options.canvas; } /** @@ -31,7 +33,7 @@ export default class CanvasCaptureMediaStreamTrack extends MediaStreamTrack { * * @returns Clone. */ - public clone(): MediaStreamTrack { + public clone(): CanvasCaptureMediaStreamTrack { const clone = super.clone(); clone.canvas = this.canvas; return clone; diff --git a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts index ff2c6e8d8..1a4177565 100644 --- a/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts +++ b/packages/happy-dom/src/nodes/html-canvas-element/HTMLCanvasElement.ts @@ -6,6 +6,8 @@ import Blob from '../../file/Blob.js'; import OffscreenCanvas from './OffscreenCanvas.js'; import Event from '../../event/Event.js'; +const DEVICE_ID = 'S3F/aBCdEfGHIjKlMnOpQRStUvWxYz1234567890+1AbC2DEf2GHi3jK34le+ab12C3+1aBCdEf=='; + /** * HTMLCanvasElement * @@ -60,15 +62,29 @@ export default class HTMLCanvasElement extends HTMLElement { /** * Returns capture stream. * - * @param [_frameRate] Frame rate. + * @param [frameRate] Frame rate. * @returns Capture stream. */ - public captureStream(_frameRate?: number): MediaStream { + public captureStream(frameRate?: number): MediaStream { const stream = new MediaStream(); - stream.addTrack(new CanvasCaptureMediaStreamTrack(this)); - stream[PropertySymbol.capabilities].aspectRatio.max = this.width; - stream[PropertySymbol.capabilities].height.max = this.height; - stream[PropertySymbol.capabilities].width.max = this.width; + const track = new CanvasCaptureMediaStreamTrack({ + kind: 'video', + canvas: this + }); + + track[PropertySymbol.capabilities].deviceId = DEVICE_ID; + track[PropertySymbol.capabilities].aspectRatio.max = this.width; + track[PropertySymbol.capabilities].height.max = this.height; + track[PropertySymbol.capabilities].width.max = this.width; + track[PropertySymbol.settings].deviceId = DEVICE_ID; + + if (frameRate !== undefined) { + track[PropertySymbol.capabilities].frameRate.max = frameRate; + track[PropertySymbol.settings].frameRate = frameRate; + } + + stream.addTrack(track); + return stream; } diff --git a/packages/happy-dom/src/nodes/html-canvas-element/ImageBitmap.ts b/packages/happy-dom/src/nodes/html-canvas-element/ImageBitmap.ts index db82142b6..c8ea655b9 100644 --- a/packages/happy-dom/src/nodes/html-canvas-element/ImageBitmap.ts +++ b/packages/happy-dom/src/nodes/html-canvas-element/ImageBitmap.ts @@ -1,5 +1,7 @@ /** + * Image Bitmap. * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap */ export default class ImageBitmap { public height: number; diff --git a/packages/happy-dom/src/nodes/html-canvas-element/MediaStreamTrack.ts b/packages/happy-dom/src/nodes/html-canvas-element/MediaStreamTrack.ts index f653a54b8..25d1d87c4 100644 --- a/packages/happy-dom/src/nodes/html-canvas-element/MediaStreamTrack.ts +++ b/packages/happy-dom/src/nodes/html-canvas-element/MediaStreamTrack.ts @@ -4,14 +4,12 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import Crypto from 'crypto'; import IMediaTrackCapabilities from './IMediaTrackCapabilities.js'; import IMediaTrackSettings from './IMediaTrackSettings.js'; - -const DEVICE_ID = 'S3F/aBCdEfGHIjKlMnOpQRStUvWxYz1234567890+1AbC2DEf2GHi3jK34le+ab12C3+1aBCdEf=='; const CAPABILITIES: IMediaTrackCapabilities = { aspectRatio: { max: 300, min: 0.006666666666666667 }, - deviceId: DEVICE_ID, + deviceId: '', facingMode: [], frameRate: { max: 60, @@ -28,7 +26,7 @@ const CAPABILITIES: IMediaTrackCapabilities = { } }; const SETTINGS: IMediaTrackSettings = { - deviceId: DEVICE_ID, + deviceId: '', frameRate: 60, resizeMode: 'none' }; @@ -48,11 +46,11 @@ export default class MediaStreamTrack extends EventTarget { | 'detail' | 'text' = ''; public enabled = true; - public id: string = Crypto.randomUUID(); - public kind: 'audio' | 'video' = 'video'; - public label: string = DEVICE_ID; + public readonly id: string = Crypto.randomUUID(); + public readonly kind: 'audio' | 'video' = 'video'; public muted = false; public readyState: 'live' | 'ended' = 'live'; + public label: string = ''; public [PropertySymbol.constraints]: object = {}; public [PropertySymbol.capabilities]: IMediaTrackCapabilities = JSON.parse( JSON.stringify(CAPABILITIES) @@ -65,22 +63,24 @@ export default class MediaStreamTrack extends EventTarget { public onunmute: (event: Event) => void | null = null; /** - * Applies constraints. + * Constructor. * - * @param _constraints Constraints. - * @param constraints + * @param options Options. + * @param options.kind 'audio' or 'video'. */ - public async applyConstraints(constraints: object): Promise { - Object.apply(this[PropertySymbol.constraints], constraints); + constructor(options: { kind: 'audio' | 'video' }) { + super(); + this.kind = options.kind; } /** - * Returns capabilities. + * Applies constraints. * - * @returns Capabilities. + * @param _constraints Constraints. + * @param constraints */ - public getCapabilities(): IMediaTrackCapabilities { - return this[PropertySymbol.capabilities]; + public async applyConstraints(constraints: object): Promise { + this.#mergeObjects(this[PropertySymbol.constraints], constraints); } /** @@ -92,6 +92,15 @@ export default class MediaStreamTrack extends EventTarget { return this[PropertySymbol.constraints]; } + /** + * Returns capabilities. + * + * @returns Capabilities. + */ + public getCapabilities(): IMediaTrackCapabilities { + return this[PropertySymbol.capabilities]; + } + /** * Returns settings. * @@ -107,11 +116,12 @@ export default class MediaStreamTrack extends EventTarget { * @returns Clone. */ public clone(): MediaStreamTrack { - const clone = new (this.constructor)(); + const clone = new (this.constructor)({ kind: this.kind }); + clone[PropertySymbol.constraints] = this[PropertySymbol.constraints]; + clone[PropertySymbol.capabilities] = this[PropertySymbol.capabilities]; + clone[PropertySymbol.settings] = this[PropertySymbol.settings]; clone.contentHint = this.contentHint; clone.enabled = this.enabled; - clone.id = this.id; - clone.kind = this.kind; clone.label = this.label; clone.muted = this.muted; clone.readyState = this.readyState; @@ -124,4 +134,23 @@ export default class MediaStreamTrack extends EventTarget { public stop(): void { this.readyState = 'ended'; } + + /** + * Merges two objects. + * + * @param source Target. + * @param target Source. + */ + #mergeObjects(source: object, target: object): void { + for (const key in target) { + if (target[key] !== null && typeof target[key] === 'object' && !Array.isArray(target[key])) { + if (typeof source[key] !== 'object') { + source[key] = {}; + } + this.#mergeObjects(source[key], target[key]); + } else { + source[key] = target[key]; + } + } + } } diff --git a/packages/happy-dom/src/nodes/html-canvas-element/OffscreenCanvas.ts b/packages/happy-dom/src/nodes/html-canvas-element/OffscreenCanvas.ts index e95cd0fc2..3e25e9565 100644 --- a/packages/happy-dom/src/nodes/html-canvas-element/OffscreenCanvas.ts +++ b/packages/happy-dom/src/nodes/html-canvas-element/OffscreenCanvas.ts @@ -7,8 +7,8 @@ import ImageBitmap from './ImageBitmap.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas/OffscreenCanvas */ export default class OffscreenCanvas { - public width: number; - public height: number; + public readonly width: number; + public readonly height: number; /** * Constructor. @@ -43,8 +43,8 @@ export default class OffscreenCanvas { * @param [_options.quality] Quality. * @returns Blob. */ - public convertToBlob(_options?: { type?: string; quality?: any }): Promise { - return Promise.resolve(new Blob([])); + public async convertToBlob(_options?: { type?: string; quality?: any }): Promise { + return new Blob([]); } /** diff --git a/packages/happy-dom/test/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.test.ts new file mode 100644 index 000000000..fc361443f --- /dev/null +++ b/packages/happy-dom/test/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.test.ts @@ -0,0 +1,51 @@ +import HTMLCanvasElement from '../../../src/nodes/html-canvas-element/HTMLCanvasElement.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import { beforeEach, describe, it, expect } from 'vitest'; +import CanvasCaptureMediaStreamTrack from '../../../src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.js'; + +describe('CanvasCaptureMediaStreamTrack', () => { + let window: Window; + let document: Document; + let canvas: HTMLCanvasElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + canvas = document.createElement('canvas'); + }); + + describe('get canvas()', () => { + it('Returns the canvas.', () => { + const track = new CanvasCaptureMediaStreamTrack({ kind: 'video', canvas }); + expect(track.canvas).toBe(canvas); + }); + }); + + describe('requestFrame()', () => { + it('Does nothing.', () => { + const track = new CanvasCaptureMediaStreamTrack({ kind: 'video', canvas }); + expect(() => track.requestFrame()).not.toThrow(); + }); + }); + + describe('clone()', () => { + it('Clones the track.', () => { + const track = new CanvasCaptureMediaStreamTrack({ kind: 'video', canvas }); + const clone = track.clone(); + + // MediaStreamTrack + expect(clone).not.toBe(track); + expect(clone.id).not.toBe(track.id); + expect(clone.label).toBe(track.label); + expect(clone.kind).toBe(track.kind); + expect(clone.muted).toBe(track.muted); + expect(clone.readyState).toBe(track.readyState); + expect(clone.getCapabilities()).toEqual(track.getCapabilities()); + expect(clone.getSettings()).toEqual(track.getSettings()); + + // CanvasCaptureMediaStreamTrack + expect(clone.canvas).toBe(track.canvas); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts index 213ce5f91..f4aa7b6cd 100644 --- a/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts +++ b/packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts @@ -2,6 +2,12 @@ import HTMLCanvasElement from '../../../src/nodes/html-canvas-element/HTMLCanvas import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; import { beforeEach, describe, it, expect } from 'vitest'; +import CanvasCaptureMediaStreamTrack from '../../../src/nodes/html-canvas-element/CanvasCaptureMediaStreamTrack.js'; +import Blob from '../../../src/file/Blob.js'; +import OffscreenCanvas from '../../../src/nodes/html-canvas-element/OffscreenCanvas.js'; +import MediaStream from '../../../src/nodes/html-canvas-element/MediaStream.js'; + +const DEVICE_ID = 'S3F/aBCdEfGHIjKlMnOpQRStUvWxYz1234567890+1AbC2DEf2GHi3jK34le+ab12C3+1aBCdEf=='; describe('HTMLCanvasElement', () => { let window: Window; @@ -19,4 +25,130 @@ describe('HTMLCanvasElement', () => { expect(element instanceof HTMLCanvasElement).toBe(true); }); }); + + describe('get width()', () => { + it('Returns the "width" attribute.', () => { + const element = document.createElement('canvas'); + element.setAttribute('width', '100'); + expect(element.width).toBe(100); + }); + + it('Returns 300 if the "width" attribute is not set.', () => { + const element = document.createElement('canvas'); + expect(element.width).toBe(300); + }); + }); + + describe('set width()', () => { + it('Sets the attribute "width".', () => { + const element = document.createElement('canvas'); + element.width = 100; + expect(element.getAttribute('width')).toBe('100'); + }); + }); + + describe('get height()', () => { + it('Returns the "height" attribute.', () => { + const element = document.createElement('canvas'); + element.setAttribute('height', '100'); + expect(element.height).toBe(100); + }); + + it('Returns 150 if the "height" attribute is not set.', () => { + const element = document.createElement('canvas'); + expect(element.height).toBe(150); + }); + }); + + describe('set height()', () => { + it('Sets the attribute "height".', () => { + const element = document.createElement('canvas'); + element.height = 100; + expect(element.getAttribute('height')).toBe('100'); + }); + }); + + describe('captureStream()', () => { + it('Returns a MediaStream.', () => { + element.width = 800; + element.height = 600; + + const stream = element.captureStream(); + + expect(stream instanceof MediaStream).toBe(true); + expect(stream.getAudioTracks()).toEqual([]); + expect(stream.getVideoTracks().length).toBe(1); + expect(stream.getVideoTracks()[0]).toBeInstanceOf(CanvasCaptureMediaStreamTrack); + expect(stream.getVideoTracks()[0].getCapabilities()).toEqual({ + aspectRatio: { + max: 800, + min: 0.006666666666666667 + }, + deviceId: DEVICE_ID, + facingMode: [], + frameRate: { + max: 60, + min: 0 + }, + height: { + max: 600, + min: 1 + }, + resizeMode: ['none', 'crop-and-scale'], + width: { + max: 800, + min: 1 + } + }); + + expect(element.captureStream(30).getVideoTracks()[0].getSettings().frameRate).toBe(30); + expect(element.captureStream(30).getVideoTracks()[0].getCapabilities().frameRate.max).toBe( + 30 + ); + }); + }); + + describe('getContext()', () => { + it('Returns null (not implemented yet).', () => { + for (const type of ['2d', 'webgl', 'webgl2', 'webgpu', 'bitmaprenderer']) { + expect(element.getContext(<'2d'>type)).toBe(null); + expect(element.getContext(<'2d'>type, {})).toBe(null); + } + }); + }); + + describe('toDataURL()', () => { + it('Returns an empty string (not implemented yet).', () => { + expect(element.toDataURL()).toBe(''); + expect(element.toDataURL('2d')).toBe(''); + expect(element.toDataURL('2d', '')).toBe(''); + }); + }); + + describe('toBlob()', () => { + it('Returns an empty Blob (not implemented yet).', () => { + let blob: Blob | null = null; + element.toBlob((b) => (blob = b)); + expect((blob)).toBeInstanceOf(Blob); + + blob = null; + element.toBlob((b) => (blob = b), 'image/png'); + expect((blob)).toBeInstanceOf(Blob); + + blob = null; + element.toBlob((b) => (blob = b), 'image/png', 0.5); + expect((blob)).toBeInstanceOf(Blob); + }); + }); + + describe('transferControlToOffscreen()', () => { + it('Returns an OffscreenCanvas.', () => { + element.width = 800; + element.height = 600; + const offscreenCanvas = element.transferControlToOffscreen(); + expect(offscreenCanvas).toBeInstanceOf(OffscreenCanvas); + expect(offscreenCanvas.width).toBe(800); + expect(offscreenCanvas.height).toBe(600); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/html-canvas-element/ImageBitmap.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/ImageBitmap.test.ts new file mode 100644 index 000000000..3432725c0 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-canvas-element/ImageBitmap.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import ImageBitmap from '../../../src/nodes/html-canvas-element/ImageBitmap.js'; + +describe('ImageBitmap', () => { + describe('get width()', () => { + it('Returns width.', () => { + const imageBitmap = new ImageBitmap(800, 600); + expect(imageBitmap.width).toBe(800); + }); + }); + describe('get height()', () => { + it('Returns height.', () => { + const imageBitmap = new ImageBitmap(800, 600); + expect(imageBitmap.height).toBe(600); + }); + }); + + describe('close()', () => { + it('Does nothing (not implemented yet).', () => { + const imageBitmap = new ImageBitmap(800, 600); + expect(() => imageBitmap.close()).not.toThrow(); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-canvas-element/MediaStream.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/MediaStream.test.ts new file mode 100644 index 000000000..6f70281dd --- /dev/null +++ b/packages/happy-dom/test/nodes/html-canvas-element/MediaStream.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect } from 'vitest'; +import MediaStreamTrack from '../../../src/nodes/html-canvas-element/MediaStreamTrack.js'; +import MediaStream from '../../../src/nodes/html-canvas-element/MediaStream.js'; + +describe('MediaStream', () => { + describe('constructor()', () => { + it('Supports another MediaStream as argument', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + const stream = new MediaStream(); + stream.addTrack(track); + const newStream = new MediaStream(stream); + expect(newStream).toBeInstanceOf(MediaStream); + expect(newStream.getVideoTracks()).toEqual([track]); + }); + + it('Supports an array of MediaStreamTrack as argument', () => { + const track1 = new MediaStreamTrack({ kind: 'video' }); + const track2 = new MediaStreamTrack({ kind: 'video' }); + const newStream = new MediaStream([track1, track2]); + expect(newStream).toBeInstanceOf(MediaStream); + expect(newStream.getVideoTracks()).toEqual([track1, track2]); + }); + }); + + describe('addTrack()', () => { + it('Adds a track.', () => { + const stream = new MediaStream(); + const track = new MediaStreamTrack({ kind: 'audio' }); + stream.addTrack(track); + expect(stream.getAudioTracks()).toEqual([track]); + }); + + it('Does not add the same track twice.', () => { + const stream = new MediaStream(); + const track = new MediaStreamTrack({ kind: 'video' }); + stream.addTrack(track); + stream.addTrack(track); + expect(stream.getVideoTracks()).toEqual([track]); + }); + }); + + describe('clone()', () => { + it('Returns a clone.', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + const stream = new MediaStream(); + stream.addTrack(track); + const clone = stream.clone(); + expect(clone).toBeInstanceOf(MediaStream); + expect(clone.getVideoTracks()).toEqual([track]); + }); + }); + + describe('getAudioTracks()', () => { + it('Returns audio tracks.', () => { + const stream = new MediaStream(); + const audioTrack1 = new MediaStreamTrack({ kind: 'audio' }); + const audioTrack2 = new MediaStreamTrack({ kind: 'audio' }); + const videoTrack = new MediaStreamTrack({ kind: 'video' }); + stream.addTrack(audioTrack1); + stream.addTrack(audioTrack2); + stream.addTrack(videoTrack); + expect(stream.getAudioTracks()).toEqual([audioTrack1, audioTrack2]); + }); + }); + + describe('getTrackById()', () => { + it('Returns track by id.', () => { + const stream = new MediaStream(); + const track1 = new MediaStreamTrack({ kind: 'audio' }); + const track2 = new MediaStreamTrack({ kind: 'audio' }); + const track3 = new MediaStreamTrack({ kind: 'video' }); + stream.addTrack(track1); + stream.addTrack(track2); + stream.addTrack(track3); + expect(stream.getTrackById(track1.id)).toBe(track1); + expect(stream.getTrackById(track2.id)).toBe(track2); + expect(stream.getTrackById(track3.id)).toBe(track3); + }); + }); + + describe('getVideoTracks()', () => { + it('Returns video tracks.', () => { + const stream = new MediaStream(); + const audioTrack = new MediaStreamTrack({ kind: 'audio' }); + const videoTrack1 = new MediaStreamTrack({ kind: 'video' }); + const videoTrack2 = new MediaStreamTrack({ kind: 'video' }); + stream.addTrack(audioTrack); + stream.addTrack(videoTrack1); + stream.addTrack(videoTrack2); + expect(stream.getVideoTracks()).toEqual([videoTrack1, videoTrack2]); + }); + }); + + describe('removeTrack()', () => { + it('Removes a track.', () => { + const stream = new MediaStream(); + const track = new MediaStreamTrack({ kind: 'audio' }); + stream.addTrack(track); + stream.removeTrack(track); + expect(stream.getAudioTracks()).toEqual([]); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-canvas-element/MediaStreamTrack.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/MediaStreamTrack.test.ts new file mode 100644 index 000000000..55b62d068 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-canvas-element/MediaStreamTrack.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect } from 'vitest'; +import MediaStreamTrack from '../../../src/nodes/html-canvas-element/MediaStreamTrack.js'; +import * as PropertySymbol from '../../../src/PropertySymbol.js'; + +describe('MediaStreamTrack', () => { + describe('applyConstraints()', () => { + it('Applies constraints.', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + const constraints = { + width: { min: 640, ideal: 1280 }, + height: { min: 480, ideal: 720 }, + advanced: [{ width: 1920, height: 1280 }, { aspectRatio: 1.333 }] + }; + track.applyConstraints(constraints); + expect(track.getConstraints()).toEqual(constraints); + track.applyConstraints({ width: { min: 300, ideal: 500 } }); + track.applyConstraints({ width: { test: true } }); + expect(track.getConstraints()).toEqual({ + width: { min: 300, ideal: 500, test: true }, + height: { min: 480, ideal: 720 }, + advanced: [{ width: 1920, height: 1280 }, { aspectRatio: 1.333 }] + }); + }); + }); + + describe('getConstrains()', () => { + it('Returns constraints.', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + const constraints = { + width: { min: 640, ideal: 1280 }, + height: { min: 480, ideal: 720 }, + advanced: [{ width: 1920, height: 1280 }, { aspectRatio: 1.333 }] + }; + track.applyConstraints(constraints); + expect(track.getConstraints()).toEqual(constraints); + }); + }); + + describe('getCapabilities()', () => { + it('Returns capabilities.', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + expect(track.getCapabilities()).toEqual({ + aspectRatio: { + max: 300, + min: 0.006666666666666667 + }, + deviceId: '', + facingMode: [], + frameRate: { + max: 60, + min: 0 + }, + height: { + max: 150, + min: 1 + }, + resizeMode: ['none', 'crop-and-scale'], + width: { + max: 300, + min: 1 + } + }); + }); + + it('Is possible to edit object.', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + track[PropertySymbol.capabilities].width.max = 800; + expect(track.getCapabilities().width.max).toBe(800); + }); + }); + + describe('getSettings()', () => { + it('Returns settings.', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + expect(track.getSettings()).toEqual({ + deviceId: '', + frameRate: 60, + resizeMode: 'none' + }); + }); + + it('Is possible to edit object.', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + track[PropertySymbol.settings].frameRate = 30; + expect(track.getSettings().frameRate).toBe(30); + }); + }); + + describe('clone()', () => { + it('Clones the track.', () => { + const track = new MediaStreamTrack({ kind: 'video' }); + const clone = track.clone(); + expect(clone).not.toBe(track); + expect(clone.id).not.toBe(track.id); + expect(clone.label).toBe(track.label); + expect(clone.kind).toBe(track.kind); + expect(clone.muted).toBe(track.muted); + expect(clone.readyState).toBe(track.readyState); + expect(clone.getCapabilities()).toEqual(track.getCapabilities()); + expect(clone.getSettings()).toEqual(track.getSettings()); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/html-canvas-element/OffscreenCanvas.test.ts b/packages/happy-dom/test/nodes/html-canvas-element/OffscreenCanvas.test.ts new file mode 100644 index 000000000..1fef51920 --- /dev/null +++ b/packages/happy-dom/test/nodes/html-canvas-element/OffscreenCanvas.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import OffscreenCanvas from '../../../src/nodes/html-canvas-element/OffscreenCanvas.js'; +import ImageBitmap from '../../../src/nodes/html-canvas-element/ImageBitmap.js'; +import Blob from '../../../src/file/Blob.js'; + +describe('OffscreenCanvas', () => { + describe('get width()', () => { + it('Returns width.', () => { + const offscreenCanvas = new OffscreenCanvas(800, 600); + expect(offscreenCanvas.width).toBe(800); + }); + }); + describe('get height()', () => { + it('Returns height.', () => { + const offscreenCanvas = new OffscreenCanvas(800, 600); + expect(offscreenCanvas.height).toBe(600); + }); + }); + + describe('getContext()', () => { + it('Returns null (not implemented yet).', () => { + const offscreenCanvas = new OffscreenCanvas(800, 600); + for (const type of ['2d', 'webgl', 'webgl2', 'webgpu', 'bitmaprenderer']) { + expect(offscreenCanvas.getContext(<'2d'>type)).toBe(null); + expect(offscreenCanvas.getContext(<'2d'>type, {})).toBe(null); + } + }); + }); + + describe('convertToBlob()', () => { + it('Returns an empty Blob (not implemented yet).', async () => { + const offscreenCanvas = new OffscreenCanvas(800, 600); + + expect(await offscreenCanvas.convertToBlob()).toBeInstanceOf(Blob); + expect(await offscreenCanvas.convertToBlob({ type: 'image/png' })).toBeInstanceOf(Blob); + expect(await offscreenCanvas.convertToBlob({ quality: 0.5 })).toBeInstanceOf(Blob); + }); + }); + + describe('transferToImageBitmap()', () => { + it('Returns an ImageBitmap object.', () => { + const offscreenCanvas = new OffscreenCanvas(800, 600); + const imageBitmap = offscreenCanvas.transferToImageBitmap(); + expect(imageBitmap).toBeInstanceOf(ImageBitmap); + expect(imageBitmap.width).toBe(800); + expect(imageBitmap.height).toBe(600); + }); + }); +}); diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index 732ef853b..d1f354347 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -1379,10 +1379,12 @@ describe('QuerySelector', () => { ) ); expect(() => element.matches(':is')).toThrow( - new Error(`Failed to execute 'matches' on 'HTMLElement': ':is' is not a valid selector.`) + new Error(`Failed to execute 'matches' on 'HTMLDivElement': ':is' is not a valid selector.`) ); expect(() => element.matches(':where')).toThrow( - new Error(`Failed to execute 'matches' on 'HTMLElement': ':where' is not a valid selector.`) + new Error( + `Failed to execute 'matches' on 'HTMLDivElement': ':where' is not a valid selector.` + ) ); expect(() => element.matches('div:not')).toThrow( new Error( From 614bd9df81ad830ba2900fcbb81cfddd152b25fe Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 10 Apr 2024 00:39:34 +0200 Subject: [PATCH 06/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 1 + .../html-data-element/HTMLDataElement.ts | 20 ++++++- .../HTMLDataListElement.ts | 19 ++++++- .../html-input-element/HTMLInputElement.ts | 18 ++++++ .../HTMLLabelElementUtility.ts | 4 +- .../html-option-element/HTMLOptionElement.ts | 31 +++++++++- .../html-slot-element/HTMLSlotElement.ts | 2 +- packages/happy-dom/src/nodes/node/Node.ts | 9 +++ .../test/nodes/element/HTMLCollection.test.ts | 17 +++--- .../html-data-element/HTMLDataElement.test.ts | 15 +++++ .../HTMLDataListElement.test.ts | 57 +++++++++++++++++++ .../HTMLInputElement.test.ts | 45 +++++++++++++++ .../happy-dom/test/nodes/node/Node.test.ts | 5 ++ .../test/query-selector/QuerySelector.test.ts | 17 ++++++ 14 files changed, 245 insertions(+), 15 deletions(-) diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 85b2ccb0a..eacbd9ec6 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -168,3 +168,4 @@ export const tracks = Symbol('tracks'); export const constraints = Symbol('constraints'); export const capabilities = Symbol('capabilities'); export const settings = Symbol('settings'); +export const dataListNode = Symbol('dataListNode'); diff --git a/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts b/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts index 5854d9bd7..65c45d0ec 100644 --- a/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts +++ b/packages/happy-dom/src/nodes/html-data-element/HTMLDataElement.ts @@ -4,4 +4,22 @@ import HTMLElement from '../html-element/HTMLElement.js'; * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataElement */ -export default class HTMLDataElement extends HTMLElement {} +export default class HTMLDataElement extends HTMLElement { + /** + * Returns value. + * + * @returns Value. + */ + public get value(): string { + return this.getAttribute('value') || ''; + } + + /** + * Sets value. + * + * @param value Value. + */ + public set value(value: string) { + this.setAttribute('value', value); + } +} diff --git a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts index d9090ec42..736472519 100644 --- a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts +++ b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts @@ -1,7 +1,24 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import HTMLCollection from '../element/HTMLCollection.js'; +import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; +import Node from '../node/Node.js'; + /** * HTMLDataListElement * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataListElement */ -export default class HTMLDataListElement extends HTMLElement {} +export default class HTMLDataListElement extends HTMLElement { + public [PropertySymbol.options] = new HTMLCollection(); + public [PropertySymbol.dataListNode]: Node = this; + + /** + * Returns options. + * + * @returns Options. + */ + public get options(): HTMLCollection { + return this[PropertySymbol.options]; + } +} diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 566293ce0..39c0a4ee9 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -20,6 +20,9 @@ import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; import HTMLInputElementNamedNodeMap from './HTMLInputElementNamedNodeMap.js'; import PointerEvent from '../../event/events/PointerEvent.js'; import { URL } from 'url'; +import HTMLDataListElement from '../html-data-list-element/HTMLDataListElement.js'; +import Document from '../document/Document.js'; +import ShadowRoot from '../shadow-root/ShadowRoot.js'; /** * HTML Input Element. @@ -1102,6 +1105,21 @@ export default class HTMLInputElement extends HTMLElement { return HTMLLabelElementUtility.getAssociatedLabelElements(this); } + /** + * Returns associated datalist element. + * + * @returns Data list element. + */ + public get list(): HTMLDataListElement | null { + const id = this.getAttribute('list'); + if (!id) { + return null; + } + const rootNode = + this[PropertySymbol.rootNode] || this[PropertySymbol.ownerDocument]; + return rootNode.querySelector(`datalist#${id}`); + } + /** * Sets validation message. * diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts index 5ef11305a..59e719290 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts @@ -19,7 +19,9 @@ export default class HTMLLabelElementUtility { const id = element.id; let labels: NodeList; if (id) { - const rootNode = element.getRootNode(); + const rootNode = + element[PropertySymbol.rootNode] || + element[PropertySymbol.ownerDocument]; labels = >rootNode.querySelectorAll(`label[for="${id}"]`); } else { labels = new NodeList(); diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index 3381497ca..8afede915 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -1,5 +1,6 @@ import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; import * as PropertySymbol from '../../PropertySymbol.js'; +import HTMLDataListElement from '../html-data-list-element/HTMLDataListElement.js'; import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; @@ -127,15 +128,39 @@ export default class HTMLOptionElement extends HTMLElement { */ public override [PropertySymbol.connectToNode](parentNode: Node = null): void { const oldSelectNode = this[PropertySymbol.selectNode]; + const oldDataListNode = this[PropertySymbol.dataListNode]; super[PropertySymbol.connectToNode](parentNode); - if (oldSelectNode !== this[PropertySymbol.selectNode]) { + const selectNode = this[PropertySymbol.selectNode]; + + if (oldSelectNode !== selectNode) { if (oldSelectNode) { oldSelectNode[PropertySymbol.updateOptionItems](); } - if (this[PropertySymbol.selectNode]) { - (this[PropertySymbol.selectNode])[PropertySymbol.updateOptionItems](); + if (selectNode) { + selectNode[PropertySymbol.updateOptionItems](); + } + } + + const dataListNode = this[PropertySymbol.dataListNode]; + + if (oldDataListNode !== dataListNode) { + const name = this.getAttribute('name'); + const id = this.id; + if (oldDataListNode) { + const index = oldDataListNode[PropertySymbol.options].indexOf(this); + if (index !== -1) { + oldDataListNode[PropertySymbol.options].splice(index, 1); + } + + oldDataListNode[PropertySymbol.options][PropertySymbol.removeNamedItem](this, name); + oldDataListNode[PropertySymbol.options][PropertySymbol.removeNamedItem](this, id); + } + if (dataListNode) { + dataListNode[PropertySymbol.options].push(this); + dataListNode[PropertySymbol.options][PropertySymbol.appendNamedItem](this, name); + dataListNode[PropertySymbol.options][PropertySymbol.appendNamedItem](this, id); } } } diff --git a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts index 123a8e3a8..791c56be0 100644 --- a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts +++ b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts @@ -54,7 +54,7 @@ export default class HTMLSlotElement extends HTMLElement { * @returns Nodes. */ public assignedNodes(options?: { flatten?: boolean }): Node[] { - const host = (this.getRootNode())?.host; + const host = (this[PropertySymbol.rootNode])?.host; // TODO: Add support for options.flatten. We need to find an example of how it is expected to work before it can be implemented. diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 98094c4b9..a8be2c28e 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -59,6 +59,7 @@ export default class Node extends EventTarget { public [PropertySymbol.nodeType]: NodeTypeEnum; public [PropertySymbol.rootNode]: Node = null; public [PropertySymbol.formNode]: Node = null; + public [PropertySymbol.dataListNode]: Node = null; public [PropertySymbol.selectNode]: Node = null; public [PropertySymbol.textAreaNode]: Node = null; public [PropertySymbol.observers]: MutationListener[] = []; @@ -514,6 +515,7 @@ export default class Node extends EventTarget { public [PropertySymbol.connectToNode](parentNode: Node = null): void { const isConnected = !!parentNode && parentNode[PropertySymbol.isConnected]; const formNode = (this)[PropertySymbol.formNode]; + const dataListNode = (this)[PropertySymbol.dataListNode]; const selectNode = (this)[PropertySymbol.selectNode]; const textAreaNode = (this)[PropertySymbol.textAreaNode]; @@ -528,6 +530,12 @@ export default class Node extends EventTarget { : null; } + if (this['tagName'] !== 'DATALIST') { + (this)[PropertySymbol.dataListNode] = parentNode + ? (parentNode)[PropertySymbol.dataListNode] + : null; + } + if (this['tagName'] !== 'SELECT') { (this)[PropertySymbol.selectNode] = parentNode ? (parentNode)[PropertySymbol.selectNode] @@ -567,6 +575,7 @@ export default class Node extends EventTarget { } } else if ( formNode !== this[PropertySymbol.formNode] || + dataListNode !== this[PropertySymbol.dataListNode] || selectNode !== this[PropertySymbol.selectNode] || textAreaNode !== this[PropertySymbol.textAreaNode] ) { diff --git a/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts b/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts index 909518101..0d76d2a43 100644 --- a/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts +++ b/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts @@ -1,6 +1,7 @@ import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; -import { beforeEach, afterEach, describe, it, expect } from 'vitest'; +import { beforeEach, describe, it, expect } from 'vitest'; +import HTMLElement from '../../../src/nodes/html-element/HTMLElement.js'; describe('HTMLCollection', () => { let window: Window; @@ -76,10 +77,10 @@ describe('HTMLCollection', () => { it('Supports attributes only consisting of numbers.', () => { const div = document.createElement('div'); div.innerHTML = `
`; - const container1 = div.querySelector('.container1'); - const container2 = div.querySelector('.container2'); - const container3 = div.querySelector('.container3'); - const container4 = div.querySelector('.container4'); + const container1 = div.querySelector('.container1'); + const container2 = div.querySelector('.container2'); + const container3 = div.querySelector('.container3'); + const container4 = div.querySelector('.container4'); expect(div.children.length).toBe(4); expect(div.children[0] === container1).toBe(true); @@ -118,9 +119,9 @@ describe('HTMLCollection', () => { it('Supports attributes that has the same name as properties and methods of the HTMLCollection class.', () => { const div = document.createElement('div'); div.innerHTML = `
`; - const container1 = div.querySelector('.container1'); - const container2 = div.querySelector('.container2'); - const container3 = div.querySelector('.container3'); + const container1 = div.querySelector('.container1'); + const container2 = div.querySelector('.container2'); + const container3 = div.querySelector('.container3'); expect(div.children.length).toBe(3); expect(div.children[0] === container1).toBe(true); diff --git a/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts b/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts index 926f367e4..6c360ebd9 100644 --- a/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts +++ b/packages/happy-dom/test/nodes/html-data-element/HTMLDataElement.test.ts @@ -19,4 +19,19 @@ describe('HTMLDataElement', () => { expect(element instanceof HTMLDataElement).toBe(true); }); }); + + describe('get value()', () => { + it('Should return value', () => { + expect(element.value).toBe(''); + element.setAttribute('value', 'test'); + expect(element.value).toBe('test'); + }); + }); + + describe('set value()', () => { + it('Should set value', () => { + element.value = 'test'; + expect(element.getAttribute('value')).toBe('test'); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts b/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts index 42881f210..d9eb918ee 100644 --- a/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts +++ b/packages/happy-dom/test/nodes/html-data-list-element/HTMLDataListElement.test.ts @@ -2,6 +2,7 @@ import HTMLDataListElement from '../../../src/nodes/html-data-list-element/HTMLD import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; import { beforeEach, describe, it, expect } from 'vitest'; +import HTMLCollection from '../../../src/nodes/element/HTMLCollection.js'; describe('HTMLDataListElement', () => { let window: Window; @@ -19,4 +20,60 @@ describe('HTMLDataListElement', () => { expect(element instanceof HTMLDataListElement).toBe(true); }); }); + + describe('get options()', () => { + it('Should return options', () => { + expect(element.options).toBeInstanceOf(HTMLCollection); + expect(element.options.length).toBe(0); + + const option1 = document.createElement('option'); + const option2 = document.createElement('option'); + const option3 = document.createElement('option'); + + option3.setAttribute('id', 'option3_id'); + option3.setAttribute('name', 'option3_name'); + + element.appendChild(option1); + element.appendChild(option2); + element.appendChild(option3); + + expect(element.options.length).toBe(3); + + expect(element.options[0]).toBe(option1); + expect(element.options[1]).toBe(option2); + expect(element.options[2]).toBe(option3); + + expect(element.options['option3_id']).toBe(option3); + expect(element.options['option3_name']).toBe(option3); + + element.removeChild(option2); + + expect(element.options.length).toBe(2); + + expect(element.options[0]).toBe(option1); + expect(element.options[1]).toBe(option3); + + expect(element.options['option3_id']).toBe(option3); + expect(element.options['option3_name']).toBe(option3); + + element.removeChild(option3); + + expect(element.options.length).toBe(1); + + expect(element.options[0]).toBe(option1); + + expect(element.options['option3_id']).toBe(undefined); + expect(element.options['option3_name']).toBe(undefined); + + element.appendChild(option3); + + expect(element.options.length).toBe(2); + + expect(element.options[0]).toBe(option1); + expect(element.options[1]).toBe(option3); + + expect(element.options['option3_id']).toBe(option3); + expect(element.options['option3_name']).toBe(option3); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts index 8ea799d52..6a6e45604 100644 --- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts +++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts @@ -630,6 +630,51 @@ describe('HTMLInputElement', () => { }); }); + describe('get list()', () => { + it('Returns null if the attribute "list" is not set.', () => { + expect(element.list).toBe(null); + }); + + it('Returns null if no associated list element matches the attribuge "list".', () => { + element.setAttribute('list', 'datalist'); + expect(element.list).toBe(null); + }); + + it('Returns the associated datalist element.', () => { + const datalist = document.createElement('datalist'); + datalist.id = 'list_id'; + document.body.appendChild(datalist); + element.setAttribute('list', 'list_id'); + expect(element.list).toBe(datalist); + }); + + it('Finds datalist inside a shadowroot.', () => { + /* eslint-disable-next-line jsdoc/require-jsdoc */ + class MyComponent extends window.HTMLElement { + /* eslint-disable-next-line jsdoc/require-jsdoc */ + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + if (this.shadowRoot) { + this.shadowRoot.innerHTML = ` + + + + `; + } + } + } + window.customElements.define('my-component', MyComponent); + const component = document.createElement('my-component'); + document.body.appendChild(component); + const input = component.shadowRoot?.querySelector('input'); + const list = component.shadowRoot?.querySelector('datalist'); + expect(input?.list === list).toBe(true); + }); + }); + describe('set selectionEnd()', () => { it('Sets the value to the length of the property "value" if it is out of range.', () => { element.setAttribute('value', 'TEST_VALUE'); diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts index 8a4efe804..905c86a06 100644 --- a/packages/happy-dom/test/nodes/node/Node.test.ts +++ b/packages/happy-dom/test/nodes/node/Node.test.ts @@ -374,6 +374,11 @@ describe('Node', () => { it('Returns Document when called on Document', () => { expect(document.getRootNode() === document).toBe(true); }); + + it('Returns self when element is not connected to DOM', () => { + const element = document.createElement('div'); + expect(element.getRootNode() === element).toBe(true); + }); }); describe('cloneNode()', () => { diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index d1f354347..d7a5747fb 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -1297,6 +1297,23 @@ describe('QuerySelector', () => { expect(container.querySelector(':where(div)')).toBe(container.children[0]); expect(container.querySelector(':where(span[attr1="val,ue1"])')).toBe(null); }); + + it('Returns element matching selector "datalist#id"', () => { + const div = document.createElement('div'); + const datalist = document.createElement('datalist'); + const span = document.createElement('span'); + + datalist.id = 'datalist_id'; + span.id = 'span_id'; + + div.appendChild(datalist); + div.appendChild(span); + + expect(div.querySelector('datalist#span_id') === null).toBe(true); + expect(div.querySelector('datalist#datalist_id') === datalist).toBe(true); + expect(div.querySelector('span#datalist_id') === null).toBe(true); + expect(div.querySelector('span#span_id') === span).toBe(true); + }); }); describe('matches()', () => { From aa67746540b6f4db679a398d91280d845c2887ae Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 10 Apr 2024 19:52:55 +0200 Subject: [PATCH 07/51] fix: [#1332] Return URL relative to window location in HTMLIFrameElement.src --- packages/happy-dom/src/PropertySymbol.ts | 1 + .../src/named-node-map/NamedNodeMap.ts | 20 ++++- .../src/nodes/element/ElementNamedNodeMap.ts | 4 +- .../HTMLButtonElementNamedNodeMap.ts | 4 +- .../HTMLDetailsElement.ts | 34 +++++++- .../HTMLDetailsElementNamedNodeMap.ts | 42 +++++++++ .../html-dialog-element/HTMLDialogElement.ts | 3 +- .../html-element/HTMLElementNamedNodeMap.ts | 4 +- .../html-embed-element/HTMLEmbedElement.ts | 85 ++++++++++++++++++- .../HTMLHyperlinkElementNamedNodeMap.ts | 4 +- .../html-iframe-element/HTMLIFrameElement.ts | 11 ++- .../HTMLIFrameElementNamedNodeMap.ts | 4 +- .../HTMLInputElementNamedNodeMap.ts | 4 +- .../HTMLLinkElementNamedNodeMap.ts | 4 +- .../HTMLOptionElementNamedNodeMap.ts | 4 +- .../HTMLScriptElementNamedNodeMap.ts | 4 +- .../HTMLSelectElementNamedNodeMap.ts | 4 +- .../HTMLTextAreaElementNamedNodeMap.ts | 4 +- .../svg-element/SVGElementNamedNodeMap.ts | 4 +- .../HTMLDetailsElement.test.ts | 35 +++++++- .../HTMLEmbedElement.test.ts | 36 ++++++++ .../HTMLIFrameElement.test.ts | 22 ++++- 22 files changed, 303 insertions(+), 34 deletions(-) create mode 100644 packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElementNamedNodeMap.ts diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index eacbd9ec6..1dfa37589 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -169,3 +169,4 @@ export const constraints = Symbol('constraints'); export const capabilities = Symbol('capabilities'); export const settings = Symbol('settings'); export const dataListNode = Symbol('dataListNode'); +export const setNamedItem = Symbol('setNamedItem'); diff --git a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts index d1c76a54b..5fb0f942a 100644 --- a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts +++ b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts @@ -86,7 +86,7 @@ export default class NamedNodeMap { * @returns Replaced item. */ public setNamedItem(item: Attr): Attr | null { - return this[PropertySymbol.setNamedItemWithoutConsequences](item); + return this[PropertySymbol.setNamedItem](item); } /** @@ -97,7 +97,7 @@ export default class NamedNodeMap { * @returns Replaced item. */ public setNamedItemNS(item: Attr): Attr | null { - return this.setNamedItem(item); + return this[PropertySymbol.setNamedItem](item); } /** @@ -134,7 +134,19 @@ export default class NamedNodeMap { } /** - * Sets named item without calling listeners for certain attributes. + * Sets named item. + * + * This method may be overridden by subclasses to act on attribute changes. + * + * @param item Item. + * @returns Replaced item. + */ + public [PropertySymbol.setNamedItem](item: Attr): Attr | null { + return this[PropertySymbol.setNamedItemWithoutConsequences](item); + } + + /** + * Sets named item without potential overrides done in [PropertySymbol.setNamedItem](). * * @param item Item. * @returns Replaced item. @@ -164,6 +176,8 @@ export default class NamedNodeMap { /** * Removes an item without throwing if it doesn't exist. * + * This method may be overridden by subclasses to act on attribute changes. + * * @param name Name of item. * @returns Removed item, or null if it didn't exist. */ diff --git a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts index 34384b253..d21b02350 100644 --- a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts @@ -43,7 +43,7 @@ export default class ElementNamedNodeMap extends NamedNodeMap { /** * @override */ - public override setNamedItem(item: Attr): Attr | null { + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { if (!item[PropertySymbol.name]) { return null; } @@ -51,7 +51,7 @@ export default class ElementNamedNodeMap extends NamedNodeMap { item[PropertySymbol.name] = this[PropertySymbol.getAttributeName](item[PropertySymbol.name]); (item[PropertySymbol.ownerElement]) = this[PropertySymbol.ownerElement]; - const replacedItem = super.setNamedItem(item); + const replacedItem = super[PropertySymbol.setNamedItem](item); const oldValue = replacedItem ? replacedItem[PropertySymbol.value] : null; if (this[PropertySymbol.ownerElement][PropertySymbol.isConnected]) { diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts index bb188ffbb..acc458ca2 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts @@ -15,8 +15,8 @@ export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') && diff --git a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts index 3bf7e0954..9e2cc05a8 100644 --- a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts +++ b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts @@ -1,7 +1,39 @@ +import Event from '../../event/Event.js'; import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; +import HTMLDetailsElementNamedNodeMap from './HTMLDetailsElementNamedNodeMap.js'; + /** * HTMLDetailsElement * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDetailsElement */ -export default class HTMLDetailsElement extends HTMLElement {} +export default class HTMLDetailsElement extends HTMLElement { + public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLDetailsElementNamedNodeMap( + this + ); + + // Events + public ontoggle: (event: Event) => void | null = null; + + /** + * Returns the open attribute. + */ + public get open(): boolean { + return this.getAttribute('open') !== null; + } + + /** + * Sets the open attribute. + * + * @param open New value. + */ + public set open(open: boolean) { + if (open) { + this.setAttribute('open', ''); + } else { + this.removeAttribute('open'); + } + } +} diff --git a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElementNamedNodeMap.ts new file mode 100644 index 000000000..00e6f9560 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElementNamedNodeMap.ts @@ -0,0 +1,42 @@ +import Attr from '../attr/Attr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; +import Event from '../../event/Event.js'; +import HTMLDetailsElement from './HTMLDetailsElement.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class HTMLDetailsElementNamedNodeMap extends HTMLElementNamedNodeMap { + protected [PropertySymbol.ownerElement]: HTMLDetailsElement; + + /** + * @override + */ + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); + + if (item[PropertySymbol.name] === 'open') { + if (item[PropertySymbol.value] !== replacedItem?.[PropertySymbol.value]) { + this[PropertySymbol.ownerElement].dispatchEvent(new Event('toggle')); + } + } + + return replacedItem || null; + } + + /** + * @override + */ + public override [PropertySymbol.removeNamedItem](name: string): Attr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); + + if (removedItem && removedItem[PropertySymbol.name] === 'open') { + this[PropertySymbol.ownerElement].dispatchEvent(new Event('toggle')); + } + + return removedItem; + } +} diff --git a/packages/happy-dom/src/nodes/html-dialog-element/HTMLDialogElement.ts b/packages/happy-dom/src/nodes/html-dialog-element/HTMLDialogElement.ts index 33f8f244c..d5008ed85 100644 --- a/packages/happy-dom/src/nodes/html-dialog-element/HTMLDialogElement.ts +++ b/packages/happy-dom/src/nodes/html-dialog-element/HTMLDialogElement.ts @@ -5,8 +5,7 @@ import * as PropertySymbol from '../../PropertySymbol.js'; /** * HTML Dialog Element. * - * Reference: - * https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement. + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement */ export default class HTMLDialogElement extends HTMLElement { // Internal properties diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts index 7349e1481..f915e7cd7 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts @@ -14,8 +14,8 @@ export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( item[PropertySymbol.name] === 'style' && diff --git a/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts b/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts index 93bf31f01..37487fcdc 100644 --- a/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts +++ b/packages/happy-dom/src/nodes/html-embed-element/HTMLEmbedElement.ts @@ -1,7 +1,90 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; + /** * HTMLEmbedElement * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLEmbedElement */ -export default class HTMLEmbedElement extends HTMLElement {} +export default class HTMLEmbedElement extends HTMLElement { + /** + * Returns height. + * + * @returns Height. + */ + public get height(): string { + return this.getAttribute('height') || ''; + } + + /** + * Sets height. + * + * @param height Height. + */ + public set height(height: string) { + this.setAttribute('height', height); + } + + /** + * Returns width. + * + * @returns Width. + */ + public get width(): string { + return this.getAttribute('width') || ''; + } + + /** + * Sets width. + * + * @param width Width. + */ + public set width(width: string) { + this.setAttribute('width', width); + } + + /** + * Returns source. + * + * @returns Source. + */ + public get src(): string { + if (!this.hasAttribute('src')) { + return ''; + } + + try { + return new URL(this.getAttribute('src'), this[PropertySymbol.ownerDocument].location.href) + .href; + } catch (e) { + return this.getAttribute('src'); + } + } + + /** + * Sets source. + * + * @param src Source. + */ + public set src(src: string) { + this.setAttribute('src', src); + } + + /** + * Returns type. + * + * @returns Type. + */ + public get type(): string { + return this.getAttribute('type') || ''; + } + + /** + * Sets type. + * + * @param type Type. + */ + public set type(type: string) { + this.setAttribute('type', type); + } +} diff --git a/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts index 5a884640a..771c64f0f 100644 --- a/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts @@ -15,8 +15,8 @@ export default class HTMLHyperlinkElementNamedNodeMap extends HTMLElementNamedNo /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( item[PropertySymbol.name] === 'rel' && diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 11b00bf00..d2568befe 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -56,7 +56,16 @@ export default class HTMLIFrameElement extends HTMLElement { * @returns Source. */ public get src(): string { - return this.getAttribute('src') || ''; + if (!this.hasAttribute('src')) { + return ''; + } + + try { + return new URL(this.getAttribute('src'), this[PropertySymbol.ownerDocument].location.href) + .href; + } catch (e) { + return this.getAttribute('src'); + } } /** diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts index 28a8be90e..7e8013ad8 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts @@ -43,8 +43,8 @@ export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedAttribute = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedAttribute = super[PropertySymbol.setNamedItem](item); if ( item[PropertySymbol.name] === 'src' && diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts index 324c95357..8d2b3a8bf 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts @@ -15,8 +15,8 @@ export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMa /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') && diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts index 7e8fbd653..063ef4f4d 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts @@ -28,8 +28,8 @@ export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( item[PropertySymbol.name] === 'rel' && diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts index 3ffe72d2b..7446a6e5b 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts @@ -15,8 +15,8 @@ export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( !this[PropertySymbol.ownerElement][PropertySymbol.dirtyness] && diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts index 0a5cbd48f..a9fa2c5ad 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts @@ -27,8 +27,8 @@ export default class HTMLScriptElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( item[PropertySymbol.name] === 'src' && diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts index 8ba99d9cb..4ed796366 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts @@ -15,8 +15,8 @@ export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') && diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts index 3bbfe257b..dde3f294f 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts @@ -15,8 +15,8 @@ export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNod /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') && diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts index 5315570ec..d8d096885 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts @@ -14,8 +14,8 @@ export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { /** * @override */ - public override setNamedItem(item: Attr): Attr | null { - const replacedItem = super.setNamedItem(item); + public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { + const replacedItem = super[PropertySymbol.setNamedItem](item); if ( item[PropertySymbol.name] === 'style' && diff --git a/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts b/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts index 2124697e5..0d1eb5925 100644 --- a/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts +++ b/packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts @@ -1,7 +1,8 @@ import HTMLDetailsElement from '../../../src/nodes/html-details-element/HTMLDetailsElement.js'; import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; -import { beforeEach, describe, it, expect } from 'vitest'; +import { beforeEach, describe, it, expect, vi } from 'vitest'; +import Event from '../../../src/event/Event.js'; describe('HTMLDetailsElement', () => { let window: Window; @@ -19,4 +20,36 @@ describe('HTMLDetailsElement', () => { expect(element instanceof HTMLDetailsElement).toBe(true); }); }); + + describe('get open()', () => { + it('Should return false by default', () => { + expect(element.open).toBe(false); + }); + + it('Should return true if the "open" attribute has been set', () => { + element.setAttribute('open', ''); + expect(element.open).toBe(true); + }); + }); + + describe('set open()', () => { + it('Should set the "open" attribute', () => { + element.open = true; + expect(element.hasAttribute('open')).toBe(true); + element.open = false; + expect(element.hasAttribute('open')).toBe(false); + }); + }); + + describe('dispatchEvent()', () => { + it('Should dispatch a "toggle" event when the "open" attribute is set', () => { + let triggeredEvent: Event | null = null; + element.addEventListener('toggle', (event) => (triggeredEvent = event)); + element.open = true; + expect(((triggeredEvent)).type).toBe('toggle'); + triggeredEvent = null; + element.open = false; + expect(((triggeredEvent)).type).toBe('toggle'); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts b/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts index fe6677a67..8acb448ad 100644 --- a/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts +++ b/packages/happy-dom/test/nodes/html-embed-element/HTMLEmbedElement.test.ts @@ -19,4 +19,40 @@ describe('HTMLEmbedElement', () => { expect(element instanceof HTMLEmbedElement).toBe(true); }); }); + + for (const property of ['height', 'width', 'type']) { + describe(`get ${property}()`, () => { + it(`Returns the "${property}" attribute.`, () => { + element.setAttribute(property, 'value'); + expect(element[property]).toBe('value'); + }); + }); + + describe(`set ${property}()`, () => { + it(`Sets the attribute "${property}".`, () => { + element[property] = 'value'; + expect(element.getAttribute(property)).toBe('value'); + }); + }); + } + + describe('get src()', () => { + it('Returns the "src" attribute.', () => { + element.setAttribute('src', 'test'); + expect(element.src).toBe('test'); + }); + + it('Returns URL relative to window location.', () => { + window.happyDOM.setURL('https://localhost:8080/test/path/'); + element.setAttribute('src', 'test'); + expect(element.src).toBe('https://localhost:8080/test/path/test'); + }); + }); + + describe('set src()', () => { + it('Sets the attribute "src".', () => { + element.src = 'test'; + expect(element.getAttribute('src')).toBe('test'); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts index 74d750243..a8ae1b02e 100644 --- a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts +++ b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts @@ -35,7 +35,7 @@ describe('HTMLIFrameElement', () => { }); }); - for (const property of ['src', 'allow', 'height', 'width', 'name', 'srcdoc']) { + for (const property of ['allow', 'height', 'width', 'name', 'srcdoc']) { describe(`get ${property}()`, () => { it(`Returns the "${property}" attribute.`, () => { element.setAttribute(property, 'value'); @@ -51,6 +51,26 @@ describe('HTMLIFrameElement', () => { }); } + describe('get src()', () => { + it('Returns the "src" attribute.', () => { + element.setAttribute('src', 'test'); + expect(element.src).toBe('test'); + }); + + it('Returns URL relative to window location.', () => { + window.happyDOM.setURL('https://localhost:8080/test/path/'); + element.setAttribute('src', 'test'); + expect(element.src).toBe('https://localhost:8080/test/path/test'); + }); + }); + + describe('set src()', () => { + it('Sets the attribute "src".', () => { + element.src = 'test'; + expect(element.getAttribute('src')).toBe('test'); + }); + }); + describe('get sandbox()', () => { it('Returns DOMTokenList', () => { expect(element.sandbox).toBeInstanceOf(DOMTokenList); From 8aac0a1d71db24ca6e91180e90ca8339dbccc007 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sat, 4 May 2024 00:59:59 +0200 Subject: [PATCH 08/51] chore: [#1332] Continues on implementation --- packages/global-registrator/tsconfig.json | 2 +- packages/happy-dom/src/PropertySymbol.ts | 37 +- .../AbstractCSSStyleDeclaration.ts | 23 +- .../happy-dom/src/dom-parser/DOMParser.ts | 19 +- packages/happy-dom/src/form-data/FormData.ts | 4 +- packages/happy-dom/src/index.ts | 2 +- .../src/named-node-map/NamedNodeMap.ts | 246 ---------- packages/happy-dom/src/nodes/attr/Attr.ts | 3 + .../src/nodes/child-node/ChildNodeUtility.ts | 20 +- .../document-fragment/DocumentFragment.ts | 70 +-- .../happy-dom/src/nodes/document/Document.ts | 93 ++-- .../src/nodes/element/DatasetFactory.ts | 7 +- .../happy-dom/src/nodes/element/Element.ts | 287 ++++++++---- .../src/nodes/element/ElementNamedNodeMap.ts | 241 ---------- .../src/nodes/element/ElementUtility.ts | 217 --------- .../src/nodes/element/HTMLCollection.ts | 441 ++++++++++++++++-- .../src/nodes/element/IHTMLCollection.ts | 137 ++++++ .../src/nodes/element/NamedNodeMap.ts | 333 +++++++++++++ .../nodes/element/THTMLCollectionListener.ts | 7 + .../nodes/element/TNamedNodeMapListener.ts | 4 + .../html-anchor-element/HTMLAnchorElement.ts | 42 +- .../html-area-element/HTMLAreaElement.ts | 42 +- .../html-button-element/HTMLButtonElement.ts | 72 ++- .../HTMLButtonElementNamedNodeMap.ts | 58 --- .../HTMLDataListElement.ts | 2 +- .../HTMLDetailsElement.ts | 47 +- .../HTMLDetailsElementNamedNodeMap.ts | 42 -- .../src/nodes/html-element/HTMLElement.ts | 146 ++++-- .../html-element/HTMLElementNamedNodeMap.ts | 46 -- .../HTMLFieldSetElement.ts | 183 +++++++- .../HTMLFormControlsCollection.ts | 237 ++++++---- .../html-form-element/HTMLFormElement.ts | 254 ++++++++-- .../nodes/html-form-element/RadioNodeList.ts | 8 +- .../THTMLFormControlElement.ts | 14 + .../HTMLHyperlinkElementNamedNodeMap.ts | 46 -- .../html-iframe-element/HTMLIFrameElement.ts | 201 +++++++- .../HTMLIFrameElementNamedNodeMap.ts | 102 ---- .../HTMLIFrameElementPageLoader.ts | 109 ----- .../html-input-element/HTMLInputElement.ts | 72 ++- .../HTMLInputElementNamedNodeMap.ts | 58 --- .../html-label-element/HTMLLabelElement.ts | 36 +- .../HTMLLabelElementUtility.ts | 2 +- .../html-link-element/HTMLLinkElement.ts | 141 +++++- .../HTMLLinkElementNamedNodeMap.ts | 72 --- .../HTMLLinkElementStyleSheetLoader.ts | 110 ----- .../html-option-element/HTMLOptionElement.ts | 71 ++- .../HTMLOptionElementNamedNodeMap.ts | 64 --- .../html-script-element/HTMLScriptElement.ts | 152 +++++- .../HTMLScriptElementNamedNodeMap.ts | 43 -- .../HTMLScriptElementScriptLoader.ts | 132 ------ .../HTMLOptionsCollection.ts | 84 +++- .../html-select-element/HTMLSelectElement.ts | 118 ++--- .../HTMLSelectElementNamedNodeMap.ts | 58 --- .../html-slot-element/HTMLSlotElement.ts | 4 +- .../html-style-element/HTMLStyleElement.ts | 17 +- .../HTMLTextAreaElement.ts | 75 +-- .../HTMLTextAreaElementNamedNodeMap.ts | 58 --- .../happy-dom/src/nodes/node/INodeList.ts | 156 +++++++ packages/happy-dom/src/nodes/node/Node.ts | 345 ++++++++++---- packages/happy-dom/src/nodes/node/NodeList.ts | 214 ++++++++- .../happy-dom/src/nodes/node/NodeUtility.ts | 212 --------- .../src/nodes/node/TNodeListListener.ts | 2 + .../src/nodes/parent-node/IParentNode.ts | 2 +- .../nodes/parent-node/ParentNodeUtility.ts | 33 +- .../src/nodes/svg-element/SVGElement.ts | 41 +- .../svg-element/SVGElementNamedNodeMap.ts | 46 -- packages/happy-dom/src/nodes/text/Text.ts | 18 - .../src/query-selector/QuerySelector.ts | 39 +- .../src/query-selector/SelectorItem.ts | 4 +- .../happy-dom/src/window/BrowserWindow.ts | 4 +- .../CSSStyleDeclarationElementStyle.test.ts | 1 - .../element}/NamedNodeMap.test.ts | 14 +- .../HTMLButtonElement.test.ts | 13 +- .../HTMLFieldSetElement.test.ts | 65 +++ .../HTMLInputElement.test.ts | 13 +- .../HTMLLabelElement.test.ts | 18 +- .../HTMLTextAreaElement.test.ts | 25 + packages/happy-dom/tsconfig.json | 4 +- packages/jest-environment/test/tsconfig.json | 4 +- packages/jest-environment/tsconfig.json | 4 +- 80 files changed, 3621 insertions(+), 2867 deletions(-) delete mode 100644 packages/happy-dom/src/named-node-map/NamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/element/ElementUtility.ts create mode 100644 packages/happy-dom/src/nodes/element/IHTMLCollection.ts create mode 100644 packages/happy-dom/src/nodes/element/NamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/element/THTMLCollectionListener.ts create mode 100644 packages/happy-dom/src/nodes/element/TNamedNodeMapListener.ts delete mode 100644 packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/html-form-element/THTMLFormControlElement.ts delete mode 100644 packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts delete mode 100644 packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts delete mode 100644 packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts delete mode 100644 packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts delete mode 100644 packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts create mode 100644 packages/happy-dom/src/nodes/node/INodeList.ts create mode 100644 packages/happy-dom/src/nodes/node/TNodeListListener.ts delete mode 100644 packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts rename packages/happy-dom/test/{named-node-map => nodes/element}/NamedNodeMap.test.ts (91%) diff --git a/packages/global-registrator/tsconfig.json b/packages/global-registrator/tsconfig.json index 73f85b24d..407ec16ff 100644 --- a/packages/global-registrator/tsconfig.json +++ b/packages/global-registrator/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "target": "ES2020", + "target": "ES2022", "declaration": true, "declarationMap": true, "module": "Node16", diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 1dfa37589..80bedb4dc 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -1,7 +1,5 @@ export const abort = Symbol('abort'); export const activeElement = Symbol('activeElement'); -export const appendFormControlItem = Symbol('appendFormControlItem'); -export const appendNamedItem = Symbol('appendNamedItem'); export const asyncTaskManager = Symbol('asyncTaskManager'); export const bodyBuffer = Symbol('bodyBuffer'); export const buffer = Symbol('buffer'); @@ -14,7 +12,8 @@ export const childNodes = Symbol('childNodes'); export const children = Symbol('children'); export const classList = Symbol('classList'); export const computedStyle = Symbol('computedStyle'); -export const connectToNode = Symbol('connectToNode'); +export const connectedToDocument = Symbol('connectedToDocument'); +export const disconnectedFromDocument = Symbol('disconnectedFromDocument'); export const contentLength = Symbol('contentLength'); export const contentType = Symbol('contentType'); export const cssText = Symbol('cssText'); @@ -53,16 +52,11 @@ export const readyStateManager = Symbol('readyStateManager'); export const referrer = Symbol('referrer'); export const registry = Symbol('registry'); export const relList = Symbol('relList'); -export const removeFormControlItem = Symbol('removeFormControlItem'); -export const removeNamedItem = Symbol('removeNamedItem'); -export const removeNamedItemIndex = Symbol('removeNamedItemIndex'); -export const removeNamedItemWithoutConsequences = Symbol('removeNamedItemWithoutConsequences'); export const resetSelection = Symbol('resetSelection'); export const rootNode = Symbol('rootNode'); export const selectNode = Symbol('selectNode'); export const selectedness = Symbol('selectedness'); export const selection = Symbol('selection'); -export const setNamedItemWithoutConsequences = Symbol('setNamedItemWithoutConsequences'); export const setupVMContext = Symbol('setupVMContext'); export const shadowRoot = Symbol('shadowRoot'); export const start = Symbol('start'); @@ -170,3 +164,30 @@ export const capabilities = Symbol('capabilities'); export const settings = Symbol('settings'); export const dataListNode = Symbol('dataListNode'); export const setNamedItem = Symbol('setNamedItem'); +export const fieldSetNode = Symbol('fieldSetNode'); +export const addRemoveListener = Symbol('addRemoveListener'); +export const addSetListener = Symbol('addSetListener'); +export const removeSetListener = Symbol('removeSetListener'); +export const removeRemoveListener = Symbol('removeRemoveListener'); +export const appendFormControlItemByName = Symbol('appendFormControlItemByName'); +export const removeFormControlItemByName = Symbol('removeFormControlItemByName'); +export const clone = Symbol('clone'); +export const addItem = Symbol('addItem'); +export const addNamedItem = Symbol('addNamedItem'); +export const removeItem = Symbol('removeItem'); +export const removeNamedItem = Symbol('removeNamedItem'); +export const items = Symbol('items'); +export const removeItemIndex = Symbol('removeItemIndex'); +export const indexOf = Symbol('indexOf'); +export const updateNamedItem = Symbol('updateNamedItem'); +export const childNodesFlatten = Symbol('childNodesFlatten'); +export const includes = Symbol('includes'); +export const insertItem = Symbol('insertItem'); +export const addEventListener = Symbol('addEventListener'); +export const removeEventListener = Symbol('removeEventListener'); +export const htmlCollections = Symbol('htmlCollections'); +export const namedItemListeners = Symbol('namedItemListeners'); +export const dispatchEvent = Symbol('dispatchEvent'); +export const getNamedItems = Symbol('getNamedItems'); +export const setNamedItemProperty = Symbol('setNamedItemProperty'); +export const selectedOptions = Symbol('selectedOptions'); diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 96ec4e38b..de312663e 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -6,7 +6,7 @@ import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import DOMException from '../../exception/DOMException.js'; import CSSStyleDeclarationElementStyle from './element-style/CSSStyleDeclarationElementStyle.js'; import CSSStyleDeclarationPropertyManager from './property-manager/CSSStyleDeclarationPropertyManager.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; +import NamedNodeMap from '../../nodes/element/NamedNodeMap.js'; /** * CSS Style Declaration. @@ -83,10 +83,10 @@ export default abstract class AbstractCSSStyleDeclaration { if (!styleAttribute) { styleAttribute = this.#ownerElement[PropertySymbol.ownerDocument].createAttribute('style'); - // We use "[PropertySymbol.setNamedItemWithoutConsequences]" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. - (this.#ownerElement[PropertySymbol.attributes])[ - PropertySymbol.setNamedItemWithoutConsequences - ](styleAttribute); + (this.#ownerElement[PropertySymbol.attributes])[PropertySymbol.setNamedItem]( + styleAttribute, + true + ); } if (this.#ownerElement[PropertySymbol.isConnected]) { @@ -141,10 +141,10 @@ export default abstract class AbstractCSSStyleDeclaration { if (!styleAttribute) { styleAttribute = this.#ownerElement[PropertySymbol.ownerDocument].createAttribute('style'); - // We use "[PropertySymbol.setNamedItemWithoutConsequences]" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. - (this.#ownerElement[PropertySymbol.attributes])[ - PropertySymbol.setNamedItemWithoutConsequences - ](styleAttribute); + (this.#ownerElement[PropertySymbol.attributes])[PropertySymbol.setNamedItem]( + styleAttribute, + true + ); } if (this.#ownerElement[PropertySymbol.isConnected]) { @@ -188,10 +188,9 @@ export default abstract class AbstractCSSStyleDeclaration { (this.#ownerElement[PropertySymbol.attributes]['style'])[PropertySymbol.value] = newCSSText; } else { - // We use "[PropertySymbol.removeNamedItemWithoutConsequences]" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. (this.#ownerElement[PropertySymbol.attributes])[ - PropertySymbol.removeNamedItemWithoutConsequences - ]('style'); + PropertySymbol.removeNamedItem + ]('style', true); } } else { this.#style.remove(name); diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index 3ee3e29d5..85d80c2ea 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -38,14 +38,15 @@ export default class DOMParser { const newDocument = this.#createDocument(mimeType); - newDocument[PropertySymbol.childNodes].length = 0; - newDocument[PropertySymbol.children].length = 0; + while (newDocument[PropertySymbol.childNodes][PropertySymbol.items].length) { + newDocument.removeChild(newDocument[PropertySymbol.childNodes][PropertySymbol.items][0]); + } const root = XMLParser.parse(newDocument, string, { evaluateScripts: true }); let documentElement = null; let documentTypeNode = null; - for (const node of root[PropertySymbol.childNodes]) { + for (const node of root[PropertySymbol.childNodes][PropertySymbol.items]) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { @@ -64,16 +65,16 @@ export default class DOMParser { newDocument.appendChild(documentElement); const body = newDocument.body; if (body) { - for (const child of root[PropertySymbol.childNodes].slice()) { - body.appendChild(child); + while (root[PropertySymbol.childNodes][PropertySymbol.items].length) { + body.appendChild(root[PropertySymbol.childNodes][PropertySymbol.items][0]); } } } else { switch (mimeType) { case 'image/svg+xml': { - for (const node of root[PropertySymbol.childNodes].slice()) { - newDocument.appendChild(node); + while (root[PropertySymbol.childNodes][PropertySymbol.items].length) { + newDocument.appendChild(root[PropertySymbol.childNodes][PropertySymbol.items][0]); } } break; @@ -88,8 +89,8 @@ export default class DOMParser { documentElement.appendChild(bodyElement); newDocument.appendChild(documentElement); - for (const node of root[PropertySymbol.childNodes].slice()) { - bodyElement.appendChild(node); + while (root[PropertySymbol.childNodes][PropertySymbol.items].length) { + bodyElement.appendChild(root[PropertySymbol.childNodes][PropertySymbol.items][0]); } } break; diff --git a/packages/happy-dom/src/form-data/FormData.ts b/packages/happy-dom/src/form-data/FormData.ts index 0fd240e86..5a6feb42d 100644 --- a/packages/happy-dom/src/form-data/FormData.ts +++ b/packages/happy-dom/src/form-data/FormData.ts @@ -59,8 +59,8 @@ export default class FormData implements Iterable<[string, string | File]> { this.append(node.name, file); } } - } else if (node.value) { - this.append(node.name, node.value); + } else if ((node).value) { + this.append(node.name, (node).value); } } } diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index 3cfa631fe..0ae142731 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -72,7 +72,7 @@ import DocumentType from './nodes/document-type/DocumentType.js'; import Document from './nodes/document/Document.js'; import DOMRect from './nodes/element/DOMRect.js'; import Element from './nodes/element/Element.js'; -import HTMLCollection from './nodes/element/HTMLCollection.js'; +import HTMLCollection from './nodes/element/HTMLCollection2.js'; import HTMLAnchorElement from './nodes/html-anchor-element/HTMLAnchorElement.js'; import HTMLAreaElement from './nodes/html-area-element/HTMLAreaElement.js'; import HTMLAudioElement from './nodes/html-audio-element/HTMLAudioElement.js'; diff --git a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts deleted file mode 100644 index 5fb0f942a..000000000 --- a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts +++ /dev/null @@ -1,246 +0,0 @@ -import * as PropertySymbol from '../PropertySymbol.js'; -import Attr from '../nodes/attr/Attr.js'; -import DOMException from '../exception/DOMException.js'; -import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class NamedNodeMap { - [index: number]: Attr; - public length = 0; - protected [PropertySymbol.namedItems]: { [k: string]: Attr } = {}; - - /** - * Returns string. - * - * @returns string. - */ - public get [Symbol.toStringTag](): string { - return 'NamedNodeMap'; - } - - /** - * Iterator. - * - * @returns Iterator. - */ - public *[Symbol.iterator](): IterableIterator { - for (let i = 0, max = this.length; i < max; i++) { - yield this[i]; - } - } - - /** - * Returns item by index. - * - * @param index Index. - */ - public item(index: number): Attr | null { - return index >= 0 && this[index] ? this[index] : null; - } - - /** - * Returns named item. - * - * @param name Name. - * @returns Item. - */ - public getNamedItem(name: string): Attr | null { - return this[PropertySymbol.namedItems][name] || null; - } - - /** - * Returns item by name and namespace. - * - * @param namespace Namespace. - * @param localName Local name of the attribute. - * @returns Item. - */ - public getNamedItemNS(namespace: string, localName: string): Attr | null { - const attribute = this.getNamedItem(localName); - - if ( - attribute && - attribute[PropertySymbol.namespaceURI] === namespace && - attribute.localName === localName - ) { - return attribute; - } - - for (let i = 0, max = this.length; i < max; i++) { - if (this[i][PropertySymbol.namespaceURI] === namespace && this[i].localName === localName) { - return this[i]; - } - } - - return null; - } - - /** - * Sets named item. - * - * @param item Item. - * @returns Replaced item. - */ - public setNamedItem(item: Attr): Attr | null { - return this[PropertySymbol.setNamedItem](item); - } - - /** - * Adds a new namespaced item. - * - * @alias setNamedItem() - * @param item Item. - * @returns Replaced item. - */ - public setNamedItemNS(item: Attr): Attr | null { - return this[PropertySymbol.setNamedItem](item); - } - - /** - * Removes an item. - * - * @throws DOMException - * @param name Name of item. - * @returns Removed item. - */ - public removeNamedItem(name: string): Attr { - const item = this[PropertySymbol.removeNamedItem](name); - if (!item) { - throw new DOMException( - `Failed to execute 'removeNamedItem' on 'NamedNodeMap': No item with name '${name}' was found.`, - DOMExceptionNameEnum.notFoundError - ); - } - return item; - } - - /** - * Removes a namespaced item. - * - * @param namespace Namespace. - * @param localName Local name of the item. - * @returns Removed item. - */ - public removeNamedItemNS(namespace: string, localName: string): Attr | null { - const attribute = this.getNamedItemNS(namespace, localName); - if (attribute) { - return this.removeNamedItem(attribute[PropertySymbol.name]); - } - return null; - } - - /** - * Sets named item. - * - * This method may be overridden by subclasses to act on attribute changes. - * - * @param item Item. - * @returns Replaced item. - */ - public [PropertySymbol.setNamedItem](item: Attr): Attr | null { - return this[PropertySymbol.setNamedItemWithoutConsequences](item); - } - - /** - * Sets named item without potential overrides done in [PropertySymbol.setNamedItem](). - * - * @param item Item. - * @returns Replaced item. - */ - public [PropertySymbol.setNamedItemWithoutConsequences](item: Attr): Attr | null { - if (item[PropertySymbol.name]) { - const replacedItem = this[PropertySymbol.namedItems][item[PropertySymbol.name]] || null; - - this[PropertySymbol.namedItems][item[PropertySymbol.name]] = item; - - if (replacedItem) { - this[PropertySymbol.removeNamedItemIndex](replacedItem); - } - - this[this.length] = item; - this.length++; - - if (this[PropertySymbol.isValidPropertyName](item[PropertySymbol.name])) { - this[item[PropertySymbol.name]] = item; - } - - return replacedItem; - } - return null; - } - - /** - * Removes an item without throwing if it doesn't exist. - * - * This method may be overridden by subclasses to act on attribute changes. - * - * @param name Name of item. - * @returns Removed item, or null if it didn't exist. - */ - public [PropertySymbol.removeNamedItem](name: string): Attr | null { - return this[PropertySymbol.removeNamedItemWithoutConsequences](name); - } - - /** - * Removes an item without calling listeners for certain attributes. - * - * @param name Name of item. - * @returns Removed item, or null if it didn't exist. - */ - public [PropertySymbol.removeNamedItemWithoutConsequences](name: string): Attr | null { - const removedItem = this[PropertySymbol.namedItems][name] || null; - - if (!removedItem) { - return null; - } - - this[PropertySymbol.removeNamedItemIndex](removedItem); - - if (this[name] === removedItem) { - delete this[name]; - } - - delete this[PropertySymbol.namedItems][name]; - - return removedItem; - } - - /** - * Removes an item from index. - * - * @param item Item. - */ - protected [PropertySymbol.removeNamedItemIndex](item: Attr): void { - for (let i = 0; i < this.length; i++) { - if (this[i] === item) { - for (let b = i; b < this.length; b++) { - if (b < this.length - 1) { - this[b] = this[b + 1]; - } else { - delete this[b]; - } - } - this.length--; - break; - } - } - } - - /** - * Returns "true" if the property name is valid. - * - * @param name Name. - * @returns True if the property name is valid. - */ - protected [PropertySymbol.isValidPropertyName](name: string): boolean { - return ( - !!name && - !this.constructor.prototype.hasOwnProperty(name) && - (isNaN(Number(name)) || name.includes('.')) - ); - } -} diff --git a/packages/happy-dom/src/nodes/attr/Attr.ts b/packages/happy-dom/src/nodes/attr/Attr.ts index cb40b3b66..f8de79962 100644 --- a/packages/happy-dom/src/nodes/attr/Attr.ts +++ b/packages/happy-dom/src/nodes/attr/Attr.ts @@ -9,6 +9,9 @@ import NodeTypeEnum from '../node/NodeTypeEnum.js'; * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Attr. */ export default class Attr extends Node implements Attr { + // Public properties + public cloneNode: (deep?: boolean) => Attr; + public [PropertySymbol.nodeType] = NodeTypeEnum.attributeNode; public [PropertySymbol.namespaceURI]: string | null = null; public [PropertySymbol.name]: string | null = null; diff --git a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts index 2a39e386b..11d7da6d5 100644 --- a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts +++ b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts @@ -39,9 +39,9 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes].slice(); - for (const newChildNode of newChildNodes) { - parent.insertBefore(newChildNode, childNode); + ))[PropertySymbol.childNodes][PropertySymbol.items]; + while (newChildNodes.length) { + parent.insertBefore(newChildNodes[0], childNode); } } else { parent.insertBefore(node, childNode); @@ -68,9 +68,9 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes].slice(); - for (const newChildNode of newChildNodes) { - parent.insertBefore(newChildNode, childNode); + ))[PropertySymbol.childNodes][PropertySymbol.items]; + while (newChildNodes.length) { + parent.insertBefore(newChildNodes[0], childNode); } } else { parent.insertBefore(node, childNode); @@ -97,12 +97,12 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes].slice(); - for (const newChildNode of newChildNodes) { + ))[PropertySymbol.childNodes][PropertySymbol.items]; + while (newChildNodes.length) { if (!nextSibling) { - parent.appendChild(newChildNode); + parent.appendChild(newChildNodes[0]); } else { - parent.insertBefore(newChildNode, nextSibling); + parent.insertBefore(newChildNodes[0], nextSibling); } } } else if (!nextSibling) { diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index ffa1f7202..a8d2af70f 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -3,8 +3,7 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import Element from '../element/Element.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; -import HTMLCollection from '../element/HTMLCollection.js'; -import ElementUtility from '../element/ElementUtility.js'; +import HTMLCollection from '../element/HTMLCollection2.js'; import NodeList from '../node/NodeList.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import IHTMLElementTagNameMap from '../../config/IHTMLElementTagNameMap.js'; @@ -19,6 +18,16 @@ export default class DocumentFragment extends Node { public [PropertySymbol.nodeType] = NodeTypeEnum.documentFragmentNode; public cloneNode: (deep?: boolean) => DocumentFragment; + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.childNodes][PropertySymbol.attachHTMLCollection]( + this[PropertySymbol.children] + ); + } + /** * Returns the document fragment children. */ @@ -32,7 +41,7 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get childElementCount(): number { - return this[PropertySymbol.children].length; + return this[PropertySymbol.children][PropertySymbol.items].length; } /** @@ -41,7 +50,7 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get firstElementChild(): Element { - return this[PropertySymbol.children][0] ?? null; + return this[PropertySymbol.children][PropertySymbol.items][0] ?? null; } /** @@ -50,7 +59,8 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get lastElementChild(): Element { - return this[PropertySymbol.children][this[PropertySymbol.children].length - 1] ?? null; + const children = this[PropertySymbol.children][PropertySymbol.items]; + return children[children.length - 1] ?? null; } /** @@ -60,7 +70,7 @@ export default class DocumentFragment extends Node { */ public get textContent(): string { let result = ''; - for (const childNode of this[PropertySymbol.childNodes]) { + for (const childNode of this[PropertySymbol.childNodes][PropertySymbol.items]) { if ( childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode || childNode[PropertySymbol.nodeType] === NodeTypeEnum.textNode @@ -77,8 +87,9 @@ export default class DocumentFragment extends Node { * @param textContent Text content. */ public set textContent(textContent: string) { - for (const child of this[PropertySymbol.childNodes].slice()) { - this.removeChild(child); + const childNodes = this[PropertySymbol.childNodes][PropertySymbol.items]; + while (childNodes.length) { + this.removeChild(childNodes[0]); } if (textContent) { this.appendChild(this[PropertySymbol.ownerDocument].createTextNode(textContent)); @@ -194,48 +205,7 @@ export default class DocumentFragment extends Node { * @param id ID. * @returns Matching element. */ - public getElementById(id: string): Element { + public getElementById(id: string): Element | null { return ParentNodeUtility.getElementById(this, id); } - - /** - * @override - */ - public override [PropertySymbol.cloneNode](deep = false): DocumentFragment { - const clone = super[PropertySymbol.cloneNode](deep); - - if (deep) { - for (const node of clone[PropertySymbol.childNodes]) { - if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - clone[PropertySymbol.children].push(node); - } - } - } - - return clone; - } - - /** - * @override - */ - public override [PropertySymbol.appendChild](node: Node): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.appendChild(this, node); - } - - /** - * @override - */ - public override [PropertySymbol.removeChild](node: Node): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.removeChild(this, node); - } - - /** - * @override - */ - public override [PropertySymbol.insertBefore](newNode: Node, referenceNode: Node | null): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.insertBefore(this, newNode, referenceNode); - } } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 44c0b560a..582d451e0 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -20,7 +20,7 @@ import HTMLElement from '../html-element/HTMLElement.js'; import Comment from '../comment/Comment.js'; import Text from '../text/Text.js'; import NodeList from '../node/NodeList.js'; -import HTMLCollection from '../element/HTMLCollection.js'; +import HTMLCollection from '../element/HTMLCollection2.js'; import HTMLLinkElement from '../html-link-element/HTMLLinkElement.js'; import HTMLStyleElement from '../html-style-element/HTMLStyleElement.js'; import DocumentReadyStateEnum from './DocumentReadyStateEnum.js'; @@ -31,7 +31,6 @@ import Range from '../../range/Range.js'; import HTMLBaseElement from '../html-base-element/HTMLBaseElement.js'; import Attr from '../attr/Attr.js'; import ProcessingInstruction from '../processing-instruction/ProcessingInstruction.js'; -import ElementUtility from '../element/ElementUtility.js'; import VisibilityStateEnum from './VisibilityStateEnum.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import CookieStringUtility from '../../cookie/urilities/CookieStringUtility.js'; @@ -197,6 +196,9 @@ export default class Document extends Node { super(); this.#browserFrame = injected.browserFrame; this[PropertySymbol.ownerWindow] = injected.window; + this[PropertySymbol.childNodes][PropertySymbol.attachHTMLCollection]( + this[PropertySymbol.children] + ); } /** @@ -328,7 +330,7 @@ export default class Document extends Node { * @returns Element. */ public get childElementCount(): number { - return this[PropertySymbol.children].length; + return this[PropertySymbol.children][PropertySymbol.items].length; } /** @@ -337,7 +339,7 @@ export default class Document extends Node { * @returns Element. */ public get firstElementChild(): Element { - return this[PropertySymbol.children][0] ?? null; + return this[PropertySymbol.children][PropertySymbol.items][0] ?? null; } /** @@ -346,7 +348,8 @@ export default class Document extends Node { * @returns Element. */ public get lastElementChild(): Element { - return this[PropertySymbol.children][this[PropertySymbol.children].length - 1] ?? null; + const children = this[PropertySymbol.children][PropertySymbol.items]; + return children[children.length - 1] ?? null; } /** @@ -401,7 +404,7 @@ export default class Document extends Node { * @returns Document type. */ public get doctype(): DocumentType { - for (const node of this[PropertySymbol.childNodes]) { + for (const node of this[PropertySymbol.childNodes][PropertySymbol.items]) { if (node instanceof DocumentType) { return node; } @@ -771,7 +774,7 @@ export default class Document extends Node { * @param id ID. * @returns Matching element. */ - public getElementById(id: string): Element { + public getElementById(id: string): Element | null { return ParentNodeUtility.getElementById(this, id); } @@ -787,12 +790,14 @@ export default class Document extends Node { name: string ): NodeList => { const matches = new NodeList(); - for (const child of (parentNode)[PropertySymbol.children]) { + for (const child of (parentNode)[PropertySymbol.children][ + PropertySymbol.items + ]) { if (child.getAttributeNS(null, 'name') === name) { - matches.push(child); + matches[PropertySymbol.addItem](child); } for (const match of getElementsByName(child, name)) { - matches.push(match); + matches[PropertySymbol.addItem](match); } } return matches; @@ -800,47 +805,6 @@ export default class Document extends Node { return getElementsByName(this, name); } - /** - * @override - */ - public override [PropertySymbol.cloneNode](deep = false): Document { - const clone = super[PropertySymbol.cloneNode](deep); - - if (deep) { - for (const node of clone[PropertySymbol.childNodes]) { - if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - clone[PropertySymbol.children].push(node); - } - } - } - - return clone; - } - - /** - * @override - */ - public override [PropertySymbol.appendChild](node: Node): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.appendChild(this, node); - } - - /** - * @override - */ - public override [PropertySymbol.removeChild](node: Node): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.removeChild(this, node); - } - - /** - * @override - */ - public override [PropertySymbol.insertBefore](newNode: Node, referenceNode: Node | null): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.insertBefore(this, newNode, referenceNode); - } - /** * Replaces the document HTML with new HTML. * @@ -862,7 +826,7 @@ export default class Document extends Node { let documentElement = null; let documentTypeNode = null; - for (const node of root[PropertySymbol.childNodes]) { + for (const node of root[PropertySymbol.childNodes][PropertySymbol.items]) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { @@ -897,8 +861,9 @@ export default class Document extends Node { const rootBody = ParentNodeUtility.getElementByTagName(root, 'body'); const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (rootBody && body) { - for (const child of rootBody[PropertySymbol.childNodes].slice()) { - body.appendChild(child); + const childNodes = rootBody[PropertySymbol.childNodes][PropertySymbol.items]; + while (childNodes.length) { + body.appendChild(childNodes[0]); } } } @@ -906,7 +871,9 @@ export default class Document extends Node { // Remaining nodes outside the element are added to the element. const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (body) { - for (const child of root[PropertySymbol.childNodes].slice()) { + const childNodes = root[PropertySymbol.childNodes][PropertySymbol.items]; + while (childNodes.length) { + const child = childNodes[0]; if ( child['tagName'] !== 'HTML' && child[PropertySymbol.nodeType] !== NodeTypeEnum.documentTypeNode @@ -919,9 +886,10 @@ export default class Document extends Node { const documentElement = this.createElement('html'); const bodyElement = this.createElement('body'); const headElement = this.createElement('head'); + const childNodes = root[PropertySymbol.childNodes][PropertySymbol.items]; - for (const child of root[PropertySymbol.childNodes].slice()) { - bodyElement.appendChild(child); + while (childNodes.length) { + bodyElement.appendChild(childNodes[0]); } documentElement.appendChild(headElement); @@ -932,8 +900,11 @@ export default class Document extends Node { } else { const bodyNode = ParentNodeUtility.getElementByTagName(root, 'body'); const body = ParentNodeUtility.getElementByTagName(this, 'body'); - for (const child of ((bodyNode || root))[PropertySymbol.childNodes].slice()) { - body.appendChild(child); + const childNodes = ((bodyNode || root))[PropertySymbol.childNodes][ + PropertySymbol.items + ]; + while (childNodes.length) { + body.appendChild(childNodes[0]); } } } @@ -955,7 +926,7 @@ export default class Document extends Node { } } - for (const child of this[PropertySymbol.childNodes].slice()) { + for (const child of this[PropertySymbol.childNodes][PropertySymbol.items]) { this.removeChild(child); } @@ -1343,7 +1314,7 @@ export default class Document extends Node { #importNode(node: Node): void { node[PropertySymbol.ownerDocument] = this; - for (const child of node[PropertySymbol.childNodes]) { + for (const child of node[PropertySymbol.childNodes][PropertySymbol.items]) { this.#importNode(child); } } diff --git a/packages/happy-dom/src/nodes/element/DatasetFactory.ts b/packages/happy-dom/src/nodes/element/DatasetFactory.ts index 74939fab2..1017968b8 100644 --- a/packages/happy-dom/src/nodes/element/DatasetFactory.ts +++ b/packages/happy-dom/src/nodes/element/DatasetFactory.ts @@ -1,6 +1,5 @@ import Element from './Element.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import DatasetUtility from './DatasetUtility.js'; import IDataset from './IDataset.js'; @@ -47,9 +46,9 @@ export default class DatasetFactory { return true; }, deleteProperty(dataset: IDataset, key: string): boolean { - (element[PropertySymbol.attributes])[ - PropertySymbol.removeNamedItem - ]('data-' + DatasetUtility.camelCaseToKebab(key)); + element[PropertySymbol.attributes][PropertySymbol.removeNamedItem]( + 'data-' + DatasetUtility.camelCaseToKebab(key) + ); return delete dataset[key]; }, ownKeys(dataset: IDataset): string[] { diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 2cb1f56d4..21e767893 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -11,17 +11,15 @@ import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; import NonDocumentChildNodeUtility from '../child-node/NonDocumentChildNodeUtility.js'; import DOMException from '../../exception/DOMException.js'; import HTMLCollection from './HTMLCollection.js'; -import NodeList from '../node/NodeList.js'; +import IHTMLCollection from './IHTMLCollection.js'; import Text from '../text/Text.js'; import DOMRectList from './DOMRectList.js'; import Attr from '../attr/Attr.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; +import NamedNodeMap from './NamedNodeMap.js'; import Event from '../../event/Event.js'; -import ElementUtility from './ElementUtility.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; -import ElementNamedNodeMap from './ElementNamedNodeMap.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js'; @@ -32,6 +30,10 @@ import ISVGElementTagNameMap from '../../config/ISVGElementTagNameMap.js'; import IChildNode from '../child-node/IChildNode.js'; import INonDocumentTypeChildNode from '../child-node/INonDocumentTypeChildNode.js'; import IParentNode from '../parent-node/IParentNode.js'; +import MutationListener from '../../mutation-observer/MutationListener.js'; +import MutationRecord from '../../mutation-observer/MutationRecord.js'; +import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; +import INodeList from '../node/INodeList.js'; type InsertAdjacentPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'; @@ -88,7 +90,6 @@ export default class Element public ontouchstart: (event: Event) => void | null = null; // Internal properties - public [PropertySymbol.children]: HTMLCollection = new HTMLCollection(); public [PropertySymbol.classList]: DOMTokenList = null; public [PropertySymbol.isValue]: string | null = null; public [PropertySymbol.computedStyle]: CSSStyleDeclaration | null = null; @@ -102,9 +103,39 @@ export default class Element public [PropertySymbol.scrollWidth] = 0; public [PropertySymbol.scrollTop] = 0; public [PropertySymbol.scrollLeft] = 0; - public [PropertySymbol.attributes]: NamedNodeMap = new ElementNamedNodeMap(this); + public [PropertySymbol.attributes] = new NamedNodeMap(this); public [PropertySymbol.namespaceURI]: string | null = this.constructor[PropertySymbol.namespaceURI] || null; + public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); + + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('add', (item: Node) => + this[PropertySymbol.children][PropertySymbol.addItem](item) + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( + 'insert', + (item: Node, referenceItem?: Node) => + this[PropertySymbol.children][PropertySymbol.insertItem]( + item, + referenceItem + ) + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('remove', (item: Node) => + this[PropertySymbol.children][PropertySymbol.removeItem](item) + ); + } /** * Returns tag name. @@ -209,7 +240,7 @@ export default class Element /** * Returns element children. */ - public get children(): HTMLCollection { + public get children(): IHTMLCollection { return this[PropertySymbol.children]; } @@ -322,7 +353,7 @@ export default class Element */ public get textContent(): string { let result = ''; - for (const childNode of this[PropertySymbol.childNodes]) { + for (const childNode of this[PropertySymbol.childNodes][PropertySymbol.items]) { if ( childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode || childNode[PropertySymbol.nodeType] === NodeTypeEnum.textNode @@ -339,8 +370,9 @@ export default class Element * @param textContent Text content. */ public set textContent(textContent: string) { - for (const child of this[PropertySymbol.childNodes].slice()) { - this.removeChild(child); + const childNodes = this[PropertySymbol.childNodes][PropertySymbol.items]; + while (childNodes.length) { + this.removeChild(childNodes[0]); } if (textContent) { this.appendChild(this[PropertySymbol.ownerDocument].createTextNode(textContent)); @@ -362,8 +394,10 @@ export default class Element * @param html HTML. */ public set innerHTML(html: string) { - for (const child of this[PropertySymbol.childNodes].slice()) { - this.removeChild(child); + const childNodes = this[PropertySymbol.childNodes][PropertySymbol.items]; + + while (childNodes.length) { + this.removeChild(childNodes[0]); } XMLParser.parse(this[PropertySymbol.ownerDocument], html, { rootNode: this }); @@ -388,21 +422,21 @@ export default class Element } /** - * First element child. + * Last element child. * * @returns Element. */ - public get firstElementChild(): Element { - return this[PropertySymbol.children][0] ?? null; + public get childElementCount(): number { + return this[PropertySymbol.children][PropertySymbol.items].length; } /** - * Last element child. + * First element child. * * @returns Element. */ - public get lastElementChild(): Element { - return this[PropertySymbol.children][this[PropertySymbol.children].length - 1] ?? null; + public get firstElementChild(): Element { + return this[PropertySymbol.children][PropertySymbol.items][0] ?? null; } /** @@ -410,8 +444,9 @@ export default class Element * * @returns Element. */ - public get childElementCount(): number { - return this[PropertySymbol.children].length; + public get lastElementChild(): Element { + const children = this[PropertySymbol.children][PropertySymbol.items]; + return children[children.length - 1] ?? null; } /** @@ -458,7 +493,7 @@ export default class Element escapeEntities: false }); let xml = ''; - for (const node of this[PropertySymbol.childNodes]) { + for (const node of this[PropertySymbol.childNodes][PropertySymbol.items]) { xml += xmlSerializer.serializeToString(node); } return xml; @@ -474,54 +509,9 @@ export default class Element clone[PropertySymbol.localName] = this[PropertySymbol.localName]; clone[PropertySymbol.namespaceURI] = this[PropertySymbol.namespaceURI]; - for (let i = 0, max = this[PropertySymbol.attributes].length; i < max; i++) { - const attribute = this[PropertySymbol.attributes][i]; - clone[PropertySymbol.attributes].setNamedItem( - Object.assign( - this[PropertySymbol.ownerDocument].createAttributeNS( - attribute[PropertySymbol.namespaceURI], - attribute[PropertySymbol.name] - ), - attribute - ) - ); - } - - if (deep) { - for (const node of clone[PropertySymbol.childNodes]) { - if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - clone[PropertySymbol.children].push(node); - } - } - } - return clone; } - /** - * @override - */ - public override [PropertySymbol.appendChild](node: Node): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.appendChild(this, node); - } - - /** - * @override - */ - public override [PropertySymbol.removeChild](node: Node): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.removeChild(this, node); - } - - /** - * @override - */ - public override [PropertySymbol.insertBefore](newNode: Node, referenceNode: Node | null): Node { - // We do not call super here as this will be handled by ElementUtility to improve performance by avoiding validation and other checks. - return ElementUtility.insertBefore(this, newNode, referenceNode); - } - /** * Removes the node from its parent. */ @@ -619,10 +609,11 @@ export default class Element * @param text HTML string to insert. */ public insertAdjacentHTML(position: InsertAdjacentPosition, text: string): void { - for (const node of (( + const childNodes = (( XMLParser.parse(this[PropertySymbol.ownerDocument], text) - ))[PropertySymbol.childNodes].slice()) { - this.insertAdjacentElement(position, node); + ))[PropertySymbol.childNodes][PropertySymbol.items]; + while (childNodes.length) { + this.insertAdjacentElement(position, childNodes[0]); } } @@ -803,7 +794,7 @@ export default class Element shadowRoot[PropertySymbol.host] = this; shadowRoot[PropertySymbol.mode] = init.mode; - (shadowRoot)[PropertySymbol.connectToNode](this); + (shadowRoot)[PropertySymbol.connectedToDocument](); return this[PropertySymbol.shadowRoot]; } @@ -878,7 +869,7 @@ export default class Element */ public querySelectorAll( selector: K - ): NodeList; + ): INodeList; /** * Query CSS selector to find matching elments. @@ -888,7 +879,7 @@ export default class Element */ public querySelectorAll( selector: K - ): NodeList; + ): INodeList; /** * Query CSS selector to find matching elments. @@ -896,7 +887,7 @@ export default class Element * @param selector CSS selector. * @returns Matching elements. */ - public querySelectorAll(selector: string): NodeList; + public querySelectorAll(selector: string): INodeList; /** * Query CSS selector to find matching elments. @@ -904,7 +895,7 @@ export default class Element * @param selector CSS selector. * @returns Matching elements. */ - public querySelectorAll(selector: string): NodeList { + public querySelectorAll(selector: string): INodeList { return QuerySelector.querySelectorAll(this, selector); } @@ -952,7 +943,7 @@ export default class Element * @param className Tag name. * @returns Matching element. */ - public getElementsByClassName(className: string): HTMLCollection { + public getElementsByClassName(className: string): IHTMLCollection { return ParentNodeUtility.getElementsByClassName(this, className); } @@ -964,7 +955,7 @@ export default class Element */ public getElementsByTagName( tagName: K - ): HTMLCollection; + ): IHTMLCollection; /** * Returns an elements by tag name. @@ -974,7 +965,7 @@ export default class Element */ public getElementsByTagName( tagName: K - ): HTMLCollection; + ): IHTMLCollection; /** * Returns an elements by tag name. @@ -982,7 +973,7 @@ export default class Element * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagName(tagName: string): HTMLCollection; + public getElementsByTagName(tagName: string): IHTMLCollection; /** * Returns an elements by tag name. @@ -990,7 +981,7 @@ export default class Element * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagName(tagName: string): HTMLCollection { + public getElementsByTagName(tagName: string): IHTMLCollection { return ParentNodeUtility.getElementsByTagName(this, tagName); } @@ -1004,7 +995,7 @@ export default class Element public getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/1999/xhtml', tagName: K - ): HTMLCollection; + ): IHTMLCollection; /** * Returns an elements by tag name and namespace. @@ -1016,7 +1007,7 @@ export default class Element public getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/2000/svg', tagName: K - ): HTMLCollection; + ): IHTMLCollection; /** * Returns an elements by tag name and namespace. @@ -1025,7 +1016,7 @@ export default class Element * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection; + public getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection; /** * Returns an elements by tag name and namespace. @@ -1034,7 +1025,7 @@ export default class Element * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection { + public getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection { return ParentNodeUtility.getElementsByTagNameNS(this, namespaceURI, tagName); } @@ -1202,4 +1193,134 @@ export default class Element return returnValue; } + + /** + * Triggered when an attribute is set. + * + * @param attribute Attribute. + * @param replacedAttribute Replaced attribute. + */ + #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + if (!attribute[PropertySymbol.name]) { + return null; + } + + if (this[PropertySymbol.isConnected]) { + this.ownerDocument[PropertySymbol.cacheID]++; + } + + const oldValue = replacedAttribute ? replacedAttribute[PropertySymbol.value] : null; + + if (attribute[PropertySymbol.name] === 'class' && this[PropertySymbol.classList]) { + this[PropertySymbol.classList][PropertySymbol.updateIndices](); + } + + if (attribute[PropertySymbol.name] === 'id' || attribute[PropertySymbol.name] === 'name') { + const parent = this[PropertySymbol.parentNode]; + while (parent) { + this[PropertySymbol.parentNode][PropertySymbol.childNodes][ + PropertySymbol.htmlCollections + ].updateNamedItem(this, attribute, replacedAttribute); + let parent = this[PropertySymbol.parentNode]; + while (parent) { + parent[PropertySymbol.childNodesFlatten][PropertySymbol.htmlCollections].updateNamedItem( + this, + attribute, + replacedAttribute + ); + parent = parent[PropertySymbol.parentNode]; + } + } + } + + if ( + this.attributeChangedCallback && + (this.constructor)[PropertySymbol.observedAttributes] && + (this.constructor)[PropertySymbol.observedAttributes].includes( + attribute[PropertySymbol.name] + ) + ) { + this.attributeChangedCallback( + attribute[PropertySymbol.name], + oldValue, + attribute[PropertySymbol.value] + ); + } + + // MutationObserver + if (this[PropertySymbol.observers].length > 0) { + for (const observer of this[PropertySymbol.observers]) { + if ( + observer.options?.attributes && + (!observer.options.attributeFilter || + observer.options.attributeFilter.includes(attribute[PropertySymbol.name])) + ) { + observer.report( + new MutationRecord({ + target: this, + type: MutationTypeEnum.attributes, + attributeName: attribute[PropertySymbol.name], + oldValue: observer.options.attributeOldValue ? oldValue : null + }) + ); + } + } + } + } + + /** + * Triggered when an attribute is set. + * + * @param removedAttribute Attribute. + */ + #onRemoveAttribute(removedAttribute: Attr): void { + if (this[PropertySymbol.isConnected]) { + this.ownerDocument[PropertySymbol.cacheID]++; + } + + if (removedAttribute[PropertySymbol.name] === 'class' && this[PropertySymbol.classList]) { + this[PropertySymbol.classList][PropertySymbol.updateIndices](); + } + + if ( + this[PropertySymbol.parentNode] && + (removedAttribute[PropertySymbol.name] === 'id' || + removedAttribute[PropertySymbol.name] === 'name') + ) { + this[PropertySymbol.parentNode][PropertySymbol.childNodes][ + PropertySymbol.htmlCollections + ].updateNamedItem(this, null, removedAttribute); + let parent = this[PropertySymbol.parentNode]; + while (parent) { + parent[PropertySymbol.childNodesFlatten][PropertySymbol.htmlCollections].updateNamedItem( + this, + null, + removedAttribute + ); + parent = parent[PropertySymbol.parentNode]; + } + } + + // MutationObserver + if (this[PropertySymbol.observers].length > 0) { + for (const observer of this[PropertySymbol.observers]) { + if ( + observer.options?.attributes && + (!observer.options.attributeFilter || + observer.options.attributeFilter.includes(removedAttribute[PropertySymbol.name])) + ) { + observer.report( + new MutationRecord({ + target: this, + type: MutationTypeEnum.attributes, + attributeName: removedAttribute[PropertySymbol.name], + oldValue: observer.options.attributeOldValue + ? removedAttribute[PropertySymbol.value] + : null + }) + ); + } + } + } + } } diff --git a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts deleted file mode 100644 index d21b02350..000000000 --- a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts +++ /dev/null @@ -1,241 +0,0 @@ -import NamespaceURI from '../../config/NamespaceURI.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import MutationRecord from '../../mutation-observer/MutationRecord.js'; -import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import Attr from '../attr/Attr.js'; -import Element from './Element.js'; -import HTMLCollection from './HTMLCollection.js'; -import MutationListener from '../../mutation-observer/MutationListener.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class ElementNamedNodeMap extends NamedNodeMap { - protected [PropertySymbol.ownerElement]: Element; - - /** - * Constructor. - * - * @param ownerElement Owner element. - */ - constructor(ownerElement: Element) { - super(); - this[PropertySymbol.ownerElement] = ownerElement; - } - - /** - * @override - */ - public override getNamedItem(name: string): Attr | null { - return this[PropertySymbol.namedItems][this[PropertySymbol.getAttributeName](name)] || null; - } - - /** - * @override - */ - public override getNamedItemNS(namespace: string, localName: string): Attr | null { - return super.getNamedItemNS(namespace, this[PropertySymbol.getAttributeName](localName)); - } - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - if (!item[PropertySymbol.name]) { - return null; - } - - item[PropertySymbol.name] = this[PropertySymbol.getAttributeName](item[PropertySymbol.name]); - (item[PropertySymbol.ownerElement]) = this[PropertySymbol.ownerElement]; - - const replacedItem = super[PropertySymbol.setNamedItem](item); - const oldValue = replacedItem ? replacedItem[PropertySymbol.value] : null; - - if (this[PropertySymbol.ownerElement][PropertySymbol.isConnected]) { - this[PropertySymbol.ownerElement].ownerDocument[PropertySymbol.cacheID]++; - } - - if ( - item[PropertySymbol.name] === 'class' && - this[PropertySymbol.ownerElement][PropertySymbol.classList] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.classList][PropertySymbol.updateIndices](); - } - - if (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') { - if ( - this[PropertySymbol.ownerElement][PropertySymbol.parentNode] && - (this[PropertySymbol.ownerElement][PropertySymbol.parentNode])[ - PropertySymbol.children - ] && - item[PropertySymbol.value] !== oldValue - ) { - if (oldValue) { - (>( - (this[PropertySymbol.ownerElement][PropertySymbol.parentNode])[ - PropertySymbol.children - ] - ))[PropertySymbol.removeNamedItem](this[PropertySymbol.ownerElement], oldValue); - } - if (item[PropertySymbol.value]) { - (>( - (this[PropertySymbol.ownerElement][PropertySymbol.parentNode])[ - PropertySymbol.children - ] - ))[PropertySymbol.appendNamedItem]( - this[PropertySymbol.ownerElement], - item[PropertySymbol.value] - ); - } - } - } - - if ( - this[PropertySymbol.ownerElement].attributeChangedCallback && - (this[PropertySymbol.ownerElement].constructor)[ - PropertySymbol.observedAttributes - ] && - (this[PropertySymbol.ownerElement].constructor)[ - PropertySymbol.observedAttributes - ].includes(item[PropertySymbol.name]) - ) { - this[PropertySymbol.ownerElement].attributeChangedCallback( - item[PropertySymbol.name], - oldValue, - item[PropertySymbol.value] - ); - } - - // MutationObserver - if (this[PropertySymbol.ownerElement][PropertySymbol.observers].length > 0) { - for (const observer of ( - this[PropertySymbol.ownerElement][PropertySymbol.observers] - )) { - if ( - observer.options?.attributes && - (!observer.options.attributeFilter || - observer.options.attributeFilter.includes(item[PropertySymbol.name])) - ) { - observer.report( - new MutationRecord({ - target: this[PropertySymbol.ownerElement], - type: MutationTypeEnum.attributes, - attributeName: item[PropertySymbol.name], - oldValue: observer.options.attributeOldValue ? oldValue : null - }) - ); - } - } - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem]( - this[PropertySymbol.getAttributeName](name) - ); - - if (!removedItem) { - return null; - } - - if (this[PropertySymbol.ownerElement][PropertySymbol.isConnected]) { - this[PropertySymbol.ownerElement].ownerDocument[PropertySymbol.cacheID]++; - } - - if ( - removedItem[PropertySymbol.name] === 'class' && - this[PropertySymbol.ownerElement][PropertySymbol.classList] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.classList][PropertySymbol.updateIndices](); - } - - if (removedItem[PropertySymbol.name] === 'id' || removedItem[PropertySymbol.name] === 'name') { - if ( - this[PropertySymbol.ownerElement][PropertySymbol.parentNode] && - (this[PropertySymbol.ownerElement][PropertySymbol.parentNode])[ - PropertySymbol.children - ] && - removedItem[PropertySymbol.value] - ) { - (>( - (this[PropertySymbol.ownerElement][PropertySymbol.parentNode])[ - PropertySymbol.children - ] - ))[PropertySymbol.removeNamedItem]( - this[PropertySymbol.ownerElement], - removedItem[PropertySymbol.value] - ); - } - } - - if ( - this[PropertySymbol.ownerElement].attributeChangedCallback && - (this[PropertySymbol.ownerElement].constructor)[ - PropertySymbol.observedAttributes - ] && - (this[PropertySymbol.ownerElement].constructor)[ - PropertySymbol.observedAttributes - ].includes(removedItem[PropertySymbol.name]) - ) { - this[PropertySymbol.ownerElement].attributeChangedCallback( - removedItem[PropertySymbol.name], - removedItem[PropertySymbol.value], - null - ); - } - - // MutationObserver - if (this[PropertySymbol.ownerElement][PropertySymbol.observers].length > 0) { - for (const observer of ( - this[PropertySymbol.ownerElement][PropertySymbol.observers] - )) { - if ( - observer.options?.attributes && - (!observer.options.attributeFilter || - observer.options.attributeFilter.includes(removedItem[PropertySymbol.name])) - ) { - observer.report( - new MutationRecord({ - target: this[PropertySymbol.ownerElement], - type: MutationTypeEnum.attributes, - attributeName: removedItem[PropertySymbol.name], - oldValue: observer.options.attributeOldValue - ? removedItem[PropertySymbol.value] - : null - }) - ); - } - } - } - - return removedItem; - } - - /** - * @override - */ - public override removeNamedItemNS(namespace: string, localName: string): Attr | null { - return super.removeNamedItemNS(namespace, this[PropertySymbol.getAttributeName](localName)); - } - - /** - * Returns attribute name. - * - * @param name Name. - * @returns Attribute name based on namespace. - */ - protected [PropertySymbol.getAttributeName](name): string { - if (this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI] === NamespaceURI.svg) { - return name; - } - return name.toLowerCase(); - } -} diff --git a/packages/happy-dom/src/nodes/element/ElementUtility.ts b/packages/happy-dom/src/nodes/element/ElementUtility.ts deleted file mode 100644 index cc5d861e2..000000000 --- a/packages/happy-dom/src/nodes/element/ElementUtility.ts +++ /dev/null @@ -1,217 +0,0 @@ -import NodeTypeEnum from '../node/NodeTypeEnum.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import Element from './Element.js'; -import Node from '../node/Node.js'; -import HTMLCollection from './HTMLCollection.js'; -import Document from '../document/Document.js'; -import DocumentFragment from '../document-fragment/DocumentFragment.js'; -import HTMLElement from '../html-element/HTMLElement.js'; -import NodeUtility from '../node/NodeUtility.js'; -import DOMException from '../../exception/DOMException.js'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; - -const NAMED_ITEM_ATTRIBUTES = ['id', 'name']; - -/** - * Element utility. - */ -export default class ElementUtility { - /** - * Handles appending a child element to the "children" property. - * - * @param ancestorNode Ancestor node. - * @param node Node to append. - * @param [options] Options. - * @param [options.disableAncestorValidation] Disables validation for checking if the node is an ancestor of the ancestorNode. - * @returns Appended node. - */ - public static appendChild( - ancestorNode: Element | Document | DocumentFragment, - node: Node, - options?: { disableAncestorValidation?: boolean } - ): Node { - if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && node !== ancestorNode) { - if ( - !options?.disableAncestorValidation && - NodeUtility.isInclusiveAncestor(node, ancestorNode) - ) { - throw new DOMException( - "Failed to execute 'appendChild' on 'Node': The new node is a parent of the node to insert to.", - DOMExceptionNameEnum.domException - ); - } - if (node[PropertySymbol.parentNode]) { - const parentNodeChildren = >( - (node[PropertySymbol.parentNode])[PropertySymbol.children] - ); - - if (parentNodeChildren) { - const index = parentNodeChildren.indexOf(node); - if (index !== -1) { - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { - const attribute = (node)[PropertySymbol.attributes].getNamedItem( - attributeName - ); - if (attribute) { - parentNodeChildren[PropertySymbol.removeNamedItem]( - node, - attribute[PropertySymbol.value] - ); - } - } - - parentNodeChildren.splice(index, 1); - } - } - } - const ancestorNodeChildren = >( - (ancestorNode)[PropertySymbol.children] - ); - - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { - const attribute = (node)[PropertySymbol.attributes].getNamedItem(attributeName); - if (attribute) { - ancestorNodeChildren[PropertySymbol.appendNamedItem]( - node, - attribute[PropertySymbol.value] - ); - } - } - - ancestorNodeChildren.push(node); - - NodeUtility.appendChild(ancestorNode, node, { disableAncestorValidation: true }); - } else { - NodeUtility.appendChild(ancestorNode, node, options); - } - - return node; - } - - /** - * Handles removing a child element from the "children" property. - * - * @param ancestorNode Ancestor node. - * @param node Node. - * @returns Removed node. - */ - public static removeChild(ancestorNode: Element | Document | DocumentFragment, node: Node): Node { - if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - const ancestorNodeChildren = >( - (ancestorNode)[PropertySymbol.children] - ); - const index = ancestorNodeChildren.indexOf(node); - if (index !== -1) { - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { - const attribute = (node)[PropertySymbol.attributes].getNamedItem(attributeName); - if (attribute) { - ancestorNodeChildren[PropertySymbol.removeNamedItem]( - node, - attribute[PropertySymbol.value] - ); - } - } - ancestorNodeChildren.splice(index, 1); - } - } - - NodeUtility.removeChild(ancestorNode, node); - - return node; - } - - /** - * - * Handles inserting a child element to the "children" property. - * - * @param ancestorNode Ancestor node. - * @param newNode Node to insert. - * @param referenceNode Node to insert before. - * @param [options] Options. - * @param [options.disableAncestorValidation] Disables validation for checking if the node is an ancestor of the ancestorNode. - * @returns Inserted node. - */ - public static insertBefore( - ancestorNode: Element | Document | DocumentFragment, - newNode: Node, - referenceNode: Node | null, - options?: { disableAncestorValidation?: boolean } - ): Node { - // NodeUtility.insertBefore() will call appendChild() for the scenario where "referenceNode" is "null" or "undefined" - if (newNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && referenceNode) { - if ( - !options?.disableAncestorValidation && - NodeUtility.isInclusiveAncestor(newNode, ancestorNode) - ) { - throw new DOMException( - "Failed to execute 'insertBefore' on 'Node': The new node is a parent of the node to insert to.", - DOMExceptionNameEnum.domException - ); - } - if (newNode[PropertySymbol.parentNode]) { - const parentNodeChildren = >( - (newNode[PropertySymbol.parentNode])[PropertySymbol.children] - ); - - if (parentNodeChildren) { - const index = parentNodeChildren.indexOf(newNode); - if (index !== -1) { - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { - const attribute = (newNode)[PropertySymbol.attributes].getNamedItem( - attributeName - ); - if (attribute) { - parentNodeChildren[PropertySymbol.removeNamedItem]( - newNode, - attribute[PropertySymbol.value] - ); - } - } - - parentNodeChildren.splice(index, 1); - } - } - } - - const ancestorNodeChildren = >( - (ancestorNode)[PropertySymbol.children] - ); - - if (referenceNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - const index = ancestorNodeChildren.indexOf(referenceNode); - if (index !== -1) { - ancestorNodeChildren.splice(index, 0, newNode); - } - } else { - ancestorNodeChildren.length = 0; - - for (const node of (ancestorNode)[PropertySymbol.childNodes]) { - if (node === referenceNode) { - ancestorNodeChildren.push(newNode); - } - if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - ancestorNodeChildren.push(node); - } - } - } - - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { - const attribute = (newNode)[PropertySymbol.attributes].getNamedItem(attributeName); - if (attribute) { - ancestorNodeChildren[PropertySymbol.appendNamedItem]( - newNode, - attribute[PropertySymbol.value] - ); - } - } - - NodeUtility.insertBefore(ancestorNode, newNode, referenceNode, { - disableAncestorValidation: true - }); - } else { - NodeUtility.insertBefore(ancestorNode, newNode, referenceNode, options); - } - - return newNode; - } -} diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index 7c3643761..97ec54df9 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -1,17 +1,91 @@ import * as PropertySymbol from '../../PropertySymbol.js'; +import Attr from '../attr/Attr.js'; +import Node from '../node/Node.js'; +import NodeTypeEnum from '../node/NodeTypeEnum.js'; +import Element from './Element.js'; +import IHTMLCollection from './IHTMLCollection.js'; +import THTMLCollectionListener from './THTMLCollectionListener.js'; +import TNamedNodeMapListener from './TNamedNodeMapListener.js'; + +const NAMED_ITEM_ATTRIBUTES = ['id', 'name']; /** - * HTML collection. + * HTMLCollection. + * + * We are extending Array here to improve performance. + * However, we should not expose Array methods to the outside. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection */ -export default class HTMLCollection extends Array implements HTMLCollection { - protected [PropertySymbol.namedItems]: { [k: string]: T[] } = {}; +class HTMLCollection extends Array implements IHTMLCollection { + public [PropertySymbol.namedItems] = new Map>(); + #namedNodeMapListeners = new Map< + T, + { set: TNamedNodeMapListener; remove: TNamedNodeMapListener } + >(); + #eventListeners: { + indexChange: WeakRef>[]; + propertyChange: WeakRef>[]; + } = { + indexChange: [], + propertyChange: [] + }; + #filter: (item: T) => boolean | null; + + /** + * Constructor. + * + * @param [filter] Filter. + * @param items + */ + constructor( + filter?: (item: T) => boolean, + items?: Array<{ [index: number]: T; length: number }> + ) { + super(); + + this.#filter = filter || null; + + if (items) { + for (let i = 0, max = items.length; i < max; i++) { + this[PropertySymbol.addItem](items[i]); + } + } + } + + /** + * Returns `Symbol.toStringTag`. + * + * @returns `Symbol.toStringTag`. + */ + public get [Symbol.toStringTag](): string { + return this.constructor.name; + } + + /** + * Returns `[object HTMLCollection]`. + * + * @returns `[object HTMLCollection]`. + */ + public toLocaleString(): string { + return `[object ${this.constructor.name}]`; + } + + /** + * Returns `[object HTMLCollection]`. + * + * @returns `[object HTMLCollection]`. + */ + public toString(): string { + return `[object ${this.constructor.name}]`; + } /** * Returns item by index. * * @param index Index. */ - public item(index: number): T | null { + public item(index: number): T { return index >= 0 && this[index] ? this[index] : null; } @@ -21,57 +95,344 @@ export default class HTMLCollection extends Array implements HTMLCollectionreferenceItem).parentNode?.[PropertySymbol.childNodes]; + let referenceItemIndex: number = -1; + + if (referenceItem[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + referenceItemIndex = parentChildNodes[PropertySymbol.indexOf](referenceItem); + } else { + for ( + let i = parentChildNodes[PropertySymbol.indexOf](referenceItem), + max = parentChildNodes.length; + i < max; + i++ + ) { + if ( + parentChildNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(parentChildNodes[i])) + ) { + referenceItemIndex = i; + break; + } + } + } + + if (referenceItemIndex === -1) { + return this[PropertySymbol.addItem](newItem); + } + + super.splice(referenceItemIndex, 0, newItem); + + this[PropertySymbol.addNamedItem](newItem); + this[PropertySymbol.dispatchEvent]('indexChange', { index: referenceItemIndex, item: newItem }); + + return true; + } + + /** + * Removes item. + * + * @param item Item. + * @returns True if removed. + */ + public [PropertySymbol.removeItem](item: T): boolean { + const index = super.indexOf(item); + + if (index === -1) { + return false; + } + + super.splice(index, 1); + + this[PropertySymbol.removeNamedItem](item); + + return true; + } + + /** + * Index of item. + * + * @param item Item. + * @returns Index. + */ + public [PropertySymbol.indexOf](item: T): number { + return super.indexOf(item); + } + + /** + * Returns true if the item is in the list. + * + * @param item Item. + * @returns True if the item is in the list. */ - public [PropertySymbol.appendNamedItem](node: T, name: string): void { + public [PropertySymbol.includes](item: T): boolean { + return super.includes(item); + } + + /** + * Adds event listener. + * + * @param type Type. + * @param listener Listener. + */ + public [PropertySymbol.addEventListener]( + type: 'indexChange' | 'propertyChange', + listener: THTMLCollectionListener + ): void { + this.#eventListeners[type].push(new WeakRef(listener)); + } + + /** + * Removes event listener. + * + * @param type Type. + * @param listener Listener. + */ + public [PropertySymbol.removeEventListener]( + type: 'indexChange' | 'propertyChange', + listener: THTMLCollectionListener + ): void { + const listeners = this.#eventListeners[type]; + for (let i = 0, max = listeners.length; i < max; i++) { + if (listeners[i].deref() === listener) { + listeners.splice(i, 1); + return; + } + } + } + + /** + * Dispatches event. + * + * @param type Type. + * @param details Options. + * @param [details.index] Index. + * @param [details.item] Item. + * @param [details.propertyName] Property name. + * @param [details.propertyValue] Property value. + */ + public [PropertySymbol.dispatchEvent]( + type: 'indexChange' | 'propertyChange', + details: { + index?: number; + item?: T; + propertyName?: string; + propertyValue?: any; + } + ): void { + const listeners = this.#eventListeners[type]; + for (let i = 0, max = listeners.length; i < max; i++) { + const listener = listeners[i].deref(); + if (listener) { + listener(details); + } else { + listeners.splice(i, 1); + i--; + max--; + } + } + } + + /** + * Updates named item. + * + * @param item Item. + * @param attributeName Attribute name. + */ + public [PropertySymbol.updateNamedItem](item: T, attributeName: string): void { + if (!this.#filter(item)) { + return; + } + + const name = (item)[PropertySymbol.attributes][attributeName]?.value; + if (name) { - this[PropertySymbol.namedItems][name] = this[PropertySymbol.namedItems][name] || []; + const namedItems = this[PropertySymbol.getNamedItems](name); - if (!this[PropertySymbol.namedItems][name].includes(node)) { - this[PropertySymbol.namedItems][name].push(node); + if (!namedItems.includes(item)) { + this[PropertySymbol.namedItems].set(name, namedItems); + this[PropertySymbol.setNamedItemProperty](name); } + } else { + const namedItems = this[PropertySymbol.getNamedItems](name); + const index = namedItems.indexOf(item); - if (!this.hasOwnProperty(name) && this[PropertySymbol.isValidPropertyName](name)) { - this[name] = this[PropertySymbol.namedItems][name][0]; + if (index !== -1) { + namedItems.splice(index, 1); } + + this[PropertySymbol.setNamedItemProperty](name); } } /** - * Appends named item. + * Adds named item to collection. * - * @param node Node. - * @param name Name. + * @param item Item. + */ + protected [PropertySymbol.addNamedItem](item: T): void { + const listeners = { + set: (attribute: Attr) => { + if (NAMED_ITEM_ATTRIBUTES.includes(attribute.name)) { + this[PropertySymbol.updateNamedItem](item, attribute.name); + } + }, + remove: (attribute: Attr) => { + if (NAMED_ITEM_ATTRIBUTES.includes(attribute.name)) { + this[PropertySymbol.updateNamedItem](item, attribute.name); + } + } + }; + + item[PropertySymbol.attributes][PropertySymbol.addEventListener]('set', listeners.set); + item[PropertySymbol.attributes][PropertySymbol.addEventListener]('remove', listeners.remove); + + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { + const name = (item)[PropertySymbol.attributes][attributeName]?.value; + if (name) { + const namedItems = this[PropertySymbol.getNamedItems](name); + + if (namedItems.includes(item)) { + return; + } + + this[PropertySymbol.namedItems].set(name, namedItems); + + this[PropertySymbol.setNamedItemProperty](name); + } + } + } + + /** + * Removes named item from collection. + * + * @param item Item. */ - public [PropertySymbol.removeNamedItem](node: T, name: string): void { - if (name && this[PropertySymbol.namedItems][name]) { - const index = this[PropertySymbol.namedItems][name].indexOf(node); + protected [PropertySymbol.removeNamedItem](item: T): void { + const listeners = this.#namedNodeMapListeners.get(item); - if (index > -1) { - this[PropertySymbol.namedItems][name].splice(index, 1); + if (listeners) { + item[PropertySymbol.attributes][PropertySymbol.removeEventListener]('set', listeners.set); + item[PropertySymbol.attributes][PropertySymbol.removeEventListener]( + 'remove', + listeners.remove + ); + } - if (this[PropertySymbol.namedItems][name].length === 0) { - delete this[PropertySymbol.namedItems][name]; - if (this.hasOwnProperty(name) && this[PropertySymbol.isValidPropertyName](name)) { - delete this[name]; - } - } else if (this[PropertySymbol.isValidPropertyName](name)) { - this[name] = this[PropertySymbol.namedItems][name][0]; + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { + const name = (item)[PropertySymbol.attributes][attributeName]?.value; + if (name) { + const namedItems = this[PropertySymbol.getNamedItems](name); + + const index = namedItems.indexOf(item); + + if (index === -1) { + return; } + + namedItems.splice(index, 1); + + this[PropertySymbol.setNamedItemProperty](name); } } } + /** + * Returns named items. + * + * @param name Name. + * @returns Named items. + */ + protected [PropertySymbol.getNamedItems](name: string): T[] { + return this[PropertySymbol.namedItems].get(name) || []; + } + + /** + * Sets named item property. + * + * @param name Name. + */ + protected [PropertySymbol.setNamedItemProperty](name: string): void { + if (!this[PropertySymbol.isValidPropertyName](name)) { + return; + } + + const namedItems = this[PropertySymbol.namedItems].get(name); + + if (namedItems?.length) { + if (Object.getOwnPropertyDescriptor(this, name)?.value !== namedItems[0]) { + Object.defineProperty(this, name, { + value: namedItems[0], + writable: false, + enumerable: true, + configurable: true + }); + } + } else { + delete this[name]; + } + + this[PropertySymbol.dispatchEvent]('propertyChange', { + propertyName: name, + propertyValue: this[name] ?? null + }); + } + /** * Returns "true" if the property name is valid. * @@ -82,8 +443,28 @@ export default class HTMLCollection extends Array implements HTMLCollection {}, + get: descriptor.get + }); + } else { + if (typeof descriptor.value === 'function') { + Object.defineProperty(HTMLCollection.prototype, key, {}); + } + } +} + +// Forces the type to be an interface to hide Array methods from the outside. +export default < + new (filter?: (item: T) => boolean) => IHTMLCollection +>(HTMLCollection); diff --git a/packages/happy-dom/src/nodes/element/IHTMLCollection.ts b/packages/happy-dom/src/nodes/element/IHTMLCollection.ts new file mode 100644 index 000000000..fdc7bfe68 --- /dev/null +++ b/packages/happy-dom/src/nodes/element/IHTMLCollection.ts @@ -0,0 +1,137 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable filenames/match-exported */ + +import * as PropertySymbol from '../../PropertySymbol.js'; +import THTMLCollectionListener from './THTMLCollectionListener.js'; + +/** + * HTMLCollection. + * + * This interface is used to hide Array methods from the outside. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection + */ +export default interface IHTMLCollection { + [index: number]: T; + + /** + * Returns the number of items in the collection. + * + * @returns Number of items. + */ + readonly length: number; + + /** + * Returns item by index. + * + * @param index Index. + * @returns Item. + */ + item(index: number): T | null; + + /** + * Returns item by name. + * + * @param name Name. + * @returns Item. + */ + namedItem(name: string): NamedItem | null; + + /** + * Appends item. + * + * @param item Item. + * @returns True if added. + */ + [PropertySymbol.addItem](item: T): boolean; + + /** + * Inserts item before another item. + * + * @param newItem New item. + * @param [referenceItem] Reference item. + * @returns True if inserted. + */ + [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean; + + /** + * Removes item. + * + * @param item Item. + * @returns True if removed. + */ + [PropertySymbol.removeItem](item: T): boolean; + + /** + * Adds event listener. + * + * @param type Type. + * @param listener Listener. + */ + [PropertySymbol.addEventListener]( + type: 'indexChange' | 'propertyChange', + listener: THTMLCollectionListener + ): void; + + /** + * Removes event listener. + * + * @param type Type. + * @param listener Listener. + */ + [PropertySymbol.removeEventListener]( + type: 'indexChange' | 'propertyChange', + listener: THTMLCollectionListener + ): void; + + /** + * Dispatches event. + * + * @param type Type. + * @param details Options. + * @param [details.index] Index. + * @param [details.item] Item. + * @param [details.propertyName] Property name. + * @param [details.propertyValue] Property value. + */ + [PropertySymbol.dispatchEvent]( + type: 'indexChange' | 'propertyChange', + details: { + index?: number; + item?: T; + propertyName?: string; + propertyValue?: any; + } + ): void; + + /** + * Updates named item. + * + * @param item Item. + * @param attributeName Attribute name. + */ + [PropertySymbol.updateNamedItem](item: T, attributeName: string): void; + + /** + * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. + * + * @returns Iterator. + */ + [Symbol.iterator](): IterableIterator; + + /** + * Index of item. + * + * @param item Item. + * @returns Index. + */ + [PropertySymbol.indexOf](item?: T): number; + + /** + * Returns true if the item is in the list. + * + * @param item Item. + * @returns True if the item is in the list. + */ + [PropertySymbol.includes](item: T): boolean; +} diff --git a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts new file mode 100644 index 000000000..5c247f5f9 --- /dev/null +++ b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts @@ -0,0 +1,333 @@ +import * as PropertySymbol from '../../PropertySymbol.js'; +import Attr from '../attr/Attr.js'; +import DOMException from '../../exception/DOMException.js'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import Element from './Element.js'; +import NamespaceURI from '../../config/NamespaceURI.js'; +import TNamedNodeMapListener from './TNamedNodeMapListener.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class NamedNodeMap { + [index: number]: Attr; + + public length = 0; + public [PropertySymbol.namedItems]: Map = new Map(); + public [PropertySymbol.ownerElement]: Element; + + #eventListeners: { + set: WeakRef[]; + remove: WeakRef[]; + } = { + set: [], + remove: [] + }; + + /** + * Constructor. + * + * @param ownerElement Owner element. + */ + constructor(ownerElement: Element) { + this[PropertySymbol.ownerElement] = ownerElement; + } + + /** + * Returns string. + * + * @returns string. + */ + public get [Symbol.toStringTag](): string { + return 'NamedNodeMap'; + } + + /** + * Iterator. + * + * @returns Iterator. + */ + public *[Symbol.iterator](): IterableIterator { + for (let i = 0, max = this.length; i < max; i++) { + yield this[i]; + } + } + + /** + * Returns item by index. + * + * @param index Index. + */ + public item(index: number): Attr | null { + return index >= 0 && this[index] ? this[index] : null; + } + + /** + * Returns named item. + * + * @param name Name. + * @returns Item. + */ + public getNamedItem(name: string): Attr | null { + return this[PropertySymbol.namedItems].get(this.#getAttributeName(name)) || null; + } + + /** + * Returns item by name and namespace. + * + * @param namespace Namespace. + * @param localName Local name of the attribute. + * @returns Item. + */ + public getNamedItemNS(namespace: string, localName: string): Attr | null { + const attribute = this.getNamedItem(localName); + + if ( + attribute && + attribute[PropertySymbol.namespaceURI] === namespace && + attribute.localName === localName + ) { + return attribute; + } + + for (let i = 0, max = this.length; i < max; i++) { + if (this[i][PropertySymbol.namespaceURI] === namespace && this[i].localName === localName) { + return this[i]; + } + } + + return null; + } + + /** + * Sets named item. + * + * @param item Item. + * @returns Replaced item. + */ + public setNamedItem(item: Attr): Attr | null { + return this[PropertySymbol.setNamedItem](item); + } + + /** + * Adds a new namespaced item. + * + * @alias setNamedItem() + * @param item Item. + * @returns Replaced item. + */ + public setNamedItemNS(item: Attr): Attr | null { + return this[PropertySymbol.setNamedItem](item); + } + + /** + * Removes an item. + * + * @throws DOMException + * @param name Name of item. + * @returns Removed item. + */ + public removeNamedItem(name: string): Attr { + const item = this[PropertySymbol.removeNamedItem](name); + if (!item) { + throw new DOMException( + `Failed to execute 'removeNamedItem' on 'NamedNodeMap': No item with name '${name}' was found.`, + DOMExceptionNameEnum.notFoundError + ); + } + return item; + } + + /** + * Removes a namespaced item. + * + * @param namespace Namespace. + * @param localName Local name of the item. + * @returns Removed item. + */ + public removeNamedItemNS(namespace: string, localName: string): Attr | null { + const attribute = this.getNamedItemNS(namespace, this.#getAttributeName(localName)); + if (attribute) { + return this.removeNamedItem(attribute[PropertySymbol.name]); + } + return null; + } + + /** + * Adds event listener. + * + * @param type Type. + * @param listener Listener. + */ + public [PropertySymbol.addEventListener]( + type: 'set' | 'remove', + listener: TNamedNodeMapListener + ): void { + this.#eventListeners[type].push(new WeakRef(listener)); + } + + /** + * Removes event listener. + * + * @param type Type. + * @param listener Listener. + */ + public [PropertySymbol.removeEventListener]( + type: 'set' | 'remove', + listener: TNamedNodeMapListener + ): void { + const listeners = this.#eventListeners[type]; + for (let i = 0, max = listeners.length; i < max; i++) { + if (listeners[i].deref() === listener) { + listeners.splice(i, 1); + return; + } + } + } + + /** + * Dispatches event. + * + * @param type Type. + * @param attribute Attribute. + * @param replacedAttribute Replaced attribute. + */ + public [PropertySymbol.dispatchEvent]( + type: 'set' | 'remove', + attribute: Attr, + replacedAttribute?: Attr | null + ): void { + const listeners = this.#eventListeners[type]; + for (let i = 0, max = listeners.length; i < max; i++) { + const listener = listeners[i].deref(); + if (listener) { + listener(attribute, replacedAttribute); + } else { + listeners.splice(i, 1); + i--; + max--; + } + } + } + + /** + * Sets named item. + * + * @param item Item. + * @param [ignoreListeners] Ignores listeners. + * @returns Replaced item. + */ + public [PropertySymbol.setNamedItem](item: Attr, ignoreListeners = false): Attr | null { + if (!item[PropertySymbol.name]) { + return null; + } + + item[PropertySymbol.name] = this.#getAttributeName(item[PropertySymbol.name]); + (item[PropertySymbol.ownerElement]) = this[PropertySymbol.ownerElement]; + + if (this[PropertySymbol.ownerElement][PropertySymbol.isConnected]) { + this[PropertySymbol.ownerElement][PropertySymbol.ownerDocument][PropertySymbol.cacheID]++; + } + + const name = item[PropertySymbol.name]; + const replacedItem = this[PropertySymbol.namedItems].get(name) || null; + + this[PropertySymbol.namedItems].set(name, item); + + if (replacedItem) { + this.#removeNamedItemIndex(replacedItem); + } + + this[this.length] = item; + this.length++; + + if (this.#isValidPropertyName(name)) { + this[name] = item; + } + + if (!ignoreListeners && replacedItem?.value !== item.value) { + this[PropertySymbol.dispatchEvent]('set', item, replacedItem); + } + + return replacedItem; + } + + /** + * Removes an item without throwing if it doesn't exist. + * + * @param name Name of item. + * @param [ignoreListeners] Ignores listeners. + * @returns Removed item, or null if it didn't exist. + */ + public [PropertySymbol.removeNamedItem](name: string, ignoreListeners = false): Attr | null { + const removedItem = this[PropertySymbol.namedItems].get(this.#getAttributeName(name)); + + if (!removedItem) { + return null; + } + + this.#removeNamedItemIndex(removedItem); + + if (this[name] === removedItem) { + delete this[name]; + } + + this[PropertySymbol.namedItems].delete(name); + + if (!ignoreListeners) { + this[PropertySymbol.dispatchEvent]('remove', removedItem); + } + + return removedItem; + } + + /** + * Removes an item from index. + * + * @param item Item. + */ + #removeNamedItemIndex(item: Attr): void { + for (let i = 0; i < this.length; i++) { + if (this[i] === item) { + for (let b = i; b < this.length; b++) { + if (b < this.length - 1) { + this[b] = this[b + 1]; + } else { + delete this[b]; + } + } + this.length--; + break; + } + } + } + + /** + * Returns "true" if the property name is valid. + * + * @param name Name. + * @returns True if the property name is valid. + */ + #isValidPropertyName(name: string): boolean { + return ( + !!name && + !this.constructor.prototype.hasOwnProperty(name) && + (isNaN(Number(name)) || name.includes('.')) + ); + } + + /** + * Returns attribute name. + * + * @param name Name. + * @returns Attribute name based on namespace. + */ + #getAttributeName(name): string { + if (this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI] === NamespaceURI.svg) { + return name; + } + return name.toLowerCase(); + } +} diff --git a/packages/happy-dom/src/nodes/element/THTMLCollectionListener.ts b/packages/happy-dom/src/nodes/element/THTMLCollectionListener.ts new file mode 100644 index 000000000..9c4f6736c --- /dev/null +++ b/packages/happy-dom/src/nodes/element/THTMLCollectionListener.ts @@ -0,0 +1,7 @@ +type THTMLCollectionListener = (details: { + index?: number; + item?: T; + propertyName?: string; + propertyValue?: any; +}) => void; +export default THTMLCollectionListener; diff --git a/packages/happy-dom/src/nodes/element/TNamedNodeMapListener.ts b/packages/happy-dom/src/nodes/element/TNamedNodeMapListener.ts new file mode 100644 index 000000000..d3238a879 --- /dev/null +++ b/packages/happy-dom/src/nodes/element/TNamedNodeMapListener.ts @@ -0,0 +1,4 @@ +import Attr from '../attr/Attr.js'; + +type TNamedNodeMapListener = (attribute: Attr, replacedAttribute?: Attr | null) => void; +export default TNamedNodeMapListener; diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index 5c5813888..d1b0c34e7 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -1,13 +1,12 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLHyperlinkElementNamedNodeMap from '../html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.js'; import Event from '../../event/Event.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import PointerEvent from '../../event/events/PointerEvent.js'; import HTMLHyperlinkElementUtility from '../html-hyperlink-element/HTMLHyperlinkElementUtility.js'; import IHTMLHyperlinkElement from '../html-hyperlink-element/IHTMLHyperlinkElement.js'; +import Attr from '../attr/Attr.js'; /** * HTML Anchor Element. @@ -16,12 +15,24 @@ import IHTMLHyperlinkElement from '../html-hyperlink-element/IHTMLHyperlinkEleme * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement. */ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyperlinkElement { - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLHyperlinkElementNamedNodeMap( - this - ); public [PropertySymbol.relList]: DOMTokenList = null; #htmlHyperlinkElementUtility = new HTMLHyperlinkElementUtility(this); + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } + /** * Returns download. * @@ -401,4 +412,25 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper return returnValue; } + + /** + * Triggered when an attribute is set. + * @param item + */ + #onSetAttribute(item: Attr): void { + if (item[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + this[PropertySymbol.relList][PropertySymbol.updateIndices](); + } + } + + /** + * Triggered when an attribute is removed. + * @param name + * @param removedItem + */ + #onRemoveAttribute(removedItem: Attr): void { + if (removedItem[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + this[PropertySymbol.relList][PropertySymbol.updateIndices](); + } + } } diff --git a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts index 2a3071803..d5b01bc0d 100644 --- a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts @@ -1,13 +1,12 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLHyperlinkElementNamedNodeMap from '../html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; import HTMLHyperlinkElementUtility from '../html-hyperlink-element/HTMLHyperlinkElementUtility.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; import IHTMLHyperlinkElement from '../html-hyperlink-element/IHTMLHyperlinkElement.js'; import PointerEvent from '../../event/events/PointerEvent.js'; import Event from '../../event/Event.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; +import Attr from '../attr/Attr.js'; /** * HTMLAreaElement @@ -15,12 +14,24 @@ import EventPhaseEnum from '../../event/EventPhaseEnum.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLAreaElement */ export default class HTMLAreaElement extends HTMLElement implements IHTMLHyperlinkElement { - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLHyperlinkElementNamedNodeMap( - this - ); public [PropertySymbol.relList]: DOMTokenList = null; #htmlHyperlinkElementUtility = new HTMLHyperlinkElementUtility(this); + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } + /** * Returns alt. * @@ -400,4 +411,25 @@ export default class HTMLAreaElement extends HTMLElement implements IHTMLHyperli return returnValue; } + + /** + * Triggered when an attribute is set. + * @param item + */ + #onSetAttribute(item: Attr): void { + if (item[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + this[PropertySymbol.relList][PropertySymbol.updateIndices](); + } + } + + /** + * Triggered when an attribute is removed. + * @param name + * @param removedItem + */ + #onRemoveAttribute(removedItem: Attr): void { + if (removedItem[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + this[PropertySymbol.relList][PropertySymbol.updateIndices](); + } + } } diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index 216daffd5..10b256ff4 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -1,7 +1,6 @@ import Event from '../../event/Event.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; import ValidityState from '../../validity-state/ValidityState.js'; import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; @@ -9,9 +8,11 @@ import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtili import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import Node from '../node/Node.js'; import NodeList from '../node/NodeList.js'; -import HTMLButtonElementNamedNodeMap from './HTMLButtonElementNamedNodeMap.js'; import PointerEvent from '../../event/events/PointerEvent.js'; import { URL } from 'url'; +import HTMLFieldSetElement from '../html-field-set-element/HTMLFieldSetElement.js'; +import Document from '../document/Document.js'; +import Attr from '../attr/Attr.js'; const BUTTON_TYPES = ['submit', 'reset', 'button', 'menu']; @@ -22,11 +23,24 @@ const BUTTON_TYPES = ['submit', 'reset', 'button', 'menu']; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement. */ export default class HTMLButtonElement extends HTMLElement { - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLButtonElementNamedNodeMap( - this - ); public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); + public [PropertySymbol.formNode]: HTMLFormElement | null = null; + + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } /** * Returns validation message. @@ -232,17 +246,19 @@ export default class HTMLButtonElement extends HTMLElement { * * @returns Form. */ - public get form(): HTMLFormElement | null { - if (this[PropertySymbol.formNode]) { - return this[PropertySymbol.formNode]; - } - if (!this.isConnected) { - return null; - } + public get form(): HTMLFormElement { const formID = this.getAttribute('form'); - return formID - ? this[PropertySymbol.ownerDocument].getElementById(formID) - : null; + + if (formID !== null) { + if (!this[PropertySymbol.isConnected]) { + return null; + } + return formID + ? (this[PropertySymbol.rootNode]).getElementById(formID) + : null; + } + + return this[PropertySymbol.formNode]; } /** @@ -328,32 +344,6 @@ export default class HTMLButtonElement extends HTMLElement { return returnValue; } - /** - * @override - */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - const oldFormNode = this[PropertySymbol.formNode]; - - super[PropertySymbol.connectToNode](parentNode); - - if (oldFormNode !== this[PropertySymbol.formNode]) { - if (oldFormNode) { - oldFormNode[PropertySymbol.removeFormControlItem](this, this.name); - oldFormNode[PropertySymbol.removeFormControlItem](this, this.id); - } - if (this[PropertySymbol.formNode]) { - (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( - this, - this.name - ); - (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( - this, - this.id - ); - } - } - } - /** * Sanitizes type. * diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts deleted file mode 100644 index acc458ca2..000000000 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; -import HTMLButtonElement from './HTMLButtonElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLButtonElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') && - this[PropertySymbol.ownerElement][PropertySymbol.formNode] - ) { - if (replacedItem?.[PropertySymbol.value]) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.removeFormControlItem - ](this[PropertySymbol.ownerElement], replacedItem[PropertySymbol.value]); - } - if (item[PropertySymbol.value]) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.appendFormControlItem - ](this[PropertySymbol.ownerElement], item[PropertySymbol.value]); - } - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem && - (removedItem[PropertySymbol.name] === 'id' || removedItem[PropertySymbol.name] === 'name') && - this[PropertySymbol.ownerElement][PropertySymbol.formNode] - ) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.removeFormControlItem - ](this[PropertySymbol.ownerElement], removedItem[PropertySymbol.value]); - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts index 736472519..d66aaa248 100644 --- a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts +++ b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts @@ -1,6 +1,6 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLCollection from '../element/HTMLCollection.js'; +import HTMLCollection from '../element/HTMLCollection2.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; import Node from '../node/Node.js'; diff --git a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts index 9e2cc05a8..df9dd6ad7 100644 --- a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts +++ b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts @@ -1,8 +1,7 @@ import Event from '../../event/Event.js'; import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLDetailsElementNamedNodeMap from './HTMLDetailsElementNamedNodeMap.js'; +import Attr from '../attr/Attr.js'; /** * HTMLDetailsElement @@ -10,13 +9,24 @@ import HTMLDetailsElementNamedNodeMap from './HTMLDetailsElementNamedNodeMap.js' * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDetailsElement */ export default class HTMLDetailsElement extends HTMLElement { - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLDetailsElementNamedNodeMap( - this - ); - // Events public ontoggle: (event: Event) => void | null = null; + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } + /** * Returns the open attribute. */ @@ -36,4 +46,29 @@ export default class HTMLDetailsElement extends HTMLElement { this.removeAttribute('open'); } } + + /** + * Triggered when an attribute is set. + * + * @param attribute Attribute + * @param replacedAttribute Replaced item + */ + #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + if (attribute[PropertySymbol.name] === 'open') { + if (attribute[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value]) { + this.dispatchEvent(new Event('toggle')); + } + } + } + + /** + * Triggered when an attribute is removed. + * + * @param removedAttribute Removed attribute. + */ + #onRemoveAttribute(removedAttribute: Attr): void { + if (removedAttribute && removedAttribute[PropertySymbol.name] === 'open') { + this.dispatchEvent(new Event('toggle')); + } + } } diff --git a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElementNamedNodeMap.ts deleted file mode 100644 index 00e6f9560..000000000 --- a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElementNamedNodeMap.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import Event from '../../event/Event.js'; -import HTMLDetailsElement from './HTMLDetailsElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLDetailsElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLDetailsElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if (item[PropertySymbol.name] === 'open') { - if (item[PropertySymbol.value] !== replacedItem?.[PropertySymbol.value]) { - this[PropertySymbol.ownerElement].dispatchEvent(new Event('toggle')); - } - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if (removedItem && removedItem[PropertySymbol.name] === 'open') { - this[PropertySymbol.ownerElement].dispatchEvent(new Event('toggle')); - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 6022e22a9..91a11914c 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -6,13 +6,13 @@ import NodeTypeEnum from '../node/NodeTypeEnum.js'; import DOMException from '../../exception/DOMException.js'; import Event from '../../event/Event.js'; import HTMLElementUtility from './HTMLElementUtility.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLElementNamedNodeMap from './HTMLElementNamedNodeMap.js'; import NodeList from '../node/NodeList.js'; import Node from '../node/Node.js'; -import HTMLCollection from '../element/HTMLCollection.js'; +import HTMLCollection from '../element/HTMLCollection2.js'; import DatasetFactory from '../element/DatasetFactory.js'; import IDataset from '../element/IDataset.js'; +import Attr from '../attr/Attr.js'; +import NamedNodeMap from '../element/NamedNodeMap.js'; /** * HTML Element. @@ -52,7 +52,6 @@ export default class HTMLElement extends Element { public ontransitionstart: (event: Event) => void | null = null; // Internal properties - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLElementNamedNodeMap(this); public [PropertySymbol.accessKey] = ''; public [PropertySymbol.contentEditable] = 'inherit'; public [PropertySymbol.isContentEditable] = false; @@ -70,6 +69,21 @@ export default class HTMLElement extends Element { #dataset: IDataset = null; #customElementDefineCallback: () => void = null; + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } + /** * Returns access key. * @@ -275,17 +289,20 @@ export default class HTMLElement extends Element { * @param innerText Inner text. */ public set innerText(text: string) { - for (const child of this[PropertySymbol.childNodes].slice()) { - this.removeChild(child); + const childNodes = this[PropertySymbol.childNodes][PropertySymbol.items]; + + while (childNodes.length) { + this.removeChild(childNodes[0]); } const texts = text.split(/[\n\r]/); + const ownerDocument = this[PropertySymbol.ownerDocument]; for (let i = 0, max = texts.length; i < max; i++) { if (i !== 0) { - this.appendChild(this[PropertySymbol.ownerDocument].createElement('br')); + this.appendChild(ownerDocument.createElement('br')); } - this.appendChild(this[PropertySymbol.ownerDocument].createTextNode(texts[i])); + this.appendChild(ownerDocument.createTextNode(texts[i])); } } @@ -506,10 +523,10 @@ export default class HTMLElement extends Element { /** * Connects this element to another element. * + * @override * @see https://html.spec.whatwg.org/multipage/dom.html#htmlelement - * @param parentNode Parent node. */ - public [PropertySymbol.connectToNode](parentNode: Node = null): void { + public [PropertySymbol.connectedToDocument](): void { const localName = this[PropertySymbol.localName]; // This element can potentially be a custom element that has not been defined yet @@ -526,7 +543,7 @@ export default class HTMLElement extends Element { PropertySymbol.callbacks ]; - if (parentNode && !this.#customElementDefineCallback) { + if (!this.#customElementDefineCallback) { const callback = (): void => { if (this[PropertySymbol.parentNode]) { const newElement = ( @@ -553,46 +570,38 @@ export default class HTMLElement extends Element { (>this[PropertySymbol.childNodes]) = new NodeList(); (>this[PropertySymbol.children]) = new HTMLCollection(); + this[PropertySymbol.childNodes][PropertySymbol.attachHTMLCollection]( + this[PropertySymbol.children] + ); this[PropertySymbol.rootNode] = null; this[PropertySymbol.formNode] = null; this[PropertySymbol.selectNode] = null; this[PropertySymbol.textAreaNode] = null; this[PropertySymbol.observers] = []; this[PropertySymbol.isValue] = null; - (this[PropertySymbol.attributes]) = - new HTMLElementNamedNodeMap(this); - - for ( - let i = 0, - max = (this[PropertySymbol.parentNode])[PropertySymbol.childNodes] - .length; - i < max; - i++ - ) { - if ( - (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][i] === - this - ) { - (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][i] = - newElement; + (this[PropertySymbol.attributes]) = new NamedNodeMap(this); + + const childNodes = (this[PropertySymbol.parentNode])[ + PropertySymbol.childNodes + ]?.[PropertySymbol.items]; + const childNodesItems = childNodes[PropertySymbol.items]; + for (let i = 0, max = childNodesItems.length; i < max; i++) { + if (childNodesItems[i] === this) { + (childNodes[i]) = newElement; + (childNodesItems[i]) = newElement; break; } } - if ((this[PropertySymbol.parentNode])[PropertySymbol.children]) { - for ( - let i = 0, - max = (this[PropertySymbol.parentNode])[PropertySymbol.children] - .length; - i < max; - i++ - ) { - if ( - (this[PropertySymbol.parentNode])[PropertySymbol.children][i] === - this - ) { - (this[PropertySymbol.parentNode])[PropertySymbol.children][i] = - newElement; + const children = (this[PropertySymbol.parentNode])[ + PropertySymbol.children + ]; + if (children) { + const childrenItems = children[PropertySymbol.items]; + for (let i = 0, max = childrenItems.length; i < max; i++) { + if (childrenItems[i] === this) { + children[i] = newElement; + childrenItems[i] = newElement; break; } } @@ -602,13 +611,40 @@ export default class HTMLElement extends Element { newElement.connectedCallback(); } - this[PropertySymbol.connectToNode](null); + this[PropertySymbol.connectedToDocument](null); } }; callbacks[localName] = callbacks[localName] || []; callbacks[localName].push(callback); this.#customElementDefineCallback = callback; - } else if (!parentNode && callbacks[localName] && this.#customElementDefineCallback) { + } + } + + super[PropertySymbol.connectedToDocument](); + } + + /** + * Called when disconnected from document. + * @param e + */ + public [PropertySymbol.disconnectedFromDocument](): void { + const localName = this[PropertySymbol.localName]; + + // This element can potentially be a custom element that has not been defined yet + // Therefore we need to register a callback for when it is defined in CustomElementRegistry and replace it with the registered element (see #404) + if ( + this.constructor === HTMLElement && + localName.includes('-') && + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].customElements[ + PropertySymbol.callbacks + ] + ) { + const callbacks = + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].customElements[ + PropertySymbol.callbacks + ]; + + if (callbacks[localName] && this.#customElementDefineCallback) { const index = callbacks[localName].indexOf(this.#customElementDefineCallback); if (index !== -1) { callbacks[localName].splice(index, 1); @@ -620,6 +656,28 @@ export default class HTMLElement extends Element { } } - super[PropertySymbol.connectToNode](parentNode); + super[PropertySymbol.disconnectedFromDocument](); + } + + /** + * Triggered when an attribute is set. + * + * @param item Item. + */ + #onSetAttribute(item: Attr): void { + if (item[PropertySymbol.name] === 'style' && this[PropertySymbol.style]) { + this[PropertySymbol.style].cssText = item[PropertySymbol.value]; + } + } + + /** + * Triggered when an attribute is removed. + * + * @param removedItem Removed item. + */ + #onRemoveAttribute(removedItem: Attr): void { + if (removedItem && removedItem[PropertySymbol.name] === 'style' && this[PropertySymbol.style]) { + this[PropertySymbol.style].cssText = ''; + } } } diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts deleted file mode 100644 index f915e7cd7..000000000 --- a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import ElementNamedNodeMap from '../element/ElementNamedNodeMap.js'; -import HTMLElement from './HTMLElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - item[PropertySymbol.name] === 'style' && - this[PropertySymbol.ownerElement][PropertySymbol.style] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.style].cssText = item[PropertySymbol.value]; - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem && - removedItem[PropertySymbol.name] === 'style' && - this[PropertySymbol.ownerElement][PropertySymbol.style] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.style].cssText = ''; - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts index 007850854..f8a68838a 100644 --- a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts +++ b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts @@ -1,7 +1,188 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import HTMLCollection from '../element/HTMLCollection2.js'; +import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; +import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; +import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; +import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; +import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; +import Document from '../document/Document.js'; +import Attr from '../attr/Attr.js'; + /** * HTMLFieldSetElement * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFieldSetElement */ -export default class HTMLFieldSetElement extends HTMLElement {} +export default class HTMLFieldSetElement extends HTMLElement { + // Internal properties + public [PropertySymbol.elements] = new HTMLCollection< + HTMLInputElement | HTMLButtonElement | HTMLTextAreaElement | HTMLSelectElement + >(); + public [PropertySymbol.formNode]: HTMLFormElement | null = null; + + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } + + /** + * Returns elements. + * + * @returns Elements. + */ + public get elements(): HTMLCollection< + HTMLInputElement | HTMLButtonElement | HTMLTextAreaElement | HTMLSelectElement + > { + return this[PropertySymbol.elements]; + } + + /** + * Returns the parent form element. + * + * @returns Form. + */ + public get form(): HTMLFormElement { + const formID = this.getAttribute('form'); + + if (formID !== null) { + if (!this[PropertySymbol.isConnected]) { + return null; + } + return formID + ? (this[PropertySymbol.rootNode]).getElementById(formID) + : null; + } + + return this[PropertySymbol.formNode]; + } + + /** + * Returns name. + * + * @returns Name. + */ + public get name(): string { + return this.getAttribute('name') || ''; + } + + /** + * Sets name. + * + * @param name Name. + */ + public set name(name: string) { + this.setAttribute('name', name); + } + + /** + * Returns type "fieldset". + * + * @returns Type. + */ + public get type(): string { + return 'fieldset'; + } + + /** + * Returns empty string as fieldset never candidates for constraint validation. + */ + public get validationMessage(): string { + return ''; + } + + /** + * Returns will validate state. + * + * Always returns false as fieldset never candidates for constraint validation. + * + * @returns Will validate state. + */ + public get willValidate(): boolean { + return false; + } + + /** + * Returns disabled. + * + * @returns Disabled. + */ + public get disabled(): boolean { + return this.getAttribute('disabled') !== null; + } + + /** + * Sets disabled. + * + * @param disabled Disabled. + */ + public set disabled(disabled: boolean) { + if (!disabled) { + this.removeAttribute('disabled'); + } else { + this.setAttribute('disabled', ''); + } + } + + /** + * Checks validity. + * + * Always returns true as fieldset never candidates for constraint validation. + * + * @returns "true" if the field is valid. + */ + public checkValidity(): boolean { + return true; + } + + /** + * Reports validity. + * + * Always returns true as fieldset never candidates for constraint validation. + * + * @returns Validity. + */ + public reportValidity(): boolean { + return true; + } + + /** + * Sets validation message. + * + * Does nothing as fieldset never candidates for constraint validation. + * + * @param _message Message. + */ + public setCustomValidity(_message: string): void { + // Do nothing as fieldset never candidates for constraint validation. + } + + /** + * Triggered when an attribute is set. + * + * @param attribute Attribute. + * @param replacedAttribute Replaced attribute. + */ + #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + this.form?.[PropertySymbol.appendFormControlItem](this, attribute, replacedAttribute); + } + + /** + * Triggered when an attribute is removed. + * + * @param removedAttribute Removed attribute. + */ + #onRemoveAttribute(removedAttribute: Attr): void { + this.form?.[PropertySymbol.removeFormControlItem](this, removedAttribute); + } +} diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index d35da7bb8..a0cb921d0 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -1,126 +1,189 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; -import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; -import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; +import Attr from '../attr/Attr.js'; +import Element from '../element/Element.js'; +import HTMLCollection from '../element/HTMLCollection.js'; +import TNamedNodeMapListener from '../element/TNamedNodeMapListener.js'; +import HTMLFormElement from './HTMLFormElement.js'; import RadioNodeList from './RadioNodeList.js'; -import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; +import THTMLFormControlElement from './THTMLFormControlElement.js'; /** * HTMLFormControlsCollection. * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormControlsCollection */ -export default class HTMLFormControlsCollection - extends Array - implements HTMLFormControlsCollection -{ - public [PropertySymbol.namedItems]: { [k: string]: RadioNodeList } = {}; +export default class HTMLFormControlsCollection extends HTMLCollection< + THTMLFormControlElement, + THTMLFormControlElement | RadioNodeList +> { + #namedNodeMapListeners = new Map(); /** - * Returns item by index. - * - * @param index Index. + * Constructor. + * @param formElement */ - public item( - index: number - ): HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement | null { - return index >= 0 && this[index] ? this[index] : null; + constructor(formElement: HTMLFormElement) { + super((item: Element) => { + if ( + item[PropertySymbol.tagName] !== 'INPUT' && + item[PropertySymbol.tagName] !== 'SELECT' && + item[PropertySymbol.tagName] !== 'TEXTAREA' && + item[PropertySymbol.tagName] !== 'BUTTON' && + item[PropertySymbol.tagName] !== 'FIELDSET' + ) { + return false; + } + if (formElement[PropertySymbol.childNodesFlatten][PropertySymbol.includes](item)) { + return true; + } + if ( + !item[PropertySymbol.attributes]['form'] || + !formElement[PropertySymbol.attributes]['id'] + ) { + return false; + } + return ( + item[PropertySymbol.attributes]['form'].value === + formElement[PropertySymbol.attributes]['id'].value + ); + }); } /** - * Returns named item. + * Appends item. * - * @param name Name. - * @returns Node. + * @param item Item. + * @returns True if added. */ - public namedItem( - name: string - ): - | HTMLInputElement - | HTMLTextAreaElement - | HTMLSelectElement - | HTMLButtonElement - | RadioNodeList - | null { - if (this[PropertySymbol.namedItems][name] && this[PropertySymbol.namedItems][name].length) { - if (this[PropertySymbol.namedItems][name].length === 1) { - return this[PropertySymbol.namedItems][name][0]; - } - return this[PropertySymbol.namedItems][name]; + public [PropertySymbol.addItem](item: THTMLFormControlElement): boolean { + const returnValue = super[PropertySymbol.addItem](item); + + if (!returnValue) { + return false; } - return null; + + const listener = (attribute: Attr): void => { + if (attribute.name === 'form') { + this[PropertySymbol.removeItem](item); + this[PropertySymbol.addItem](item); + } + }; + + this.#namedNodeMapListeners.set(item, listener); + item[PropertySymbol.attributes][PropertySymbol.addEventListener]('set', listener); + item[PropertySymbol.attributes][PropertySymbol.addEventListener]('remove', listener); + + return true; } /** - * Appends named item. + * Inserts item before another item. * - * @param node Node. - * @param name Name. + * @param newItem New item. + * @param [referenceItem] Reference item. + * @returns True if inserted. */ - public [PropertySymbol.appendNamedItem]( - node: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement, - name: string - ): void { - if (name) { - this[PropertySymbol.namedItems][name] = - this[PropertySymbol.namedItems][name] || new RadioNodeList(); - - if (!this[PropertySymbol.namedItems][name].includes(node)) { - this[PropertySymbol.namedItems][name].push(node); - } + public [PropertySymbol.insertItem]( + newItem: THTMLFormControlElement, + referenceItem: THTMLFormControlElement | null + ): boolean { + const returnValue = super[PropertySymbol.insertItem](newItem, referenceItem); + + if (!returnValue) { + return false; + } - if (this[PropertySymbol.isValidPropertyName](name)) { - this[name] = - this[PropertySymbol.namedItems][name].length > 1 - ? this[PropertySymbol.namedItems][name] - : this[PropertySymbol.namedItems][name][0]; + const listener = (attribute: Attr): void => { + if (attribute.name === 'form') { + this[PropertySymbol.removeItem](newItem); + this[PropertySymbol.insertItem](newItem, referenceItem); } + }; + + this.#namedNodeMapListeners.set(newItem, listener); + newItem[PropertySymbol.attributes][PropertySymbol.addEventListener]('set', listener); + newItem[PropertySymbol.attributes][PropertySymbol.addEventListener]('remove', listener); + + return true; + } + + /** + * Removes item. + * + * @param item Item. + * @returns True if removed. + */ + public [PropertySymbol.removeItem](item: THTMLFormControlElement): boolean { + const returnValue = super[PropertySymbol.removeItem](item); + + if (!returnValue) { + return false; } + + const listener = this.#namedNodeMapListeners.get(item); + + item[PropertySymbol.attributes][PropertySymbol.removeEventListener]('set', listener); + item[PropertySymbol.attributes][PropertySymbol.removeEventListener]('remove', listener); + + return true; } /** - * Appends named item. + * @override + */ + public namedItem(name: string): THTMLFormControlElement | RadioNodeList | null { + const namedItems = this[PropertySymbol.namedItems].get(name); + + if (!namedItems?.length) { + return null; + } + + if (namedItems.length === 1) { + return namedItems[0]; + } + + return namedItems; + } + + /** + * Returns named items. * - * @param node Node. * @param name Name. + * @returns Named items. */ - public [PropertySymbol.removeNamedItem]( - node: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement, - name: string - ): void { - if (name && this[PropertySymbol.namedItems][name]) { - const index = this[PropertySymbol.namedItems][name].indexOf(node); - - if (index > -1) { - this[PropertySymbol.namedItems][name].splice(index, 1); - - if (this[PropertySymbol.namedItems][name].length === 0) { - delete this[PropertySymbol.namedItems][name]; - if (this.hasOwnProperty(name) && this[PropertySymbol.isValidPropertyName](name)) { - delete this[name]; - } - } else if (this[PropertySymbol.isValidPropertyName](name)) { - this[name] = - this[PropertySymbol.namedItems][name].length > 1 - ? this[PropertySymbol.namedItems][name] - : this[PropertySymbol.namedItems][name][0]; - } - } - } + protected [PropertySymbol.getNamedItems](name: string): RadioNodeList { + return this[PropertySymbol.namedItems].get(name) || new RadioNodeList(); } /** - * Returns "true" if the property name is valid. + * Sets named item property. * * @param name Name. - * @returns True if the property name is valid. */ - protected [PropertySymbol.isValidPropertyName](name: string): boolean { - return ( - !!name && - !this.constructor.prototype.hasOwnProperty(name) && - !Array.prototype.hasOwnProperty(name) && - (isNaN(Number(name)) || name.includes('.')) - ); + protected [PropertySymbol.setNamedItemProperty](name: string): void { + if (!this[PropertySymbol.isValidPropertyName](name)) { + return; + } + + const namedItems = this[PropertySymbol.namedItems].get(name); + + if (namedItems?.length) { + const newValue = namedItems.length === 1 ? namedItems[0] : namedItems; + if (Object.getOwnPropertyDescriptor(this, name)?.value !== newValue) { + Object.defineProperty(this, name, { + value: newValue, + writable: false, + enumerable: true, + configurable: true + }); + } + } else { + delete this[name]; + } + + this[PropertySymbol.dispatchEvent]('propertyChange', { + propertyName: name, + propertyValue: this[name] ?? null + }); } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index 553de7bfd..c5c1a3c42 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -5,7 +5,6 @@ import SubmitEvent from '../../event/events/SubmitEvent.js'; import HTMLFormControlsCollection from './HTMLFormControlsCollection.js'; import Node from '../node/Node.js'; import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; -import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; @@ -13,6 +12,8 @@ import BrowserFrameNavigator from '../../browser/utilities/BrowserFrameNavigator import FormData from '../../form-data/FormData.js'; import Element from '../element/Element.js'; import BrowserWindow from '../../window/BrowserWindow.js'; +import Attr from '../attr/Attr.js'; +import THTMLFormControlElement from './THTMLFormControlElement.js'; /** * HTML Form Element. @@ -25,7 +26,9 @@ export default class HTMLFormElement extends HTMLElement { public cloneNode: (deep?: boolean) => HTMLFormElement; // Internal properties. - public [PropertySymbol.elements]: HTMLFormControlsCollection = new HTMLFormControlsCollection(); + public [PropertySymbol.elements]: HTMLFormControlsCollection = new HTMLFormControlsCollection( + this + ); public [PropertySymbol.length] = 0; public [PropertySymbol.formNode]: Node = this; @@ -36,6 +39,11 @@ export default class HTMLFormElement extends HTMLElement { // Private properties #browserFrame: IBrowserFrame; + #documentChildNodeListeners: { + add: (item: Node) => void; + insert: (newItem: Node, referenceItem: Node | null) => void; + remove: (item: Node) => void; + } | null = null; /** * Constructor. @@ -45,6 +53,61 @@ export default class HTMLFormElement extends HTMLElement { constructor(browserFrame: IBrowserFrame) { super(); this.#browserFrame = browserFrame; + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + + // Child nodes listeners + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { + (item)[PropertySymbol.formNode] = this; + this[PropertySymbol.elements][PropertySymbol.addItem](item); + }); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'insert', + (newItem: Node, referenceItem: Node | null) => { + (newItem)[PropertySymbol.formNode] = this; + this[PropertySymbol.elements][PropertySymbol.insertItem]( + newItem, + referenceItem + ); + } + ); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'remove', + (item: Node) => { + (item)[PropertySymbol.formNode] = null; + this[PropertySymbol.elements][PropertySymbol.removeItem](item); + } + ); + + // Form controls listeners + this[PropertySymbol.elements][PropertySymbol.addEventListener]('indexChange', (details) => { + const length = this[PropertySymbol.elements].length; + this[PropertySymbol.length] = length; + for (let i = details.index; i < length; i++) { + this[i] = this[PropertySymbol.elements][i]; + } + }); + this[PropertySymbol.elements][PropertySymbol.addEventListener]('propertyChange', (details) => { + if (!this[PropertySymbol.isValidPropertyName](details.propertyName)) { + return; + } + if (details.propertyValue) { + Object.defineProperty(this, details.propertyName, { + value: details.propertyValue, + writable: false, + enumerable: true, + configurable: true + }); + } else { + delete this[details.propertyName]; + } + }); } /** @@ -335,59 +398,100 @@ export default class HTMLFormElement extends HTMLElement { } /** - * Appends a form control item. - * - * @param node Node. - * @param name Name - */ - public [PropertySymbol.appendFormControlItem]( - node: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement, - name: string - ): void { - const elements = this[PropertySymbol.elements]; - - if (!elements.includes(node)) { - this[elements.length] = node; - elements.push(node); - this[PropertySymbol.length] = elements.length; - } + * @override + */ + public override [PropertySymbol.connectedToDocument](): void { + super[PropertySymbol.connectedToDocument](); + + // Document child nodes listeners + this.#documentChildNodeListeners = { + add: (item: Node) => { + if (!this[PropertySymbol.isConnected]) { + return; + } + (item)[PropertySymbol.formNode] = this; + this[PropertySymbol.elements][PropertySymbol.addItem](item); + }, + insert: (newItem: Node, referenceItem: Node | null) => { + if (!this[PropertySymbol.isConnected]) { + return; + } + (newItem)[PropertySymbol.formNode] = this; + this[PropertySymbol.elements][PropertySymbol.insertItem]( + newItem, + referenceItem + ); + }, + remove: (item: Node) => { + if (!this[PropertySymbol.isConnected]) { + return; + } + (item)[PropertySymbol.formNode] = null; + this[PropertySymbol.elements][PropertySymbol.removeItem](item); + } + }; + + this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ + PropertySymbol.addEventListener + ]('add', this.#documentChildNodeListeners.add); + this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ + PropertySymbol.addEventListener + ]('insert', this.#documentChildNodeListeners.insert); + this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ + PropertySymbol.addEventListener + ]('remove', this.#documentChildNodeListeners.remove); + + const id = this.id; - (elements)[PropertySymbol.appendNamedItem](node, name); + if (!id) { + return; + } - if (this[PropertySymbol.isValidPropertyName](name)) { - this[name] = elements[name]; + for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { + if ( + node[PropertySymbol.attributes]?.['form']?.value === id && + node[PropertySymbol.formNode] !== this + ) { + node[PropertySymbol.formNode] = this; + this[PropertySymbol.elements][PropertySymbol.addItem](node); + } } } /** - * Remove a form control item. - * - * @param node Node. - * @param name Name. + * @override */ - public [PropertySymbol.removeFormControlItem]( - node: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement, - name: string - ): void { - const elements = this[PropertySymbol.elements]; - const index = elements.indexOf(node); - - if (index !== -1) { - elements.splice(index, 1); - for (let i = index; i < this[PropertySymbol.length]; i++) { - this[i] = this[i + 1]; - } - delete this[this[PropertySymbol.length] - 1]; - this[PropertySymbol.length]--; + public override [PropertySymbol.disconnectedFromDocument](): void { + super[PropertySymbol.disconnectedFromDocument](); + + if (!this.#documentChildNodeListeners) { + return; } - (elements)[PropertySymbol.removeNamedItem](node, name); + // Document child nodes listeners + this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ + PropertySymbol.removeEventListener + ]('add', this.#documentChildNodeListeners.add); + this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ + PropertySymbol.removeEventListener + ]('insert', this.#documentChildNodeListeners.insert); + this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ + PropertySymbol.removeEventListener + ]('remove', this.#documentChildNodeListeners.remove); - if (this[PropertySymbol.isValidPropertyName](name)) { - if (elements[name]) { - this[name] = elements[name]; - } else { - delete this[name]; + const id = this.id; + + if (!id) { + return; + } + + for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { + if ( + node[PropertySymbol.attributes]?.['form']?.value === id && + !this[PropertySymbol.childNodesFlatten][PropertySymbol.includes](node) + ) { + node[PropertySymbol.formNode] = null; + this[PropertySymbol.elements][PropertySymbol.removeItem](node); } } } @@ -484,4 +588,64 @@ export default class HTMLFormElement extends HTMLElement { } }); } + + /** + * Triggered when an attribute is set. + * + * @param attribute Attribute. + * @param replacedAttribute Replaced attribute. + */ + #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + if (attribute.name !== 'id' || !this[PropertySymbol.isConnected]) { + return; + } + + if (replacedAttribute[PropertySymbol.value]) { + for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { + if ( + node[PropertySymbol.attributes]?.['form']?.value === replacedAttribute.value && + !this[PropertySymbol.childNodesFlatten][PropertySymbol.includes](node) + ) { + node[PropertySymbol.formNode] = null; + this[PropertySymbol.elements][PropertySymbol.removeItem](node); + } + } + } + + if (attribute[PropertySymbol.value]) { + for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { + if ( + node[PropertySymbol.attributes]?.['form']?.value === attribute[PropertySymbol.value] && + node[PropertySymbol.formNode] !== this + ) { + node[PropertySymbol.formNode] = this; + this[PropertySymbol.elements][PropertySymbol.addItem](node); + } + } + } + } + + /** + * Triggered when an attribute is removed. + * + * @param removedAttribute Removed attribute. + */ + #onRemoveAttribute(removedAttribute: Attr): void { + if ( + removedAttribute.name === 'id' && + removedAttribute[PropertySymbol.value] && + this[PropertySymbol.isConnected] + ) { + for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { + if ( + node[PropertySymbol.attributes]?.['form']?.value === + removedAttribute[PropertySymbol.value] && + !this[PropertySymbol.childNodesFlatten][PropertySymbol.includes](node) + ) { + node[PropertySymbol.formNode] = null; + this[PropertySymbol.elements][PropertySymbol.removeItem](node); + } + } + } + } } diff --git a/packages/happy-dom/src/nodes/html-form-element/RadioNodeList.ts b/packages/happy-dom/src/nodes/html-form-element/RadioNodeList.ts index 6f36a0111..5b9d4152e 100644 --- a/packages/happy-dom/src/nodes/html-form-element/RadioNodeList.ts +++ b/packages/happy-dom/src/nodes/html-form-element/RadioNodeList.ts @@ -1,17 +1,13 @@ -import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; -import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; -import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; import NodeList from '../node/NodeList.js'; +import THTMLFormControlElement from './THTMLFormControlElement.js'; /** * RadioNodeList * * @see https://developer.mozilla.org/en-US/docs/Web/API/RadioNodeList */ -export default class RadioNodeList extends NodeList< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement -> { +export default class RadioNodeList extends NodeList { /** * Returns value. * diff --git a/packages/happy-dom/src/nodes/html-form-element/THTMLFormControlElement.ts b/packages/happy-dom/src/nodes/html-form-element/THTMLFormControlElement.ts new file mode 100644 index 000000000..129fa78a2 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-form-element/THTMLFormControlElement.ts @@ -0,0 +1,14 @@ +import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; +import HTMLFieldSetElement from '../html-field-set-element/HTMLFieldSetElement.js'; +import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; +import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; +import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; + +type THTMLFormControlElement = + | HTMLInputElement + | HTMLSelectElement + | HTMLTextAreaElement + | HTMLButtonElement + | HTMLFieldSetElement; + +export default THTMLFormControlElement; diff --git a/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts deleted file mode 100644 index 771c64f0f..000000000 --- a/packages/happy-dom/src/nodes/html-hyperlink-element/HTMLHyperlinkElementNamedNodeMap.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLAnchorElement from '../html-anchor-element/HTMLAnchorElement.js'; -import HTMLAreaElement from '../html-area-element/HTMLAreaElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLHyperlinkElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLAnchorElement | HTMLAreaElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - item[PropertySymbol.name] === 'rel' && - this[PropertySymbol.ownerElement][PropertySymbol.relList] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.relList][PropertySymbol.updateIndices](); - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem?.[PropertySymbol.name] === 'rel' && - this[PropertySymbol.ownerElement][PropertySymbol.relList] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.relList][PropertySymbol.updateIndices](); - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index d2568befe..041948e0f 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -3,13 +3,32 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import BrowserWindow from '../../window/BrowserWindow.js'; import Document from '../document/Document.js'; import HTMLElement from '../html-element/HTMLElement.js'; -import Node from '../node/Node.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLIFrameElementNamedNodeMap from './HTMLIFrameElementNamedNodeMap.js'; import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import HTMLIFrameElementPageLoader from './HTMLIFrameElementPageLoader.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; +import Attr from '../attr/Attr.js'; +import BrowserFrameFactory from '../../browser/utilities/BrowserFrameFactory.js'; +import BrowserFrameURL from '../../browser/utilities/BrowserFrameURL.js'; +import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import DOMException from '../../exception/DOMException.js'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import IRequestReferrerPolicy from '../../fetch/types/IRequestReferrerPolicy.js'; + +const SANDBOX_FLAGS = [ + 'allow-downloads', + 'allow-forms', + 'allow-modals', + 'allow-orientation-lock', + 'allow-pointer-lock', + 'allow-popups', + 'allow-popups-to-escape-sandbox', + 'allow-presentation', + 'allow-same-origin', + 'allow-scripts', + 'allow-top-navigation', + 'allow-top-navigation-by-user-activation', + 'allow-top-navigation-to-custom-protocols' +]; /** * HTML Iframe Element. @@ -26,14 +45,14 @@ export default class HTMLIFrameElement extends HTMLElement { public onerror: (event: Event) => void | null = null; // Internal properties - public override [PropertySymbol.attributes]: NamedNodeMap; public [PropertySymbol.sandbox]: DOMTokenList = null; // Private properties #contentWindowContainer: { window: BrowserWindow | CrossOriginBrowserWindow | null } = { window: null }; - #pageLoader: HTMLIFrameElementPageLoader; + #browserFrame: IBrowserFrame; + #browserChildFrame: IBrowserFrame; /** * Constructor. @@ -42,12 +61,16 @@ export default class HTMLIFrameElement extends HTMLElement { */ constructor(browserFrame: IBrowserFrame) { super(); - this.#pageLoader = new HTMLIFrameElementPageLoader({ - element: this, - contentWindowContainer: this.#contentWindowContainer, - browserParentFrame: browserFrame - }); - this[PropertySymbol.attributes] = new HTMLIFrameElementNamedNodeMap(this, this.#pageLoader); + this.#browserFrame = browserFrame; + + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); } /** @@ -223,25 +246,157 @@ export default class HTMLIFrameElement extends HTMLElement { /** * @override */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - const isConnected = this[PropertySymbol.isConnected]; - const isParentConnected = parentNode ? parentNode[PropertySymbol.isConnected] : false; + public override [PropertySymbol.connectedToDocument](): void { + super[PropertySymbol.connectedToDocument](); + this.#loadPage(); + } + + /** + * Called when disconnected from document. + * @param e + */ + public [PropertySymbol.disconnectedFromDocument](): void { + super[PropertySymbol.disconnectedFromDocument](); + this.#unloadPage(); + } - super[PropertySymbol.connectToNode](parentNode); + /** + * @override + */ + public override [PropertySymbol.cloneNode](deep = false): HTMLIFrameElement { + return super[PropertySymbol.cloneNode](deep); + } - if (isConnected !== isParentConnected) { - if (isParentConnected) { - this.#pageLoader.loadPage(); + /** + * Triggered when an attribute is set. + * + * @param attribute Attribute. + * @param replacedAttribute Replaced attribute. + */ + #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + if ( + attribute[PropertySymbol.name] === 'src' && + attribute[PropertySymbol.value] && + attribute[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value] + ) { + this.#loadPage(); + } + + if (attribute[PropertySymbol.name] === 'sandbox') { + if (!this[PropertySymbol.sandbox]) { + this[PropertySymbol.sandbox] = new DOMTokenList(this, 'sandbox'); } else { - this.#pageLoader.unloadPage(); + this[PropertySymbol.sandbox][PropertySymbol.updateIndices](); } + + this.#validateSandboxFlags(); } } /** - * @override + * Triggered when an attribute is removed. */ - public override [PropertySymbol.cloneNode](deep = false): HTMLIFrameElement { - return super[PropertySymbol.cloneNode](deep); + #onRemoveAttribute(): void { + this.#unloadPage(); + } + + /** + * + * @param tokens + * @param vconsole + */ + #validateSandboxFlags(): void { + const window = this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + const values = this[PropertySymbol.sandbox].values(); + const invalidFlags: string[] = []; + + for (const token of values) { + if (!SANDBOX_FLAGS.includes(token)) { + invalidFlags.push(token); + } + } + + if (invalidFlags.length === 1) { + window.console.error( + `Error while parsing the 'sandbox' attribute: '${invalidFlags[0]}' is an invalid sandbox flag.` + ); + } else if (invalidFlags.length > 1) { + window.console.error( + `Error while parsing the 'sandbox' attribute: '${invalidFlags.join( + `', '` + )}' are invalid sandbox flags.` + ); + } + } + + /** + * Loads an iframe page. + */ + #loadPage(): void { + if (!this[PropertySymbol.isConnected]) { + if (this.#browserChildFrame) { + BrowserFrameFactory.destroyFrame(this.#browserChildFrame); + this.#browserChildFrame = null; + } + this.#contentWindowContainer.window = null; + return; + } + + const window = this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + const originURL = this.#browserFrame.window.location; + const targetURL = BrowserFrameURL.getRelativeURL(this.#browserFrame, this.src); + + if ( + this.#browserChildFrame && + this.#browserChildFrame.window.location.href === targetURL.href + ) { + return; + } + + if (this.#browserFrame.page.context.browser.settings.disableIframePageLoading) { + WindowErrorUtility.dispatchError( + this, + new DOMException( + `Failed to load iframe page "${targetURL.href}". Iframe page loading is disabled.`, + DOMExceptionNameEnum.notSupportedError + ) + ); + return; + } + + // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. + const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; + const parentWindow = isSameOrigin ? window : new CrossOriginBrowserWindow(window); + + this.#browserChildFrame = + this.#browserChildFrame ?? BrowserFrameFactory.createChildFrame(this.#browserFrame); + + ((this.#browserChildFrame.window.top)) = + parentWindow; + ((this.#browserChildFrame.window.parent)) = + parentWindow; + + this.#browserChildFrame + .goto(targetURL.href, { + referrer: originURL.origin, + referrerPolicy: this.referrerPolicy + }) + .then(() => this.dispatchEvent(new Event('load'))) + .catch((error) => WindowErrorUtility.dispatchError(this, error)); + + this.#contentWindowContainer.window = isSameOrigin + ? this.#browserChildFrame.window + : new CrossOriginBrowserWindow(this.#browserChildFrame.window, window); + } + + /** + * Unloads an iframe page. + */ + #unloadPage(): void { + if (this.#browserChildFrame) { + BrowserFrameFactory.destroyFrame(this.#browserChildFrame); + this.#browserChildFrame = null; + } + this.#contentWindowContainer.window = null; } } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts deleted file mode 100644 index 7e8013ad8..000000000 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts +++ /dev/null @@ -1,102 +0,0 @@ -import Attr from '../attr/Attr.js'; -import Element from '../element/Element.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLIFrameElementPageLoader from './HTMLIFrameElementPageLoader.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; - -const SANDBOX_FLAGS = [ - 'allow-downloads', - 'allow-forms', - 'allow-modals', - 'allow-orientation-lock', - 'allow-pointer-lock', - 'allow-popups', - 'allow-popups-to-escape-sandbox', - 'allow-presentation', - 'allow-same-origin', - 'allow-scripts', - 'allow-top-navigation', - 'allow-top-navigation-by-user-activation', - 'allow-top-navigation-to-custom-protocols' -]; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeMap { - #pageLoader: HTMLIFrameElementPageLoader; - - /** - * Constructor. - * - * @param ownerElement Owner element. - * @param pageLoader - */ - constructor(ownerElement: Element, pageLoader: HTMLIFrameElementPageLoader) { - super(ownerElement); - this.#pageLoader = pageLoader; - } - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedAttribute = super[PropertySymbol.setNamedItem](item); - - if ( - item[PropertySymbol.name] === 'src' && - item[PropertySymbol.value] && - item[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value] - ) { - this.#pageLoader.loadPage(); - } - - if (item[PropertySymbol.name] === 'sandbox') { - if (!this[PropertySymbol.ownerElement][PropertySymbol.sandbox]) { - this[PropertySymbol.ownerElement][PropertySymbol.sandbox] = new DOMTokenList( - this[PropertySymbol.ownerElement], - 'sandbox' - ); - } else { - this[PropertySymbol.ownerElement][PropertySymbol.sandbox][PropertySymbol.updateIndices](); - } - - this.#validateSandboxFlags(); - } - - return replacedAttribute || null; - } - - /** - * - * @param tokens - * @param vconsole - */ - #validateSandboxFlags(): void { - const window = - this[PropertySymbol.ownerElement][PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; - const values = this[PropertySymbol.ownerElement][PropertySymbol.sandbox].values(); - const invalidFlags: string[] = []; - - for (const token of values) { - if (!SANDBOX_FLAGS.includes(token)) { - invalidFlags.push(token); - } - } - - if (invalidFlags.length === 1) { - window.console.error( - `Error while parsing the 'sandbox' attribute: '${invalidFlags[0]}' is an invalid sandbox flag.` - ); - } else if (invalidFlags.length > 1) { - window.console.error( - `Error while parsing the 'sandbox' attribute: '${invalidFlags.join( - `', '` - )}' are invalid sandbox flags.` - ); - } - } -} diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts deleted file mode 100644 index 6783a5f68..000000000 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ /dev/null @@ -1,109 +0,0 @@ -import Event from '../../event/Event.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import BrowserWindow from '../../window/BrowserWindow.js'; -import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js'; -import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import HTMLIFrameElement from './HTMLIFrameElement.js'; -import DOMException from '../../exception/DOMException.js'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import BrowserFrameURL from '../../browser/utilities/BrowserFrameURL.js'; -import BrowserFrameFactory from '../../browser/utilities/BrowserFrameFactory.js'; -import IRequestReferrerPolicy from '../../fetch/types/IRequestReferrerPolicy.js'; - -/** - * HTML Iframe page loader. - */ -export default class HTMLIFrameElementPageLoader { - #element: HTMLIFrameElement; - #contentWindowContainer: { window: BrowserWindow | CrossOriginBrowserWindow | null }; - #browserParentFrame: IBrowserFrame; - #browserIFrame: IBrowserFrame; - - /** - * Constructor. - * - * @param options Options. - * @param options.element Iframe element. - * @param options.browserParentFrame Main browser frame. - * @param options.contentWindowContainer Content window container. - * @param options.contentWindowContainer.window Content window. - */ - constructor(options: { - element: HTMLIFrameElement; - browserParentFrame: IBrowserFrame; - contentWindowContainer: { window: BrowserWindow | CrossOriginBrowserWindow | null }; - }) { - this.#element = options.element; - this.#contentWindowContainer = options.contentWindowContainer; - this.#browserParentFrame = options.browserParentFrame; - } - - /** - * Loads an iframe page. - */ - public loadPage(): void { - if (!this.#element[PropertySymbol.isConnected]) { - if (this.#browserIFrame) { - BrowserFrameFactory.destroyFrame(this.#browserIFrame); - this.#browserIFrame = null; - } - this.#contentWindowContainer.window = null; - return; - } - - const window = this.#element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; - const originURL = this.#browserParentFrame.window.location; - const targetURL = BrowserFrameURL.getRelativeURL(this.#browserParentFrame, this.#element.src); - - if (this.#browserIFrame && this.#browserIFrame.window.location.href === targetURL.href) { - return; - } - - if (this.#browserParentFrame.page.context.browser.settings.disableIframePageLoading) { - WindowErrorUtility.dispatchError( - this.#element, - new DOMException( - `Failed to load iframe page "${targetURL.href}". Iframe page loading is disabled.`, - DOMExceptionNameEnum.notSupportedError - ) - ); - return; - } - - // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. - const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; - const parentWindow = isSameOrigin ? window : new CrossOriginBrowserWindow(window); - - this.#browserIFrame = - this.#browserIFrame ?? BrowserFrameFactory.createChildFrame(this.#browserParentFrame); - - ((this.#browserIFrame.window.top)) = - parentWindow; - ((this.#browserIFrame.window.parent)) = - parentWindow; - - this.#browserIFrame - .goto(targetURL.href, { - referrer: originURL.origin, - referrerPolicy: this.#element.referrerPolicy - }) - .then(() => this.#element.dispatchEvent(new Event('load'))) - .catch((error) => WindowErrorUtility.dispatchError(this.#element, error)); - - this.#contentWindowContainer.window = isSameOrigin - ? this.#browserIFrame.window - : new CrossOriginBrowserWindow(this.#browserIFrame.window, window); - } - - /** - * Unloads an iframe page. - */ - public unloadPage(): void { - if (this.#browserIFrame) { - BrowserFrameFactory.destroyFrame(this.#browserIFrame); - this.#browserIFrame = null; - } - this.#contentWindowContainer.window = null; - } -} diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 39c0a4ee9..265201b65 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -16,13 +16,13 @@ import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import HTMLInputElementDateUtility from './HTMLInputElementDateUtility.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLInputElementNamedNodeMap from './HTMLInputElementNamedNodeMap.js'; import PointerEvent from '../../event/events/PointerEvent.js'; import { URL } from 'url'; import HTMLDataListElement from '../html-data-list-element/HTMLDataListElement.js'; import Document from '../document/Document.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; +import HTMLFieldSetElement from '../html-field-set-element/HTMLFieldSetElement.js'; +import Attr from '../attr/Attr.js'; /** * HTML Input Element. @@ -42,10 +42,6 @@ export default class HTMLInputElement extends HTMLElement { public oninvalid: (event: Event) => void | null = null; public onselectionchange: (event: Event) => void | null = null; - // Internal properties - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLInputElementNamedNodeMap( - this - ); public [PropertySymbol.value] = null; public [PropertySymbol.height] = 0; public [PropertySymbol.width] = 0; @@ -54,6 +50,7 @@ export default class HTMLInputElement extends HTMLElement { public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.files]: FileList = new FileList(); + public [PropertySymbol.formNode]: HTMLFormElement | null = null; // Private properties #selectionStart: number = null; @@ -61,6 +58,21 @@ export default class HTMLInputElement extends HTMLElement { #selectionDirection: HTMLInputElementSelectionDirectionEnum = HTMLInputElementSelectionDirectionEnum.none; + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } + /** * Returns default checked. * @@ -207,17 +219,19 @@ export default class HTMLInputElement extends HTMLElement { * * @returns Form. */ - public get form(): HTMLFormElement | null { - if (this[PropertySymbol.formNode]) { - return this[PropertySymbol.formNode]; - } - if (!this.isConnected) { - return null; - } + public get form(): HTMLFormElement { const formID = this.getAttribute('form'); - return formID - ? this[PropertySymbol.ownerDocument].getElementById(formID) - : null; + + if (formID !== null) { + if (!this[PropertySymbol.isConnected]) { + return null; + } + return formID + ? (this[PropertySymbol.rootNode]).getElementById(formID) + : null; + } + + return this[PropertySymbol.formNode]; } /** @@ -1394,32 +1408,6 @@ export default class HTMLInputElement extends HTMLElement { return returnValue; } - /** - * @override - */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - const oldFormNode = this[PropertySymbol.formNode]; - - super[PropertySymbol.connectToNode](parentNode); - - if (oldFormNode !== this[PropertySymbol.formNode]) { - if (oldFormNode) { - oldFormNode[PropertySymbol.removeFormControlItem](this, this.name); - oldFormNode[PropertySymbol.removeFormControlItem](this, this.id); - } - if (this[PropertySymbol.formNode]) { - (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( - this, - this.name - ); - (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( - this, - this.id - ); - } - } - } - /** * Checks is selection is supported. * diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts deleted file mode 100644 index 8d2b3a8bf..000000000 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; -import HTMLInputElement from './HTMLInputElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLInputElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') && - this[PropertySymbol.ownerElement][PropertySymbol.formNode] - ) { - if (replacedItem && replacedItem[PropertySymbol.value]) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.removeFormControlItem - ](this[PropertySymbol.ownerElement], replacedItem[PropertySymbol.value]); - } - if (item[PropertySymbol.value]) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.appendFormControlItem - ](this[PropertySymbol.ownerElement], item[PropertySymbol.value]); - } - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem && - (removedItem[PropertySymbol.name] === 'id' || removedItem[PropertySymbol.name] === 'name') && - this[PropertySymbol.ownerElement][PropertySymbol.formNode] - ) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.removeFormControlItem - ](this[PropertySymbol.ownerElement], removedItem[PropertySymbol.value]); - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts index 74833b03d..9c5bc1350 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts @@ -4,6 +4,8 @@ import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import Event from '../../event/Event.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import PointerEvent from '../../event/events/PointerEvent.js'; +import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; +import Document from '../document/Document.js'; /** * HTML Label Element. @@ -42,13 +44,31 @@ export default class HTMLLabelElement extends HTMLElement { * * @returns Control element. */ - public get control(): HTMLElement { - const htmlFor = this.htmlFor; - if (htmlFor) { - const control = this[PropertySymbol.ownerDocument].getElementById(htmlFor); - return control !== this ? control : null; + public get control(): HTMLElement | null { + const htmlFor = this.getAttribute('for'); + if (htmlFor !== null) { + if (!htmlFor || !this[PropertySymbol.isConnected]) { + return null; + } + const control = ( + (this[PropertySymbol.rootNode]).getElementById(htmlFor) + ); + switch (control.tagName) { + case 'input': + return (control).type !== 'hidden' ? control : null; + case 'button': + case 'meter': + case 'output': + case 'progress': + case 'select': + case 'textarea': + case 'textarea': + return control; + default: + return null; + } } - return ( + return ( this.querySelector('button,input:not([type="hidden"]),meter,output,progress,select,textarea') ); } @@ -58,8 +78,8 @@ export default class HTMLLabelElement extends HTMLElement { * * @returns Form. */ - public get form(): HTMLFormElement { - return this[PropertySymbol.formNode]; + public get form(): HTMLFormElement | null { + return (this.control)?.form || null; } /** diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts index 59e719290..e42208ce4 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts @@ -18,7 +18,7 @@ export default class HTMLLabelElementUtility { public static getAssociatedLabelElements(element: HTMLElement): NodeList { const id = element.id; let labels: NodeList; - if (id) { + if (id && element[PropertySymbol.isConnected]) { const rootNode = element[PropertySymbol.rootNode] || element[PropertySymbol.ownerDocument]; diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index 8d0bc3576..fe9c4c5cb 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -5,10 +5,13 @@ import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; import Node from '../../nodes/node/Node.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLLinkElementNamedNodeMap from './HTMLLinkElementNamedNodeMap.js'; -import HTMLLinkElementStyleSheetLoader from './HTMLLinkElementStyleSheetLoader.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; +import Attr from '../attr/Attr.js'; +import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import DOMException from '../../exception/DOMException.js'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import ResourceFetch from '../../fetch/ResourceFetch.js'; +import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; /** * HTML Link Element. @@ -22,11 +25,11 @@ export default class HTMLLinkElement extends HTMLElement { public onload: (event: Event) => void = null; // Internal properties - public override [PropertySymbol.attributes]: NamedNodeMap; public [PropertySymbol.sheet]: CSSStyleSheet = null; public [PropertySymbol.evaluateCSS] = true; public [PropertySymbol.relList]: DOMTokenList = null; - #styleSheetLoader: HTMLLinkElementStyleSheetLoader; + #loadedStyleSheetURL: string | null = null; + #browserFrame: IBrowserFrame; /** * Constructor. @@ -36,12 +39,15 @@ export default class HTMLLinkElement extends HTMLElement { constructor(browserFrame: IBrowserFrame) { super(); - this.#styleSheetLoader = new HTMLLinkElementStyleSheetLoader({ - element: this, - browserFrame - }); - - this[PropertySymbol.attributes] = new HTMLLinkElementNamedNodeMap(this, this.#styleSheetLoader); + this.#browserFrame = browserFrame; + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); } /** @@ -219,18 +225,111 @@ export default class HTMLLinkElement extends HTMLElement { /** * @override */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - const isConnected = this[PropertySymbol.isConnected]; - const isParentConnected = parentNode ? parentNode[PropertySymbol.isConnected] : false; + public override [PropertySymbol.connectedToDocument](): void { + super[PropertySymbol.connectedToDocument](); + if (this[PropertySymbol.evaluateCSS]) { + this.#loadStyleSheet(this.getAttribute('href'), this.getAttribute('rel')); + } + } + + /** + * Triggered when an attribute is set. + * + * @param item Item + */ + #onSetAttribute(item: Attr): void { + if (item[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + this[PropertySymbol.relList][PropertySymbol.updateIndices](); + } + + if (item[PropertySymbol.name] === 'rel') { + this.#loadStyleSheet(this.getAttribute('href'), item[PropertySymbol.value]); + } else if (item[PropertySymbol.name] === 'href') { + this.#loadStyleSheet(item[PropertySymbol.value], this.getAttribute('rel')); + } + } + + /** + * Triggered when an attribute is removed. + * + * @param removedItem Removed item. + */ + #onRemoveAttribute(removedItem: Attr): void { + if (removedItem[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + this[PropertySymbol.relList][PropertySymbol.updateIndices](); + } + } + + /** + * Returns a URL relative to the given Location object. + * + * @param url URL. + * @param rel Rel. + */ + async #loadStyleSheet(url: string | null, rel: string | null): Promise { + const browserSettings = this.#browserFrame.page.context.browser.settings; + const window = this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + + if (!url || !rel || rel.toLowerCase() !== 'stylesheet' || !this[PropertySymbol.isConnected]) { + return; + } + + let absoluteURL: string; + try { + absoluteURL = new URL(url, window.location.href).href; + } catch (error) { + return; + } + + if (this.#loadedStyleSheetURL === absoluteURL) { + return; + } + + if (browserSettings.disableCSSFileLoading) { + if (browserSettings.handleDisabledFileLoadingAsSuccess) { + this.dispatchEvent(new Event('load')); + } else { + WindowErrorUtility.dispatchError( + this, + new DOMException( + `Failed to load external stylesheet "${absoluteURL}". CSS file loading is disabled.`, + DOMExceptionNameEnum.notSupportedError + ) + ); + } + return; + } + + const resourceFetch = new ResourceFetch({ + browserFrame: this.#browserFrame, + window: window + }); + const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( + (window) + ))[PropertySymbol.readyStateManager]; + + this.#loadedStyleSheetURL = absoluteURL; + + readyStateManager.startTask(); + + let code: string | null = null; + let error: Error | null = null; + + try { + code = await resourceFetch.fetch(absoluteURL); + } catch (e) { + error = e; + } - super[PropertySymbol.connectToNode](parentNode); + readyStateManager.endTask(); - if ( - isParentConnected && - isConnected !== isParentConnected && - this[PropertySymbol.evaluateCSS] - ) { - this.#styleSheetLoader.loadStyleSheet(this.getAttribute('href'), this.getAttribute('rel')); + if (error) { + WindowErrorUtility.dispatchError(this, error); + } else { + const styleSheet = new CSSStyleSheet(); + styleSheet.replaceSync(code); + this[PropertySymbol.sheet] = styleSheet; + this.dispatchEvent(new Event('load')); } } } diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts deleted file mode 100644 index 063ef4f4d..000000000 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLLinkElement from './HTMLLinkElement.js'; -import HTMLLinkElementStyleSheetLoader from './HTMLLinkElementStyleSheetLoader.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLLinkElement; - #styleSheetLoader: HTMLLinkElementStyleSheetLoader; - - /** - * Constructor. - * - * @param ownerElement Owner element. - * @param stylesheetLoader Stylesheet loader. - * @param styleSheetLoader - */ - constructor(ownerElement: HTMLLinkElement, styleSheetLoader: HTMLLinkElementStyleSheetLoader) { - super(ownerElement); - this.#styleSheetLoader = styleSheetLoader; - } - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - item[PropertySymbol.name] === 'rel' && - this[PropertySymbol.ownerElement][PropertySymbol.relList] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.relList][PropertySymbol.updateIndices](); - } - - if (item[PropertySymbol.name] === 'rel') { - this.#styleSheetLoader.loadStyleSheet( - this[PropertySymbol.ownerElement].getAttribute('href'), - item[PropertySymbol.value] - ); - } else if (item[PropertySymbol.name] === 'href') { - this.#styleSheetLoader.loadStyleSheet( - item[PropertySymbol.value], - this[PropertySymbol.ownerElement].getAttribute('rel') - ); - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem && - removedItem[PropertySymbol.name] === 'rel' && - this[PropertySymbol.ownerElement][PropertySymbol.relList] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.relList][PropertySymbol.updateIndices](); - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts deleted file mode 100644 index d74bb7043..000000000 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts +++ /dev/null @@ -1,110 +0,0 @@ -import Event from '../../event/Event.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import ResourceFetch from '../../fetch/ResourceFetch.js'; -import CSSStyleSheet from '../../css/CSSStyleSheet.js'; -import DOMException from '../../exception/DOMException.js'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; -import HTMLLinkElement from './HTMLLinkElement.js'; -import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; - -/** - * Helper class for getting the URL relative to a Location object. - */ -export default class HTMLLinkElementStyleSheetLoader { - #element: HTMLLinkElement; - #browserFrame: IBrowserFrame; - #loadedStyleSheetURL: string | null = null; - - /** - * Constructor. - * - * @param options Options. - * @param options.element Element. - * @param options.browserFrame Browser frame. - */ - constructor(options: { element: HTMLLinkElement; browserFrame: IBrowserFrame }) { - this.#element = options.element; - this.#browserFrame = options.browserFrame; - } - - /** - * Returns a URL relative to the given Location object. - * - * @param url URL. - * @param rel Rel. - */ - public async loadStyleSheet(url: string | null, rel: string | null): Promise { - const element = this.#element; - const browserSettings = this.#browserFrame.page.context.browser.settings; - const window = element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; - - if ( - !url || - !rel || - rel.toLowerCase() !== 'stylesheet' || - !element[PropertySymbol.isConnected] - ) { - return; - } - - let absoluteURL: string; - try { - absoluteURL = new URL(url, window.location.href).href; - } catch (error) { - return; - } - - if (this.#loadedStyleSheetURL === absoluteURL) { - return; - } - - if (browserSettings.disableCSSFileLoading) { - if (browserSettings.handleDisabledFileLoadingAsSuccess) { - element.dispatchEvent(new Event('load')); - } else { - WindowErrorUtility.dispatchError( - element, - new DOMException( - `Failed to load external stylesheet "${absoluteURL}". CSS file loading is disabled.`, - DOMExceptionNameEnum.notSupportedError - ) - ); - } - return; - } - - const resourceFetch = new ResourceFetch({ - browserFrame: this.#browserFrame, - window: window - }); - const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( - (window) - ))[PropertySymbol.readyStateManager]; - - this.#loadedStyleSheetURL = absoluteURL; - - readyStateManager.startTask(); - - let code: string | null = null; - let error: Error | null = null; - - try { - code = await resourceFetch.fetch(absoluteURL); - } catch (e) { - error = e; - } - - readyStateManager.endTask(); - - if (error) { - WindowErrorUtility.dispatchError(element, error); - } else { - const styleSheet = new CSSStyleSheet(); - styleSheet.replaceSync(code); - element[PropertySymbol.sheet] = styleSheet; - element.dispatchEvent(new Event('load')); - } - } -} diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index 8afede915..5b3dbb8a1 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -1,11 +1,10 @@ -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; import * as PropertySymbol from '../../PropertySymbol.js'; +import Attr from '../attr/Attr.js'; import HTMLDataListElement from '../html-data-list-element/HTMLDataListElement.js'; import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import Node from '../node/Node.js'; -import HTMLOptionElementNamedNodeMap from './HTMLOptionElementNamedNodeMap.js'; /** * HTML Option Element. @@ -14,11 +13,24 @@ import HTMLOptionElementNamedNodeMap from './HTMLOptionElementNamedNodeMap.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionElement. */ export default class HTMLOptionElement extends HTMLElement { - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLOptionElementNamedNodeMap( - this - ); public [PropertySymbol.selectedness] = false; public [PropertySymbol.dirtyness] = false; + public [PropertySymbol.selectNode]: HTMLSelectElement | null = null; + + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } /** * Returns inner text, which is the rendered appearance of text. @@ -55,7 +67,7 @@ export default class HTMLOptionElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - return this[PropertySymbol.formNode]; + return (this[PropertySymbol.selectNode])?.form; } /** @@ -126,11 +138,11 @@ export default class HTMLOptionElement extends HTMLElement { /** * @override */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { + public override [PropertySymbol.connectedToDocument](parentNode: Node = null): void { const oldSelectNode = this[PropertySymbol.selectNode]; const oldDataListNode = this[PropertySymbol.dataListNode]; - super[PropertySymbol.connectToNode](parentNode); + super[PropertySymbol.connectedToDocument](parentNode); const selectNode = this[PropertySymbol.selectNode]; @@ -164,4 +176,47 @@ export default class HTMLOptionElement extends HTMLElement { } } } + + /** + * Triggered when an attribute is set. + * + * @param attribute Attribute. + * @param replacedAttribute Replaced attribute. + */ + #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + if ( + !this[PropertySymbol.dirtyness] && + attribute[PropertySymbol.name] === 'selected' && + replacedAttribute?.[PropertySymbol.value] !== attribute[PropertySymbol.value] + ) { + const selectNode = this[PropertySymbol.selectNode]; + + this[PropertySymbol.selectedness] = true; + + if (selectNode) { + selectNode[PropertySymbol.updateOptionItems](this); + } + } + } + + /** + * Triggered when an attribute is removed. + * + * @param removedAttribute Removed attribute. + */ + #onRemoveAttribute(removedAttribute: Attr): void { + if ( + removedAttribute && + !this[PropertySymbol.dirtyness] && + removedAttribute[PropertySymbol.name] === 'selected' + ) { + const selectNode = this[PropertySymbol.selectNode]; + + this[PropertySymbol.selectedness] = false; + + if (selectNode) { + selectNode[PropertySymbol.updateOptionItems](); + } + } + } } diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts deleted file mode 100644 index 7446a6e5b..000000000 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; -import HTMLOptionElement from './HTMLOptionElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLOptionElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - !this[PropertySymbol.ownerElement][PropertySymbol.dirtyness] && - item[PropertySymbol.name] === 'selected' && - replacedItem?.[PropertySymbol.value] !== item[PropertySymbol.value] - ) { - const selectNode = ( - this[PropertySymbol.ownerElement][PropertySymbol.selectNode] - ); - - this[PropertySymbol.ownerElement][PropertySymbol.selectedness] = true; - - if (selectNode) { - selectNode[PropertySymbol.updateOptionItems](this[PropertySymbol.ownerElement]); - } - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem && - !this[PropertySymbol.ownerElement][PropertySymbol.dirtyness] && - removedItem[PropertySymbol.name] === 'selected' - ) { - const selectNode = ( - this[PropertySymbol.ownerElement][PropertySymbol.selectNode] - ); - - this[PropertySymbol.ownerElement][PropertySymbol.selectedness] = false; - - if (selectNode) { - selectNode[PropertySymbol.updateOptionItems](); - } - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index aa2ca7381..17b34b9ee 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -3,13 +3,15 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; import Node from '../../nodes/node/Node.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLScriptElementNamedNodeMap from './HTMLScriptElementNamedNodeMap.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; -import HTMLScriptElementScriptLoader from './HTMLScriptElementScriptLoader.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js'; +import Attr from '../attr/Attr.js'; +import DOMException from '../../exception/DOMException.js'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import ResourceFetch from '../../fetch/ResourceFetch.js'; +import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; /** * HTML Script Element. @@ -26,11 +28,11 @@ export default class HTMLScriptElement extends HTMLElement { public onload: (event: Event) => void = null; // Internal properties - public override [PropertySymbol.attributes]: NamedNodeMap; public [PropertySymbol.evaluateScript] = true; // Private properties - #scriptLoader: HTMLScriptElementScriptLoader; + #browserFrame: IBrowserFrame; + #loadedScriptURL: string | null = null; /** * Constructor. @@ -40,12 +42,11 @@ export default class HTMLScriptElement extends HTMLElement { constructor(browserFrame: IBrowserFrame) { super(); - this.#scriptLoader = new HTMLScriptElementScriptLoader({ - element: this, - browserFrame - }); - - this[PropertySymbol.attributes] = new HTMLScriptElementNamedNodeMap(this, this.#scriptLoader); + this.#browserFrame = browserFrame; + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); } /** @@ -201,24 +202,18 @@ export default class HTMLScriptElement extends HTMLElement { /** * @override */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - const isConnected = this[PropertySymbol.isConnected]; - const isParentConnected = parentNode ? parentNode[PropertySymbol.isConnected] : false; + public override [PropertySymbol.connectedToDocument](): void { const browserSettings = WindowBrowserSettingsReader.getSettings( this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow] ); - super[PropertySymbol.connectToNode](parentNode); + super[PropertySymbol.connectedToDocument](); - if ( - isParentConnected && - isConnected !== isParentConnected && - this[PropertySymbol.evaluateScript] - ) { + if (this[PropertySymbol.evaluateScript]) { const src = this.getAttribute('src'); if (src !== null) { - this.#scriptLoader.loadScript(src); + this.#loadScript(src); } else if (!browserSettings.disableJavaScriptEvaluation) { const textContent = this.textContent; const type = this.getAttribute('type'); @@ -253,4 +248,119 @@ export default class HTMLScriptElement extends HTMLElement { } } } + + /** + * Triggered when an attribute is set. + * + * @param item Item + */ + #onSetAttribute(item: Attr): void { + if ( + item[PropertySymbol.name] === 'src' && + item[PropertySymbol.value] !== null && + this[PropertySymbol.isConnected] + ) { + this.#loadScript(item[PropertySymbol.value]); + } + } + + /** + * Returns a URL relative to the given Location object. + * + * @param url URL. + */ + async #loadScript(url: string): Promise { + const browserSettings = this.#browserFrame.page.context.browser.settings; + const async = this.getAttribute('async') !== null; + + if (!url || !this[PropertySymbol.isConnected]) { + return; + } + + let absoluteURL: string; + try { + absoluteURL = new URL( + url, + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].location.href + ).href; + } catch (error) { + return; + } + + if (this.#loadedScriptURL === absoluteURL) { + return; + } + + if ( + browserSettings.disableJavaScriptFileLoading || + browserSettings.disableJavaScriptEvaluation + ) { + if (browserSettings.handleDisabledFileLoadingAsSuccess) { + this.dispatchEvent(new Event('load')); + } else { + WindowErrorUtility.dispatchError( + this, + new DOMException( + `Failed to load external script "${absoluteURL}". JavaScript file loading is disabled.`, + DOMExceptionNameEnum.notSupportedError + ) + ); + } + return; + } + + const resourceFetch = new ResourceFetch({ + browserFrame: this.#browserFrame, + window: this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow] + }); + let code: string | null = null; + let error: Error | null = null; + + this.#loadedScriptURL = absoluteURL; + + if (async) { + const readyStateManager = (< + { [PropertySymbol.readyStateManager]: DocumentReadyStateManager } + >(this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]))[ + PropertySymbol.readyStateManager + ]; + + readyStateManager.startTask(); + + try { + code = await resourceFetch.fetch(absoluteURL); + } catch (e) { + error = e; + } + + readyStateManager.endTask(); + } else { + try { + code = resourceFetch.fetchSync(absoluteURL); + } catch (e) { + error = e; + } + } + + if (error) { + WindowErrorUtility.dispatchError(this, error); + } else { + this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = this; + code = '//# sourceURL=' + absoluteURL + '\n' + code; + + if ( + browserSettings.disableErrorCapturing || + browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch + ) { + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code); + } else { + WindowErrorUtility.captureError( + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow], + () => this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code) + ); + } + this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = null; + this.dispatchEvent(new Event('load')); + } + } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts deleted file mode 100644 index a9fa2c5ad..000000000 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts +++ /dev/null @@ -1,43 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLScriptElement from './HTMLScriptElement.js'; -import HTMLScriptElementScriptLoader from './HTMLScriptElementScriptLoader.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLScriptElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLScriptElement; - #scriptLoader: HTMLScriptElementScriptLoader; - - /** - * Constructor. - * - * @param ownerElement Owner element. - * @param scriptLoader Script loader. - */ - constructor(ownerElement: HTMLScriptElement, scriptLoader: HTMLScriptElementScriptLoader) { - super(ownerElement); - this.#scriptLoader = scriptLoader; - } - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - item[PropertySymbol.name] === 'src' && - item[PropertySymbol.value] !== null && - this[PropertySymbol.ownerElement][PropertySymbol.isConnected] - ) { - this.#scriptLoader.loadScript(item[PropertySymbol.value]); - } - - return replacedItem || null; - } -} diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts deleted file mode 100644 index 1cc436901..000000000 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts +++ /dev/null @@ -1,132 +0,0 @@ -import Event from '../../event/Event.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import DOMException from '../../exception/DOMException.js'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import ResourceFetch from '../../fetch/ResourceFetch.js'; -import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; -import HTMLScriptElement from './HTMLScriptElement.js'; -import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js'; - -/** - * Helper class for getting the URL relative to a Location object. - */ -export default class HTMLScriptElementScriptLoader { - #element: HTMLScriptElement; - #browserFrame: IBrowserFrame; - #loadedScriptURL: string | null = null; - - /** - * Constructor. - * - * @param options Options. - * @param options.element Element. - * @param options.browserFrame Browser frame. - */ - constructor(options: { element: HTMLScriptElement; browserFrame: IBrowserFrame }) { - this.#element = options.element; - this.#browserFrame = options.browserFrame; - } - - /** - * Returns a URL relative to the given Location object. - * - * @param url URL. - */ - public async loadScript(url: string): Promise { - const browserSettings = this.#browserFrame.page.context.browser.settings; - const element = this.#element; - const async = element.getAttribute('async') !== null; - - if (!url || !element[PropertySymbol.isConnected]) { - return; - } - - let absoluteURL: string; - try { - absoluteURL = new URL( - url, - element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].location.href - ).href; - } catch (error) { - return; - } - - if (this.#loadedScriptURL === absoluteURL) { - return; - } - - if ( - browserSettings.disableJavaScriptFileLoading || - browserSettings.disableJavaScriptEvaluation - ) { - if (browserSettings.handleDisabledFileLoadingAsSuccess) { - element.dispatchEvent(new Event('load')); - } else { - WindowErrorUtility.dispatchError( - element, - new DOMException( - `Failed to load external script "${absoluteURL}". JavaScript file loading is disabled.`, - DOMExceptionNameEnum.notSupportedError - ) - ); - } - return; - } - - const resourceFetch = new ResourceFetch({ - browserFrame: this.#browserFrame, - window: element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow] - }); - let code: string | null = null; - let error: Error | null = null; - - this.#loadedScriptURL = absoluteURL; - - if (async) { - const readyStateManager = (< - { [PropertySymbol.readyStateManager]: DocumentReadyStateManager } - >(element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]))[ - PropertySymbol.readyStateManager - ]; - - readyStateManager.startTask(); - - try { - code = await resourceFetch.fetch(absoluteURL); - } catch (e) { - error = e; - } - - readyStateManager.endTask(); - } else { - try { - code = resourceFetch.fetchSync(absoluteURL); - } catch (e) { - error = e; - } - } - - if (error) { - WindowErrorUtility.dispatchError(element, error); - } else { - element[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = element; - code = '//# sourceURL=' + absoluteURL + '\n' + code; - - if ( - browserSettings.disableErrorCapturing || - browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch - ) { - element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code); - } else { - WindowErrorUtility.captureError( - element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow], - () => element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].eval(code) - ); - } - element[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = null; - element.dispatchEvent(new Event('load')); - } - } -} diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts index 65495e4d5..0b561add5 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts @@ -2,6 +2,8 @@ import DOMException from '../../exception/DOMException.js'; import HTMLCollection from '../element/HTMLCollection.js'; import HTMLSelectElement from './HTMLSelectElement.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; +import Element from '../element/Element.js'; +import { PropertySymbol } from '../../index.js'; /** * HTML Options Collection. @@ -10,6 +12,7 @@ import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection. */ export default class HTMLOptionsCollection extends HTMLCollection { + #selectedIndex: number | null = null; #selectElement: HTMLSelectElement; /** @@ -17,7 +20,7 @@ export default class HTMLOptionsCollection extends HTMLCollection element[PropertySymbol.tagName] === 'OPTION'); this.#selectElement = selectElement; } @@ -28,7 +31,18 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[PropertySymbol.options][i])[PropertySymbol.selectedness]) { + this.#selectedIndex = i; + return i; + } + } + this.#selectedIndex = -1; + return -1; } /** @@ -37,7 +51,23 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[PropertySymbol.options][i])[PropertySymbol.selectedness] = false; + } + + const selectedOption = this[PropertySymbol.options][selectedIndex]; + + if (!selectedOption) { + return; + } + + selectedOption[PropertySymbol.selectedness] = true; + selectedOption[PropertySymbol.dirtyness] = true; + this.#selectedIndex = selectedIndex; } /** @@ -65,11 +95,19 @@ export default class HTMLOptionsCollection extends HTMLCollectionbefore]); + const optionsElement = this[before]; + + if (!optionsElement) { + throw new DOMException( + "Failed to execute 'add' on 'DOMException': The node before which the new node is to be inserted is not a child of this node." + ); + } + + this.#selectElement.insertBefore(element, optionsElement); return; } - const index = this.indexOf(before); + const index = this[PropertySymbol.indexOf](before); if (index === -1) { throw new DOMException( @@ -90,4 +128,40 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[index]); } } + + /** + * @override + */ + public [PropertySymbol.addItem](item: HTMLOptionElement): boolean { + const returnValue = super[PropertySymbol.addItem](item); + if (returnValue) { + this.#selectedIndex = null; + } + return returnValue; + } + + /** + * @override + */ + public [PropertySymbol.insertItem]( + newItem: HTMLOptionElement, + referenceItem: HTMLOptionElement | null + ): boolean { + const returnValue = super[PropertySymbol.insertItem](newItem, referenceItem); + if (returnValue) { + this.#selectedIndex = null; + } + return returnValue; + } + + /** + * @override + */ + public [PropertySymbol.removeItem](item: HTMLOptionElement): boolean { + const returnValue = super[PropertySymbol.removeItem](item); + if (returnValue) { + this.#selectedIndex = null; + } + return returnValue; + } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 32696ca2b..075b464e5 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -5,14 +5,15 @@ import ValidityState from '../../validity-state/ValidityState.js'; import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; import HTMLOptionsCollection from './HTMLOptionsCollection.js'; -import NodeList from '../node/NodeList.js'; import Event from '../../event/Event.js'; import Node from '../node/Node.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLSelectElementNamedNodeMap from './HTMLSelectElementNamedNodeMap.js'; import HTMLCollection from '../element/HTMLCollection.js'; +import Document from '../document/Document.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; +import Element from '../element/Element.js'; +import NodeList from '../node/INodeList.js'; /** * HTML Select Element. @@ -22,19 +23,58 @@ import HTMLCollection from '../element/HTMLCollection.js'; */ export default class HTMLSelectElement extends HTMLElement { // Internal properties. - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLSelectElementNamedNodeMap( - this - ); public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.selectNode]: Node = this; public [PropertySymbol.length] = 0; public [PropertySymbol.options]: HTMLOptionsCollection = new HTMLOptionsCollection(this); + public [PropertySymbol.formNode]: HTMLFormElement | null = null; + public [PropertySymbol.selectedOptions]: IHTMLCollection = + new HTMLCollection( + (element: Element) => + element[PropertySymbol.tagName] === 'OPTION' && element[PropertySymbol.selectedness] + ); // Events public onchange: (event: Event) => void | null = null; public oninput: (event: Event) => void | null = null; + /** + * Constructor. + */ + constructor() { + super(); + + // Child nodes listeners + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { + (item)[PropertySymbol.selectNode] = this; + this[PropertySymbol.options][PropertySymbol.addItem](item); + this[PropertySymbol.selectedOptions][PropertySymbol.addItem](item); + }); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'insert', + (newItem: Node, referenceItem: Node | null) => { + (newItem)[PropertySymbol.selectNode] = this; + this[PropertySymbol.options][PropertySymbol.insertItem]( + newItem, + referenceItem + ); + this[PropertySymbol.selectedOptions][PropertySymbol.insertItem]( + newItem, + referenceItem + ); + } + ); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'remove', + (item: Node) => { + (item)[PropertySymbol.selectNode] = null; + this[PropertySymbol.options][PropertySymbol.removeItem](item); + this[PropertySymbol.selectedOptions][PropertySymbol.removeItem](item); + } + ); + } + /** * Returns length. * @@ -225,12 +265,7 @@ export default class HTMLSelectElement extends HTMLElement { * @returns Value. */ public get selectedIndex(): number { - for (let i = 0, max = this[PropertySymbol.options].length; i < max; i++) { - if ((this[PropertySymbol.options][i])[PropertySymbol.selectedness]) { - return i; - } - } - return -1; + return this[PropertySymbol.options].selectedIndex; } /** @@ -239,17 +274,7 @@ export default class HTMLSelectElement extends HTMLElement { * @param selectedIndex Selected index. */ public set selectedIndex(selectedIndex: number) { - if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) { - for (let i = 0, max = this[PropertySymbol.options].length; i < max; i++) { - (this[PropertySymbol.options][i])[PropertySymbol.selectedness] = false; - } - - const selectedOption = this[PropertySymbol.options][selectedIndex]; - if (selectedOption) { - selectedOption[PropertySymbol.selectedness] = true; - selectedOption[PropertySymbol.dirtyness] = true; - } - } + this[PropertySymbol.options].selectedIndex = selectedIndex; } /** @@ -257,14 +282,8 @@ export default class HTMLSelectElement extends HTMLElement { * * @returns HTMLCollection. */ - public get selectedOptions(): HTMLCollection { - const selectedOptions = new HTMLCollection(); - for (let i = 0, max = this[PropertySymbol.options].length; i < max; i++) { - if ((this[PropertySymbol.options][i])[PropertySymbol.selectedness]) { - selectedOptions.push(this[PropertySymbol.options][i]); - } - } - return selectedOptions; + public get selectedOptions(): IHTMLCollection { + return this[PropertySymbol.selectedOptions]; } /** @@ -282,6 +301,17 @@ export default class HTMLSelectElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { + const formID = this.getAttribute('form'); + + if (formID !== null) { + if (!this[PropertySymbol.isConnected]) { + return null; + } + return formID + ? (this[PropertySymbol.rootNode]).getElementById(formID) + : null; + } + return this[PropertySymbol.formNode]; } @@ -437,32 +467,6 @@ export default class HTMLSelectElement extends HTMLElement { } } - /** - * @override - */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - const oldFormNode = this[PropertySymbol.formNode]; - - super[PropertySymbol.connectToNode](parentNode); - - if (oldFormNode !== this[PropertySymbol.formNode]) { - if (oldFormNode) { - oldFormNode[PropertySymbol.removeFormControlItem](this, this.name); - oldFormNode[PropertySymbol.removeFormControlItem](this, this.id); - } - if (this[PropertySymbol.formNode]) { - (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( - this, - this.name - ); - (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( - this, - this.id - ); - } - } - } - /** * Returns display size. * diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts deleted file mode 100644 index 4ed796366..000000000 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; -import HTMLSelectElement from './HTMLSelectElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLSelectElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') && - this[PropertySymbol.ownerElement][PropertySymbol.formNode] - ) { - if (replacedItem && replacedItem[PropertySymbol.value]) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.removeFormControlItem - ](this[PropertySymbol.ownerElement], replacedItem[PropertySymbol.value]); - } - if (item[PropertySymbol.value]) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.appendFormControlItem - ](this[PropertySymbol.ownerElement], item[PropertySymbol.value]); - } - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem && - (removedItem[PropertySymbol.name] === 'id' || removedItem[PropertySymbol.name] === 'name') && - this[PropertySymbol.ownerElement][PropertySymbol.formNode] - ) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.removeFormControlItem - ](this[PropertySymbol.ownerElement], removedItem[PropertySymbol.value]); - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts index 791c56be0..7472b28f0 100644 --- a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts +++ b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts @@ -89,7 +89,7 @@ export default class HTMLSlotElement extends HTMLElement { if (name) { const assignedElements = []; - for (const child of (host)[PropertySymbol.children]) { + for (const child of (host)[PropertySymbol.children][PropertySymbol.items]) { if (child.slot === name) { assignedElements.push(child); } @@ -98,7 +98,7 @@ export default class HTMLSlotElement extends HTMLElement { return assignedElements; } - return (host)[PropertySymbol.children].slice(); + return (host)[PropertySymbol.children][PropertySymbol.items].slice(); } return []; diff --git a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts index 2d0ec33e0..d05052746 100644 --- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts +++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts @@ -115,14 +115,19 @@ export default class HTMLStyleElement extends HTMLElement { /** * @override */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - super[PropertySymbol.connectToNode](parentNode); - - if (parentNode) { + public override [PropertySymbol.connectedToDocument](parentNode: Node): void { + super[PropertySymbol.connectedToDocument](parentNode); + if (this[PropertySymbol.isConnected]) { this[PropertySymbol.sheet] = new CSSStyleSheet(); this[PropertySymbol.sheet].replaceSync(this.textContent); - } else { - this[PropertySymbol.sheet] = null; } } + + /** + * @override + */ + public override [PropertySymbol.disconnectedFromDocument](parentNode: Node): void { + super[PropertySymbol.disconnectedFromDocument](parentNode); + this[PropertySymbol.sheet] = null; + } } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index 8fc7ccc36..bfd481afa 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -6,13 +6,13 @@ import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLInputElementSelectionDirectionEnum from '../html-input-element/HTMLInputElementSelectionDirectionEnum.js'; import HTMLInputElementSelectionModeEnum from '../html-input-element/HTMLInputElementSelectionModeEnum.js'; -import Node from '../node/Node.js'; import ValidityState from '../../validity-state/ValidityState.js'; import NodeList from '../node/NodeList.js'; import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import HTMLTextAreaElementNamedNodeMap from './HTMLTextAreaElementNamedNodeMap.js'; +import Document from '../document/Document.js'; +import Text from '../text/Text.js'; +import Node from '../node/Node.js'; /** * HTML Text Area Element. @@ -30,19 +30,45 @@ export default class HTMLTextAreaElement extends HTMLElement { public onselectionchange: (event: Event) => void | null = null; // Internal properties - public override [PropertySymbol.attributes]: NamedNodeMap = new HTMLTextAreaElementNamedNodeMap( - this - ); public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.value] = null; public [PropertySymbol.textAreaNode]: HTMLTextAreaElement = this; + public [PropertySymbol.formNode]: HTMLFormElement | null = null; // Private properties #selectionStart = null; #selectionEnd = null; #selectionDirection = HTMLInputElementSelectionDirectionEnum.none; + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { + if (item instanceof Text) { + this[PropertySymbol.resetSelection](); + } + }); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'insert', + (newItem: Node) => { + if (newItem instanceof Text) { + this[PropertySymbol.resetSelection](); + } + } + ); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'remove', + (item: Node) => { + if (item instanceof Text) { + this[PropertySymbol.resetSelection](); + } + } + ); + } + /** * Returns validation message. * @@ -416,6 +442,17 @@ export default class HTMLTextAreaElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { + const formID = this.getAttribute('form'); + + if (formID !== null) { + if (!this[PropertySymbol.isConnected]) { + return null; + } + return formID + ? (this[PropertySymbol.rootNode]).getElementById(formID) + : null; + } + return this[PropertySymbol.formNode]; } @@ -591,30 +628,4 @@ export default class HTMLTextAreaElement extends HTMLElement { this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; } } - - /** - * @override - */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - const oldFormNode = this[PropertySymbol.formNode]; - - super[PropertySymbol.connectToNode](parentNode); - - if (oldFormNode !== this[PropertySymbol.formNode]) { - if (oldFormNode) { - oldFormNode[PropertySymbol.removeFormControlItem](this, this.name); - oldFormNode[PropertySymbol.removeFormControlItem](this, this.id); - } - if (this[PropertySymbol.formNode]) { - (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( - this, - this.name - ); - (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( - this, - this.id - ); - } - } - } } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts deleted file mode 100644 index dde3f294f..000000000 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; -import HTMLTextAreaElement from './HTMLTextAreaElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: HTMLTextAreaElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - (item[PropertySymbol.name] === 'id' || item[PropertySymbol.name] === 'name') && - this[PropertySymbol.ownerElement][PropertySymbol.formNode] - ) { - if (replacedItem && replacedItem[PropertySymbol.value]) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.removeFormControlItem - ](this[PropertySymbol.ownerElement], replacedItem[PropertySymbol.value]); - } - if (item[PropertySymbol.value]) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.appendFormControlItem - ](this[PropertySymbol.ownerElement], item[PropertySymbol.value]); - } - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem && - (removedItem[PropertySymbol.name] === 'id' || removedItem[PropertySymbol.name] === 'name') && - this[PropertySymbol.ownerElement][PropertySymbol.formNode] - ) { - (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[ - PropertySymbol.removeFormControlItem - ](this[PropertySymbol.ownerElement], removedItem[PropertySymbol.value]); - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/node/INodeList.ts b/packages/happy-dom/src/nodes/node/INodeList.ts new file mode 100644 index 000000000..6710a7d02 --- /dev/null +++ b/packages/happy-dom/src/nodes/node/INodeList.ts @@ -0,0 +1,156 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable filenames/match-exported */ + +import * as PropertySymbol from '../../PropertySymbol.js'; +import TNodeListListener from './TNodeListListener.js'; + +/** + * NodeList. + * + * This interface is used to hide Array methods from the outside. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList + */ +interface NodeList { + readonly [index: number]: T; + + /** + * The number of items in the NodeList. + */ + readonly length: number; + + /** + * Returns `Symbol.toStringTag`. + * + * @returns `Symbol.toStringTag`. + */ + readonly [Symbol.toStringTag]: string; + + /** + * Returns `[object NodeList]`. + * + * @returns `[object NodeList]`. + */ + toLocaleString(): string; + + /** + * Returns `[object NodeList]`. + * + * @returns `[object NodeList]`. + */ + toString(): string; + + /** + * Returns item by index. + * + * @param index Index. + */ + item(index: number): T; + + /** + * Appends item. + * + * @param item Item. + * @returns True if added. + */ + [PropertySymbol.addItem](item: T): boolean; + + /** + * Inserts item before another item. + * + * @param newItem New item. + * @param [referenceItem] Reference item. + * @returns True if inserted. + */ + [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean; + + /** + * Removes item. + * + * @param item Item. + * @returns True if removed. + */ + [PropertySymbol.removeItem](item: T): boolean; + + /** + * Adds event listener. + * + * @param type Type. + * @param listener Listener. + */ + [PropertySymbol.addEventListener]( + type: 'add' | 'insert' | 'remove', + listener: TNodeListListener + ): void; + + /** + * Removes event listener. + * + * @param type Type. + * @param listener Listener. + */ + [PropertySymbol.removeEventListener]( + type: 'add' | 'insert' | 'remove', + listener: TNodeListListener + ): void; + + /** + * Dispatches event. + * + * @param type Type. + * @param item Item. + * @param referenceItem Reference item. + */ + [PropertySymbol.dispatchEvent]( + type: 'add' | 'insert' | 'remove', + item: T, + referenceItem?: T | null + ): void; + + /** + * Index of item. + * + * @param item Item. + * @returns Index. + */ + [PropertySymbol.indexOf](item: T): number; + + /** + * Returns true if the item is in the list. + * + * @param item Item. + * @returns True if the item is in the list. + */ + [PropertySymbol.includes](item: T): boolean; + + /** + * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. + * + * @returns Iterator. + */ + [Symbol.iterator](): IterableIterator; + + /** + * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. + * + * @returns Iterator. + */ + values(): IterableIterator; + + /** + * Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object. + * + * @returns Iterator. + * + */ + keys(): IterableIterator; + + /** + * Returns an iterator, allowing you to go through all key/value pairs contained in this object. + * + * @returns Iterator. + */ + entries(): IterableIterator<[number, T]>; +} + +export default NodeList; diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index a8be2c28e..1ffb3d8d9 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -9,6 +9,11 @@ import NodeUtility from './NodeUtility.js'; import Attr from '../attr/Attr.js'; import NodeList from './NodeList.js'; import NodeFactory from '../NodeFactory.js'; +import MutationRecord from '../../mutation-observer/MutationRecord.js'; +import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; +import DOMException from '../../exception/DOMException.js'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import INodeList from './INodeList.js'; /** * Node. @@ -58,12 +63,9 @@ export default class Node extends EventTarget { public [PropertySymbol.parentNode]: Node | null = null; public [PropertySymbol.nodeType]: NodeTypeEnum; public [PropertySymbol.rootNode]: Node = null; - public [PropertySymbol.formNode]: Node = null; - public [PropertySymbol.dataListNode]: Node = null; - public [PropertySymbol.selectNode]: Node = null; - public [PropertySymbol.textAreaNode]: Node = null; public [PropertySymbol.observers]: MutationListener[] = []; - public [PropertySymbol.childNodes]: NodeList = new NodeList(); + public [PropertySymbol.childNodes]: INodeList = new NodeList(); + public [PropertySymbol.childNodesFlatten]: INodeList = new NodeList(); /** * Constructor. @@ -135,7 +137,7 @@ export default class Node extends EventTarget { * * @returns Child nodes list. */ - public get childNodes(): NodeList { + public get childNodes(): INodeList { return this[PropertySymbol.childNodes]; } @@ -191,9 +193,9 @@ export default class Node extends EventTarget { */ public get previousSibling(): Node { if (this[PropertySymbol.parentNode]) { - const index = (this[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf( - this - ); + const index = (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][ + PropertySymbol.indexOf + ](this); if (index > 0) { return (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][index - 1]; } @@ -208,9 +210,9 @@ export default class Node extends EventTarget { */ public get nextSibling(): Node { if (this[PropertySymbol.parentNode]) { - const index = (this[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf( - this - ); + const index = (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][ + PropertySymbol.indexOf + ](this); if ( index > -1 && index + 1 < (this[PropertySymbol.parentNode])[PropertySymbol.childNodes].length @@ -339,6 +341,11 @@ export default class Node extends EventTarget { * @returns Appended node. */ public appendChild(node: Node): Node { + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'appendChild' on 'Node': 1 argument required, but only 0 present` + ); + } return this[PropertySymbol.appendChild](node); } @@ -349,6 +356,11 @@ export default class Node extends EventTarget { * @returns Removed node. */ public removeChild(node: Node): Node { + if (arguments.length < 1) { + throw new TypeError( + `Failed to execute 'removeChild' on 'Node': 1 argument required, but only 0 present` + ); + } return this[PropertySymbol.removeChild](node); } @@ -376,6 +388,11 @@ export default class Node extends EventTarget { * @returns Replaced node. */ public replaceChild(newChild: Node, oldChild: Node): Node { + if (arguments.length < 2) { + throw new TypeError( + `Failed to execute 'replaceChild' on 'Node': 2 arguments required, but only ${arguments.length} present.` + ); + } return this[PropertySymbol.replaceChild](newChild, oldChild); } @@ -393,8 +410,9 @@ export default class Node extends EventTarget { // Document has childNodes directly when it is created if (clone[PropertySymbol.childNodes].length) { - for (const node of clone[PropertySymbol.childNodes].slice()) { - node[PropertySymbol.parentNode].removeChild(node); + const childNodes = clone[PropertySymbol.childNodes]; + while (childNodes.length) { + clone.removeChild(childNodes[0]); } } @@ -402,7 +420,7 @@ export default class Node extends EventTarget { for (const childNode of this[PropertySymbol.childNodes]) { const childClone = childNode.cloneNode(true); childClone[PropertySymbol.parentNode] = clone; - clone[PropertySymbol.childNodes].push(childClone); + clone[PropertySymbol.childNodes][PropertySymbol.appendChild](childClone); } } @@ -416,7 +434,82 @@ export default class Node extends EventTarget { * @returns Appended node. */ public [PropertySymbol.appendChild](node: Node): Node { - return NodeUtility.appendChild(this, node); + if (node === this) { + throw new DOMException( + "Failed to execute 'appendChild' on 'Node': Not possible to append a node as a child of itself." + ); + } + + if (NodeUtility.isInclusiveAncestor(node, this, true)) { + throw new DOMException( + "Failed to execute 'appendChild' on 'Node': The new node is a parent of the node to insert to.", + DOMExceptionNameEnum.domException + ); + } + + // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. + // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment + if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode) { + const childNodes = node[PropertySymbol.childNodes]; + while (childNodes.length) { + this.appendChild(childNodes[0]); + } + return node; + } + + // Remove the node from its previous parent if it has any. + if (node[PropertySymbol.parentNode]) { + node[PropertySymbol.parentNode][PropertySymbol.childNodes][PropertySymbol.removeChild](node); + let parent = node[PropertySymbol.parentNode]; + while (parent) { + node[PropertySymbol.parentNode][PropertySymbol.childNodesFlatten][ + PropertySymbol.removeChild + ](node); + parent = node[PropertySymbol.parentNode]; + } + } + + if (this[PropertySymbol.isConnected]) { + (this[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; + } + + this[PropertySymbol.childNodes][PropertySymbol.appendChild](node); + + let parent: Node = this; + while (parent) { + parent[PropertySymbol.childNodesFlatten][PropertySymbol.appendChild](node); + parent = parent[PropertySymbol.parentNode]; + } + + node[PropertySymbol.parentNode] = this; + + if (this[PropertySymbol.isConnected] && !(node)[PropertySymbol.isConnected]) { + (node)[PropertySymbol.isConnected] = true; + (node)[PropertySymbol.connectedToDocument](); + } else if (!this[PropertySymbol.isConnected] && (node)[PropertySymbol.isConnected]) { + (node)[PropertySymbol.isConnected] = false; + (node)[PropertySymbol.disconnectedFromDocument](); + } + + // MutationObserver + if ((this)[PropertySymbol.observers].length > 0) { + const record = new MutationRecord({ + target: this, + type: MutationTypeEnum.childList, + addedNodes: [node] + }); + + for (const observer of (this)[PropertySymbol.observers]) { + if (observer.options?.subtree) { + (node)[PropertySymbol.observe](observer); + } + if (observer.options?.childList) { + observer.report(record); + } + } + } + + return node; } /** @@ -426,7 +519,41 @@ export default class Node extends EventTarget { * @returns Removed node. */ public [PropertySymbol.removeChild](node: Node): Node { - return NodeUtility.removeChild(this, node); + if (this[PropertySymbol.isConnected]) { + (this[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; + } + + this[PropertySymbol.childNodes][PropertySymbol.removeChild](node); + + let parent: Node = this; + while (parent) { + parent[PropertySymbol.childNodesFlatten][PropertySymbol.removeChild](node); + parent = parent[PropertySymbol.parentNode]; + } + + if ((node)[PropertySymbol.isConnected]) { + (node)[PropertySymbol.disconnectedFromDocument](); + } + + // MutationObserver + if ((this)[PropertySymbol.observers].length > 0) { + const record = new MutationRecord({ + target: this, + type: MutationTypeEnum.childList, + removedNodes: [node] + }); + + for (const observer of (this)[PropertySymbol.observers]) { + if (observer.options?.subtree) { + (node)[PropertySymbol.unobserve](observer); + } + if (observer.options?.childList) { + observer.report(record); + } + } + } + + return node; } /** @@ -437,7 +564,88 @@ export default class Node extends EventTarget { * @returns Inserted node. */ public [PropertySymbol.insertBefore](newNode: Node, referenceNode: Node | null): Node { - return NodeUtility.insertBefore(this, newNode, referenceNode); + if (NodeUtility.isInclusiveAncestor(newNode, this, true)) { + throw new DOMException( + "Failed to execute 'insertBefore' on 'Node': The new node is a parent of the node to insert to.", + DOMExceptionNameEnum.domException + ); + } + + // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. + // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment + if (newNode[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode) { + const childNodes = (newNode)[PropertySymbol.childNodes]; + while (childNodes.length > 0) { + this.insertBefore(childNodes[0], referenceNode); + } + return newNode; + } + + // If the referenceNode is null or undefined, then the newNode should be appended to the ancestorNode. + // According to spec only null is valid, but browsers support undefined as well. + if (!referenceNode) { + this.appendChild(newNode); + return newNode; + } + + if (!this[PropertySymbol.childNodes][PropertySymbol.includes](referenceNode)) { + throw new DOMException( + "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node." + ); + } + + if (this[PropertySymbol.isConnected]) { + (this[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; + } + + if (newNode[PropertySymbol.parentNode]) { + newNode[PropertySymbol.parentNode][PropertySymbol.childNodes][PropertySymbol.removeChild]( + newNode + ); + let parent: Node = newNode[PropertySymbol.parentNode]; + while (parent) { + parent[PropertySymbol.childNodesFlatten][PropertySymbol.removeChild](newNode); + parent = parent[PropertySymbol.parentNode]; + } + } + + this[PropertySymbol.childNodes][PropertySymbol.insertBefore](newNode, referenceNode); + + let parent: Node = this; + while (parent) { + parent[PropertySymbol.childNodesFlatten][PropertySymbol.insertBefore](newNode, referenceNode); + parent = parent[PropertySymbol.parentNode]; + } + + newNode[PropertySymbol.parentNode] = this; + + if (this[PropertySymbol.isConnected] && !(newNode)[PropertySymbol.isConnected]) { + (newNode)[PropertySymbol.isConnected] = true; + (newNode)[PropertySymbol.connectedToDocument](); + } else if (!this[PropertySymbol.isConnected] && (newNode)[PropertySymbol.isConnected]) { + (newNode)[PropertySymbol.isConnected] = false; + (newNode)[PropertySymbol.disconnectedFromDocument](); + } + + // MutationObserver + if ((this)[PropertySymbol.observers].length > 0) { + const record = new MutationRecord({ + target: this, + type: MutationTypeEnum.childList, + addedNodes: [newNode] + }); + + for (const observer of (this)[PropertySymbol.observers]) { + if (observer.options?.subtree) { + (newNode)[PropertySymbol.observe](observer); + } + if (observer.options?.childList) { + observer.report(record); + } + } + } + + return newNode; } /** @@ -508,80 +716,51 @@ export default class Node extends EventTarget { } /** - * Connects this element to another element. - * - * @param parentNode Parent node. + * Called when connected to document. */ - public [PropertySymbol.connectToNode](parentNode: Node = null): void { - const isConnected = !!parentNode && parentNode[PropertySymbol.isConnected]; - const formNode = (this)[PropertySymbol.formNode]; - const dataListNode = (this)[PropertySymbol.dataListNode]; - const selectNode = (this)[PropertySymbol.selectNode]; - const textAreaNode = (this)[PropertySymbol.textAreaNode]; - + public [PropertySymbol.connectedToDocument](): void { if (this[PropertySymbol.nodeType] !== NodeTypeEnum.documentFragmentNode) { - this[PropertySymbol.parentNode] = parentNode; - this[PropertySymbol.rootNode] = - isConnected && parentNode ? (parentNode)[PropertySymbol.rootNode] : null; - - if (this['tagName'] !== 'FORM') { - (this)[PropertySymbol.formNode] = parentNode - ? (parentNode)[PropertySymbol.formNode] - : null; - } + this[PropertySymbol.rootNode] = this[PropertySymbol.parentNode][PropertySymbol.rootNode]; + } - if (this['tagName'] !== 'DATALIST') { - (this)[PropertySymbol.dataListNode] = parentNode - ? (parentNode)[PropertySymbol.dataListNode] - : null; - } + if (this.connectedCallback) { + this.connectedCallback(); + } - if (this['tagName'] !== 'SELECT') { - (this)[PropertySymbol.selectNode] = parentNode - ? (parentNode)[PropertySymbol.selectNode] - : null; - } + for (const child of this[PropertySymbol.childNodes]) { + (child)[PropertySymbol.connectedToDocument](); + } - if (this['tagName'] !== 'TEXTAREA') { - (this)[PropertySymbol.textAreaNode] = parentNode - ? (parentNode)[PropertySymbol.textAreaNode] - : null; - } + // eslint-disable-next-line + if ((this)[PropertySymbol.shadowRoot]) { + // eslint-disable-next-line + (this)[PropertySymbol.shadowRoot][PropertySymbol.connectedToDocument](); } + } - if (this[PropertySymbol.isConnected] !== isConnected) { - this[PropertySymbol.isConnected] = isConnected; + /** + * Called when disconnected from document. + * @param e + */ + public [PropertySymbol.disconnectedFromDocument](): void { + this[PropertySymbol.rootNode] = null; - if (!isConnected) { - if (this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] === this) { - this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] = null; - } - } + if (this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] === this) { + this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] = null; + } - if (isConnected && this.connectedCallback) { - this.connectedCallback(); - } else if (!isConnected && this.disconnectedCallback) { - this.disconnectedCallback(); - } + if (this.disconnectedCallback) { + this.disconnectedCallback(); + } - for (const child of this[PropertySymbol.childNodes]) { - (child)[PropertySymbol.connectToNode](this); - } + for (const child of this[PropertySymbol.childNodes]) { + (child)[PropertySymbol.disconnectedFromDocument](); + } + // eslint-disable-next-line + if ((this)[PropertySymbol.shadowRoot]) { // eslint-disable-next-line - if ((this)[PropertySymbol.shadowRoot]) { - // eslint-disable-next-line - (this)[PropertySymbol.shadowRoot][PropertySymbol.connectToNode](this); - } - } else if ( - formNode !== this[PropertySymbol.formNode] || - dataListNode !== this[PropertySymbol.dataListNode] || - selectNode !== this[PropertySymbol.selectNode] || - textAreaNode !== this[PropertySymbol.textAreaNode] - ) { - for (const child of this[PropertySymbol.childNodes]) { - (child)[PropertySymbol.connectToNode](this); - } + (this)[PropertySymbol.shadowRoot][PropertySymbol.disconnectedFromDocument](); } } @@ -729,9 +908,9 @@ export default class Node extends EventTarget { const node2Node = reverseArrayIndex(node2Ancestors, commonAncestorIndex + 1); const node1Node = reverseArrayIndex(node1Ancestors, commonAncestorIndex + 1); - const computeNodeIndexes = (nodes: Node[]): void => { + const computeNodeIndexes = (nodes: INodeList): void => { for (const childNode of nodes) { - computeNodeIndexes((childNode)[PropertySymbol.childNodes]); + computeNodeIndexes(childNode[PropertySymbol.childNodes]); if (childNode === node2Node) { node2Index = indexes; @@ -747,7 +926,7 @@ export default class Node extends EventTarget { } }; - computeNodeIndexes((commonAncestor)[PropertySymbol.childNodes]); + computeNodeIndexes(commonAncestor[PropertySymbol.childNodes]); /** * 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING. diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts index e5b6095fd..9685fc26b 100644 --- a/packages/happy-dom/src/nodes/node/NodeList.ts +++ b/packages/happy-dom/src/nodes/node/NodeList.ts @@ -1,14 +1,53 @@ +import * as PropertySymbol from '../../PropertySymbol.js'; +import INodeList from './INodeList.js'; +import DOMException from '../../exception/DOMException.js'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import TNodeListListener from './TNodeListListener.js'; + /** - * Class list. + * NodeList. + * + * We are extending Array here to improve performance. + * However, we should not expose Array methods to the outside. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList */ -export default class NodeList extends Array { +class NodeList extends Array implements INodeList { + #eventListeners: { + add: WeakRef>[]; + insert: WeakRef>[]; + remove: WeakRef>[]; + } = { + add: [], + insert: [], + remove: [] + }; + /** * Returns `Symbol.toStringTag`. * * @returns `Symbol.toStringTag`. */ public get [Symbol.toStringTag](): string { - return this.constructor.name; + return 'NodeList'; + } + + /** + * Returns `[object NodeList]`. + * + * @returns `[object NodeList]`. + */ + public toLocaleString(): string { + return '[object NodeList]'; + } + + /** + * Returns `[object NodeList]`. + * + * @returns `[object NodeList]`. + */ + public toString(): string { + return '[object NodeList]'; } /** @@ -19,4 +58,173 @@ export default class NodeList extends Array { public item(index: number): T { return index >= 0 && this[index] ? this[index] : null; } + + /** + * Appends item. + * + * @param item Item. + * @returns True if added. + */ + public [PropertySymbol.addItem](item: T): boolean { + if (super.includes(item)) { + this[PropertySymbol.removeItem](item); + } + + super.push(item); + + this[PropertySymbol.dispatchEvent]('add', item); + + return true; + } + + /** + * Inserts item before another item. + * + * @param newItem New item. + * @param [referenceItem] Reference item. + * @returns True if inserted. + */ + public [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean { + if (!referenceItem) { + return this[PropertySymbol.appendChild](newItem); + } + + if (super.includes(newItem)) { + this[PropertySymbol.removeItem](newItem); + } + + const index = super.indexOf(referenceItem); + + if (index === -1) { + throw new DOMException( + "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.", + DOMExceptionNameEnum.notFoundError + ); + } + + super.splice(index, 0, newItem); + + this[PropertySymbol.dispatchEvent]('insert', newItem, referenceItem); + + return true; + } + + /** + * Removes item. + * + * @param item Item. + * @returns True if removed. + */ + public [PropertySymbol.removeItem](item: T): boolean { + const index = super.indexOf(item); + + if (index === -1) { + throw new DOMException( + "Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.", + DOMExceptionNameEnum.notFoundError + ); + } + + super.splice(index, 1); + + this[PropertySymbol.dispatchEvent]('remove', item); + + return true; + } + + /** + * Adds event listener. + * + * @param type Type. + * @param listener Listener. + */ + public [PropertySymbol.addEventListener]( + type: 'add' | 'insert' | 'remove', + listener: TNodeListListener + ): void { + this.#eventListeners[type].push(new WeakRef(listener)); + } + + /** + * Removes event listener. + * + * @param type Type. + * @param listener Listener. + */ + public [PropertySymbol.removeEventListener]( + type: 'add' | 'insert' | 'remove', + listener: TNodeListListener + ): void { + const listeners = this.#eventListeners[type]; + for (let i = 0, max = listeners.length; i < max; i++) { + if (listeners[i].deref() === listener) { + listeners.splice(i, 1); + return; + } + } + } + + /** + * Dispatches event. + * + * @param type Type. + * @param item Item. + * @param referenceItem Reference item. + */ + public [PropertySymbol.dispatchEvent]( + type: 'add' | 'insert' | 'remove', + item: T, + referenceItem?: T | null + ): void { + const listeners = this.#eventListeners[type]; + for (let i = 0, max = listeners.length; i < max; i++) { + const listener = listeners[i].deref(); + if (listener) { + listener(item, referenceItem); + } else { + listeners.splice(i, 1); + i--; + max--; + } + } + } + + /** + * Index of item. + * + * @param item Item. + * @returns Index. + */ + public [PropertySymbol.indexOf](item: T): number { + return super.indexOf(item); + } + + /** + * Returns true if the item is in the list. + * + * @param item Item. + * @returns True if the item is in the list. + */ + public [PropertySymbol.includes](item: T): boolean { + return super.includes(item); + } } + +// Removes Array methods from NodeList. +const descriptors = Object.getOwnPropertyDescriptors(Array.prototype); +for (const key of Object.keys(descriptors)) { + const descriptor = descriptors[key]; + if (key === 'length') { + Object.defineProperty(NodeList.prototype, key, { + set: () => {}, + get: descriptor.get + }); + } else if (key !== 'values' && key !== 'keys' && key !== 'entries') { + if (typeof descriptor.value === 'function') { + Object.defineProperty(NodeList.prototype, key, {}); + } + } +} + +// Forces the type to be an interface to hide Array methods from the outside. +export default () => INodeList>(NodeList); diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index 1382f6892..2bb749248 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -8,223 +8,11 @@ import DocumentType from '../document-type/DocumentType.js'; import Attr from '../attr/Attr.js'; import ProcessingInstruction from '../processing-instruction/ProcessingInstruction.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; -import DOMException from '../../exception/DOMException.js'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import MutationRecord from '../../mutation-observer/MutationRecord.js'; -import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; /** * Node utility. */ export default class NodeUtility { - /** - * Append a child node to childNodes. - * - * @param ancestorNode Ancestor node. - * @param node Node to append. - * @param [options] Options. - * @param [options.disableAncestorValidation] Disables validation for checking if the node is an ancestor of the ancestorNode. - * @returns Appended node. - */ - public static appendChild( - ancestorNode: Node, - node: Node, - options?: { disableAncestorValidation?: boolean } - ): Node { - if (node === ancestorNode) { - throw new DOMException( - "Failed to execute 'appendChild' on 'Node': Not possible to append a node as a child of itself." - ); - } - - if (!options?.disableAncestorValidation && this.isInclusiveAncestor(node, ancestorNode, true)) { - throw new DOMException( - "Failed to execute 'appendChild' on 'Node': The new node is a parent of the node to insert to.", - DOMExceptionNameEnum.domException - ); - } - - // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. - // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment - if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode) { - for (const child of (node)[PropertySymbol.childNodes].slice()) { - ancestorNode.appendChild(child); - } - return node; - } - - // Remove the node from its previous parent if it has any. - if (node[PropertySymbol.parentNode]) { - const index = (node[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf( - node - ); - if (index !== -1) { - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes].splice(index, 1); - } - } - - if (ancestorNode[PropertySymbol.isConnected]) { - (ancestorNode[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; - } - - (ancestorNode)[PropertySymbol.childNodes].push(node); - - (node)[PropertySymbol.connectToNode](ancestorNode); - - // MutationObserver - if ((ancestorNode)[PropertySymbol.observers].length > 0) { - const record = new MutationRecord({ - target: ancestorNode, - type: MutationTypeEnum.childList, - addedNodes: [node] - }); - - for (const observer of (ancestorNode)[PropertySymbol.observers]) { - if (observer.options?.subtree) { - (node)[PropertySymbol.observe](observer); - } - if (observer.options?.childList) { - observer.report(record); - } - } - } - - return node; - } - - /** - * Remove Child element from childNodes array. - * - * @param ancestorNode Ancestor node. - * @param node Node to remove. - * @returns Removed node. - */ - public static removeChild(ancestorNode: Node, node: Node): Node { - const index = (ancestorNode)[PropertySymbol.childNodes].indexOf(node); - - if (index === -1) { - throw new DOMException('Failed to remove node. Node is not child of parent.'); - } - - if (ancestorNode[PropertySymbol.isConnected]) { - (ancestorNode[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; - } - - (ancestorNode)[PropertySymbol.childNodes].splice(index, 1); - - (node)[PropertySymbol.connectToNode](null); - - // MutationObserver - if ((ancestorNode)[PropertySymbol.observers].length > 0) { - const record = new MutationRecord({ - target: ancestorNode, - type: MutationTypeEnum.childList, - removedNodes: [node] - }); - - for (const observer of (ancestorNode)[PropertySymbol.observers]) { - if (observer.options?.subtree) { - (node)[PropertySymbol.unobserve](observer); - } - if (observer.options?.childList) { - observer.report(record); - } - } - } - - return node; - } - - /** - * Inserts a node before another. - * - * @param ancestorNode Ancestor node. - * @param newNode Node to insert. - * @param referenceNode Node to insert before. - * @param [options] Options. - * @param [options.disableAncestorValidation] Disables validation for checking if the node is an ancestor of the ancestorNode. - * @returns Inserted node. - */ - public static insertBefore( - ancestorNode: Node, - newNode: Node, - referenceNode: Node | null, - options?: { disableAncestorValidation?: boolean } - ): Node { - if ( - !options?.disableAncestorValidation && - this.isInclusiveAncestor(newNode, ancestorNode, true) - ) { - throw new DOMException( - "Failed to execute 'insertBefore' on 'Node': The new node is a parent of the node to insert to.", - DOMExceptionNameEnum.domException - ); - } - - // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. - // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment - if (newNode[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode) { - for (const child of (newNode)[PropertySymbol.childNodes].slice()) { - ancestorNode.insertBefore(child, referenceNode); - } - return newNode; - } - - // If the referenceNode is null or undefined, then the newNode should be appended to the ancestorNode. - // According to spec only null is valid, but browsers support undefined as well. - if (!referenceNode) { - ancestorNode.appendChild(newNode); - return newNode; - } - - if ((ancestorNode)[PropertySymbol.childNodes].indexOf(referenceNode) === -1) { - throw new DOMException( - "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node." - ); - } - - if (ancestorNode[PropertySymbol.isConnected]) { - (ancestorNode[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; - } - - if (newNode[PropertySymbol.parentNode]) { - const index = (newNode[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf( - newNode - ); - if (index !== -1) { - (newNode[PropertySymbol.parentNode])[PropertySymbol.childNodes].splice(index, 1); - } - } - - (ancestorNode)[PropertySymbol.childNodes].splice( - (ancestorNode)[PropertySymbol.childNodes].indexOf(referenceNode), - 0, - newNode - ); - - (newNode)[PropertySymbol.connectToNode](ancestorNode); - - // MutationObserver - if ((ancestorNode)[PropertySymbol.observers].length > 0) { - const record = new MutationRecord({ - target: ancestorNode, - type: MutationTypeEnum.childList, - addedNodes: [newNode] - }); - - for (const observer of (ancestorNode)[PropertySymbol.observers]) { - if (observer.options?.subtree) { - (newNode)[PropertySymbol.observe](observer); - } - if (observer.options?.childList) { - observer.report(record); - } - } - } - - return newNode; - } - /** * Returns whether the passed node is a text node, and narrows its type. * diff --git a/packages/happy-dom/src/nodes/node/TNodeListListener.ts b/packages/happy-dom/src/nodes/node/TNodeListListener.ts new file mode 100644 index 000000000..a7d9f6153 --- /dev/null +++ b/packages/happy-dom/src/nodes/node/TNodeListListener.ts @@ -0,0 +1,2 @@ +type TNodeListListener = (item: T, referenceItem?: T | null) => void; +export default TNodeListListener; diff --git a/packages/happy-dom/src/nodes/parent-node/IParentNode.ts b/packages/happy-dom/src/nodes/parent-node/IParentNode.ts index 1aa2af346..7fdc108bc 100644 --- a/packages/happy-dom/src/nodes/parent-node/IParentNode.ts +++ b/packages/happy-dom/src/nodes/parent-node/IParentNode.ts @@ -1,4 +1,4 @@ -import HTMLCollection from '../element/HTMLCollection.js'; +import HTMLCollection from '../element/HTMLCollection2.js'; import Element from '../element/Element.js'; import Node from '../node/Node.js'; import NodeList from '../node/NodeList.js'; diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index 44b837da8..001423f15 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -3,7 +3,7 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import Document from '../document/Document.js'; import Element from '../element/Element.js'; -import HTMLCollection from '../element/HTMLCollection.js'; +import HTMLCollection from '../element/HTMLCollection2.js'; import Node from '../node/Node.js'; import NamespaceURI from '../../config/NamespaceURI.js'; @@ -67,8 +67,12 @@ export default class ParentNodeUtility { parentNode: Element | Document | DocumentFragment, ...nodes: (string | Node)[] ): void { - for (const node of (parentNode)[PropertySymbol.childNodes].slice()) { - parentNode.removeChild(node); + const childNodes = (parentNode)[PropertySymbol.childNodes][ + PropertySymbol.items + ]; + + while (childNodes.length) { + parentNode.removeChild(childNodes[0]); } this.append(parentNode, ...nodes); @@ -85,15 +89,20 @@ export default class ParentNodeUtility { parentNode: Element | DocumentFragment | Document, className: string ): HTMLCollection { - let matches = new HTMLCollection(); + const matches = new HTMLCollection(); - for (const child of (parentNode)[PropertySymbol.children]) { + for (const child of (parentNode)[PropertySymbol.children][ + PropertySymbol.items + ]) { if (child.className.split(' ').includes(className)) { - matches.push(child); + matches[PropertySymbol.addItem](child); + } + + for (const subChild of this.getElementsByClassName(child, className)[ + PropertySymbol.items + ]) { + matches[PropertySymbol.addItem](subChild); } - matches = >( - matches.concat(this.getElementsByClassName(child, className)) - ); } return matches; @@ -114,7 +123,9 @@ export default class ParentNodeUtility { const includeAll = tagName === '*'; let matches = new HTMLCollection(); - for (const child of (parentNode)[PropertySymbol.children]) { + for (const child of (parentNode)[PropertySymbol.children][ + PropertySymbol.items + ]) { if (includeAll || child[PropertySymbol.tagName].toUpperCase() === upperTagName) { matches.push(child); } @@ -196,7 +207,7 @@ export default class ParentNodeUtility { public static getElementById( parentNode: Element | DocumentFragment | Document, id: string - ): Element { + ): Element | null { id = String(id); for (const child of (parentNode)[PropertySymbol.children]) { if (child.id === id) { diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts index 80146d9ad..ee3f9aaf7 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts @@ -4,10 +4,9 @@ import Element from '../element/Element.js'; import SVGSVGElement from './SVGSVGElement.js'; import Event from '../../event/Event.js'; import HTMLElementUtility from '../html-element/HTMLElementUtility.js'; -import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; -import SVGElementNamedNodeMap from './SVGElementNamedNodeMap.js'; import DatasetFactory from '../element/DatasetFactory.js'; import IDataset from '../element/IDataset.js'; +import Attr from '../attr/Attr.js'; /** * SVG Element. @@ -25,12 +24,26 @@ export default class SVGElement extends Element { public onunload: (event: Event) => void | null = null; // Internal properties - public override [PropertySymbol.attributes]: NamedNodeMap = new SVGElementNamedNodeMap(this); public [PropertySymbol.style]: CSSStyleDeclaration | null = null; // Private properties #dataset: IDataset = null; + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } + /** * Returns viewport. * @@ -114,4 +127,26 @@ export default class SVGElement extends Element { public focus(): void { HTMLElementUtility.focus(this); } + + /** + * Triggered when an attribute is set. + * + * @param item Item + */ + #onSetAttribute(item: Attr): void { + if (item[PropertySymbol.name] === 'style' && this[PropertySymbol.style]) { + this[PropertySymbol.style].cssText = item[PropertySymbol.value]; + } + } + + /** + * Triggered when an attribute is removed. + * + * @param removedItem Removed item. + */ + #onRemoveAttribute(removedItem: Attr): void { + if (removedItem && removedItem[PropertySymbol.name] === 'style' && this[PropertySymbol.style]) { + this[PropertySymbol.style].cssText = ''; + } + } } diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts deleted file mode 100644 index d8d096885..000000000 --- a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Attr from '../attr/Attr.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import ElementNamedNodeMap from '../element/ElementNamedNodeMap.js'; -import SVGElement from './SVGElement.js'; - -/** - * Named Node Map. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap - */ -export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { - protected [PropertySymbol.ownerElement]: SVGElement; - - /** - * @override - */ - public override [PropertySymbol.setNamedItem](item: Attr): Attr | null { - const replacedItem = super[PropertySymbol.setNamedItem](item); - - if ( - item[PropertySymbol.name] === 'style' && - this[PropertySymbol.ownerElement][PropertySymbol.style] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.style].cssText = item[PropertySymbol.value]; - } - - return replacedItem || null; - } - - /** - * @override - */ - public override [PropertySymbol.removeNamedItem](name: string): Attr | null { - const removedItem = super[PropertySymbol.removeNamedItem](name); - - if ( - removedItem && - removedItem[PropertySymbol.name] === 'style' && - this[PropertySymbol.ownerElement][PropertySymbol.style] - ) { - this[PropertySymbol.ownerElement][PropertySymbol.style].cssText = ''; - } - - return removedItem; - } -} diff --git a/packages/happy-dom/src/nodes/text/Text.ts b/packages/happy-dom/src/nodes/text/Text.ts index eb29afc3d..2baebb60e 100644 --- a/packages/happy-dom/src/nodes/text/Text.ts +++ b/packages/happy-dom/src/nodes/text/Text.ts @@ -85,22 +85,4 @@ export default class Text extends CharacterData { public override [PropertySymbol.cloneNode](deep = false): Text { return super[PropertySymbol.cloneNode](deep); } - - /** - * @override - */ - public override [PropertySymbol.connectToNode](parentNode: Node = null): void { - const oldTextAreaNode = this[PropertySymbol.textAreaNode]; - - super[PropertySymbol.connectToNode](parentNode); - - if (oldTextAreaNode !== this[PropertySymbol.textAreaNode]) { - if (oldTextAreaNode) { - oldTextAreaNode[PropertySymbol.resetSelection](); - } - if (this[PropertySymbol.textAreaNode]) { - (this[PropertySymbol.textAreaNode])[PropertySymbol.resetSelection](); - } - } - } } diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index b4e42f885..233006f96 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -333,6 +333,7 @@ export default class QuerySelector { for (let i = 0, max = children.length; i < max; i++) { const child = children[i]; + const childrenOfChild = (child)[PropertySymbol.children][PropertySymbol.items]; const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i); if (selectorItem.match(child)) { @@ -360,29 +361,16 @@ export default class QuerySelector { case SelectorCombinatorEnum.descendant: case SelectorCombinatorEnum.child: matched = matched.concat( - this.findAll( - rootElement, - (child)[PropertySymbol.children], - selectorItems.slice(1), - position - ) + this.findAll(rootElement, childrenOfChild, selectorItems.slice(1), position) ); break; } } } - if ( - selectorItem.combinator === SelectorCombinatorEnum.descendant && - (child)[PropertySymbol.children].length - ) { + if (selectorItem.combinator === SelectorCombinatorEnum.descendant && childrenOfChild.length) { matched = matched.concat( - this.findAll( - rootElement, - (child)[PropertySymbol.children], - selectorItems, - position - ) + this.findAll(rootElement, childrenOfChild, selectorItems, position) ); } } @@ -407,6 +395,8 @@ export default class QuerySelector { const nextSelectorItem = selectorItems[1]; for (const child of children) { + const childrenOfChild = (child)[PropertySymbol.children][PropertySymbol.items]; + if (selectorItem.match(child)) { if (!nextSelectorItem) { if (rootElement !== child) { @@ -428,11 +418,7 @@ export default class QuerySelector { break; case SelectorCombinatorEnum.descendant: case SelectorCombinatorEnum.child: - const match = this.findFirst( - rootElement, - (child)[PropertySymbol.children], - selectorItems.slice(1) - ); + const match = this.findFirst(rootElement, childrenOfChild, selectorItems.slice(1)); if (match) { return match; } @@ -441,15 +427,8 @@ export default class QuerySelector { } } - if ( - selectorItem.combinator === SelectorCombinatorEnum.descendant && - (child)[PropertySymbol.children].length - ) { - const match = this.findFirst( - rootElement, - (child)[PropertySymbol.children], - selectorItems - ); + if (selectorItem.combinator === SelectorCombinatorEnum.descendant && childrenOfChild.length) { + const match = this.findFirst(rootElement, childrenOfChild, selectorItems); if (match) { return match; diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index 82aa134a9..fffd10913 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -236,7 +236,9 @@ export default class SelectorItem { ? { priorityWeight: 10 } : null; case 'empty': - return !(element)[PropertySymbol.children].length ? { priorityWeight: 10 } : null; + return !(element)[PropertySymbol.children][PropertySymbol.items].length + ? { priorityWeight: 10 } + : null; case 'root': return element[PropertySymbol.tagName] === 'HTML' ? { priorityWeight: 10 } : null; case 'not': diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 0b386ec88..22a7683ff 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -70,7 +70,7 @@ import Location from '../location/Location.js'; import MediaQueryList from '../match-media/MediaQueryList.js'; import MutationObserver from '../mutation-observer/MutationObserver.js'; import MutationRecord from '../mutation-observer/MutationRecord.js'; -import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; +import NamedNodeMap from '../nodes/element/NamedNodeMap.js'; import MimeType from '../navigator/MimeType.js'; import MimeTypeArray from '../navigator/MimeTypeArray.js'; import Navigator from '../navigator/Navigator.js'; @@ -86,7 +86,7 @@ import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js' import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; import DOMRect from '../nodes/element/DOMRect.js'; import Element from '../nodes/element/Element.js'; -import HTMLCollection from '../nodes/element/HTMLCollection.js'; +import HTMLCollection from '../nodes/element/HTMLCollection2.js'; import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; import HTMLAreaElement from '../nodes/html-area-element/HTMLAreaElement.js'; import AudioImplementation from '../nodes/html-audio-element/Audio.js'; diff --git a/packages/happy-dom/test/css/declaration/element-style/CSSStyleDeclarationElementStyle.test.ts b/packages/happy-dom/test/css/declaration/element-style/CSSStyleDeclarationElementStyle.test.ts index 98777d18c..a6a588fe9 100644 --- a/packages/happy-dom/test/css/declaration/element-style/CSSStyleDeclarationElementStyle.test.ts +++ b/packages/happy-dom/test/css/declaration/element-style/CSSStyleDeclarationElementStyle.test.ts @@ -1,5 +1,4 @@ import Window from '../../../../src/window/Window.js'; -import Window from '../../../../src/window/Window.js'; import Document from '../../../../src/nodes/document/Document.js'; import HTMLElement from '../../../../src/nodes/html-element/HTMLElement.js'; import CSSStyleDeclarationElementStyle from '../../../../src/css/declaration/element-style/CSSStyleDeclarationElementStyle.js'; diff --git a/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts b/packages/happy-dom/test/nodes/element/NamedNodeMap.test.ts similarity index 91% rename from packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts rename to packages/happy-dom/test/nodes/element/NamedNodeMap.test.ts index ac14a6493..e9164de74 100644 --- a/packages/happy-dom/test/named-node-map/NamedNodeMap.test.ts +++ b/packages/happy-dom/test/nodes/element/NamedNodeMap.test.ts @@ -1,10 +1,10 @@ -import Window from '../../src/window/Window.js'; -import Document from '../../src/nodes/document/Document.js'; -import Element from '../../src/nodes/element/Element.js'; -import NamedNodeMap from '../../src/named-node-map/NamedNodeMap.js'; -import Attr from '../../src/nodes/attr/Attr.js'; -import DOMException from '../../src/exception/DOMException.js'; -import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum.js'; +import Window from '../../../src/window/Window.js'; +import Document from '../../../src/nodes/document/Document.js'; +import Element from '../../../src/nodes/element/Element.js'; +import NamedNodeMap from '../../../src/nodes/element/NamedNodeMap.js'; +import Attr from '../../../src/nodes/attr/Attr.js'; +import DOMException from '../../../src/exception/DOMException.js'; +import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum.js'; import { beforeEach, describe, it, expect } from 'vitest'; describe('NamedNodeMap', () => { diff --git a/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts index 8c1fe28cf..0dab51447 100644 --- a/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts +++ b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts @@ -237,7 +237,7 @@ describe('HTMLButtonElement', () => { expect(element.form).toBe(form); }); - it('Returns form element by id if the form attribute is set.', () => { + it('Returns form element by id if the form attribute is set when connecting node to DOM.', () => { const form = document.createElement('form'); form.id = 'form'; document.body.appendChild(form); @@ -245,6 +245,17 @@ describe('HTMLButtonElement', () => { expect(element.form).toBe(null); document.body.appendChild(element); expect(element.form).toBe(form); + expect(form.elements.includes(element)).toBe(true); + }); + + it('Returns form element by id if the form attribute is set when element is connected to DOM.', () => { + const form = document.createElement('form'); + form.id = 'form'; + document.body.appendChild(form); + document.body.appendChild(element); + element.setAttribute('form', 'form'); + expect(element.form).toBe(form); + expect(form.elements.includes(element)).toBe(true); }); }); diff --git a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts index 9ae355ecc..eef54a02f 100644 --- a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts +++ b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts @@ -2,6 +2,8 @@ import HTMLFieldSetElement from '../../../src/nodes/html-field-set-element/HTMLF import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; import { beforeEach, describe, it, expect } from 'vitest'; +import HTMLFormElement from '../../../src/nodes/html-form-element/HTMLFormElement.js'; +import HTMLElement from '../../../src/nodes/html-element/HTMLElement.js'; describe('HTMLFieldSetElement', () => { let window: Window; @@ -19,4 +21,67 @@ describe('HTMLFieldSetElement', () => { expect(element instanceof HTMLFieldSetElement).toBe(true); }); }); + + describe('get form()', () => { + it('Returns null if no parent form element exists.', () => { + expect(element.form).toBe(null); + }); + + it('Returns parent form element.', () => { + const form = document.createElement('form'); + const div = document.createElement('div'); + div.appendChild(element); + form.appendChild(div); + expect(element.form).toBe(form); + }); + + it('Returns form element by id if the form attribute is set when connecting node to DOM.', () => { + const form = document.createElement('form'); + form.id = 'form'; + document.body.appendChild(form); + element.setAttribute('form', 'form'); + expect(element.form).toBe(null); + document.body.appendChild(element); + expect(element.form).toBe(form); + expect(form.elements.includes(element)).toBe(true); + }); + + it('Returns form element by id if the form attribute is set when element is connected to DOM.', () => { + const form = document.createElement('form'); + form.id = 'form'; + document.body.appendChild(form); + document.body.appendChild(element); + element.setAttribute('form', 'form'); + expect(element.form).toBe(form); + expect(form.elements.includes(element)).toBe(true); + }); + }); + + describe('get name()', () => { + it(`Returns the attribute "name".`, () => { + element.setAttribute('name', 'VALUE'); + expect(element.name).toBe('VALUE'); + }); + }); + + describe('set name()', () => { + it(`Sets the attribute "name".`, () => { + element.name = 'VALUE'; + expect(element.getAttribute('name')).toBe('VALUE'); + }); + + it(`Sets name as property in parent form elements.`, () => { + const form = document.createElement('form'); + form.appendChild(element); + element.name = 'button1'; + expect(form.elements['button1']).toBe(element); + }); + + it(`Sets name as property in parent element children.`, () => { + const div = document.createElement('div'); + div.appendChild(element); + element.name = 'button1'; + expect(div.children['button1']).toBe(element); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts index 6a6e45604..fa120c7e2 100644 --- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts +++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts @@ -619,7 +619,7 @@ describe('HTMLInputElement', () => { expect(element.form).toBe(form); }); - it('Returns form element by id if the form attribute is set.', () => { + it('Returns form element by id if the form attribute is set when connecting node to DOM.', () => { const form = document.createElement('form'); form.id = 'form'; document.body.appendChild(form); @@ -627,6 +627,17 @@ describe('HTMLInputElement', () => { expect(element.form).toBe(null); document.body.appendChild(element); expect(element.form).toBe(form); + expect(form.elements.includes(element)).toBe(true); + }); + + it('Returns form element by id if the form attribute is set when element is connected to DOM.', () => { + const form = document.createElement('form'); + form.id = 'form'; + document.body.appendChild(form); + document.body.appendChild(element); + element.setAttribute('form', 'form'); + expect(element.form).toBe(form); + expect(form.elements.includes(element)).toBe(true); }); }); diff --git a/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts b/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts index 88a61b1e7..4fcb96efb 100644 --- a/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts +++ b/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts @@ -70,12 +70,28 @@ describe('HTMLLabelElement', () => { }); describe('get form()', () => { + it('Returns null if no parent form element exists.', () => { + expect(element.form).toBe(null); + }); + it('Returns parent form element.', () => { const form = document.createElement('form'); const div = document.createElement('div'); div.appendChild(element); form.appendChild(div); - expect(element.form === form).toBe(true); + expect(element.form).toBe(form); + }); + + it('Returns associated control form element.', () => { + const form = document.createElement('form'); + const input = document.createElement('input'); + form.id = 'form'; + document.body.appendChild(form); + input.id = 'input'; + input.setAttribute('form', 'form'); + element.htmlFor = 'input'; + document.body.appendChild(element); + expect(element.form).toBe(form); }); }); diff --git a/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts b/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts index 6ae44af12..4a84b55f8 100644 --- a/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts +++ b/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts @@ -110,6 +110,10 @@ describe('HTMLTextAreaElement', () => { }); describe('get form()', () => { + it('Returns null if no parent form element exists.', () => { + expect(element.form).toBe(null); + }); + it('Returns parent form element.', () => { const form = document.createElement('form'); const div = document.createElement('div'); @@ -117,6 +121,27 @@ describe('HTMLTextAreaElement', () => { form.appendChild(div); expect(element.form).toBe(form); }); + + it('Returns form element by id if the form attribute is set when connecting node to DOM.', () => { + const form = document.createElement('form'); + form.id = 'form'; + document.body.appendChild(form); + element.setAttribute('form', 'form'); + expect(element.form).toBe(null); + document.body.appendChild(element); + expect(element.form).toBe(form); + expect(form.elements.includes(element)).toBe(true); + }); + + it('Returns form element by id if the form attribute is set when element is connected to DOM.', () => { + const form = document.createElement('form'); + form.id = 'form'; + document.body.appendChild(form); + document.body.appendChild(element); + element.setAttribute('form', 'form'); + expect(element.form).toBe(form); + expect(form.elements.includes(element)).toBe(true); + }); }); for (const property of ['disabled', 'autofocus', 'required', 'readOnly']) { diff --git a/packages/happy-dom/tsconfig.json b/packages/happy-dom/tsconfig.json index 86c146131..02f73ca2b 100644 --- a/packages/happy-dom/tsconfig.json +++ b/packages/happy-dom/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "target": "ES2020", + "target": "ES2022", "declaration": true, "declarationMap": true, "module": "Node16", @@ -21,7 +21,7 @@ "composite": false, "incremental": false, "lib": [ - "es2020" + "ES2022" ], "types": [ "node" diff --git a/packages/jest-environment/test/tsconfig.json b/packages/jest-environment/test/tsconfig.json index 52d8f026c..00d32d3a5 100644 --- a/packages/jest-environment/test/tsconfig.json +++ b/packages/jest-environment/test/tsconfig.json @@ -3,7 +3,7 @@ "outDir": "../tmp", "rootDir": ".", "tsBuildInfoFile": "../tmp/.tsbuildinfo-test", - "target": "es2020", + "target": "ES2022", "declaration": true, "module": "CommonJS", "moduleResolution": "node", @@ -25,7 +25,7 @@ "jest" ], "lib": [ - "es2020", + "ES2022", "dom" ] }, diff --git a/packages/jest-environment/tsconfig.json b/packages/jest-environment/tsconfig.json index 115a566c2..7d417d2eb 100644 --- a/packages/jest-environment/tsconfig.json +++ b/packages/jest-environment/tsconfig.json @@ -3,7 +3,7 @@ "outDir": "lib", "rootDir": "src", "tsBuildInfoFile": "tmp/.tsbuildinfo", - "target": "es2020", + "target": "ES2022", "declaration": true, "declarationMap": true, "module": "CommonJS", @@ -25,7 +25,7 @@ "node" ], "lib": [ - "es2020", + "ES2022", "dom" ] }, From e053650148ae653938399fd33e8f9c2a790d47a4 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 8 May 2024 00:45:25 +0200 Subject: [PATCH 09/51] chore: [#1332] Merge fixes in HTMLIFrameElement --- .../html-iframe-element/HTMLIFrameElement.ts | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 041948e0f..d7213880f 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -53,6 +53,7 @@ export default class HTMLIFrameElement extends HTMLElement { }; #browserFrame: IBrowserFrame; #browserChildFrame: IBrowserFrame; + #loadedSrcdoc: string | null = null; /** * Constructor. @@ -274,9 +275,14 @@ export default class HTMLIFrameElement extends HTMLElement { * @param replacedAttribute Replaced attribute. */ #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + if (attribute[PropertySymbol.name] === 'srcdoc') { + this.#loadPage(); + } + if ( attribute[PropertySymbol.name] === 'src' && attribute[PropertySymbol.value] && + this[PropertySymbol.attributes]['srcdoc']?.value === undefined && attribute[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value] ) { this.#loadPage(); @@ -295,9 +301,16 @@ export default class HTMLIFrameElement extends HTMLElement { /** * Triggered when an attribute is removed. + * + * @param removedAttribute Removed attribute. */ - #onRemoveAttribute(): void { - this.#unloadPage(); + #onRemoveAttribute(removedAttribute: Attr): void { + if ( + removedAttribute[PropertySymbol.name] === 'srcdoc' || + removedAttribute[PropertySymbol.name] === 'src' + ) { + this.#loadPage(); + } } /** @@ -334,15 +347,43 @@ export default class HTMLIFrameElement extends HTMLElement { */ #loadPage(): void { if (!this[PropertySymbol.isConnected]) { - if (this.#browserChildFrame) { - BrowserFrameFactory.destroyFrame(this.#browserChildFrame); - this.#browserChildFrame = null; - } - this.#contentWindowContainer.window = null; + this.#unloadPage(); return; } + const srcdoc = this.getAttribute('srcdoc'); const window = this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + + if (srcdoc !== null) { + if (this.#loadedSrcdoc === srcdoc) { + return; + } + + this.#unloadPage(); + + this.#browserChildFrame = BrowserFrameFactory.createChildFrame(this.#browserFrame); + this.#browserChildFrame.url = 'about:srcdoc'; + + this.#contentWindowContainer.window = this.#browserChildFrame.window; + + (this.#browserChildFrame.window.top) = this.#browserFrame.window.top; + (this.#browserChildFrame.window.parent) = this.#browserFrame.window; + + this.#browserChildFrame.window.document.open(); + this.#browserChildFrame.window.document.write(srcdoc); + + this.#loadedSrcdoc = srcdoc; + + this[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].requestAnimationFrame(() => + this.dispatchEvent(new Event('load')) + ); + return; + } + + if (this.#loadedSrcdoc !== null) { + this.#unloadPage(); + } + const originURL = this.#browserFrame.window.location; const targetURL = BrowserFrameURL.getRelativeURL(this.#browserFrame, this.src); @@ -398,5 +439,6 @@ export default class HTMLIFrameElement extends HTMLElement { this.#browserChildFrame = null; } this.#contentWindowContainer.window = null; + this.#loadedSrcdoc = null; } } From 9474554e06de6acbae774f667dc54d0e1fc23596 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 24 May 2024 14:29:48 +0200 Subject: [PATCH 10/51] chore: [#1332] Continue on implementation --- .../happy-dom/src/nodes/html-form-element/HTMLFormElement.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index c5c1a3c42..739489893 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -479,13 +479,15 @@ export default class HTMLFormElement extends HTMLElement { PropertySymbol.removeEventListener ]('remove', this.#documentChildNodeListeners.remove); + this.#documentChildNodeListeners = null; + const id = this.id; if (!id) { return; } - for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { + for (const node of this[PropertySymbol.elements]) { if ( node[PropertySymbol.attributes]?.['form']?.value === id && !this[PropertySymbol.childNodesFlatten][PropertySymbol.includes](node) From 645edd71b4b0aced55e90860ea8af6d1e8db4c4f Mon Sep 17 00:00:00 2001 From: David Ortner Date: Mon, 27 May 2024 15:03:27 +0200 Subject: [PATCH 11/51] chore: [#1332] Continue on implementation --- .../src/nodes/html-option-element/HTMLOptionElement.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index 5b3dbb8a1..0e875cdd7 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -138,7 +138,7 @@ export default class HTMLOptionElement extends HTMLElement { /** * @override */ - public override [PropertySymbol.connectedToDocument](parentNode: Node = null): void { + public override [PropertySymbol.connectedToDocument](): void { const oldSelectNode = this[PropertySymbol.selectNode]; const oldDataListNode = this[PropertySymbol.dataListNode]; From 155de2ebb058908fa9684161b0a7f96f4754c7d3 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 29 May 2024 20:59:25 +0200 Subject: [PATCH 12/51] chore: [#1332] Continue on implementation --- .../document-fragment/DocumentFragment.ts | 32 +++++++++++++------ .../happy-dom/src/nodes/document/Document.ts | 25 +++++++++++---- .../HTMLTextAreaElement.ts | 3 ++ .../happy-dom/src/nodes/node/INodeList.ts | 7 +--- packages/happy-dom/src/nodes/node/NodeList.ts | 8 ++--- 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index a8d2af70f..fce8325a6 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -3,17 +3,18 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import Element from '../element/Element.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; -import HTMLCollection from '../element/HTMLCollection2.js'; -import NodeList from '../node/NodeList.js'; +import HTMLCollection from '../element/HTMLCollection.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import IHTMLElementTagNameMap from '../../config/IHTMLElementTagNameMap.js'; import ISVGElementTagNameMap from '../../config/ISVGElementTagNameMap.js'; +import INodeList from '../node/INodeList.js'; /** * DocumentFragment. */ export default class DocumentFragment extends Node { - public readonly [PropertySymbol.children]: HTMLCollection = new HTMLCollection(); + public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); public [PropertySymbol.rootNode]: Node = this; public [PropertySymbol.nodeType] = NodeTypeEnum.documentFragmentNode; public cloneNode: (deep?: boolean) => DocumentFragment; @@ -23,15 +24,26 @@ export default class DocumentFragment extends Node { */ constructor() { super(); - this[PropertySymbol.childNodes][PropertySymbol.attachHTMLCollection]( - this[PropertySymbol.children] + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('add', (item: Node) => + this[PropertySymbol.children][PropertySymbol.addItem](item) + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( + 'insert', + (item: Node, referenceItem?: Node) => + this[PropertySymbol.children][PropertySymbol.insertItem]( + item, + referenceItem + ) + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('remove', (item: Node) => + this[PropertySymbol.children][PropertySymbol.removeItem](item) ); } /** * Returns the document fragment children. */ - public get children(): HTMLCollection { + public get children(): IHTMLCollection { return this[PropertySymbol.children]; } @@ -131,7 +143,7 @@ export default class DocumentFragment extends Node { */ public querySelectorAll( selector: K - ): NodeList; + ): INodeList; /** * Query CSS selector to find matching elments. @@ -141,7 +153,7 @@ export default class DocumentFragment extends Node { */ public querySelectorAll( selector: K - ): NodeList; + ): INodeList; /** * Query CSS selector to find matching elments. @@ -149,7 +161,7 @@ export default class DocumentFragment extends Node { * @param selector CSS selector. * @returns Matching elements. */ - public querySelectorAll(selector: string): NodeList; + public querySelectorAll(selector: string): INodeList; /** * Query CSS selector to find matching elments. @@ -157,7 +169,7 @@ export default class DocumentFragment extends Node { * @param selector CSS selector. * @returns Matching elements. */ - public querySelectorAll(selector: string): NodeList { + public querySelectorAll(selector: string): INodeList { return QuerySelector.querySelectorAll(this, selector); } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index a9405cd5a..4077a202b 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -20,7 +20,9 @@ import HTMLElement from '../html-element/HTMLElement.js'; import Comment from '../comment/Comment.js'; import Text from '../text/Text.js'; import NodeList from '../node/NodeList.js'; -import HTMLCollection from '../element/HTMLCollection2.js'; +import INodeList from '../node/INodeList.js'; +import HTMLCollection from '../element/HTMLCollection.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; import HTMLLinkElement from '../html-link-element/HTMLLinkElement.js'; import HTMLStyleElement from '../html-style-element/HTMLStyleElement.js'; import DocumentReadyStateEnum from './DocumentReadyStateEnum.js'; @@ -51,7 +53,7 @@ const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; */ export default class Document extends Node { // Internal properties - public [PropertySymbol.children]: HTMLCollection = new HTMLCollection(); + public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); public [PropertySymbol.activeElement]: HTMLElement | SVGElement = null; public [PropertySymbol.nextActiveElement]: HTMLElement | SVGElement = null; public [PropertySymbol.currentScript]: HTMLScriptElement = null; @@ -196,8 +198,19 @@ export default class Document extends Node { super(); this.#browserFrame = injected.browserFrame; this[PropertySymbol.ownerWindow] = injected.window; - this[PropertySymbol.childNodes][PropertySymbol.attachHTMLCollection]( - this[PropertySymbol.children] + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('add', (item: Node) => + this[PropertySymbol.children][PropertySymbol.addItem](item) + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( + 'insert', + (item: Node, referenceItem?: Node) => + this[PropertySymbol.children][PropertySymbol.insertItem]( + item, + referenceItem + ) + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('remove', (item: Node) => + this[PropertySymbol.children][PropertySymbol.removeItem](item) ); } @@ -258,7 +271,7 @@ export default class Document extends Node { /** * Returns document children. */ - public get children(): HTMLCollection { + public get children(): IHTMLCollection { return this[PropertySymbol.children]; } @@ -313,7 +326,7 @@ export default class Document extends Node { /** * Returns a collection of all area elements and a elements in a document with a value for the href attribute. */ - public get links(): NodeList { + public get links(): INodeList { return >this.querySelectorAll('a[href],area[href]'); } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index bfd481afa..9b4cfca40 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -48,6 +48,7 @@ export default class HTMLTextAreaElement extends HTMLElement { super(); this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { if (item instanceof Text) { + item[PropertySymbol.textAreaNode] = this; this[PropertySymbol.resetSelection](); } }); @@ -55,6 +56,7 @@ export default class HTMLTextAreaElement extends HTMLElement { 'insert', (newItem: Node) => { if (newItem instanceof Text) { + item[PropertySymbol.textAreaNode] = this; this[PropertySymbol.resetSelection](); } } @@ -63,6 +65,7 @@ export default class HTMLTextAreaElement extends HTMLElement { 'remove', (item: Node) => { if (item instanceof Text) { + item[PropertySymbol.textAreaNode] = null; this[PropertySymbol.resetSelection](); } } diff --git a/packages/happy-dom/src/nodes/node/INodeList.ts b/packages/happy-dom/src/nodes/node/INodeList.ts index 6710a7d02..e947341ee 100644 --- a/packages/happy-dom/src/nodes/node/INodeList.ts +++ b/packages/happy-dom/src/nodes/node/INodeList.ts @@ -1,6 +1,3 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable filenames/match-exported */ - import * as PropertySymbol from '../../PropertySymbol.js'; import TNodeListListener from './TNodeListListener.js'; @@ -11,7 +8,7 @@ import TNodeListListener from './TNodeListListener.js'; * * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList */ -interface NodeList { +export default interface INodeList { readonly [index: number]: T; /** @@ -152,5 +149,3 @@ interface NodeList { */ entries(): IterableIterator<[number, T]>; } - -export default NodeList; diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts index 9685fc26b..58a45376e 100644 --- a/packages/happy-dom/src/nodes/node/NodeList.ts +++ b/packages/happy-dom/src/nodes/node/NodeList.ts @@ -1,15 +1,12 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import INodeList from './INodeList.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import INodeList from './INodeList.js'; import TNodeListListener from './TNodeListListener.js'; /** * NodeList. * - * We are extending Array here to improve performance. - * However, we should not expose Array methods to the outside. - * * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList */ class NodeList extends Array implements INodeList { @@ -226,5 +223,4 @@ for (const key of Object.keys(descriptors)) { } } -// Forces the type to be an interface to hide Array methods from the outside. -export default () => INodeList>(NodeList); +export default NodeList; From 76cb84d4d5f186430db08d1fd83fc2954e2e11dc Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 12 Jun 2024 01:42:36 +0200 Subject: [PATCH 13/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 3 +- .../happy-dom/src/dom-parser/DOMParser.ts | 18 +- packages/happy-dom/src/nodes/attr/Attr.ts | 2 +- .../src/nodes/character-data/CharacterData.ts | 2 +- .../src/nodes/child-node/ChildNodeUtility.ts | 6 +- .../happy-dom/src/nodes/comment/Comment.ts | 2 +- .../document-fragment/DocumentFragment.ts | 12 +- .../src/nodes/document-type/DocumentType.ts | 2 +- .../happy-dom/src/nodes/document/Document.ts | 43 ++- .../happy-dom/src/nodes/element/Element.ts | 144 +++++--- .../src/nodes/element/HTMLCollection.ts | 154 ++++---- .../src/nodes/element/IHTMLCollection.ts | 8 - .../html-base-element/HTMLBaseElement.ts | 2 +- .../html-button-element/HTMLButtonElement.ts | 12 - .../HTMLDataListElement.ts | 39 +- .../src/nodes/html-element/HTMLElement.ts | 111 ++++-- .../HTMLFieldSetElement.ts | 82 ++--- .../HTMLFormControlsCollection.ts | 35 +- .../html-form-element/HTMLFormElement.ts | 12 +- .../html-iframe-element/HTMLIFrameElement.ts | 2 +- .../html-image-element/HTMLImageElement.ts | 2 +- .../html-input-element/HTMLInputElement.ts | 13 +- .../html-label-element/HTMLLabelElement.ts | 2 +- .../html-link-element/HTMLLinkElement.ts | 1 - .../html-media-element/HTMLMediaElement.ts | 2 +- .../html-option-element/HTMLOptionElement.ts | 52 +-- .../html-script-element/HTMLScriptElement.ts | 3 +- .../HTMLOptionsCollection.ts | 2 +- .../html-select-element/HTMLSelectElement.ts | 107 +++--- .../html-slot-element/HTMLSlotElement.ts | 150 ++++++-- .../HTMLTemplateElement.ts | 7 +- .../HTMLTextAreaElement.ts | 18 +- .../HTMLTextAreaPropertyAttributes.json | 16 - packages/happy-dom/src/nodes/node/Node.ts | 127 ++++--- packages/happy-dom/src/nodes/node/NodeList.ts | 11 + .../src/nodes/parent-node/IParentNode.ts | 28 +- .../nodes/parent-node/ParentNodeUtility.ts | 122 ++++--- .../src/nodes/shadow-root/ShadowRoot.ts | 8 +- .../src/nodes/svg-element/SVGSVGElement.ts | 2 +- packages/happy-dom/src/nodes/text/Text.ts | 2 +- .../src/query-selector/QuerySelector.ts | 16 +- .../src/query-selector/SelectorItem.ts | 63 +++- packages/happy-dom/src/range/Range.ts | 40 +- packages/happy-dom/src/range/RangeUtility.ts | 5 +- .../happy-dom/src/tree-walker/TreeWalker.ts | 4 +- .../happy-dom/src/window/BrowserWindow.ts | 158 ++++---- packages/happy-dom/test/CustomElement.ts | 2 +- .../HTMLFieldSetElement.test.ts | 63 ++++ .../HTMLIFrameElement.test.ts | 3 +- .../CustomElementWithNamedSlots.ts | 1 + .../html-slot-element/HTMLSlotElement.test.ts | 345 +++++++++++++++--- .../happy-dom/test/nodes/node/Node.test.ts | 20 +- .../parent-node/ParentNodeUtility.test.ts | 30 +- 53 files changed, 1334 insertions(+), 782 deletions(-) delete mode 100644 packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaPropertyAttributes.json diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 632425278..550cba705 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -65,7 +65,7 @@ export const target = Symbol('target'); export const textAreaNode = Symbol('textAreaNode'); export const unobserve = Symbol('unobserve'); export const updateIndices = Symbol('updateIndices'); -export const updateOptionItems = Symbol('updateOptionItems'); +export const updateSelectedness = Symbol('updateSelectedness'); export const url = Symbol('url'); export const value = Symbol('value'); export const width = Symbol('width'); @@ -193,3 +193,4 @@ export const setNamedItemProperty = Symbol('setNamedItemProperty'); export const selectedOptions = Symbol('selectedOptions'); export const styleNode = Symbol('styleNode'); export const updateSheet = Symbol('updateSheet'); +export const slice = Symbol('slice'); diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index 85d80c2ea..c3036d3d4 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -38,15 +38,15 @@ export default class DOMParser { const newDocument = this.#createDocument(mimeType); - while (newDocument[PropertySymbol.childNodes][PropertySymbol.items].length) { - newDocument.removeChild(newDocument[PropertySymbol.childNodes][PropertySymbol.items][0]); + while (newDocument[PropertySymbol.childNodes].length) { + newDocument.removeChild(newDocument[PropertySymbol.childNodes][0]); } const root = XMLParser.parse(newDocument, string, { evaluateScripts: true }); let documentElement = null; let documentTypeNode = null; - for (const node of root[PropertySymbol.childNodes][PropertySymbol.items]) { + for (const node of root[PropertySymbol.childNodes]) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { @@ -65,16 +65,16 @@ export default class DOMParser { newDocument.appendChild(documentElement); const body = newDocument.body; if (body) { - while (root[PropertySymbol.childNodes][PropertySymbol.items].length) { - body.appendChild(root[PropertySymbol.childNodes][PropertySymbol.items][0]); + while (root[PropertySymbol.childNodes].length) { + body.appendChild(root[PropertySymbol.childNodes][0]); } } } else { switch (mimeType) { case 'image/svg+xml': { - while (root[PropertySymbol.childNodes][PropertySymbol.items].length) { - newDocument.appendChild(root[PropertySymbol.childNodes][PropertySymbol.items][0]); + while (root[PropertySymbol.childNodes].length) { + newDocument.appendChild(root[PropertySymbol.childNodes][0]); } } break; @@ -89,8 +89,8 @@ export default class DOMParser { documentElement.appendChild(bodyElement); newDocument.appendChild(documentElement); - while (root[PropertySymbol.childNodes][PropertySymbol.items].length) { - bodyElement.appendChild(root[PropertySymbol.childNodes][PropertySymbol.items][0]); + while (root[PropertySymbol.childNodes].length) { + bodyElement.appendChild(root[PropertySymbol.childNodes][0]); } } break; diff --git a/packages/happy-dom/src/nodes/attr/Attr.ts b/packages/happy-dom/src/nodes/attr/Attr.ts index f8de79962..c7a5b2a90 100644 --- a/packages/happy-dom/src/nodes/attr/Attr.ts +++ b/packages/happy-dom/src/nodes/attr/Attr.ts @@ -10,7 +10,7 @@ import NodeTypeEnum from '../node/NodeTypeEnum.js'; */ export default class Attr extends Node implements Attr { // Public properties - public cloneNode: (deep?: boolean) => Attr; + public declare cloneNode: (deep?: boolean) => Attr; public [PropertySymbol.nodeType] = NodeTypeEnum.attributeNode; public [PropertySymbol.namespaceURI]: string | null = null; diff --git a/packages/happy-dom/src/nodes/character-data/CharacterData.ts b/packages/happy-dom/src/nodes/character-data/CharacterData.ts index 215fab7f0..4447c87e9 100644 --- a/packages/happy-dom/src/nodes/character-data/CharacterData.ts +++ b/packages/happy-dom/src/nodes/character-data/CharacterData.ts @@ -20,7 +20,7 @@ export default abstract class CharacterData implements IChildNode, INonDocumentTypeChildNode { public [PropertySymbol.data] = ''; - public cloneNode: (deep?: boolean) => CharacterData; + public declare cloneNode: (deep?: boolean) => CharacterData; /** * Constructor. diff --git a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts index 11d7da6d5..81efd2989 100644 --- a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts +++ b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts @@ -39,7 +39,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes][PropertySymbol.items]; + ))[PropertySymbol.childNodes]; while (newChildNodes.length) { parent.insertBefore(newChildNodes[0], childNode); } @@ -68,7 +68,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes][PropertySymbol.items]; + ))[PropertySymbol.childNodes]; while (newChildNodes.length) { parent.insertBefore(newChildNodes[0], childNode); } @@ -97,7 +97,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes][PropertySymbol.items]; + ))[PropertySymbol.childNodes]; while (newChildNodes.length) { if (!nextSibling) { parent.appendChild(newChildNodes[0]); diff --git a/packages/happy-dom/src/nodes/comment/Comment.ts b/packages/happy-dom/src/nodes/comment/Comment.ts index 7128b4a90..87a4f8bc1 100644 --- a/packages/happy-dom/src/nodes/comment/Comment.ts +++ b/packages/happy-dom/src/nodes/comment/Comment.ts @@ -7,7 +7,7 @@ import NodeTypeEnum from '../node/NodeTypeEnum.js'; */ export default class Comment extends CharacterData { public [PropertySymbol.nodeType] = NodeTypeEnum.commentNode; - public cloneNode: (deep?: boolean) => Comment; + public declare cloneNode: (deep?: boolean) => Comment; /** * Node name. diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index fce8325a6..74f3bd70e 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -17,7 +17,7 @@ export default class DocumentFragment extends Node { public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); public [PropertySymbol.rootNode]: Node = this; public [PropertySymbol.nodeType] = NodeTypeEnum.documentFragmentNode; - public cloneNode: (deep?: boolean) => DocumentFragment; + public declare cloneNode: (deep?: boolean) => DocumentFragment; /** * Constructor. @@ -53,7 +53,7 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get childElementCount(): number { - return this[PropertySymbol.children][PropertySymbol.items].length; + return this[PropertySymbol.children].length; } /** @@ -62,7 +62,7 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get firstElementChild(): Element { - return this[PropertySymbol.children][PropertySymbol.items][0] ?? null; + return this[PropertySymbol.children][0] ?? null; } /** @@ -71,7 +71,7 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get lastElementChild(): Element { - const children = this[PropertySymbol.children][PropertySymbol.items]; + const children = this[PropertySymbol.children]; return children[children.length - 1] ?? null; } @@ -82,7 +82,7 @@ export default class DocumentFragment extends Node { */ public get textContent(): string { let result = ''; - for (const childNode of this[PropertySymbol.childNodes][PropertySymbol.items]) { + for (const childNode of this[PropertySymbol.childNodes]) { if ( childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode || childNode[PropertySymbol.nodeType] === NodeTypeEnum.textNode @@ -99,7 +99,7 @@ export default class DocumentFragment extends Node { * @param textContent Text content. */ public set textContent(textContent: string) { - const childNodes = this[PropertySymbol.childNodes][PropertySymbol.items]; + const childNodes = this[PropertySymbol.childNodes]; while (childNodes.length) { this.removeChild(childNodes[0]); } diff --git a/packages/happy-dom/src/nodes/document-type/DocumentType.ts b/packages/happy-dom/src/nodes/document-type/DocumentType.ts index fa4c0bfaf..8b7903159 100644 --- a/packages/happy-dom/src/nodes/document-type/DocumentType.ts +++ b/packages/happy-dom/src/nodes/document-type/DocumentType.ts @@ -10,7 +10,7 @@ export default class DocumentType extends Node { public [PropertySymbol.name] = ''; public [PropertySymbol.publicId] = ''; public [PropertySymbol.systemId] = ''; - public cloneNode: (deep?: boolean) => DocumentType; + public declare cloneNode: (deep?: boolean) => DocumentType; /** * Returns name. diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 4077a202b..b6a40fdb1 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -70,7 +70,7 @@ export default class Document extends Node { public [PropertySymbol.referrer] = ''; public [PropertySymbol.defaultView]: BrowserWindow | null = null; public [PropertySymbol.ownerWindow]: BrowserWindow; - public cloneNode: (deep?: boolean) => Document; + public declare cloneNode: (deep?: boolean) => Document; // Private properties #selection: Selection = null; @@ -343,7 +343,7 @@ export default class Document extends Node { * @returns Element. */ public get childElementCount(): number { - return this[PropertySymbol.children][PropertySymbol.items].length; + return this[PropertySymbol.children].length; } /** @@ -352,7 +352,7 @@ export default class Document extends Node { * @returns Element. */ public get firstElementChild(): Element { - return this[PropertySymbol.children][PropertySymbol.items][0] ?? null; + return this[PropertySymbol.children][0] ?? null; } /** @@ -361,7 +361,7 @@ export default class Document extends Node { * @returns Element. */ public get lastElementChild(): Element { - const children = this[PropertySymbol.children][PropertySymbol.items]; + const children = this[PropertySymbol.children]; return children[children.length - 1] ?? null; } @@ -417,7 +417,7 @@ export default class Document extends Node { * @returns Document type. */ public get doctype(): DocumentType { - for (const node of this[PropertySymbol.childNodes][PropertySymbol.items]) { + for (const node of this[PropertySymbol.childNodes]) { if (node instanceof DocumentType) { return node; } @@ -709,7 +709,7 @@ export default class Document extends Node { * @param className Tag name. * @returns Matching element. */ - public getElementsByClassName(className: string): HTMLCollection { + public getElementsByClassName(className: string): IHTMLCollection { return ParentNodeUtility.getElementsByClassName(this, className); } @@ -739,7 +739,7 @@ export default class Document extends Node { * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagName(tagName: string): HTMLCollection; + public getElementsByTagName(tagName: string): IHTMLCollection; /** * Returns an elements by tag name. @@ -747,7 +747,7 @@ export default class Document extends Node { * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagName(tagName: string): HTMLCollection { + public getElementsByTagName(tagName: string): IHTMLCollection { return ParentNodeUtility.getElementsByTagName(this, tagName); } @@ -761,7 +761,7 @@ export default class Document extends Node { public getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/1999/xhtml', tagName: K - ): HTMLCollection; + ): IHTMLCollection; /** * Returns an elements by tag name and namespace. @@ -773,7 +773,7 @@ export default class Document extends Node { public getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/2000/svg', tagName: K - ): HTMLCollection; + ): IHTMLCollection; /** * Returns an elements by tag name and namespace. @@ -782,7 +782,7 @@ export default class Document extends Node { * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection; + public getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection; /** * Returns an elements by tag name and namespace. @@ -791,7 +791,7 @@ export default class Document extends Node { * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection { + public getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection { return ParentNodeUtility.getElementsByTagNameNS(this, namespaceURI, tagName); } @@ -817,9 +817,7 @@ export default class Document extends Node { name: string ): NodeList => { const matches = new NodeList(); - for (const child of (parentNode)[PropertySymbol.children][ - PropertySymbol.items - ]) { + for (const child of (parentNode)[PropertySymbol.children]) { if (child.getAttributeNS(null, 'name') === name) { matches[PropertySymbol.addItem](child); } @@ -853,7 +851,7 @@ export default class Document extends Node { let documentElement = null; let documentTypeNode = null; - for (const node of root[PropertySymbol.childNodes][PropertySymbol.items]) { + for (const node of root[PropertySymbol.childNodes]) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { @@ -888,7 +886,7 @@ export default class Document extends Node { const rootBody = ParentNodeUtility.getElementByTagName(root, 'body'); const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (rootBody && body) { - const childNodes = rootBody[PropertySymbol.childNodes][PropertySymbol.items]; + const childNodes = rootBody[PropertySymbol.childNodes]; while (childNodes.length) { body.appendChild(childNodes[0]); } @@ -898,7 +896,7 @@ export default class Document extends Node { // Remaining nodes outside the element are added to the element. const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (body) { - const childNodes = root[PropertySymbol.childNodes][PropertySymbol.items]; + const childNodes = root[PropertySymbol.childNodes]; while (childNodes.length) { const child = childNodes[0]; if ( @@ -913,7 +911,7 @@ export default class Document extends Node { const documentElement = this.createElement('html'); const bodyElement = this.createElement('body'); const headElement = this.createElement('head'); - const childNodes = root[PropertySymbol.childNodes][PropertySymbol.items]; + const childNodes = root[PropertySymbol.childNodes]; while (childNodes.length) { bodyElement.appendChild(childNodes[0]); @@ -953,8 +951,9 @@ export default class Document extends Node { } } - for (const child of this[PropertySymbol.childNodes][PropertySymbol.items]) { - this.removeChild(child); + const childNodes = this[PropertySymbol.childNodes]; + while (childNodes.length) { + this.removeChild(childNodes[0]); } return this; @@ -1352,7 +1351,7 @@ export default class Document extends Node { #importNode(node: Node): void { node[PropertySymbol.ownerDocument] = this; - for (const child of node[PropertySymbol.childNodes][PropertySymbol.items]) { + for (const child of node[PropertySymbol.childNodes]) { this.#importNode(child); } } diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 21e767893..8f55496fc 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -51,7 +51,7 @@ export default class Element public static [PropertySymbol.localName]: string | null = null; public static [PropertySymbol.namespaceURI]: string | null = null; public static observedAttributes: string[]; - public cloneNode: (deep?: boolean) => Element; + public declare cloneNode: (deep?: boolean) => Element; // Events public oncancel: (event: Event) => void | null = null; @@ -113,28 +113,28 @@ export default class Element */ constructor() { super(); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('add', (item: Node) => - this[PropertySymbol.children][PropertySymbol.addItem](item) - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( - 'insert', - (item: Node, referenceItem?: Node) => - this[PropertySymbol.children][PropertySymbol.insertItem]( - item, - referenceItem - ) - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('remove', (item: Node) => - this[PropertySymbol.children][PropertySymbol.removeItem](item) - ); + const attributes = this[PropertySymbol.attributes]; + attributes[PropertySymbol.addEventListener]('set', this.#onSetAttribute.bind(this)); + attributes[PropertySymbol.addEventListener]('remove', this.#onRemoveAttribute.bind(this)); + + // Use variable here instead of referencing the property to work with HTMLElement[PropertySymbol.connectedToDocument] (when it defines custom element) + // Otherwise it will be connected to the new children collection set on the children property + const children = this[PropertySymbol.children]; + const childNodes = this[PropertySymbol.childNodes]; + + childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { + children[PropertySymbol.addItem](item); + this.#onNodeListChange(item); + }); + + childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { + children[PropertySymbol.insertItem](item, referenceItem); + this.#onNodeListChange(item); + }); + childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { + children[PropertySymbol.removeItem](item); + this.#onNodeListChange(item); + }); } /** @@ -353,7 +353,7 @@ export default class Element */ public get textContent(): string { let result = ''; - for (const childNode of this[PropertySymbol.childNodes][PropertySymbol.items]) { + for (const childNode of this[PropertySymbol.childNodes]) { if ( childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode || childNode[PropertySymbol.nodeType] === NodeTypeEnum.textNode @@ -370,7 +370,7 @@ export default class Element * @param textContent Text content. */ public set textContent(textContent: string) { - const childNodes = this[PropertySymbol.childNodes][PropertySymbol.items]; + const childNodes = this[PropertySymbol.childNodes]; while (childNodes.length) { this.removeChild(childNodes[0]); } @@ -394,7 +394,7 @@ export default class Element * @param html HTML. */ public set innerHTML(html: string) { - const childNodes = this[PropertySymbol.childNodes][PropertySymbol.items]; + const childNodes = this[PropertySymbol.childNodes]; while (childNodes.length) { this.removeChild(childNodes[0]); @@ -427,7 +427,7 @@ export default class Element * @returns Element. */ public get childElementCount(): number { - return this[PropertySymbol.children][PropertySymbol.items].length; + return this[PropertySymbol.children].length; } /** @@ -436,7 +436,7 @@ export default class Element * @returns Element. */ public get firstElementChild(): Element { - return this[PropertySymbol.children][PropertySymbol.items][0] ?? null; + return this[PropertySymbol.children][0] ?? null; } /** @@ -445,7 +445,7 @@ export default class Element * @returns Element. */ public get lastElementChild(): Element { - const children = this[PropertySymbol.children][PropertySymbol.items]; + const children = this[PropertySymbol.children]; return children[children.length - 1] ?? null; } @@ -493,7 +493,7 @@ export default class Element escapeEntities: false }); let xml = ''; - for (const node of this[PropertySymbol.childNodes][PropertySymbol.items]) { + for (const node of this[PropertySymbol.childNodes]) { xml += xmlSerializer.serializeToString(node); } return xml; @@ -611,7 +611,7 @@ export default class Element public insertAdjacentHTML(position: InsertAdjacentPosition, text: string): void { const childNodes = (( XMLParser.parse(this[PropertySymbol.ownerDocument], text) - ))[PropertySymbol.childNodes][PropertySymbol.items]; + ))[PropertySymbol.childNodes]; while (childNodes.length) { this.insertAdjacentElement(position, childNodes[0]); } @@ -1215,20 +1215,24 @@ export default class Element this[PropertySymbol.classList][PropertySymbol.updateIndices](); } - if (attribute[PropertySymbol.name] === 'id' || attribute[PropertySymbol.name] === 'name') { - const parent = this[PropertySymbol.parentNode]; - while (parent) { - this[PropertySymbol.parentNode][PropertySymbol.childNodes][ - PropertySymbol.htmlCollections - ].updateNamedItem(this, attribute, replacedAttribute); - let parent = this[PropertySymbol.parentNode]; - while (parent) { - parent[PropertySymbol.childNodesFlatten][PropertySymbol.htmlCollections].updateNamedItem( - this, - attribute, - replacedAttribute - ); - parent = parent[PropertySymbol.parentNode]; + if ( + attribute[PropertySymbol.name] === 'slot' && + this[PropertySymbol.parentNode] && + this[PropertySymbol.parentNode][PropertySymbol.shadowRoot] + ) { + const shadowRoot = this[PropertySymbol.parentNode][PropertySymbol.shadowRoot]; + if (oldValue && oldValue !== attribute[PropertySymbol.value]) { + const slot = shadowRoot.querySelector( + `slot[name="${replacedAttribute[PropertySymbol.value]}"]` + ); + if (slot) { + slot.dispatchEvent(new Event('slotchange')); + } + } + if (attribute[PropertySymbol.value]) { + const slot = shadowRoot.querySelector(`slot[name="${attribute[PropertySymbol.value]}"]`); + if (slot) { + slot.dispatchEvent(new Event('slotchange')); } } } @@ -1283,21 +1287,14 @@ export default class Element } if ( + removedAttribute[PropertySymbol.name] === 'slot' && this[PropertySymbol.parentNode] && - (removedAttribute[PropertySymbol.name] === 'id' || - removedAttribute[PropertySymbol.name] === 'name') + this[PropertySymbol.parentNode][PropertySymbol.shadowRoot] ) { - this[PropertySymbol.parentNode][PropertySymbol.childNodes][ - PropertySymbol.htmlCollections - ].updateNamedItem(this, null, removedAttribute); - let parent = this[PropertySymbol.parentNode]; - while (parent) { - parent[PropertySymbol.childNodesFlatten][PropertySymbol.htmlCollections].updateNamedItem( - this, - null, - removedAttribute - ); - parent = parent[PropertySymbol.parentNode]; + const shadowRoot = this[PropertySymbol.parentNode][PropertySymbol.shadowRoot]; + const slot = shadowRoot.querySelector(`slot[name="${this.getAttribute('slot')}"]`); + if (slot) { + slot.dispatchEvent(new Event('slotchange')); } } @@ -1323,4 +1320,35 @@ export default class Element } } } + + /** + * Triggered when child nodes are changed. + * + * @param node Changed node. + */ + #onNodeListChange(node: Node): void { + if (this[PropertySymbol.shadowRoot]) { + if (node['slot']) { + const slot = this[PropertySymbol.shadowRoot].querySelector(`slot[name="${node['slot']}"]`); + if (slot) { + slot.dispatchEvent(new Event('slotchange')); + } + } else if (node[PropertySymbol.nodeType] !== NodeTypeEnum.commentNode) { + const slot = this[PropertySymbol.shadowRoot].querySelector('slot:not([slot])'); + if (slot) { + slot.dispatchEvent(new Event('slotchange')); + } + } + } else if ( + this[PropertySymbol.parentNode] && + this[PropertySymbol.parentNode][PropertySymbol.shadowRoot] && + this.getAttribute('slot') + ) { + const shadowRoot = this[PropertySymbol.parentNode][PropertySymbol.shadowRoot]; + const slot = shadowRoot.querySelector(`slot[name="${this.getAttribute('slot')}"]`); + if (slot) { + slot.dispatchEvent(new Event('slotchange')); + } + } + } } diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index 97ec54df9..efece61ed 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -108,7 +108,9 @@ class HTMLCollection extends Array implements IHTMLCollecti * @returns True if the item was added. */ public [PropertySymbol.addItem](item: T): boolean { - if (item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || !this.#filter?.(item)) { + const filter = this.#filter; + + if (item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || (filter && !filter?.(item))) { return false; } @@ -118,7 +120,7 @@ class HTMLCollection extends Array implements IHTMLCollecti super.push(item); - this[PropertySymbol.addNamedItem](item); + this.#addNamedItem(item); this[PropertySymbol.dispatchEvent]('indexChange', { index: this.length - 1, item }); return true; @@ -138,7 +140,10 @@ class HTMLCollection extends Array implements IHTMLCollecti const filter = this.#filter; - if (newItem[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || !filter?.(newItem)) { + if ( + newItem[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || + (filter && !filter?.(newItem)) + ) { return false; } @@ -150,12 +155,12 @@ class HTMLCollection extends Array implements IHTMLCollecti return this[PropertySymbol.addItem](newItem); } - const parentChildNodes = (referenceItem).parentNode?.[PropertySymbol.childNodes]; let referenceItemIndex: number = -1; if (referenceItem[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - referenceItemIndex = parentChildNodes[PropertySymbol.indexOf](referenceItem); + referenceItemIndex = this[PropertySymbol.indexOf](referenceItem); } else { + const parentChildNodes = (referenceItem).parentNode?.[PropertySymbol.childNodes]; for ( let i = parentChildNodes[PropertySymbol.indexOf](referenceItem), max = parentChildNodes.length; @@ -166,19 +171,21 @@ class HTMLCollection extends Array implements IHTMLCollecti parentChildNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && (!filter || filter(parentChildNodes[i])) ) { - referenceItemIndex = i; + referenceItemIndex = this[PropertySymbol.indexOf](parentChildNodes[i]); break; } } } if (referenceItemIndex === -1) { - return this[PropertySymbol.addItem](newItem); + throw new Error( + 'Failed to execute "insertItem" on "HTMLCollection": The node before which the new node is to be inserted is not an item of this collection.' + ); } super.splice(referenceItemIndex, 0, newItem); - this[PropertySymbol.addNamedItem](newItem); + this.#addNamedItem(newItem); this[PropertySymbol.dispatchEvent]('indexChange', { index: referenceItemIndex, item: newItem }); return true; @@ -199,7 +206,8 @@ class HTMLCollection extends Array implements IHTMLCollecti super.splice(index, 1); - this[PropertySymbol.removeNamedItem](item); + this.#removeNamedItem(item); + this[PropertySymbol.dispatchEvent]('indexChange', { index: 0, item: null }); return true; } @@ -288,14 +296,71 @@ class HTMLCollection extends Array implements IHTMLCollecti } } + /** + * Returns named items. + * + * @param name Name. + * @returns Named items. + */ + protected [PropertySymbol.getNamedItems](name: string): T[] { + return this[PropertySymbol.namedItems].get(name) || []; + } + + /** + * Sets named item property. + * + * @param name Name. + */ + protected [PropertySymbol.setNamedItemProperty](name: string): void { + if (!this[PropertySymbol.isValidPropertyName](name)) { + return; + } + + const namedItems = this[PropertySymbol.namedItems].get(name); + + if (namedItems?.length) { + if (Object.getOwnPropertyDescriptor(this, name)?.value !== namedItems[0]) { + Object.defineProperty(this, name, { + value: namedItems[0], + writable: false, + enumerable: true, + configurable: true + }); + } + } else { + delete this[name]; + } + + this[PropertySymbol.dispatchEvent]('propertyChange', { + propertyName: name, + propertyValue: this[name] ?? null + }); + } + + /** + * Returns "true" if the property name is valid. + * + * @param name Name. + * @returns True if the property name is valid. + */ + protected [PropertySymbol.isValidPropertyName](name: string): boolean { + return ( + !!name && + !this.constructor.prototype.hasOwnProperty(name) && + (isNaN(Number(name)) || name.includes('.')) + ); + } + /** * Updates named item. * * @param item Item. * @param attributeName Attribute name. */ - public [PropertySymbol.updateNamedItem](item: T, attributeName: string): void { - if (!this.#filter(item)) { + #updateNamedItem(item: T, attributeName: string): void { + const filter = this.#filter; + + if (item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || (filter && !filter?.(item))) { return; } @@ -325,16 +390,16 @@ class HTMLCollection extends Array implements IHTMLCollecti * * @param item Item. */ - protected [PropertySymbol.addNamedItem](item: T): void { + #addNamedItem(item: T): void { const listeners = { set: (attribute: Attr) => { if (NAMED_ITEM_ATTRIBUTES.includes(attribute.name)) { - this[PropertySymbol.updateNamedItem](item, attribute.name); + this.#updateNamedItem(item, attribute.name); } }, remove: (attribute: Attr) => { if (NAMED_ITEM_ATTRIBUTES.includes(attribute.name)) { - this[PropertySymbol.updateNamedItem](item, attribute.name); + this.#updateNamedItem(item, attribute.name); } } }; @@ -363,7 +428,7 @@ class HTMLCollection extends Array implements IHTMLCollecti * * @param item Item. */ - protected [PropertySymbol.removeNamedItem](item: T): void { + #removeNamedItem(item: T): void { const listeners = this.#namedNodeMapListeners.get(item); if (listeners) { @@ -391,61 +456,6 @@ class HTMLCollection extends Array implements IHTMLCollecti } } } - - /** - * Returns named items. - * - * @param name Name. - * @returns Named items. - */ - protected [PropertySymbol.getNamedItems](name: string): T[] { - return this[PropertySymbol.namedItems].get(name) || []; - } - - /** - * Sets named item property. - * - * @param name Name. - */ - protected [PropertySymbol.setNamedItemProperty](name: string): void { - if (!this[PropertySymbol.isValidPropertyName](name)) { - return; - } - - const namedItems = this[PropertySymbol.namedItems].get(name); - - if (namedItems?.length) { - if (Object.getOwnPropertyDescriptor(this, name)?.value !== namedItems[0]) { - Object.defineProperty(this, name, { - value: namedItems[0], - writable: false, - enumerable: true, - configurable: true - }); - } - } else { - delete this[name]; - } - - this[PropertySymbol.dispatchEvent]('propertyChange', { - propertyName: name, - propertyValue: this[name] ?? null - }); - } - - /** - * Returns "true" if the property name is valid. - * - * @param name Name. - * @returns True if the property name is valid. - */ - protected [PropertySymbol.isValidPropertyName](name: string): boolean { - return ( - !!name && - !this.constructor.prototype.hasOwnProperty(name) && - (isNaN(Number(name)) || name.includes('.')) - ); - } } // Removes Array methods from HTMLCollection. @@ -465,6 +475,4 @@ for (const key of Object.keys(descriptors)) { } // Forces the type to be an interface to hide Array methods from the outside. -export default < - new (filter?: (item: T) => boolean) => IHTMLCollection ->(HTMLCollection); +export default HTMLCollection; diff --git a/packages/happy-dom/src/nodes/element/IHTMLCollection.ts b/packages/happy-dom/src/nodes/element/IHTMLCollection.ts index fdc7bfe68..49da2a930 100644 --- a/packages/happy-dom/src/nodes/element/IHTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/IHTMLCollection.ts @@ -104,14 +104,6 @@ export default interface IHTMLCollection { } ): void; - /** - * Updates named item. - * - * @param item Item. - * @param attributeName Attribute name. - */ - [PropertySymbol.updateNamedItem](item: T, attributeName: string): void; - /** * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. * diff --git a/packages/happy-dom/src/nodes/html-base-element/HTMLBaseElement.ts b/packages/happy-dom/src/nodes/html-base-element/HTMLBaseElement.ts index ee88f6146..d532d6035 100644 --- a/packages/happy-dom/src/nodes/html-base-element/HTMLBaseElement.ts +++ b/packages/happy-dom/src/nodes/html-base-element/HTMLBaseElement.ts @@ -8,7 +8,7 @@ import * as PropertySymbol from '../../PropertySymbol.js'; * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base. */ export default class HTMLBaseElement extends HTMLElement { - public cloneNode: (deep?: boolean) => HTMLBaseElement; + public declare cloneNode: (deep?: boolean) => HTMLBaseElement; /** * Returns href. diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index eff3fe74e..6bbf984c6 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -7,7 +7,6 @@ import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import { URL } from 'url'; -import Document from '../document/Document.js'; import MouseEvent from '../../event/events/MouseEvent.js'; import NodeList from '../node/INodeList.js'; @@ -229,17 +228,6 @@ export default class HTMLButtonElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - const formID = this.getAttribute('form'); - - if (formID !== null) { - if (!this[PropertySymbol.isConnected]) { - return null; - } - return formID - ? (this[PropertySymbol.rootNode]).getElementById(formID) - : null; - } - return this[PropertySymbol.formNode]; } diff --git a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts index d66aaa248..4904a4ca1 100644 --- a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts +++ b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts @@ -1,6 +1,7 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLCollection from '../element/HTMLCollection2.js'; +import HTMLCollection from '../element/HTMLCollection.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; import Node from '../node/Node.js'; @@ -10,15 +11,45 @@ import Node from '../node/Node.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataListElement */ export default class HTMLDataListElement extends HTMLElement { - public [PropertySymbol.options] = new HTMLCollection(); - public [PropertySymbol.dataListNode]: Node = this; + public [PropertySymbol.options] = new HTMLCollection( + (item: Node) => item[PropertySymbol.tagName] === 'OPTION' + ); + + /** + * Constructor. + * + * @param browserFrame Browser frame. + */ + constructor() { + super(); + // Child nodes listeners + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { + this[PropertySymbol.elements][PropertySymbol.addItem](item); + }); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'insert', + (newItem: Node, referenceItem: Node | null) => { + this[PropertySymbol.elements][PropertySymbol.insertItem]( + newItem, + referenceItem + ); + } + ); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'remove', + (item: Node) => { + (item)[PropertySymbol.formNode] = null; + this[PropertySymbol.elements][PropertySymbol.removeItem](item); + } + ); + } /** * Returns options. * * @returns Options. */ - public get options(): HTMLCollection { + public get options(): IHTMLCollection { return this[PropertySymbol.options]; } } diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 0ec54170d..3fb05954a 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -8,11 +8,12 @@ import Event from '../../event/Event.js'; import HTMLElementUtility from './HTMLElementUtility.js'; import NodeList from '../node/NodeList.js'; import Node from '../node/Node.js'; -import HTMLCollection from '../element/HTMLCollection2.js'; +import HTMLCollection from '../element/HTMLCollection.js'; import DatasetFactory from '../element/DatasetFactory.js'; import IDataset from '../element/IDataset.js'; import Attr from '../attr/Attr.js'; import NamedNodeMap from '../element/NamedNodeMap.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; /** * HTML Element. @@ -22,7 +23,7 @@ import NamedNodeMap from '../element/NamedNodeMap.js'; */ export default class HTMLElement extends Element { // Public properties - public cloneNode: (deep?: boolean) => HTMLElement; + public declare cloneNode: (deep?: boolean) => HTMLElement; // Events public oncopy: (event: Event) => void | null = null; @@ -289,7 +290,7 @@ export default class HTMLElement extends Element { * @param innerText Inner text. */ public set innerText(text: string) { - const childNodes = this[PropertySymbol.childNodes][PropertySymbol.items]; + const childNodes = this[PropertySymbol.childNodes]; while (childNodes.length) { this.removeChild(childNodes[0]); @@ -549,9 +550,13 @@ export default class HTMLElement extends Element { const newElement = ( this[PropertySymbol.ownerDocument].createElement(localName) ); - (>newElement[PropertySymbol.childNodes]) = - this[PropertySymbol.childNodes]; - (>newElement[PropertySymbol.children]) = + (>newElement[PropertySymbol.childNodes]) = >( + this[PropertySymbol.childNodes] + ); + (>newElement[PropertySymbol.childNodesFlatten]) = >( + this[PropertySymbol.childNodesFlatten] + ); + (>newElement[PropertySymbol.children]) = this[PropertySymbol.children]; (newElement[PropertySymbol.isConnected]) = this[PropertySymbol.isConnected]; @@ -568,10 +573,14 @@ export default class HTMLElement extends Element { ); } - (>this[PropertySymbol.childNodes]) = new NodeList(); - (>this[PropertySymbol.children]) = new HTMLCollection(); - this[PropertySymbol.childNodes][PropertySymbol.attachHTMLCollection]( - this[PropertySymbol.children] + const children = new HTMLCollection(); + const childNodes = new NodeList(); + const childNodesFlatten = new NodeList(); + + (>this[PropertySymbol.childNodes]) = childNodes; + (>this[PropertySymbol.childNodesFlatten]) = childNodesFlatten; + (>this[PropertySymbol.children]) = >( + children ); this[PropertySymbol.rootNode] = null; this[PropertySymbol.formNode] = null; @@ -581,31 +590,69 @@ export default class HTMLElement extends Element { this[PropertySymbol.isValue] = null; (this[PropertySymbol.attributes]) = new NamedNodeMap(this); - const childNodes = (this[PropertySymbol.parentNode])[ - PropertySymbol.childNodes - ]?.[PropertySymbol.items]; - const childNodesItems = childNodes[PropertySymbol.items]; - for (let i = 0, max = childNodesItems.length; i < max; i++) { - if (childNodesItems[i] === this) { - (childNodes[i]) = newElement; - (childNodesItems[i]) = newElement; - break; + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( + 'add', + (item: Node) => { + let parent: Node = this; + while (parent) { + const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; + + childNodesFlatten[PropertySymbol.addItem](item); + + for (const child of item[PropertySymbol.childNodesFlatten]) { + childNodesFlatten[PropertySymbol.addItem](child); + } + + parent = parent[PropertySymbol.parentNode]; + } + + children[PropertySymbol.addItem](item); } - } + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( + 'insert', + (item: Node, referenceItem?: Node) => { + let parent: Node = this; + while (parent) { + const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; - const children = (this[PropertySymbol.parentNode])[ - PropertySymbol.children - ]; - if (children) { - const childrenItems = children[PropertySymbol.items]; - for (let i = 0, max = childrenItems.length; i < max; i++) { - if (childrenItems[i] === this) { - children[i] = newElement; - childrenItems[i] = newElement; - break; + childNodesFlatten[PropertySymbol.insertItem](item, referenceItem); + + for (const child of item[PropertySymbol.childNodesFlatten]) { + childNodesFlatten[PropertySymbol.insertItem](child, referenceItem); + } + + parent = parent[PropertySymbol.parentNode]; } + + children[PropertySymbol.insertItem](item, referenceItem); } - } + ); + this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( + 'remove', + (item: Node) => { + let parent: Node = this; + while (parent) { + const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; + + childNodesFlatten[PropertySymbol.removeItem](item); + + for (const child of item[PropertySymbol.childNodesFlatten]) { + childNodesFlatten[PropertySymbol.removeItem](child); + } + + parent = parent[PropertySymbol.parentNode]; + } + + children[PropertySymbol.removeItem](item); + } + ); + + const parentChildNodes = (this[PropertySymbol.parentNode])[ + PropertySymbol.childNodes + ]; + parentChildNodes[PropertySymbol.insertItem](newElement, this.nextElementSibling); + parentChildNodes[PropertySymbol.removeItem](this); if (newElement[PropertySymbol.isConnected] && newElement.connectedCallback) { const result = >newElement.connectedCallback(); @@ -627,7 +674,7 @@ export default class HTMLElement extends Element { } } - this[PropertySymbol.connectedToDocument](null); + this[PropertySymbol.disconnectedFromDocument](); } }; callbacks[localName] = callbacks[localName] || []; diff --git a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts index f8a68838a..0cba6cbdc 100644 --- a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts +++ b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts @@ -1,13 +1,20 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLCollection from '../element/HTMLCollection2.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; +import HTMLCollection from '../element/HTMLCollection.js'; import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; -import Document from '../document/Document.js'; -import Attr from '../attr/Attr.js'; +import Node from '../node/Node.js'; +import Element from '../element/Element.js'; + +type THTMLFieldSetElement = + | HTMLInputElement + | HTMLButtonElement + | HTMLTextAreaElement + | HTMLSelectElement; /** * HTMLFieldSetElement @@ -15,24 +22,45 @@ import Attr from '../attr/Attr.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFieldSetElement */ export default class HTMLFieldSetElement extends HTMLElement { + // Public properties + public declare cloneNode: (deep?: boolean) => HTMLFieldSetElement; + // Internal properties - public [PropertySymbol.elements] = new HTMLCollection< - HTMLInputElement | HTMLButtonElement | HTMLTextAreaElement | HTMLSelectElement - >(); + public [PropertySymbol.elements] = new HTMLCollection( + (item: Element) => + item.tagName === 'INPUT' || + item.tagName === 'BUTTON' || + item.tagName === 'TEXTAREA' || + item.tagName === 'SELECT' + ); public [PropertySymbol.formNode]: HTMLFormElement | null = null; /** * Constructor. + * + * @param browserFrame Browser frame. */ constructor() { super(); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) + // Child nodes listeners + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { + this[PropertySymbol.elements][PropertySymbol.addItem](item); + }); + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( + 'insert', + (newItem: Node, referenceItem: Node | null) => { + this[PropertySymbol.elements][PropertySymbol.insertItem]( + newItem, + referenceItem + ); + } ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( 'remove', - this.#onRemoveAttribute.bind(this) + (item: Node) => { + (item)[PropertySymbol.formNode] = null; + this[PropertySymbol.elements][PropertySymbol.removeItem](item); + } ); } @@ -41,7 +69,7 @@ export default class HTMLFieldSetElement extends HTMLElement { * * @returns Elements. */ - public get elements(): HTMLCollection< + public get elements(): IHTMLCollection< HTMLInputElement | HTMLButtonElement | HTMLTextAreaElement | HTMLSelectElement > { return this[PropertySymbol.elements]; @@ -53,17 +81,6 @@ export default class HTMLFieldSetElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - const formID = this.getAttribute('form'); - - if (formID !== null) { - if (!this[PropertySymbol.isConnected]) { - return null; - } - return formID - ? (this[PropertySymbol.rootNode]).getElementById(formID) - : null; - } - return this[PropertySymbol.formNode]; } @@ -166,23 +183,4 @@ export default class HTMLFieldSetElement extends HTMLElement { public setCustomValidity(_message: string): void { // Do nothing as fieldset never candidates for constraint validation. } - - /** - * Triggered when an attribute is set. - * - * @param attribute Attribute. - * @param replacedAttribute Replaced attribute. - */ - #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { - this.form?.[PropertySymbol.appendFormControlItem](this, attribute, replacedAttribute); - } - - /** - * Triggered when an attribute is removed. - * - * @param removedAttribute Removed attribute. - */ - #onRemoveAttribute(removedAttribute: Attr): void { - this.form?.[PropertySymbol.removeFormControlItem](this, removedAttribute); - } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index a0cb921d0..552af4a94 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -16,6 +16,7 @@ export default class HTMLFormControlsCollection extends HTMLCollection< THTMLFormControlElement, THTMLFormControlElement | RadioNodeList > { + public [PropertySymbol.namedItems] = new Map(); #namedNodeMapListeners = new Map(); /** @@ -49,6 +50,23 @@ export default class HTMLFormControlsCollection extends HTMLCollection< }); } + /** + * @override + */ + public namedItem(name: string): THTMLFormControlElement | RadioNodeList | null { + const namedItems = this[PropertySymbol.namedItems].get(name); + + if (!namedItems?.length) { + return null; + } + + if (namedItems.length === 1) { + return namedItems[0]; + } + + return namedItems; + } + /** * Appends item. * @@ -128,23 +146,6 @@ export default class HTMLFormControlsCollection extends HTMLCollection< return true; } - /** - * @override - */ - public namedItem(name: string): THTMLFormControlElement | RadioNodeList | null { - const namedItems = this[PropertySymbol.namedItems].get(name); - - if (!namedItems?.length) { - return null; - } - - if (namedItems.length === 1) { - return namedItems[0]; - } - - return namedItems; - } - /** * Returns named items. * diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index 739489893..107ea4927 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -23,13 +23,12 @@ import THTMLFormControlElement from './THTMLFormControlElement.js'; */ export default class HTMLFormElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLFormElement; + public declare cloneNode: (deep?: boolean) => HTMLFormElement; // Internal properties. public [PropertySymbol.elements]: HTMLFormControlsCollection = new HTMLFormControlsCollection( this ); - public [PropertySymbol.length] = 0; public [PropertySymbol.formNode]: Node = this; // Events @@ -85,13 +84,16 @@ export default class HTMLFormElement extends HTMLElement { } ); - // Form controls listeners + // HTMLFormControlsCollection listeners this[PropertySymbol.elements][PropertySymbol.addEventListener]('indexChange', (details) => { const length = this[PropertySymbol.elements].length; - this[PropertySymbol.length] = length; for (let i = details.index; i < length; i++) { this[i] = this[PropertySymbol.elements][i]; } + // Item removed + if (!details.item) { + delete this[length]; + } }); this[PropertySymbol.elements][PropertySymbol.addEventListener]('propertyChange', (details) => { if (!this[PropertySymbol.isValidPropertyName](details.propertyName)) { @@ -125,7 +127,7 @@ export default class HTMLFormElement extends HTMLElement { * @returns Length. */ public get length(): number { - return this[PropertySymbol.length]; + return this[PropertySymbol.elements].length; } /** diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index d7213880f..553fe9da1 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -38,7 +38,7 @@ const SANDBOX_FLAGS = [ */ export default class HTMLIFrameElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLIFrameElement; + public declare cloneNode: (deep?: boolean) => HTMLIFrameElement; // Events public onload: (event: Event) => void | null = null; diff --git a/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts b/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts index aa59e3f94..a6cace2eb 100644 --- a/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +++ b/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts @@ -15,7 +15,7 @@ export default class HTMLImageElement extends HTMLElement { public [PropertySymbol.loading] = 'auto'; public [PropertySymbol.x] = 0; public [PropertySymbol.y] = 0; - public cloneNode: (deep?: boolean) => HTMLImageElement; + public declare cloneNode: (deep?: boolean) => HTMLImageElement; /** * Returns complete. diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 9f0a0c41f..30a8a487a 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -32,7 +32,7 @@ import NodeList from '../node/INodeList.js'; */ export default class HTMLInputElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLInputElement; + public declare cloneNode: (deep?: boolean) => HTMLInputElement; // Events public oninput: (event: Event) => void | null = null; @@ -202,17 +202,6 @@ export default class HTMLInputElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - const formID = this.getAttribute('form'); - - if (formID !== null) { - if (!this[PropertySymbol.isConnected]) { - return null; - } - return formID - ? (this[PropertySymbol.rootNode]).getElementById(formID) - : null; - } - return this[PropertySymbol.formNode]; } diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts index f39cbb877..6ecf213df 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts @@ -15,7 +15,7 @@ import MouseEvent from '../../event/events/MouseEvent.js'; */ export default class HTMLLabelElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLLabelElement; + public declare cloneNode: (deep?: boolean) => HTMLLabelElement; /** * Returns a string containing the ID of the labeled control. This reflects the "for" attribute. diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index fe9c4c5cb..9d99c3ff2 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -3,7 +3,6 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElement from '../html-element/HTMLElement.js'; import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; -import Node from '../../nodes/node/Node.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import Attr from '../attr/Attr.js'; diff --git a/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts b/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts index 0c0edfd2c..4e3e6f556 100644 --- a/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts +++ b/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts @@ -20,7 +20,7 @@ interface IMediaError { */ export default class HTMLMediaElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLMediaElement; + public declare cloneNode: (deep?: boolean) => HTMLMediaElement; // Events public onabort: (event: Event) => void | null = null; diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index 0e875cdd7..1d00c32b8 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -1,10 +1,8 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import Attr from '../attr/Attr.js'; -import HTMLDataListElement from '../html-data-list-element/HTMLDataListElement.js'; import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; -import Node from '../node/Node.js'; /** * HTML Option Element. @@ -91,7 +89,9 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = Boolean(selected); if (selectNode) { - selectNode[PropertySymbol.updateOptionItems](this[PropertySymbol.selectedness] ? this : null); + selectNode[PropertySymbol.updateSelectedness]( + this[PropertySymbol.selectedness] ? this : null + ); } } @@ -135,48 +135,6 @@ export default class HTMLOptionElement extends HTMLElement { this.setAttribute('value', value); } - /** - * @override - */ - public override [PropertySymbol.connectedToDocument](): void { - const oldSelectNode = this[PropertySymbol.selectNode]; - const oldDataListNode = this[PropertySymbol.dataListNode]; - - super[PropertySymbol.connectedToDocument](parentNode); - - const selectNode = this[PropertySymbol.selectNode]; - - if (oldSelectNode !== selectNode) { - if (oldSelectNode) { - oldSelectNode[PropertySymbol.updateOptionItems](); - } - if (selectNode) { - selectNode[PropertySymbol.updateOptionItems](); - } - } - - const dataListNode = this[PropertySymbol.dataListNode]; - - if (oldDataListNode !== dataListNode) { - const name = this.getAttribute('name'); - const id = this.id; - if (oldDataListNode) { - const index = oldDataListNode[PropertySymbol.options].indexOf(this); - if (index !== -1) { - oldDataListNode[PropertySymbol.options].splice(index, 1); - } - - oldDataListNode[PropertySymbol.options][PropertySymbol.removeNamedItem](this, name); - oldDataListNode[PropertySymbol.options][PropertySymbol.removeNamedItem](this, id); - } - if (dataListNode) { - dataListNode[PropertySymbol.options].push(this); - dataListNode[PropertySymbol.options][PropertySymbol.appendNamedItem](this, name); - dataListNode[PropertySymbol.options][PropertySymbol.appendNamedItem](this, id); - } - } - } - /** * Triggered when an attribute is set. * @@ -194,7 +152,7 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = true; if (selectNode) { - selectNode[PropertySymbol.updateOptionItems](this); + selectNode[PropertySymbol.updateSelectedness](this); } } } @@ -215,7 +173,7 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = false; if (selectNode) { - selectNode[PropertySymbol.updateOptionItems](); + selectNode[PropertySymbol.updateSelectedness](); } } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index 17b34b9ee..e7a37ec41 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -2,7 +2,6 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; -import Node from '../../nodes/node/Node.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; @@ -21,7 +20,7 @@ import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js' */ export default class HTMLScriptElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLScriptElement; + public declare cloneNode: (deep?: boolean) => HTMLScriptElement; // Events public onerror: (event: ErrorEvent) => void = null; diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts index 0b561add5..15b5c7a4a 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts @@ -3,7 +3,7 @@ import HTMLCollection from '../element/HTMLCollection.js'; import HTMLSelectElement from './HTMLSelectElement.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; import Element from '../element/Element.js'; -import { PropertySymbol } from '../../index.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; /** * HTML Options Collection. diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 075b464e5..e6c40c428 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -7,13 +7,12 @@ import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; import HTMLOptionsCollection from './HTMLOptionsCollection.js'; import Event from '../../event/Event.js'; import Node from '../node/Node.js'; -import NodeTypeEnum from '../node/NodeTypeEnum.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import HTMLCollection from '../element/HTMLCollection.js'; -import Document from '../document/Document.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; import Element from '../element/Element.js'; import NodeList from '../node/INodeList.js'; +import NodeTypeEnum from '../node/NodeTypeEnum.js'; /** * HTML Select Element. @@ -25,8 +24,6 @@ export default class HTMLSelectElement extends HTMLElement { // Internal properties. public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); - public [PropertySymbol.selectNode]: Node = this; - public [PropertySymbol.length] = 0; public [PropertySymbol.options]: HTMLOptionsCollection = new HTMLOptionsCollection(this); public [PropertySymbol.formNode]: HTMLFormElement | null = null; public [PropertySymbol.selectedOptions]: IHTMLCollection = @@ -49,7 +46,7 @@ export default class HTMLSelectElement extends HTMLElement { this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { (item)[PropertySymbol.selectNode] = this; this[PropertySymbol.options][PropertySymbol.addItem](item); - this[PropertySymbol.selectedOptions][PropertySymbol.addItem](item); + this[PropertySymbol.updateSelectedness](); }); this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( 'insert', @@ -59,10 +56,7 @@ export default class HTMLSelectElement extends HTMLElement { newItem, referenceItem ); - this[PropertySymbol.selectedOptions][PropertySymbol.insertItem]( - newItem, - referenceItem - ); + this[PropertySymbol.updateSelectedness](); } ); this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( @@ -70,9 +64,36 @@ export default class HTMLSelectElement extends HTMLElement { (item: Node) => { (item)[PropertySymbol.selectNode] = null; this[PropertySymbol.options][PropertySymbol.removeItem](item); - this[PropertySymbol.selectedOptions][PropertySymbol.removeItem](item); + this[PropertySymbol.updateSelectedness](); } ); + + // HTMLOptionsCollection listeners + this[PropertySymbol.options][PropertySymbol.addEventListener]('indexChange', (details) => { + const length = this[PropertySymbol.options].length; + for (let i = details.index; i < length; i++) { + this[i] = this[PropertySymbol.options][i]; + } + // Item removed + if (!details.item) { + delete this[length]; + } + }); + this[PropertySymbol.options][PropertySymbol.addEventListener]('propertyChange', (details) => { + if (!this[PropertySymbol.isValidPropertyName](details.propertyName)) { + return; + } + if (details.propertyValue) { + Object.defineProperty(this, details.propertyName, { + value: details.propertyValue, + writable: false, + enumerable: true, + configurable: true + }); + } else { + delete this[details.propertyName]; + } + }); } /** @@ -81,7 +102,7 @@ export default class HTMLSelectElement extends HTMLElement { * @returns Length. */ public get length(): number { - return this[PropertySymbol.length]; + return this[PropertySymbol.options].length; } /** @@ -301,17 +322,6 @@ export default class HTMLSelectElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - const formID = this.getAttribute('form'); - - if (formID !== null) { - if (!this[PropertySymbol.isConnected]) { - return null; - } - return formID - ? (this[PropertySymbol.rootNode]).getElementById(formID) - : null; - } - return this[PropertySymbol.formNode]; } @@ -402,49 +412,39 @@ export default class HTMLSelectElement extends HTMLElement { * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm * @param [selectedOption] Selected option. */ - public [PropertySymbol.updateOptionItems](selectedOption?: HTMLOptionElement): void { - const optionElements = this.getElementsByTagName('option'); - - if (optionElements.length < this[PropertySymbol.options].length) { - this[PropertySymbol.options].splice( - this[PropertySymbol.options].length - 1, - this[PropertySymbol.options].length - optionElements.length - ); - - for (let i = optionElements.length - 1, max = this[PropertySymbol.length]; i < max; i++) { - delete this[i]; - } - } - - const isMultiple = this.hasAttributeNS(null, 'multiple'); + public [PropertySymbol.updateSelectedness](selectedOption?: HTMLOptionElement): void { + const options = this[PropertySymbol.options]; + const isMultiple = this.hasAttribute('multiple'); + const selectedOptions = this[PropertySymbol.selectedOptions]; const selected: HTMLOptionElement[] = []; - for (let i = 0; i < optionElements.length; i++) { - this[PropertySymbol.options][i] = optionElements[i]; - this[i] = optionElements[i]; - + for (let i = 0, max = options.length; i < max; i++) { + const option = options[i]; if (!isMultiple) { if (selectedOption) { - (optionElements[i])[PropertySymbol.selectedness] = - optionElements[i] === selectedOption; + option[PropertySymbol.selectedness] = option === selectedOption; } - if ((optionElements[i])[PropertySymbol.selectedness]) { - selected.push(optionElements[i]); + if (option[PropertySymbol.selectedness]) { + selected.push(option); + + if (!selectedOptions[PropertySymbol.includes](option)) { + selectedOptions[PropertySymbol.addItem](option); + } + } else { + selectedOptions[PropertySymbol.removeItem](selectedOptions[0]); } } } - (this[PropertySymbol.length]) = optionElements.length; - const size = this.#getDisplaySize(); if (size === 1 && !selected.length) { - for (let i = 0, max = optionElements.length; i < max; i++) { - const option = optionElements[i]; - - let disabled = option.hasAttributeNS(null, 'disabled'); + for (let i = 0, max = options.length; i < max; i++) { + const option = options[i]; const parentNode = option[PropertySymbol.parentNode]; + let disabled = option.hasAttributeNS(null, 'disabled'); + if ( parentNode && parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && @@ -460,9 +460,8 @@ export default class HTMLSelectElement extends HTMLElement { } } } else if (selected.length >= 2) { - for (let i = 0, max = optionElements.length; i < max; i++) { - (optionElements[i])[PropertySymbol.selectedness] = - i === selected.length - 1; + for (let i = 0, max = options.length; i < max; i++) { + (options[i])[PropertySymbol.selectedness] = i === selected.length - 1; } } } diff --git a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts index 7472b28f0..186bbc86b 100644 --- a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts +++ b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts @@ -5,6 +5,7 @@ import Text from '../text/Text.js'; import Element from '../element/Element.js'; import Node from '../node/Node.js'; import Event from '../../event/Event.js'; +import Attr from '../attr/Attr.js'; /** * HTML Slot Element. @@ -14,11 +15,28 @@ import Event from '../../event/Event.js'; */ export default class HTMLSlotElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLSlotElement; + public declare cloneNode: (deep?: boolean) => HTMLSlotElement; // Events public onslotchange: (event: Event) => void | null = null; + /** + * + */ + constructor() { + super(); + + // Attribute listeners + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#onSetAttribute.bind(this) + ); + this[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#onRemoveAttribute.bind(this) + ); + } + /** * Returns name. * @@ -49,65 +67,135 @@ export default class HTMLSlotElement extends HTMLElement { /** * Returns assigned nodes. * - * @param [options] Options. - * @param [options.flatten] A boolean value indicating whether to return the assigned nodes of any available child elements (true) or not (false). Defaults to false. + * @param [_options] Options. + * @param [_options.flatten] A boolean value indicating whether to return the assigned nodes of any available child elements (true) or not (false). Defaults to false. * @returns Nodes. */ - public assignedNodes(options?: { flatten?: boolean }): Node[] { - const host = (this[PropertySymbol.rootNode])?.host; + public assignedNodes(_options?: { flatten?: boolean }): Node[] { + return this.#assignedNodes(this.name, _options); + } - // TODO: Add support for options.flatten. We need to find an example of how it is expected to work before it can be implemented. + /** + * Returns assigned elements. + * + * @param [_options] Options. + * @param [_options.flatten] A boolean value indicating whether to return the assigned elements of any available child elements (true) or not (false). Defaults to false. + * @returns Nodes. + */ + public assignedElements(_options?: { flatten?: boolean }): Element[] { + return this.#assignedElements(this.name, _options); + } - if (host) { - const name = this.name; + /** + * @override + */ + public override [PropertySymbol.cloneNode](deep = false): HTMLSlotElement { + return super[PropertySymbol.cloneNode](deep); + } - if (name) { - return this.assignedElements(options); + /** + * Triggered when an attribute is set. + * + * @param attribute Attribute. + * @param replacedAttribute Replaced attribute. + */ + #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + if ( + attribute[PropertySymbol.name] === 'name' && + attribute[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value] + ) { + const replacedAssignedNodes = this.#assignedNodes(replacedAttribute?.[PropertySymbol.value]); + const assignedNodes = this.#assignedNodes(attribute.value); + + if (replacedAssignedNodes.length !== assignedNodes.length) { + this.dispatchEvent(new Event('slotchange')); } - return (host)[PropertySymbol.childNodes].slice(); + for (let i = 0, max = assignedNodes.length; i < max; i++) { + if (replacedAssignedNodes[i] !== assignedNodes[i]) { + this.dispatchEvent(new Event('slotchange')); + break; + } + } } + } - return []; + /** + * Triggered when an attribute is set. + * + * @param removedAttribute Attribute. + */ + #onRemoveAttribute(removedAttribute: Attr): void { + if ( + removedAttribute[PropertySymbol.name] === 'name' && + removedAttribute[PropertySymbol.value] && + this.#assignedNodes(removedAttribute.value).length > 0 + ) { + this.dispatchEvent(new Event('slotchange')); + } } /** - * Returns assigned elements. + * Returns assigned nodes. * + * @param name Name. * @param [_options] Options. - * @param [_options.flatten] A boolean value indicating whether to return the assigned elements of any available child elements (true) or not (false). Defaults to false. + * @param [_options.flatten] A boolean value indicating whether to return the assigned nodes of any available child elements (true) or not (false). Defaults to false. * @returns Nodes. */ - public assignedElements(_options?: { flatten?: boolean }): Element[] { + #assignedNodes(name?: string, _options?: { flatten?: boolean }): Node[] { const host = (this.getRootNode())?.host; // TODO: Add support for options.flatten. We need to find an example of how it expected to work before it can be implemented. - if (host) { - const name = this.name; + if (!host) { + return []; + } - if (name) { - const assignedElements = []; + const assignedElements = []; - for (const child of (host)[PropertySymbol.children][PropertySymbol.items]) { - if (child.slot === name) { - assignedElements.push(child); - } + for (const slotNode of (host)[PropertySymbol.childNodes]) { + if (name && slotNode['slot'] && slotNode['slot'] === name) { + for (const child of slotNode[PropertySymbol.childNodes]) { + assignedElements.push(child); } - - return assignedElements; + } else if (!name && !slotNode['slot']) { + assignedElements.push(slotNode); } - - return (host)[PropertySymbol.children][PropertySymbol.items].slice(); } - return []; + return assignedElements; } /** - * @override + * Returns assigned elements. + * + * @param name Name. + * @param [_options] Options. + * @param [_options.flatten] A boolean value indicating whether to return the assigned elements of any available child elements (true) or not (false). Defaults to false. + * @returns Nodes. */ - public override [PropertySymbol.cloneNode](deep = false): HTMLSlotElement { - return super[PropertySymbol.cloneNode](deep); + #assignedElements(name?: string, _options?: { flatten?: boolean }): Element[] { + const host = (this.getRootNode())?.host; + + // TODO: Add support for options.flatten. We need to find an example of how it expected to work before it can be implemented. + + if (!host) { + return []; + } + + const assignedElements = []; + + for (const slotElement of (host)[PropertySymbol.children]) { + if (name && slotElement.slot === name) { + for (const child of slotElement[PropertySymbol.children]) { + assignedElements.push(child); + } + } else if (!name && !slotElement.slot) { + assignedElements.push(slotElement); + } + } + + return assignedElements; } } diff --git a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts index 7b912a0c6..1231f28fd 100644 --- a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts +++ b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts @@ -13,7 +13,7 @@ import XMLParser from '../../xml-parser/XMLParser.js'; */ export default class HTMLTemplateElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLTemplateElement; + public declare cloneNode: (deep?: boolean) => HTMLTemplateElement; // Internal properties public [PropertySymbol.content]: DocumentFragment = @@ -40,9 +40,10 @@ export default class HTMLTemplateElement extends HTMLElement { */ public set innerHTML(html: string) { const content = this[PropertySymbol.content]; + const childNodes = content[PropertySymbol.childNodes]; - for (const child of content[PropertySymbol.childNodes].slice()) { - this[PropertySymbol.content].removeChild(child); + while (childNodes.length) { + content.removeChild(childNodes[0]); } XMLParser.parse(this[PropertySymbol.ownerDocument], html, { diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index 9b4cfca40..24a8b6e95 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -10,7 +10,6 @@ import ValidityState from '../../validity-state/ValidityState.js'; import NodeList from '../node/NodeList.js'; import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; -import Document from '../document/Document.js'; import Text from '../text/Text.js'; import Node from '../node/Node.js'; @@ -22,7 +21,7 @@ import Node from '../node/Node.js'; */ export default class HTMLTextAreaElement extends HTMLElement { // Public properties - public cloneNode: (deep?: boolean) => HTMLTextAreaElement; + public declare cloneNode: (deep?: boolean) => HTMLTextAreaElement; public readonly type = 'textarea'; // Events @@ -54,8 +53,8 @@ export default class HTMLTextAreaElement extends HTMLElement { }); this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( 'insert', - (newItem: Node) => { - if (newItem instanceof Text) { + (item: Node) => { + if (item instanceof Text) { item[PropertySymbol.textAreaNode] = this; this[PropertySymbol.resetSelection](); } @@ -445,17 +444,6 @@ export default class HTMLTextAreaElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - const formID = this.getAttribute('form'); - - if (formID !== null) { - if (!this[PropertySymbol.isConnected]) { - return null; - } - return formID - ? (this[PropertySymbol.rootNode]).getElementById(formID) - : null; - } - return this[PropertySymbol.formNode]; } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaPropertyAttributes.json b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaPropertyAttributes.json deleted file mode 100644 index 3bf0f45f0..000000000 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaPropertyAttributes.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "form": "form", - "name": "name", - "disabled": "disabled", - "autofocus": "autofocus", - "required": "required", - "value": "value", - "autocomplete": "autocomplete", - "minlength": "minLength", - "maxlength": "maxLength", - "pattern": "pattern", - "placeholder": "placeholder", - "readonly": "readOnly", - "size": "size", - "inputmode": "inputmode" -} \ No newline at end of file diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index ebb508042..172236533 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -85,6 +85,52 @@ export default class Node extends EventTarget { } this[PropertySymbol.ownerDocument] = ownerDocument; } + + const childNodes = this[PropertySymbol.childNodes]; + + childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { + let parent: Node = this; + while (parent) { + const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; + + childNodesFlatten[PropertySymbol.addItem](item); + + for (const child of item[PropertySymbol.childNodesFlatten]) { + childNodesFlatten[PropertySymbol.addItem](child); + } + + parent = parent[PropertySymbol.parentNode]; + } + }); + + childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { + let parent: Node = this; + while (parent) { + const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; + + childNodesFlatten[PropertySymbol.insertItem](item, referenceItem); + + for (const child of item[PropertySymbol.childNodesFlatten]) { + childNodesFlatten[PropertySymbol.insertItem](child, referenceItem); + } + + parent = parent[PropertySymbol.parentNode]; + } + }); + childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { + let parent: Node = this; + while (parent) { + const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; + + childNodesFlatten[PropertySymbol.removeItem](item); + + for (const child of item[PropertySymbol.childNodesFlatten]) { + childNodesFlatten[PropertySymbol.removeItem](child); + } + + parent = parent[PropertySymbol.parentNode]; + } + }); } /** @@ -420,7 +466,7 @@ export default class Node extends EventTarget { for (const childNode of this[PropertySymbol.childNodes]) { const childClone = childNode.cloneNode(true); childClone[PropertySymbol.parentNode] = clone; - clone[PropertySymbol.childNodes][PropertySymbol.appendChild](childClone); + clone[PropertySymbol.childNodes][PropertySymbol.addItem](childClone); } } @@ -459,36 +505,21 @@ export default class Node extends EventTarget { // Remove the node from its previous parent if it has any. if (node[PropertySymbol.parentNode]) { - node[PropertySymbol.parentNode][PropertySymbol.childNodes][PropertySymbol.removeChild](node); - let parent = node[PropertySymbol.parentNode]; - while (parent) { - node[PropertySymbol.parentNode][PropertySymbol.childNodesFlatten][ - PropertySymbol.removeChild - ](node); - parent = node[PropertySymbol.parentNode]; - } + node[PropertySymbol.parentNode][PropertySymbol.childNodes][PropertySymbol.removeItem](node); } if (this[PropertySymbol.isConnected]) { (this[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; } - this[PropertySymbol.childNodes][PropertySymbol.appendChild](node); - - let parent: Node = this; - while (parent) { - parent[PropertySymbol.childNodesFlatten][PropertySymbol.appendChild](node); - parent = parent[PropertySymbol.parentNode]; - } - node[PropertySymbol.parentNode] = this; - if (this[PropertySymbol.isConnected] && !(node)[PropertySymbol.isConnected]) { - (node)[PropertySymbol.isConnected] = true; - (node)[PropertySymbol.connectedToDocument](); - } else if (!this[PropertySymbol.isConnected] && (node)[PropertySymbol.isConnected]) { - (node)[PropertySymbol.isConnected] = false; - (node)[PropertySymbol.disconnectedFromDocument](); + this[PropertySymbol.childNodes][PropertySymbol.addItem](node); + + if (this[PropertySymbol.isConnected] && !node[PropertySymbol.isConnected]) { + node[PropertySymbol.connectedToDocument](); + } else if (!this[PropertySymbol.isConnected] && node[PropertySymbol.isConnected]) { + node[PropertySymbol.disconnectedFromDocument](); } // MutationObserver @@ -523,27 +554,23 @@ export default class Node extends EventTarget { (this[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; } - this[PropertySymbol.childNodes][PropertySymbol.removeChild](node); + node[PropertySymbol.parentNode] = null; - let parent: Node = this; - while (parent) { - parent[PropertySymbol.childNodesFlatten][PropertySymbol.removeChild](node); - parent = parent[PropertySymbol.parentNode]; - } + this[PropertySymbol.childNodes][PropertySymbol.removeItem](node); - if ((node)[PropertySymbol.isConnected]) { - (node)[PropertySymbol.disconnectedFromDocument](); + if (node[PropertySymbol.isConnected]) { + node[PropertySymbol.disconnectedFromDocument](); } // MutationObserver - if ((this)[PropertySymbol.observers].length > 0) { + if (this[PropertySymbol.observers].length > 0) { const record = new MutationRecord({ target: this, type: MutationTypeEnum.childList, removedNodes: [node] }); - for (const observer of (this)[PropertySymbol.observers]) { + for (const observer of this[PropertySymbol.observers]) { if (observer.options?.subtree) { (node)[PropertySymbol.unobserve](observer); } @@ -599,32 +626,19 @@ export default class Node extends EventTarget { } if (newNode[PropertySymbol.parentNode]) { - newNode[PropertySymbol.parentNode][PropertySymbol.childNodes][PropertySymbol.removeChild]( + newNode[PropertySymbol.parentNode][PropertySymbol.childNodes][PropertySymbol.removeItem]( newNode ); - let parent: Node = newNode[PropertySymbol.parentNode]; - while (parent) { - parent[PropertySymbol.childNodesFlatten][PropertySymbol.removeChild](newNode); - parent = parent[PropertySymbol.parentNode]; - } - } - - this[PropertySymbol.childNodes][PropertySymbol.insertBefore](newNode, referenceNode); - - let parent: Node = this; - while (parent) { - parent[PropertySymbol.childNodesFlatten][PropertySymbol.insertBefore](newNode, referenceNode); - parent = parent[PropertySymbol.parentNode]; } newNode[PropertySymbol.parentNode] = this; - if (this[PropertySymbol.isConnected] && !(newNode)[PropertySymbol.isConnected]) { - (newNode)[PropertySymbol.isConnected] = true; - (newNode)[PropertySymbol.connectedToDocument](); - } else if (!this[PropertySymbol.isConnected] && (newNode)[PropertySymbol.isConnected]) { - (newNode)[PropertySymbol.isConnected] = false; - (newNode)[PropertySymbol.disconnectedFromDocument](); + this[PropertySymbol.childNodes][PropertySymbol.insertItem](newNode, referenceNode); + + if (this[PropertySymbol.isConnected] && !newNode[PropertySymbol.isConnected]) { + newNode[PropertySymbol.connectedToDocument](); + } else if (!this[PropertySymbol.isConnected] && newNode[PropertySymbol.isConnected]) { + newNode[PropertySymbol.disconnectedFromDocument](); } // MutationObserver @@ -719,6 +733,8 @@ export default class Node extends EventTarget { * Called when connected to document. */ public [PropertySymbol.connectedToDocument](): void { + this[PropertySymbol.isConnected] = true; + if (this[PropertySymbol.nodeType] !== NodeTypeEnum.documentFragmentNode) { this[PropertySymbol.rootNode] = this[PropertySymbol.parentNode][PropertySymbol.rootNode]; } @@ -745,7 +761,7 @@ export default class Node extends EventTarget { } for (const child of this[PropertySymbol.childNodes]) { - (child)[PropertySymbol.connectedToDocument](); + child[PropertySymbol.connectedToDocument](); } // eslint-disable-next-line @@ -760,6 +776,7 @@ export default class Node extends EventTarget { * @param e */ public [PropertySymbol.disconnectedFromDocument](): void { + this[PropertySymbol.isConnected] = false; this[PropertySymbol.rootNode] = null; if (this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] === this) { @@ -771,7 +788,7 @@ export default class Node extends EventTarget { } for (const child of this[PropertySymbol.childNodes]) { - (child)[PropertySymbol.disconnectedFromDocument](); + child[PropertySymbol.disconnectedFromDocument](); } // eslint-disable-next-line diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts index 58a45376e..68c16a13d 100644 --- a/packages/happy-dom/src/nodes/node/NodeList.ts +++ b/packages/happy-dom/src/nodes/node/NodeList.ts @@ -205,6 +205,17 @@ class NodeList extends Array implements INodeList { public [PropertySymbol.includes](item: T): boolean { return super.includes(item); } + + /** + * Returns a shallow copy of a portion of an array into a new array object selected from start to end. + * + * @param [start] Start. + * @param [end] End. + * @returns A new array containing the extracted elements. + */ + public [PropertySymbol.slice](start?: number, end?: number): T[] { + return super.slice(start, end); + } } // Removes Array methods from NodeList. diff --git a/packages/happy-dom/src/nodes/parent-node/IParentNode.ts b/packages/happy-dom/src/nodes/parent-node/IParentNode.ts index 7fdc108bc..655ac4168 100644 --- a/packages/happy-dom/src/nodes/parent-node/IParentNode.ts +++ b/packages/happy-dom/src/nodes/parent-node/IParentNode.ts @@ -1,7 +1,7 @@ -import HTMLCollection from '../element/HTMLCollection2.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; import Element from '../element/Element.js'; import Node from '../node/Node.js'; -import NodeList from '../node/NodeList.js'; +import INodeList from '../node/INodeList.js'; import IHTMLElementTagNameMap from '../../config/IHTMLElementTagNameMap.js'; import ISVGElementTagNameMap from '../../config/ISVGElementTagNameMap.js'; @@ -9,7 +9,7 @@ export default interface IParentNode extends Node { readonly childElementCount: number; readonly firstElementChild: Element; readonly lastElementChild: Element; - readonly children: HTMLCollection; + readonly children: IHTMLCollection; /** * Inserts a set of Node objects or DOMString objects after the last child of the ParentNode. DOMString objects are inserted as equivalent Text nodes. @@ -47,11 +47,11 @@ export default interface IParentNode extends Node { */ querySelectorAll( selector: K - ): NodeList; + ): INodeList; querySelectorAll( selector: K - ): NodeList; - querySelectorAll(selector: string): NodeList; + ): INodeList; + querySelectorAll(selector: string): INodeList; /** * Query CSS selector to find matching nodes. @@ -59,7 +59,7 @@ export default interface IParentNode extends Node { * @param selector CSS selector. * @returns Matching elements. */ - querySelectorAll(selector: string): NodeList; + querySelectorAll(selector: string): INodeList; /** * Returns an elements by class name. @@ -67,7 +67,7 @@ export default interface IParentNode extends Node { * @param className Tag name. * @returns Matching element. */ - getElementsByClassName(className: string): HTMLCollection; + getElementsByClassName(className: string): IHTMLCollection; /** * Returns an elements by tag name. @@ -77,11 +77,11 @@ export default interface IParentNode extends Node { */ getElementsByTagName( tagName: K - ): HTMLCollection; + ): IHTMLCollection; getElementsByTagName( tagName: K - ): HTMLCollection; - getElementsByTagName(tagName: string): HTMLCollection; + ): IHTMLCollection; + getElementsByTagName(tagName: string): IHTMLCollection; /** * Returns an elements by tag name and namespace. @@ -93,12 +93,12 @@ export default interface IParentNode extends Node { getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/1999/xhtml', tagName: K - ): HTMLCollection; + ): IHTMLCollection; getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/2000/svg', tagName: K - ): HTMLCollection; - getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection; + ): IHTMLCollection; + getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection; /** * Replaces the existing children of a node with a specified new set of children. diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index 001423f15..5b86c7724 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -3,9 +3,11 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import Document from '../document/Document.js'; import Element from '../element/Element.js'; -import HTMLCollection from '../element/HTMLCollection2.js'; +import HTMLCollection from '../element/HTMLCollection.js'; import Node from '../node/Node.js'; import NamespaceURI from '../../config/NamespaceURI.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; +import NodeTypeEnum from '../node/NodeTypeEnum.js'; /** * Parent node utility. @@ -45,11 +47,13 @@ export default class ParentNodeUtility { const firstChild = parentNode.firstChild; for (const node of nodes) { if (typeof node === 'string') { - const newChildNodes = (( - XMLParser.parse(parentNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes].slice(); - for (const newChildNode of newChildNodes) { - parentNode.insertBefore(newChildNode, firstChild); + const childNodes = XMLParser.parse( + parentNode[PropertySymbol.ownerDocument], + node + )[PropertySymbol.childNodes]; + + while (childNodes.length) { + parentNode.insertBefore(childNodes[0], firstChild); } } else { parentNode.insertBefore(node, firstChild); @@ -67,9 +71,7 @@ export default class ParentNodeUtility { parentNode: Element | Document | DocumentFragment, ...nodes: (string | Node)[] ): void { - const childNodes = (parentNode)[PropertySymbol.childNodes][ - PropertySymbol.items - ]; + const childNodes = (parentNode)[PropertySymbol.childNodes]; while (childNodes.length) { parentNode.removeChild(childNodes[0]); @@ -88,20 +90,29 @@ export default class ParentNodeUtility { public static getElementsByClassName( parentNode: Element | DocumentFragment | Document, className: string - ): HTMLCollection { - const matches = new HTMLCollection(); - - for (const child of (parentNode)[PropertySymbol.children][ - PropertySymbol.items - ]) { - if (child.className.split(' ').includes(className)) { - matches[PropertySymbol.addItem](child); - } + ): IHTMLCollection { + const childNodes = parentNode[PropertySymbol.childNodesFlatten]; + const matches: IHTMLCollection = new HTMLCollection((item) => + (item).className.split(' ').includes(className) + ); + + childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { + matches[PropertySymbol.addItem](item); + }); + + childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { + matches[PropertySymbol.insertItem](item, referenceItem); + }); + childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { + matches[PropertySymbol.removeItem](item); + }); - for (const subChild of this.getElementsByClassName(child, className)[ - PropertySymbol.items - ]) { - matches[PropertySymbol.addItem](subChild); + for (const childNode of childNodes) { + if ( + childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (childNode).className.split(' ').includes(className) + ) { + matches[PropertySymbol.addItem](childNode); } } @@ -118,20 +129,33 @@ export default class ParentNodeUtility { public static getElementsByTagName( parentNode: Element | DocumentFragment | Document, tagName: string - ): HTMLCollection { + ): IHTMLCollection { const upperTagName = tagName.toUpperCase(); const includeAll = tagName === '*'; - let matches = new HTMLCollection(); + const childNodes = parentNode[PropertySymbol.childNodesFlatten]; + const matches: IHTMLCollection = new HTMLCollection( + !includeAll && ((item) => item[PropertySymbol.tagName] === upperTagName) + ); + + childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { + matches[PropertySymbol.addItem](item); + }); + + childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { + matches[PropertySymbol.insertItem](item, referenceItem); + }); - for (const child of (parentNode)[PropertySymbol.children][ - PropertySymbol.items - ]) { - if (includeAll || child[PropertySymbol.tagName].toUpperCase() === upperTagName) { - matches.push(child); + childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { + matches[PropertySymbol.removeItem](item); + }); + + for (const childNode of childNodes) { + if ( + (includeAll || childNode[PropertySymbol.tagName] === upperTagName) && + childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode + ) { + matches[PropertySymbol.addItem](childNode); } - matches = >( - matches.concat(this.getElementsByTagName(child, tagName)) - ); } return matches; @@ -149,22 +173,38 @@ export default class ParentNodeUtility { parentNode: Element | DocumentFragment | Document, namespaceURI: string, tagName: string - ): HTMLCollection { + ): IHTMLCollection { // When the namespace is HTML, the tag name is case-insensitive. const formattedTagName = namespaceURI === NamespaceURI.html ? tagName.toUpperCase() : tagName; const includeAll = tagName === '*'; - let matches = new HTMLCollection(); + const childNodes = parentNode[PropertySymbol.childNodesFlatten]; + const matches: IHTMLCollection = new HTMLCollection( + !includeAll && + ((item) => + item[PropertySymbol.tagName] === formattedTagName && + item[PropertySymbol.namespaceURI] === namespaceURI) + ); - for (const child of (parentNode)[PropertySymbol.children]) { + childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { + matches[PropertySymbol.addItem](item); + }); + + childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { + matches[PropertySymbol.insertItem](item, referenceItem); + }); + + childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { + matches[PropertySymbol.removeItem](item); + }); + + for (const childNode of childNodes) { if ( - (includeAll || child[PropertySymbol.tagName] === formattedTagName) && - child[PropertySymbol.namespaceURI] === namespaceURI + (includeAll || childNode[PropertySymbol.tagName] === formattedTagName) && + childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + childNode[PropertySymbol.namespaceURI] === namespaceURI ) { - matches.push(child); + matches[PropertySymbol.addItem](childNode); } - matches = >( - matches.concat(this.getElementsByTagNameNS(child, namespaceURI, tagName)) - ); } return matches; diff --git a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts index fdb7dad82..57b53c6db 100644 --- a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts +++ b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts @@ -13,7 +13,7 @@ import SVGElement from '../svg-element/SVGElement.js'; */ export default class ShadowRoot extends DocumentFragment { // Public properties - public cloneNode: (deep?: boolean) => ShadowRoot; + public declare cloneNode: (deep?: boolean) => ShadowRoot; // Events public onslotchange: (event: Event) => void | null = null; @@ -63,8 +63,10 @@ export default class ShadowRoot extends DocumentFragment { * @param html HTML. */ public set innerHTML(html: string) { - for (const child of this[PropertySymbol.childNodes].slice()) { - this.removeChild(child); + const childNodes = this[PropertySymbol.childNodes]; + + while (childNodes.length) { + this.removeChild(childNodes[0]); } XMLParser.parse(this[PropertySymbol.ownerDocument], html, { rootNode: this }); diff --git a/packages/happy-dom/src/nodes/svg-element/SVGSVGElement.ts b/packages/happy-dom/src/nodes/svg-element/SVGSVGElement.ts index 0d0abd61f..7fa8857ba 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGSVGElement.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGSVGElement.ts @@ -15,7 +15,7 @@ import * as PropertySymbol from '../../PropertySymbol.js'; */ export default class SVGSVGElement extends SVGGraphicsElement { // Public properties - public cloneNode: (deep?: boolean) => SVGSVGElement; + public declare cloneNode: (deep?: boolean) => SVGSVGElement; // Events public onafterprint: (event: Event) => void | null = null; diff --git a/packages/happy-dom/src/nodes/text/Text.ts b/packages/happy-dom/src/nodes/text/Text.ts index 67fcf1fc1..826693e72 100644 --- a/packages/happy-dom/src/nodes/text/Text.ts +++ b/packages/happy-dom/src/nodes/text/Text.ts @@ -10,7 +10,7 @@ import HTMLStyleElement from '../html-style-element/HTMLStyleElement.js'; * Text node. */ export default class Text extends CharacterData { - public cloneNode: (deep?: boolean) => Text; + public declare cloneNode: (deep?: boolean) => Text; public override [PropertySymbol.nodeType] = NodeTypeEnum.textNode; public override [PropertySymbol.textAreaNode]: HTMLTextAreaElement | null = null; public override [PropertySymbol.styleNode]: HTMLStyleElement | null = null; diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index 233006f96..2214d2212 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -10,6 +10,8 @@ import SelectorParser from './SelectorParser.js'; import ISelectorMatch from './ISelectorMatch.js'; import IHTMLElementTagNameMap from '../config/IHTMLElementTagNameMap.js'; import ISVGElementTagNameMap from '../config/ISVGElementTagNameMap.js'; +import IHTMLCollection from '../nodes/element/IHTMLCollection.js'; +import INodeList from '../nodes/node/INodeList.js'; type DocumentPositionAndElement = { documentPosition: string; @@ -73,7 +75,7 @@ export default class QuerySelector { public static querySelectorAll( node: Element | Document | DocumentFragment, selector: string - ): NodeList { + ): INodeList { if (selector === '') { throw new Error( `Failed to execute 'querySelectorAll' on '${node.constructor.name}': The provided selector is empty.` @@ -101,7 +103,7 @@ export default class QuerySelector { ); } - const nodeList = new NodeList(); + const nodeList: INodeList = new NodeList(); const matchesMap: { [position: string]: Element } = {}; for (let i = 0, max = matches.length; i < max; i++) { @@ -110,7 +112,7 @@ export default class QuerySelector { const keys = Object.keys(matchesMap).sort(); for (let i = 0, max = keys.length; i < max; i++) { - nodeList.push(matchesMap[keys[i]]); + nodeList[PropertySymbol.addItem](matchesMap[keys[i]]); } return nodeList; @@ -323,7 +325,7 @@ export default class QuerySelector { */ private static findAll( rootElement: Element, - children: Element[], + children: Element[] | IHTMLCollection, selectorItems: SelectorItem[], documentPosition?: string ): DocumentPositionAndElement[] { @@ -333,7 +335,7 @@ export default class QuerySelector { for (let i = 0, max = children.length; i < max; i++) { const child = children[i]; - const childrenOfChild = (child)[PropertySymbol.children][PropertySymbol.items]; + const childrenOfChild = (child)[PropertySymbol.children]; const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i); if (selectorItem.match(child)) { @@ -388,14 +390,14 @@ export default class QuerySelector { */ private static findFirst( rootElement: Element, - children: Element[], + children: Element[] | IHTMLCollection, selectorItems: SelectorItem[] ): Element { const selectorItem = selectorItems[0]; const nextSelectorItem = selectorItems[1]; for (const child of children) { - const childrenOfChild = (child)[PropertySymbol.children][PropertySymbol.items]; + const childrenOfChild = (child)[PropertySymbol.children]; if (selectorItem.match(child)) { if (!nextSelectorItem) { diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index fffd10913..de7eb6ee8 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -6,6 +6,7 @@ import SelectorCombinatorEnum from './SelectorCombinatorEnum.js'; import ISelectorAttribute from './ISelectorAttribute.js'; import ISelectorMatch from './ISelectorMatch.js'; import ISelectorPseudo from './ISelectorPseudo.js'; +import IHTMLCollection from '../nodes/element/IHTMLCollection.js'; /** * Selector item. @@ -191,7 +192,7 @@ export default class SelectorItem { */ private matchPseudoItem( element: Element, - parentChildren: Element[], + parentChildren: Element[] | IHTMLCollection, pseudo: ISelectorPseudo ): ISelectorMatch | null { switch (pseudo.name) { @@ -236,17 +237,22 @@ export default class SelectorItem { ? { priorityWeight: 10 } : null; case 'empty': - return !(element)[PropertySymbol.children][PropertySymbol.items].length - ? { priorityWeight: 10 } - : null; + return !(element)[PropertySymbol.children].length ? { priorityWeight: 10 } : null; case 'root': return element[PropertySymbol.tagName] === 'HTML' ? { priorityWeight: 10 } : null; case 'not': return !pseudo.selectorItems[0].match(element) ? { priorityWeight: 10 } : null; case 'nth-child': - const nthChildIndex = pseudo.selectorItems[0] - ? parentChildren.filter((child) => pseudo.selectorItems[0].match(child)).indexOf(element) - : parentChildren.indexOf(element); + let nthChildIndex = -1; + for (let i = 0, max = parentChildren.length; i < max; i++) { + if ( + (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) && + parentChildren[i] === element + ) { + nthChildIndex = i; + break; + } + } return nthChildIndex !== -1 && pseudo.nthFunction(nthChildIndex + 1) ? { priorityWeight: 10 } : null; @@ -254,27 +260,44 @@ export default class SelectorItem { if (!element[PropertySymbol.parentNode]) { return null; } - const nthOfTypeIndex = parentChildren - .filter((child) => child[PropertySymbol.tagName] === element[PropertySymbol.tagName]) - .indexOf(element); + let nthOfTypeIndex = -1; + for (let i = 0, max = parentChildren.length; i < max; i++) { + if ( + parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName] && + parentChildren[i] === element + ) { + nthOfTypeIndex = i; + break; + } + } return nthOfTypeIndex !== -1 && pseudo.nthFunction(nthOfTypeIndex + 1) ? { priorityWeight: 10 } : null; case 'nth-last-child': - const nthLastChildIndex = pseudo.selectorItems[0] - ? parentChildren - .filter((child) => pseudo.selectorItems[0].match(child)) - .reverse() - .indexOf(element) - : parentChildren.reverse().indexOf(element); + let nthLastChildIndex = -1; + for (let i = parentChildren.length - 1; i >= 0; i--) { + if ( + (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) && + parentChildren[i] === element + ) { + nthLastChildIndex = i; + break; + } + } return nthLastChildIndex !== -1 && pseudo.nthFunction(nthLastChildIndex + 1) ? { priorityWeight: 10 } : null; case 'nth-last-of-type': - const nthLastOfTypeIndex = parentChildren - .filter((child) => child[PropertySymbol.tagName] === element[PropertySymbol.tagName]) - .reverse() - .indexOf(element); + let nthLastOfTypeIndex = -1; + for (let i = parentChildren.length - 1; i >= 0; i--) { + if ( + parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName] && + parentChildren[i] === element + ) { + nthLastOfTypeIndex = i; + break; + } + } return nthLastOfTypeIndex !== -1 && pseudo.nthFunction(nthLastOfTypeIndex + 1) ? { priorityWeight: 10 } : null; diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 78767f346..0611a86ba 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -495,9 +495,9 @@ export default class Range { newNode = referenceNode[PropertySymbol.parentNode]; newOffset = - (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf( - referenceNode - ) + 1; + (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes][ + PropertySymbol.indexOf + ](referenceNode) + 1; } if ( @@ -656,9 +656,9 @@ export default class Range { newNode = referenceNode[PropertySymbol.parentNode]; newOffset = - (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf( - referenceNode - ) + 1; + (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes][ + PropertySymbol.indexOf + ](referenceNode) + 1; } if ( @@ -829,9 +829,9 @@ export default class Range { let newOffset = !referenceNode ? NodeUtility.getNodeLength(parent) - : (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf( - referenceNode - ); + : (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes][ + PropertySymbol.indexOf + ](referenceNode); newOffset += newNode[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode ? NodeUtility.getNodeLength(newNode) @@ -863,7 +863,7 @@ export default class Range { return true; } - const offset = (parent)[PropertySymbol.childNodes].indexOf(node); + const offset = (parent)[PropertySymbol.childNodes][PropertySymbol.indexOf](node); return ( RangeUtility.compareBoundaryPointsPosition( @@ -891,7 +891,9 @@ export default class Range { ); } - const index = (node[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf(node); + const index = (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][ + PropertySymbol.indexOf + ](node); this[PropertySymbol.start].node = node[PropertySymbol.parentNode]; this[PropertySymbol.start].offset = index; @@ -988,7 +990,9 @@ export default class Range { } this.setEnd( node[PropertySymbol.parentNode], - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf(node) + 1 + (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( + node + ) + 1 ); } @@ -1007,7 +1011,9 @@ export default class Range { } this.setEnd( node[PropertySymbol.parentNode], - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf(node) + (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( + node + ) ); } @@ -1026,7 +1032,9 @@ export default class Range { } this.setStart( node[PropertySymbol.parentNode], - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf(node) + 1 + (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( + node + ) + 1 ); } @@ -1045,7 +1053,9 @@ export default class Range { } this.setStart( node[PropertySymbol.parentNode], - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf(node) + (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( + node + ) ); } diff --git a/packages/happy-dom/src/range/RangeUtility.ts b/packages/happy-dom/src/range/RangeUtility.ts index 053674463..7b79acab9 100644 --- a/packages/happy-dom/src/range/RangeUtility.ts +++ b/packages/happy-dom/src/range/RangeUtility.ts @@ -51,8 +51,9 @@ export default class RangeUtility { } if ( - (child[PropertySymbol.parentNode])[PropertySymbol.childNodes].indexOf(child) < - pointA.offset + (child[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( + child + ) < pointA.offset ) { return 1; } diff --git a/packages/happy-dom/src/tree-walker/TreeWalker.ts b/packages/happy-dom/src/tree-walker/TreeWalker.ts index ca7790840..7cc678e12 100644 --- a/packages/happy-dom/src/tree-walker/TreeWalker.ts +++ b/packages/happy-dom/src/tree-walker/TreeWalker.ts @@ -137,7 +137,7 @@ export default class TreeWalker { const siblings = (this.currentNode[PropertySymbol.parentNode])[ PropertySymbol.childNodes ]; - const index = siblings.indexOf(this.currentNode); + const index = siblings[PropertySymbol.indexOf](this.currentNode); if (index > 0) { this.currentNode = siblings[index - 1]; @@ -167,7 +167,7 @@ export default class TreeWalker { const siblings = (this.currentNode[PropertySymbol.parentNode])[ PropertySymbol.childNodes ]; - const index = siblings.indexOf(this.currentNode); + const index = siblings[PropertySymbol.indexOf](this.currentNode); if (index + 1 < siblings.length) { this.currentNode = siblings[index + 1]; diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 1a177d6bc..7913e56cb 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -1,9 +1,10 @@ import { Buffer } from 'buffer'; import { webcrypto } from 'crypto'; +import VM from 'vm'; +import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; import Stream from 'stream'; import { ReadableStream } from 'stream/web'; import { URLSearchParams } from 'url'; -import VM from 'vm'; import * as PropertySymbol from '../PropertySymbol.js'; import Base64 from '../base64/Base64.js'; import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js'; @@ -189,7 +190,6 @@ import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; import XMLSerializer from '../xml-serializer/XMLSerializer.js'; import CrossOriginBrowserWindow from './CrossOriginBrowserWindow.js'; import INodeJSGlobal from './INodeJSGlobal.js'; -import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; import WindowErrorUtility from './WindowErrorUtility.js'; import WindowPageOpenUtility from './WindowPageOpenUtility.js'; @@ -475,65 +475,65 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public name = ''; // Node.js Globals - public Array: typeof Array; - public ArrayBuffer: typeof ArrayBuffer; - public Boolean: typeof Boolean; + public declare Array: typeof Array; + public declare ArrayBuffer: typeof ArrayBuffer; + public declare Boolean: typeof Boolean; public Buffer: typeof Buffer = Buffer; - public DataView: typeof DataView; - public Date: typeof Date; - public Error: typeof Error; - public EvalError: typeof EvalError; - public Float32Array: typeof Float32Array; - public Float64Array: typeof Float64Array; - public Function: typeof Function; - public Infinity: typeof Infinity; - public Int16Array: typeof Int16Array; - public Int32Array: typeof Int32Array; - public Int8Array: typeof Int8Array; - public Intl: typeof Intl; - public JSON: typeof JSON; - public Map: MapConstructor; - public Math: typeof Math; - public NaN: typeof NaN; - public Number: typeof Number; - public Object: typeof Object; - public Promise: typeof Promise; - public RangeError: typeof RangeError; - public ReferenceError: typeof ReferenceError; - public RegExp: typeof RegExp; - public Set: SetConstructor; - public String: typeof String; - public Symbol: Function; - public SyntaxError: typeof SyntaxError; - public TypeError: typeof TypeError; - public URIError: typeof URIError; - public Uint16Array: typeof Uint16Array; - public Uint32Array: typeof Uint32Array; - public Uint8Array: typeof Uint8Array; - public Uint8ClampedArray: typeof Uint8ClampedArray; - public WeakMap: WeakMapConstructor; - public WeakSet: WeakSetConstructor; - public decodeURI: typeof decodeURI; - public decodeURIComponent: typeof decodeURIComponent; - public encodeURI: typeof encodeURI; - public encodeURIComponent: typeof encodeURIComponent; - public eval: typeof eval; + public declare DataView: typeof DataView; + public declare Date: typeof Date; + public declare Error: typeof Error; + public declare EvalError: typeof EvalError; + public declare Float32Array: typeof Float32Array; + public declare Float64Array: typeof Float64Array; + public declare Function: typeof Function; + public declare Infinity: typeof Infinity; + public declare Int16Array: typeof Int16Array; + public declare Int32Array: typeof Int32Array; + public declare Int8Array: typeof Int8Array; + public declare Intl: typeof Intl; + public declare JSON: typeof JSON; + public declare Map: MapConstructor; + public declare Math: typeof Math; + public declare NaN: typeof NaN; + public declare Number: typeof Number; + public declare Object: typeof Object; + public declare Promise: typeof Promise; + public declare RangeError: typeof RangeError; + public declare ReferenceError: typeof ReferenceError; + public declare RegExp: typeof RegExp; + public declare Set: SetConstructor; + public declare String: typeof String; + public declare Symbol: Function; + public declare SyntaxError: typeof SyntaxError; + public declare TypeError: typeof TypeError; + public declare URIError: typeof URIError; + public declare Uint16Array: typeof Uint16Array; + public declare Uint32Array: typeof Uint32Array; + public declare Uint8Array: typeof Uint8Array; + public declare Uint8ClampedArray: typeof Uint8ClampedArray; + public declare WeakMap: WeakMapConstructor; + public declare WeakSet: WeakSetConstructor; + public declare decodeURI: typeof decodeURI; + public declare decodeURIComponent: typeof decodeURIComponent; + public declare encodeURI: typeof encodeURI; + public declare encodeURIComponent: typeof encodeURIComponent; + public declare eval: typeof eval; /** * @deprecated */ - public escape: (str: string) => string; - public global: typeof globalThis; - public isFinite: typeof isFinite; - public isNaN: typeof isNaN; - public parseFloat: typeof parseFloat; - public parseInt: typeof parseInt; - public undefined: typeof undefined; + public declare escape: (str: string) => string; + public declare global: typeof globalThis; + public declare isFinite: typeof isFinite; + public declare isNaN: typeof isNaN; + public declare parseFloat: typeof parseFloat; + public declare parseInt: typeof parseInt; + public declare undefined: typeof undefined; /** * @deprecated */ - public unescape: (str: string) => string; - public gc: () => void; - public v8debug?: unknown; + public declare unescape: (str: string) => string; + public declare gc: () => void; + public declare v8debug?: unknown; // Public internal properties @@ -557,7 +557,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal #outerWidth: number | null = null; #outerHeight: number | null = null; #devicePixelRatio: number | null = null; - #zeroTimeouts: Array | null = null; + #zeroDelayTimeout: { timeouts: Array | null } = { timeouts: null }; /** * Constructor. @@ -1067,32 +1067,39 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { // We can group timeouts with a delay of 0 into one timeout to improve performance. // Grouping timeouts will also improve the performance of the async task manager. - // It may also make the async task manager to stable as many timeouts may cause waitUntilComplete() to be resolved to early. + // It also makes the async task manager more stable as many timeouts may cause waitUntilComplete() to be resolved too early. if (!delay) { - if (!this.#zeroTimeouts) { + const zeroDelayTimeout = this.#zeroDelayTimeout; + + if (!zeroDelayTimeout.timeouts) { const settings = this.#browserFrame.page?.context?.browser?.settings; const useTryCatch = !settings || !settings.disableErrorCapturing || settings.errorCapture === BrowserErrorCaptureEnum.tryAndCatch; + const id = TIMER.setTimeout(() => { - const zeroTimeouts = this.#zeroTimeouts; - this.#zeroTimeouts = null; - for (const zeroTimeout of zeroTimeouts) { + const timeouts = zeroDelayTimeout.timeouts; + zeroDelayTimeout.timeouts = null; + for (const timeout of timeouts) { if (useTryCatch) { - WindowErrorUtility.captureError(this, () => zeroTimeout.callback()); + WindowErrorUtility.captureError(this, () => timeout.callback()); } else { - zeroTimeout.callback(); + timeout.callback(); } } this.#browserFrame[PropertySymbol.asyncTaskManager].endTimer(id); }); - this.#zeroTimeouts = []; + + zeroDelayTimeout.timeouts = []; this.#browserFrame[PropertySymbol.asyncTaskManager].startTimer(id); } - const zeroTimeout = new Timeout(() => callback(...args)); - this.#zeroTimeouts.push(zeroTimeout); - return (zeroTimeout); + + const timeout = new Timeout(() => callback(...args)); + + zeroDelayTimeout.timeouts.push(timeout); + + return (timeout); } const settings = this.#browserFrame.page?.context?.browser?.settings; @@ -1125,10 +1132,13 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal */ public clearTimeout(id: NodeJS.Timeout): void { if (id && id instanceof Timeout) { - const zeroTimeouts = this.#zeroTimeouts || []; - const index = zeroTimeouts.indexOf((id)); + const zeroDelayTimeout = this.#zeroDelayTimeout; + if (!zeroDelayTimeout.timeouts) { + return; + } + const index = zeroDelayTimeout.timeouts.indexOf((id)); if (index !== -1) { - zeroTimeouts.splice(index, 1); + zeroDelayTimeout.timeouts.splice(index, 1); } return; } @@ -1425,12 +1435,14 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this[PropertySymbol.mutationObservers] = []; // Disconnects nodes from the document, so that they can be garbage collected. - for (const node of this.document[PropertySymbol.childNodes].slice()) { + const childNodes = this.document[PropertySymbol.childNodes]; + + while (childNodes.length > 0) { // Makes sure that something won't be triggered by the disconnect. - if (node.disconnectedCallback) { - delete node.disconnectedCallback; + if (childNodes[0].disconnectedCallback) { + delete childNodes[0].disconnectedCallback; } - this.document.removeChild(node); + this.document.removeChild(childNodes[0]); } if (this.customElements[PropertySymbol.destroy]) { diff --git a/packages/happy-dom/test/CustomElement.ts b/packages/happy-dom/test/CustomElement.ts index 0d6e0ed1e..21e124a34 100644 --- a/packages/happy-dom/test/CustomElement.ts +++ b/packages/happy-dom/test/CustomElement.ts @@ -65,7 +65,7 @@ export default class CustomElement extends HTMLElement { 'key2' )}". - ${this.childNodes + ${Array.from(this.childNodes) .map( (child) => '#' + child['nodeType'] + (child['tagName'] || '') + child.textContent diff --git a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts index eef54a02f..0fa997c08 100644 --- a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts +++ b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts @@ -84,4 +84,67 @@ describe('HTMLFieldSetElement', () => { expect(div.children['button1']).toBe(element); }); }); + + describe('get elements()', () => { + it('Returns elements.', () => { + const form = document.createElement('form'); + + element.innerHTML = ` + + + + + + + + + + + `; + + form.appendChild(element); + + document.body.appendChild(form); + + expect(element.elements.length).toBe(10); + expect(element.elements[0]).toBe(element.children[0]); + expect(element.elements[1]).toBe(element.children[1]); + expect(element.elements[2]).toBe(element.children[2]); + expect(element.elements[3]).toBe(element.children[3]); + expect(element.elements[4]).toBe(element.children[4]); + expect(element.elements[5]).toBe(element.children[5]); + expect(element.elements[6]).toBe(element.children[6]); + expect(element.elements[7]).toBe(element.children[7]); + expect(element.elements[8]).toBe(element.children[8]); + expect(element.elements[9]).toBe(element.children[9]); + + expect(element.elements['text1']).toBe(element.children[0]); + expect(element.elements['textarea1']).toBe(element.children[2]); + expect(element.elements['checkbox1']).toBe(element.children[3]); + expect(element.elements['radio1']).toBe(element.children[6]); + expect(element.elements['button1']).toBe(element.children[9]); + + element.removeChild(element.children[0]); + element.removeChild(element.children[3]); + + expect(element.elements.length).toBe(8); + expect(element.elements[0]).toBe(element.children[0]); + expect(element.elements[1]).toBe(element.children[1]); + expect(element.elements[2]).toBe(element.children[2]); + expect(element.elements[3]).toBe(element.children[3]); + expect(element.elements[4]).toBe(element.children[4]); + expect(element.elements[5]).toBe(element.children[5]); + expect(element.elements[6]).toBe(element.children[6]); + expect(element.elements[7]).toBe(element.children[7]); + expect(element.elements['text1']).toBe(undefined); + expect(element.elements['textarea1']).toBe(element.children[2]); + expect(element.elements['checkbox1']).toBe(undefined); + expect(element.elements['radio1']).toBe(element.children[6]); + expect(element.elements['button1']).toBe(element.children[9]); + }); + }); }); diff --git a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts index 7a796590f..f1744cb97 100644 --- a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts +++ b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts @@ -13,6 +13,7 @@ import IRequestInfo from '../../../src/fetch/types/IRequestInfo.js'; import Headers from '../../../src/fetch/Headers.js'; import Browser from '../../../src/browser/Browser.js'; import DOMTokenList from '../../../src/dom-token-list/DOMTokenList.js'; +import BrowserErrorCaptureEnum from '../../../src/browser/enums/BrowserErrorCaptureEnum.js'; describe('HTMLIFrameElement', () => { let window: Window; @@ -269,7 +270,7 @@ describe('HTMLIFrameElement', () => { }); }); - it(`Does'nt load anything if the Happy DOM setting "disableIframePageLoading" is set to true.`, () => { + it(`Doesn't load anything if the Happy DOM setting "disableIframePageLoading" is set to true.`, () => { const browser = new Browser({ settings: { disableIframePageLoading: true } }); const page = browser.newPage(); const window = page.mainFrame.window; diff --git a/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithNamedSlots.ts b/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithNamedSlots.ts index 3345d6001..7d7ac1be8 100644 --- a/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithNamedSlots.ts +++ b/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithNamedSlots.ts @@ -20,6 +20,7 @@ export default class CustomElementWithNamedSlots extends HTMLElement {
+
`; } diff --git a/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts b/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts index 89a585a24..1f5c5eeb7 100644 --- a/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts +++ b/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts @@ -3,7 +3,7 @@ import Document from '../../../src/nodes/document/Document.js'; import HTMLSlotElement from '../../../src/nodes/html-slot-element/HTMLSlotElement.js'; import CustomElementWithNamedSlots from './CustomElementWithNamedSlots.js'; import CustomElementWithSlot from './CustomElementWithSlot.js'; -import NodeList from '../../../src/nodes/node/NodeList.js'; +import Event from '../../../src/event/Event.js'; import { beforeEach, describe, it, expect } from 'vitest'; describe('HTMLSlotElement', () => { @@ -56,82 +56,327 @@ describe('HTMLSlotElement', () => { }); describe('assignedNodes()', () => { - it('Returns nodes appended to the custom element.', () => { - const slot = customElementWithSlot.shadowRoot?.querySelector('slot'); - const text = document.createTextNode('text'); - const comment = document.createComment('text'); + it('Returns assigned nodes.', () => { + const slot1 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') + ); + const slot2 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot2"]') + ); + const slot3 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot:not([name])') + ); + + const slot1Element = document.createElement('div'); + slot1Element.setAttribute('slot', 'slot1'); + const slot2Element = document.createElement('div'); + slot2Element.setAttribute('slot', 'slot2'); + + const text1 = document.createTextNode('text1'); + const comment1 = document.createComment('comment1'); + const element1 = document.createElement('span'); + + slot1Element.appendChild(text1); + slot1Element.appendChild(comment1); + slot1Element.appendChild(element1); + + const text2 = document.createTextNode('text2'); + const comment2 = document.createComment('comment2'); + const element2 = document.createElement('span'); + + slot2Element.appendChild(text2); + slot2Element.appendChild(comment2); + slot2Element.appendChild(element2); + + const text3 = document.createTextNode('text3'); + const comment3 = document.createComment('comment3'); + const element3 = document.createElement('span'); + + customElementWithNamedSlots.appendChild(slot1Element); + customElementWithNamedSlots.appendChild(slot2Element); + customElementWithNamedSlots.appendChild(text3); + customElementWithNamedSlots.appendChild(comment3); + customElementWithNamedSlots.appendChild(element3); + + expect(slot1.assignedNodes()).toEqual([text1, comment1, element1]); + + expect(slot2.assignedNodes()).toEqual([text2, comment2, element2]); + + expect(slot3.assignedNodes()).toEqual([text3, comment3, element3]); + }); + }); + + describe('assignedElements()', () => { + it('Returns assigned elements.', () => { + const slot1 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') + ); + const slot2 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot2"]') + ); + const slot3 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot:not([name])') + ); + + const slot1Element = document.createElement('div'); + slot1Element.setAttribute('slot', 'slot1'); + const slot2Element = document.createElement('div'); + slot2Element.setAttribute('slot', 'slot2'); + + const text1 = document.createTextNode('text1'); + const comment1 = document.createComment('comment1'); + const element1 = document.createElement('span'); + + slot1Element.appendChild(text1); + slot1Element.appendChild(comment1); + slot1Element.appendChild(element1); + + const text2 = document.createTextNode('text2'); + const comment2 = document.createComment('comment2'); + const element2 = document.createElement('span'); + + slot2Element.appendChild(text2); + slot2Element.appendChild(comment2); + slot2Element.appendChild(element2); + + const text3 = document.createTextNode('text3'); + const comment3 = document.createComment('comment3'); + const element3 = document.createElement('span'); + + customElementWithNamedSlots.appendChild(slot1Element); + customElementWithNamedSlots.appendChild(slot2Element); + customElementWithNamedSlots.appendChild(text3); + customElementWithNamedSlots.appendChild(comment3); + customElementWithNamedSlots.appendChild(element3); + + expect(slot1.assignedNodes()).toEqual([element1]); + + expect(slot2.assignedNodes()).toEqual([element2]); + + expect(slot3.assignedNodes()).toEqual([element3]); + }); + }); + + describe('dispatchEvent()', () => { + it('Doesn\'n dispatch "slotchange" event when setting "name" attribute if assigned nodes isn\'t changed.', () => { + const slot = customElementWithNamedSlots.shadowRoot?.querySelector('slot'); + let dispatchedEvent: Event | null = null; + + slot.addEventListener('slotchange', (event) => (dispatchedEvent = event)); + slot.setAttribute('name', 'new-name'); + + expect(dispatchedEvent).toBe(null); + }); + + it('Doesn\'n dispatch "slotchange" event when changing "name" attribute if assigned nodes are changed, even if there is a child with matching "slot" attribute.', () => { + const slot = customElementWithNamedSlots.shadowRoot?.querySelector('slot'); + let dispatchedEvent: Event | null = null; + const div = document.createElement('div'); - const span = document.createElement('span'); + div.setAttribute('slot', 'slot1'); + customElementWithNamedSlots.appendChild(div); - customElementWithSlot.appendChild(text); - customElementWithSlot.appendChild(comment); - customElementWithSlot.appendChild(div); - customElementWithSlot.appendChild(span); + slot.addEventListener('slotchange', (event) => (dispatchedEvent = event)); + slot.setAttribute('name', 'new-name'); - expect(slot.assignedNodes()).toEqual(customElementWithSlot.childNodes); + expect(dispatchedEvent).toBe(null); }); - it('Only return elements that has the proeprty "slot" set to the same value as the property "name" of the slot.', () => { - const text = document.createTextNode('text'); - const comment = document.createComment('text'); + it('Dispatches "slotchange" event when changing "name" attribute if assigned nodes are changed.', () => { + const slot = customElementWithNamedSlots.shadowRoot?.querySelector('slot'); + let dispatchedEvent: Event | null = null; + const div = document.createElement('div'); + div.setAttribute('slot', 'slot1'); const span = document.createElement('span'); + div.appendChild(span); + customElementWithNamedSlots.appendChild(div); - div.slot = 'slot1'; + slot.addEventListener('slotchange', (event) => (dispatchedEvent = event)); + slot.setAttribute('name', 'new-name'); - customElementWithNamedSlots.appendChild(text); - customElementWithNamedSlots.appendChild(comment); - customElementWithNamedSlots.appendChild(div); - customElementWithNamedSlots.appendChild(span); + expect(((dispatchedEvent)).type).toBe('slotchange'); + }); - const slots = >( - customElementWithNamedSlots.shadowRoot.querySelectorAll('slot') + it('Dispatches "slotchange" event when changing "slot" attribute of slotted element if assigned nodes are changed.', () => { + const slot1 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') ); + const slot2 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot2"]') + ); + const slot3 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot:not([name])') + ); + let dispatchedEvent1: Event | null = null; + let dispatchedEvent2: Event | null = null; + const dispatchedEvent3: Event | null = null; + + const div = document.createElement('div'); + div.setAttribute('slot', 'slot1'); + const span = document.createElement('span'); + div.appendChild(span); + customElementWithNamedSlots.appendChild(div); - expect(slots[0].assignedNodes()).toEqual([div]); - expect(slots[1].assignedNodes()).toEqual([]); + slot1.addEventListener('slotchange', (event) => (dispatchedEvent1 = event)); + slot2.addEventListener('slotchange', (event) => (dispatchedEvent2 = event)); + slot3.addEventListener('slotchange', (event) => (dispatchedEvent2 = event)); + + div.setAttribute('slot', 'slot2'); + + expect(((dispatchedEvent1)).type).toBe('slotchange'); + expect(((dispatchedEvent2)).type).toBe('slotchange'); + expect(dispatchedEvent3).toBe(null); + + dispatchedEvent1 = null; + dispatchedEvent2 = null; + + div.removeAttribute('slot'); + + expect(dispatchedEvent1).toBe(null); + expect(dispatchedEvent2).toBe(null); + expect(((dispatchedEvent3)).type).toBe('slotchange'); }); - }); - describe('assignedElements()', () => { - it('Returns elements appended to the custom element.', () => { - const slot = customElementWithSlot.shadowRoot?.querySelector('slot'); - const text = document.createTextNode('text'); - const comment = document.createComment('text'); + it('Dispatches "slotchange" event adding an element to a named slot', () => { + const slot = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') + ); + let dispatchedEvent: Event | null = null; + const div = document.createElement('div'); + div.setAttribute('slot', 'slot1'); const span = document.createElement('span'); + div.appendChild(span); + customElementWithNamedSlots.appendChild(div); + + slot.addEventListener('slotchange', (event) => (dispatchedEvent = event)); + + const newNode = document.createElement('span'); - customElementWithSlot.appendChild(text); - customElementWithSlot.appendChild(comment); - customElementWithSlot.appendChild(div); - customElementWithSlot.appendChild(span); + div.appendChild(newNode); - expect(slot.assignedElements()).toEqual(customElementWithSlot.children); + expect(((dispatchedEvent)).type).toBe('slotchange'); }); - it('Only return elements that has the proeprty "slot" set to the same value as the property "name" of the slot.', () => { - const text = document.createTextNode('text'); - const comment = document.createComment('text'); + it('Dispatches "slotchange" event adding a text node to a named slot', () => { + const slot = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') + ); + let dispatchedEvent: Event | null = null; + const div = document.createElement('div'); - const span1 = document.createElement('span'); - const span2 = document.createElement('span'); + div.setAttribute('slot', 'slot1'); + const span = document.createElement('span'); + div.appendChild(span); + customElementWithNamedSlots.appendChild(div); + + slot.addEventListener('slotchange', (event) => (dispatchedEvent = event)); + + const newNode = document.createTextNode('test'); - div.slot = 'slot1'; - span1.slot = 'slot1'; - span2.slot = 'slot2'; + div.appendChild(newNode); - customElementWithNamedSlots.appendChild(text); - customElementWithNamedSlots.appendChild(comment); + expect(((dispatchedEvent)).type).toBe('slotchange'); + }); + + it('Doesn\'t dispatch "slotchange" event adding a comment node to a named slot', () => { + const slot = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') + ); + let dispatchedEvent: Event | null = null; + + const div = document.createElement('div'); + div.setAttribute('slot', 'slot1'); + const span = document.createElement('span'); + div.appendChild(span); customElementWithNamedSlots.appendChild(div); - customElementWithNamedSlots.appendChild(span1); - customElementWithNamedSlots.appendChild(span2); - const slots = >( - customElementWithNamedSlots.shadowRoot.querySelectorAll('slot') + slot.addEventListener('slotchange', (event) => (dispatchedEvent = event)); + + const newNode = document.createComment('test'); + + div.appendChild(newNode); + + expect(dispatchedEvent).toBe(null); + }); + + it('Dispatches "slotchange" event adding an element to a default slot', () => { + const slot1 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') + ); + const slot2 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot2"]') + ); + const slot3 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot:not([name])') + ); + let dispatchedEvent1: Event | null = null; + let dispatchedEvent2: Event | null = null; + const dispatchedEvent3: Event | null = null; + + slot1.addEventListener('slotchange', (event) => (dispatchedEvent1 = event)); + slot2.addEventListener('slotchange', (event) => (dispatchedEvent2 = event)); + slot3.addEventListener('slotchange', (event) => (dispatchedEvent2 = event)); + + const newNode = document.createElement('span'); + customElementWithNamedSlots.appendChild(newNode); + + expect(dispatchedEvent1).toBe(null); + expect(dispatchedEvent2).toBe(null); + expect(((dispatchedEvent3)).type).toBe('slotchange'); + }); + + it('Dispatches "slotchange" event adding a text node to a default slot', () => { + const slot1 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') ); + const slot2 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot2"]') + ); + const slot3 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot:not([name])') + ); + let dispatchedEvent1: Event | null = null; + let dispatchedEvent2: Event | null = null; + const dispatchedEvent3: Event | null = null; + + slot1.addEventListener('slotchange', (event) => (dispatchedEvent1 = event)); + slot2.addEventListener('slotchange', (event) => (dispatchedEvent2 = event)); + slot3.addEventListener('slotchange', (event) => (dispatchedEvent2 = event)); + + const newNode = document.createTextNode('test'); + customElementWithNamedSlots.appendChild(newNode); + + expect(dispatchedEvent1).toBe(null); + expect(dispatchedEvent2).toBe(null); + expect(((dispatchedEvent3)).type).toBe('slotchange'); + }); + + it('Doesn\'t dispatch "slotchange" event adding a comment node to a default slot', () => { + const slot1 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot1"]') + ); + const slot2 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot[name="slot2"]') + ); + const slot3 = ( + customElementWithNamedSlots.shadowRoot?.querySelector('slot:not([name])') + ); + let dispatchedEvent1: Event | null = null; + let dispatchedEvent2: Event | null = null; + const dispatchedEvent3: Event | null = null; + + slot1.addEventListener('slotchange', (event) => (dispatchedEvent1 = event)); + slot2.addEventListener('slotchange', (event) => (dispatchedEvent2 = event)); + slot3.addEventListener('slotchange', (event) => (dispatchedEvent2 = event)); + + const newNode = document.createComment('test'); + customElementWithNamedSlots.appendChild(newNode); - expect(slots[0].assignedElements()).toEqual([div, span1]); - expect(slots[1].assignedElements()).toEqual([span2]); + expect(dispatchedEvent1).toBe(null); + expect(dispatchedEvent2).toBe(null); + expect(dispatchedEvent3).toBe(null); }); }); }); diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts index 905c86a06..1483f550a 100644 --- a/packages/happy-dom/test/nodes/node/Node.test.ts +++ b/packages/happy-dom/test/nodes/node/Node.test.ts @@ -327,12 +327,12 @@ describe('Node', () => { it('Returns "false" if match node is null.', () => { const div = document.createElement('div'); - expect(div.contains(null)).toBe(false); + expect(div.contains((null))).toBe(false); }); it('Returns "false" if match node is undefined.', () => { const div = document.createElement('div'); - expect(div.contains(undefined)).toBe(false); + expect(div.contains((undefined))).toBe(false); }); }); @@ -424,7 +424,7 @@ describe('Node', () => { expect(div !== clone).toBe(true); expect(Array.from(clone.children)).toEqual( - Array.from(clone.childNodes.filter((node) => node.nodeType === Node.ELEMENT_NODE)) + Array.from(clone.childNodes).filter((node) => node.nodeType === Node.ELEMENT_NODE) ); }); @@ -1020,7 +1020,7 @@ describe('Node', () => { expect( document .getElementById('element') - .compareDocumentPosition(document.getElementById('element')) + ?.compareDocumentPosition(document.getElementById('element')) ).toEqual(0); }); @@ -1036,7 +1036,9 @@ describe('Node', () => { document.body.appendChild(div); expect( - document.getElementById('span1').compareDocumentPosition(document.getElementById('span2')) + document + .getElementById('span1') + ?.compareDocumentPosition(document.getElementById('span2')) ).toEqual(4); }); @@ -1052,7 +1054,9 @@ describe('Node', () => { document.body.appendChild(div); expect( - document.getElementById('span2').compareDocumentPosition(document.getElementById('span1')) + document + .getElementById('span2') + ?.compareDocumentPosition(document.getElementById('span1')) ).toEqual(2); }); @@ -1067,7 +1071,7 @@ describe('Node', () => { const position = document .getElementById('parent') - .compareDocumentPosition(document.getElementById('child')); + ?.compareDocumentPosition(document.getElementById('child')); expect(position).toEqual(20); }); @@ -1082,7 +1086,7 @@ describe('Node', () => { const position = document .getElementById('child') - .compareDocumentPosition(document.getElementById('parent')); + ?.compareDocumentPosition(document.getElementById('parent')); expect(position).toEqual(10); }); }); diff --git a/packages/happy-dom/test/nodes/parent-node/ParentNodeUtility.test.ts b/packages/happy-dom/test/nodes/parent-node/ParentNodeUtility.test.ts index b48de0396..95e359719 100644 --- a/packages/happy-dom/test/nodes/parent-node/ParentNodeUtility.test.ts +++ b/packages/happy-dom/test/nodes/parent-node/ParentNodeUtility.test.ts @@ -26,7 +26,11 @@ describe('ParentNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -45,7 +49,11 @@ describe('ParentNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -63,7 +71,11 @@ describe('ParentNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -82,7 +94,11 @@ describe('ParentNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -106,7 +122,11 @@ describe('ParentNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); From 240821cb535151608fca5ddc20273100accb97e6 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 13 Jun 2024 01:42:28 +0200 Subject: [PATCH 14/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 2 +- .../src/nodes/character-data/CharacterData.ts | 6 +- .../happy-dom/src/nodes/document/Document.ts | 2 - .../happy-dom/src/nodes/element/Element.ts | 12 +- .../html-form-element/HTMLFormElement.ts | 3 +- .../IHTMLFormControlsCollection.ts | 6 + .../HTMLLabelElementUtility.ts | 9 +- .../html-option-element/HTMLOptionElement.ts | 6 +- .../HTMLOptionsCollection.ts | 121 ++++++++++++++---- .../html-select-element/HTMLSelectElement.ts | 85 ------------ packages/happy-dom/src/nodes/node/Node.ts | 29 +++++ .../HTMLSelectElement.test.ts | 4 +- .../HTMLTextAreaElement.test.ts | 4 +- 13 files changed, 161 insertions(+), 128 deletions(-) create mode 100644 packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 550cba705..263656eb7 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -3,7 +3,6 @@ export const activeElement = Symbol('activeElement'); export const asyncTaskManager = Symbol('asyncTaskManager'); export const bodyBuffer = Symbol('bodyBuffer'); export const buffer = Symbol('buffer'); -export const cacheID = Symbol('cacheID'); export const cachedResponse = Symbol('cachedResponse'); export const callbacks = Symbol('callbacks'); export const captureEventListenerCount = Symbol('captureEventListenerCount'); @@ -194,3 +193,4 @@ export const selectedOptions = Symbol('selectedOptions'); export const styleNode = Symbol('styleNode'); export const updateSheet = Symbol('updateSheet'); export const slice = Symbol('slice'); +export const mutationCacheID = Symbol('mutationCacheID'); diff --git a/packages/happy-dom/src/nodes/character-data/CharacterData.ts b/packages/happy-dom/src/nodes/character-data/CharacterData.ts index 4447c87e9..1bb41d851 100644 --- a/packages/happy-dom/src/nodes/character-data/CharacterData.ts +++ b/packages/happy-dom/src/nodes/character-data/CharacterData.ts @@ -62,8 +62,10 @@ export default abstract class CharacterData const oldValue = this[PropertySymbol.data]; this[PropertySymbol.data] = String(data); - if (this[PropertySymbol.isConnected]) { - this[PropertySymbol.ownerDocument][PropertySymbol.cacheID]++; + let parent: Node = this; + while (parent) { + parent[PropertySymbol.mutationCacheID].characterData++; + parent = parent[PropertySymbol.parentNode]; } // MutationObserver diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index b6a40fdb1..4d8510aa9 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -58,8 +58,6 @@ export default class Document extends Node { public [PropertySymbol.nextActiveElement]: HTMLElement | SVGElement = null; public [PropertySymbol.currentScript]: HTMLScriptElement = null; public [PropertySymbol.rootNode] = this; - // Used as an unique identifier which is updated whenever the DOM gets modified. - public [PropertySymbol.cacheID] = 0; public [PropertySymbol.isFirstWrite] = true; public [PropertySymbol.isFirstWriteAfterOpen] = false; public [PropertySymbol.nodeType] = NodeTypeEnum.documentNode; diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 8f55496fc..d11c33849 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -1205,8 +1205,10 @@ export default class Element return null; } - if (this[PropertySymbol.isConnected]) { - this.ownerDocument[PropertySymbol.cacheID]++; + let parent: Node = this; + while (parent) { + parent[PropertySymbol.mutationCacheID].attributes++; + parent = parent[PropertySymbol.parentNode]; } const oldValue = replacedAttribute ? replacedAttribute[PropertySymbol.value] : null; @@ -1278,8 +1280,10 @@ export default class Element * @param removedAttribute Attribute. */ #onRemoveAttribute(removedAttribute: Attr): void { - if (this[PropertySymbol.isConnected]) { - this.ownerDocument[PropertySymbol.cacheID]++; + let parent: Node = this; + while (parent) { + parent[PropertySymbol.mutationCacheID].attributes++; + parent = parent[PropertySymbol.parentNode]; } if (removedAttribute[PropertySymbol.name] === 'class' && this[PropertySymbol.classList]) { diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index 107ea4927..f2f72ff9c 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -14,6 +14,7 @@ import Element from '../element/Element.js'; import BrowserWindow from '../../window/BrowserWindow.js'; import Attr from '../attr/Attr.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; +import IHTMLFormControlsCollection from './IHTMLFormControlsCollection.js'; /** * HTML Form Element. @@ -117,7 +118,7 @@ export default class HTMLFormElement extends HTMLElement { * * @returns Elements. */ - public get elements(): HTMLFormControlsCollection { + public get elements(): IHTMLFormControlsCollection { return this[PropertySymbol.elements]; } diff --git a/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts new file mode 100644 index 000000000..a41df25b9 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts @@ -0,0 +1,6 @@ +import IHTMLCollection from '../element/IHTMLCollection.js'; +import RadioNodeList from './RadioNodeList.js'; +import THTMLFormControlElement from './THTMLFormControlElement.js'; + +export default interface IHTMLFormControlsCollection + extends IHTMLCollection {} diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts index e42208ce4..a92231b1c 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts @@ -4,6 +4,7 @@ import HTMLLabelElement from './HTMLLabelElement.js'; import NodeList from '../node/NodeList.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; import * as PropertySymbol from '../../PropertySymbol.js'; +import INodeList from '../node/INodeList.js'; /** * Utility for finding labels associated with a form element. @@ -15,14 +16,14 @@ export default class HTMLLabelElementUtility { * @param element Element to get labels for. * @returns Label elements. */ - public static getAssociatedLabelElements(element: HTMLElement): NodeList { + public static getAssociatedLabelElements(element: HTMLElement): INodeList { const id = element.id; - let labels: NodeList; + let labels: INodeList; if (id && element[PropertySymbol.isConnected]) { const rootNode = element[PropertySymbol.rootNode] || element[PropertySymbol.ownerDocument]; - labels = >rootNode.querySelectorAll(`label[for="${id}"]`); + labels = >rootNode.querySelectorAll(`label[for="${id}"]`); } else { labels = new NodeList(); } @@ -30,7 +31,7 @@ export default class HTMLLabelElementUtility { let parent = element[PropertySymbol.parentNode]; while (parent) { if (parent['tagName'] === 'LABEL') { - labels.push(parent); + labels[PropertySymbol.addItem](parent); break; } parent = parent[PropertySymbol.parentNode]; diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index 1d00c32b8..d8f3ae9ad 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -89,7 +89,7 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = Boolean(selected); if (selectNode) { - selectNode[PropertySymbol.updateSelectedness]( + selectNode[PropertySymbol.options][PropertySymbol.updateSelectedness]( this[PropertySymbol.selectedness] ? this : null ); } @@ -152,7 +152,7 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = true; if (selectNode) { - selectNode[PropertySymbol.updateSelectedness](this); + selectNode[PropertySymbol.options][PropertySymbol.updateSelectedness](this); } } } @@ -173,7 +173,7 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = false; if (selectNode) { - selectNode[PropertySymbol.updateSelectedness](); + selectNode[PropertySymbol.options][PropertySymbol.updateSelectedness](); } } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts index 15b5c7a4a..794b1fa0c 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts @@ -4,6 +4,8 @@ import HTMLSelectElement from './HTMLSelectElement.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; import Element from '../element/Element.js'; import * as PropertySymbol from '../../PropertySymbol.js'; +import HTMLElement from '../html-element/HTMLElement.js'; +import NodeTypeEnum from '../node/NodeTypeEnum.js'; /** * HTML Options Collection. @@ -12,7 +14,7 @@ import * as PropertySymbol from '../../PropertySymbol.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection. */ export default class HTMLOptionsCollection extends HTMLCollection { - #selectedIndex: number | null = null; + #selectedIndex: number = -1; #selectElement: HTMLSelectElement; /** @@ -31,18 +33,7 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[PropertySymbol.options][i])[PropertySymbol.selectedness]) { - this.#selectedIndex = i; - return i; - } - } - this.#selectedIndex = -1; - return -1; + return this.#selectedIndex; } /** @@ -55,19 +46,17 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[PropertySymbol.options][i])[PropertySymbol.selectedness] = false; - } - - const selectedOption = this[PropertySymbol.options][selectedIndex]; + const selectedOption = this[selectedIndex]; if (!selectedOption) { + this.#selectedIndex = -1; return; } selectedOption[PropertySymbol.selectedness] = true; selectedOption[PropertySymbol.dirtyness] = true; - this.#selectedIndex = selectedIndex; + + this[PropertySymbol.updateSelectedness](selectedOption); } /** @@ -135,7 +124,8 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[i]; + if (!isMultiple) { + if (selectedOption) { + option[PropertySymbol.selectedness] = option === selectedOption; + } + + if (option[PropertySymbol.selectedness]) { + selected.push(option); + + if (this.#selectedIndex === null) { + this.#selectedIndex = i; + } + + if (!selectedOptions[PropertySymbol.includes](option)) { + selectedOptions[PropertySymbol.addItem](option); + } + } else { + selectedOptions[PropertySymbol.removeItem](option); + } + } + } + + const size = this.#getDisplaySize(); + + if (size === 1 && !selected.length) { + for (let i = 0, max = this.length; i < max; i++) { + const option = this[i]; + const parentNode = option[PropertySymbol.parentNode]; + let disabled = option.hasAttributeNS(null, 'disabled'); + + if ( + parentNode && + parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + parentNode[PropertySymbol.tagName] === 'OPTGROUP' && + parentNode.hasAttributeNS(null, 'disabled') + ) { + disabled = true; + } + + if (!disabled) { + this.#selectedIndex = i; + option[PropertySymbol.selectedness] = true; + break; + } + } + } else if (selected.length >= 2) { + for (let i = 0, max = this.length; i < max; i++) { + (this[i])[PropertySymbol.selectedness] = i === selected.length - 1; + } + } + } + + /** + * Returns display size. + * + * @returns Display size. + */ + #getDisplaySize(): number { + const selectElement = this.#selectElement; + if (selectElement.hasAttributeNS(null, 'size')) { + const size = parseInt(selectElement.getAttribute('size')); + if (!isNaN(size) && size >= 0) { + return size; + } + } + return selectElement.hasAttributeNS(null, 'multiple') ? 4 : 1; + } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index e6c40c428..2ba19d63d 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -12,7 +12,6 @@ import HTMLCollection from '../element/HTMLCollection.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; import Element from '../element/Element.js'; import NodeList from '../node/INodeList.js'; -import NodeTypeEnum from '../node/NodeTypeEnum.js'; /** * HTML Select Element. @@ -44,27 +43,21 @@ export default class HTMLSelectElement extends HTMLElement { // Child nodes listeners this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { - (item)[PropertySymbol.selectNode] = this; this[PropertySymbol.options][PropertySymbol.addItem](item); - this[PropertySymbol.updateSelectedness](); }); this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( 'insert', (newItem: Node, referenceItem: Node | null) => { - (newItem)[PropertySymbol.selectNode] = this; this[PropertySymbol.options][PropertySymbol.insertItem]( newItem, referenceItem ); - this[PropertySymbol.updateSelectedness](); } ); this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( 'remove', (item: Node) => { - (item)[PropertySymbol.selectNode] = null; this[PropertySymbol.options][PropertySymbol.removeItem](item); - this[PropertySymbol.updateSelectedness](); } ); @@ -402,82 +395,4 @@ export default class HTMLSelectElement extends HTMLElement { public reportValidity(): boolean { return this.checkValidity(); } - - /** - * Updates option item. - * - * Based on: - * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLSelectElement-impl.js - * - * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm - * @param [selectedOption] Selected option. - */ - public [PropertySymbol.updateSelectedness](selectedOption?: HTMLOptionElement): void { - const options = this[PropertySymbol.options]; - const isMultiple = this.hasAttribute('multiple'); - const selectedOptions = this[PropertySymbol.selectedOptions]; - const selected: HTMLOptionElement[] = []; - - for (let i = 0, max = options.length; i < max; i++) { - const option = options[i]; - if (!isMultiple) { - if (selectedOption) { - option[PropertySymbol.selectedness] = option === selectedOption; - } - - if (option[PropertySymbol.selectedness]) { - selected.push(option); - - if (!selectedOptions[PropertySymbol.includes](option)) { - selectedOptions[PropertySymbol.addItem](option); - } - } else { - selectedOptions[PropertySymbol.removeItem](selectedOptions[0]); - } - } - } - - const size = this.#getDisplaySize(); - - if (size === 1 && !selected.length) { - for (let i = 0, max = options.length; i < max; i++) { - const option = options[i]; - const parentNode = option[PropertySymbol.parentNode]; - let disabled = option.hasAttributeNS(null, 'disabled'); - - if ( - parentNode && - parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - parentNode[PropertySymbol.tagName] === 'OPTGROUP' && - parentNode.hasAttributeNS(null, 'disabled') - ) { - disabled = true; - } - - if (!disabled) { - option[PropertySymbol.selectedness] = true; - break; - } - } - } else if (selected.length >= 2) { - for (let i = 0, max = options.length; i < max; i++) { - (options[i])[PropertySymbol.selectedness] = i === selected.length - 1; - } - } - } - - /** - * Returns display size. - * - * @returns Display size. - */ - #getDisplaySize(): number { - if (this.hasAttributeNS(null, 'size')) { - const size = parseInt(this.getAttribute('size')); - if (!isNaN(size) && size >= 0) { - return size; - } - } - return this.hasAttributeNS(null, 'multiple') ? 4 : 1; - } } diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 172236533..67e7fc72c 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -66,6 +66,17 @@ export default class Node extends EventTarget { public [PropertySymbol.observers]: MutationListener[] = []; public [PropertySymbol.childNodes]: INodeList = new NodeList(); public [PropertySymbol.childNodesFlatten]: INodeList = new NodeList(); + public [PropertySymbol.mutationCacheID]: { + attributes: number; + childNodes: number; + children: number; + characterData: number; + } = { + attributes: 0, + childNodes: 0, + children: 0, + characterData: 0 + }; /** * Constructor. @@ -99,6 +110,12 @@ export default class Node extends EventTarget { childNodesFlatten[PropertySymbol.addItem](child); } + parent[PropertySymbol.mutationCacheID].childNodes++; + + if (parent[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + parent[PropertySymbol.mutationCacheID].children++; + } + parent = parent[PropertySymbol.parentNode]; } }); @@ -114,6 +131,12 @@ export default class Node extends EventTarget { childNodesFlatten[PropertySymbol.insertItem](child, referenceItem); } + parent[PropertySymbol.mutationCacheID].childNodes++; + + if (parent[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + parent[PropertySymbol.mutationCacheID].children++; + } + parent = parent[PropertySymbol.parentNode]; } }); @@ -128,6 +151,12 @@ export default class Node extends EventTarget { childNodesFlatten[PropertySymbol.removeItem](child); } + parent[PropertySymbol.mutationCacheID].childNodes++; + + if (parent[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + parent[PropertySymbol.mutationCacheID].children++; + } + parent = parent[PropertySymbol.parentNode]; } }); diff --git a/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts b/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts index c9e5d719c..d8f443c5b 100644 --- a/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts +++ b/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts @@ -168,7 +168,7 @@ describe('HTMLSelectElement', () => { expect(element.selectedOptions.length).toBe(0); }); - it('Returns selected options with "selected" attribute is defined.', () => { + it('Returns selected options when "selected" attribute is defined.', () => { const option1 = document.createElement('option'); const option2 = document.createElement('option'); @@ -191,7 +191,7 @@ describe('HTMLSelectElement', () => { expect(element.selectedOptions[0]).toBe(option1); }); - it('Multiple - Returns selected options with "selected" attribute is defined.', () => { + it('Returns selected options when "selected" attribute is defined for multiple options.', () => { element.setAttribute('multiple', ''); const option1 = document.createElement('option'); const option2 = document.createElement('option'); diff --git a/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts b/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts index 4a84b55f8..41293ac11 100644 --- a/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts +++ b/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts @@ -130,7 +130,7 @@ describe('HTMLTextAreaElement', () => { expect(element.form).toBe(null); document.body.appendChild(element); expect(element.form).toBe(form); - expect(form.elements.includes(element)).toBe(true); + expect(Array.from(form.elements).includes(element)).toBe(true); }); it('Returns form element by id if the form attribute is set when element is connected to DOM.', () => { @@ -140,7 +140,7 @@ describe('HTMLTextAreaElement', () => { document.body.appendChild(element); element.setAttribute('form', 'form'); expect(element.form).toBe(form); - expect(form.elements.includes(element)).toBe(true); + expect(Array.from(form.elements).includes(element)).toBe(true); }); }); From b0afa42ad38e20eed78be4ebfb9fe69c39a3688d Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 20 Jun 2024 01:59:48 +0200 Subject: [PATCH 15/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 20 +- .../AbstractCSSStyleDeclaration.ts | 12 - .../CSSStyleDeclarationElementStyle.ts | 54 +-- .../mutation-observer/IMutationListener.ts | 7 + .../src/mutation-observer/MutationObserver.ts | 10 +- ...istener.ts => MutationObserverListener.ts} | 10 +- .../src/nodes/character-data/CharacterData.ts | 27 +- .../document-fragment/DocumentFragment.ts | 16 +- .../happy-dom/src/nodes/document/Document.ts | 66 ++-- .../happy-dom/src/nodes/element/Element.ts | 152 +++++---- .../src/nodes/element/HTMLCollection.ts | 285 ++++++++++------ .../src/nodes/element/IHTMLCollection.ts | 46 +-- .../src/nodes/element/NamedNodeMap.ts | 4 - .../HTMLDataListElement.ts | 42 +-- .../src/nodes/html-element/HTMLElement.ts | 79 +---- .../HTMLFieldSetElement.ts | 39 +-- .../HTMLFormControlsCollection.ts | 69 +--- .../html-form-element/HTMLFormElement.ts | 268 ++++++--------- .../html-link-element/HTMLLinkElement.ts | 1 + .../HTMLOptionsCollection.ts | 51 ++- .../html-select-element/HTMLSelectElement.ts | 72 +--- .../html-style-element/HTMLStyleElement.ts | 38 +-- .../HTMLTextAreaElement.ts | 40 +-- .../src/nodes/node/ICachedMatchesItem.ts | 5 + .../nodes/node/ICachedQuerySelectorAllItem.ts | 6 + .../nodes/node/ICachedQuerySelectorItem.ts | 5 + .../happy-dom/src/nodes/node/INodeList.ts | 39 +-- packages/happy-dom/src/nodes/node/Node.ts | 312 ++++++++++-------- packages/happy-dom/src/nodes/node/NodeList.ts | 86 +---- .../nodes/parent-node/ParentNodeUtility.ts | 130 ++------ .../src/query-selector/QuerySelector.ts | 128 +++++-- .../nodes/html-element/HTMLElement.test.ts | 4 +- .../parent-node/ParentNodeUtility.test.ts | 44 +-- 33 files changed, 966 insertions(+), 1201 deletions(-) create mode 100644 packages/happy-dom/src/mutation-observer/IMutationListener.ts rename packages/happy-dom/src/mutation-observer/{MutationListener.ts => MutationObserverListener.ts} (86%) create mode 100644 packages/happy-dom/src/nodes/node/ICachedMatchesItem.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllItem.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedQuerySelectorItem.ts diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 263656eb7..1852eeee2 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -41,9 +41,10 @@ export const listenerOptions = Symbol('listenerOptions'); export const listeners = Symbol('listeners'); export const namedItems = Symbol('namedItems'); export const nextActiveElement = Symbol('nextActiveElement'); -export const observe = Symbol('observe'); +export const observeMutations = Symbol('observeMutations'); +export const observeMutationsOnce = Symbol('observeMutationsOnce'); export const observedAttributes = Symbol('observedAttributes'); -export const observers = Symbol('observers'); +export const mutationListeners = Symbol('mutationListeners'); export const ownerDocument = Symbol('ownerDocument'); export const ownerElement = Symbol('ownerElement'); export const propagationStopped = Symbol('propagationStopped'); @@ -62,7 +63,8 @@ export const start = Symbol('start'); export const style = Symbol('style'); export const target = Symbol('target'); export const textAreaNode = Symbol('textAreaNode'); -export const unobserve = Symbol('unobserve'); +export const unobserveMutations = Symbol('unobserveMutations'); +export const reportMutation = Symbol('reportMutation'); export const updateIndices = Symbol('updateIndices'); export const updateSelectedness = Symbol('updateSelectedness'); export const url = Symbol('url'); @@ -179,7 +181,6 @@ export const items = Symbol('items'); export const removeItemIndex = Symbol('removeItemIndex'); export const indexOf = Symbol('indexOf'); export const updateNamedItem = Symbol('updateNamedItem'); -export const childNodesFlatten = Symbol('childNodesFlatten'); export const includes = Symbol('includes'); export const insertItem = Symbol('insertItem'); export const addEventListener = Symbol('addEventListener'); @@ -193,4 +194,13 @@ export const selectedOptions = Symbol('selectedOptions'); export const styleNode = Symbol('styleNode'); export const updateSheet = Symbol('updateSheet'); export const slice = Symbol('slice'); -export const mutationCacheID = Symbol('mutationCacheID'); +export const replaceItems = Symbol('replaceItems'); +export const clearItems = Symbol('clearItems'); +export const addItems = Symbol('addItems'); +export const attachedHTMLCollection = Symbol('attachedHTMLCollection'); +export const querySelectorCache = Symbol('querySelectorCache'); +export const querySelectorAllCache = Symbol('querySelectorAllCache'); +export const matchesCache = Symbol('matchesCache'); +export const computedStyleCache = Symbol('computedStyleCache'); +export const clearComputedStyleCache = Symbol('clearComputedStyleCache'); +export const clearCache = Symbol('clearCache'); diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index de312663e..71f2b6dc6 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -89,10 +89,6 @@ export default abstract class AbstractCSSStyleDeclaration { ); } - if (this.#ownerElement[PropertySymbol.isConnected]) { - this.#ownerElement[PropertySymbol.ownerDocument][PropertySymbol.cacheID]++; - } - styleAttribute[PropertySymbol.value] = style.toString(); } else { this.#style = new CSSStyleDeclarationPropertyManager({ cssText }); @@ -147,10 +143,6 @@ export default abstract class AbstractCSSStyleDeclaration { ); } - if (this.#ownerElement[PropertySymbol.isConnected]) { - this.#ownerElement[PropertySymbol.ownerDocument][PropertySymbol.cacheID]++; - } - const style = this.#elementStyle.getElementStyle(); style.set(name, stringValue, !!priority); @@ -180,10 +172,6 @@ export default abstract class AbstractCSSStyleDeclaration { style.remove(name); const newCSSText = style.toString(); - if (this.#ownerElement[PropertySymbol.isConnected]) { - this.#ownerElement[PropertySymbol.ownerDocument][PropertySymbol.cacheID]++; - } - if (newCSSText) { (this.#ownerElement[PropertySymbol.attributes]['style'])[PropertySymbol.value] = newCSSText; diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 8e981ed8e..2fe075607 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -31,16 +31,6 @@ type IStyleAndElement = { * CSS Style Declaration utility */ export default class CSSStyleDeclarationElementStyle { - private cache: { - propertyManager: CSSStyleDeclarationPropertyManager; - cssText: string; - documentCacheID: number; - } = { - propertyManager: null, - cssText: null, - documentCacheID: null - }; - private element: Element; private computed: boolean; @@ -65,15 +55,23 @@ export default class CSSStyleDeclarationElementStyle { return this.getComputedElementStyle(); } + const cache = this.element[PropertySymbol.computedStyleCache]; + + if (cache?.result) { + const result = cache.result.deref(); + if (result) { + return result; + } + } + const cssText = this.element[PropertySymbol.attributes]['style']?.[PropertySymbol.value]; if (cssText) { - if (this.cache.propertyManager && this.cache.cssText === cssText) { - return this.cache.propertyManager; - } - this.cache.cssText = cssText; - this.cache.propertyManager = new CSSStyleDeclarationPropertyManager({ cssText }); - return this.cache.propertyManager; + const propertyManager = new CSSStyleDeclarationPropertyManager({ cssText }); + this.element[PropertySymbol.computedStyleCache] = { + result: new WeakRef(propertyManager) + }; + return propertyManager; } return new CSSStyleDeclarationPropertyManager(); @@ -98,15 +96,14 @@ export default class CSSStyleDeclarationElementStyle { return new CSSStyleDeclarationPropertyManager(); } - if ( - this.cache.propertyManager && - this.cache.documentCacheID === - this.element[PropertySymbol.ownerDocument][PropertySymbol.cacheID] - ) { - return this.cache.propertyManager; - } + const cache = this.element[PropertySymbol.computedStyleCache]; - this.cache.documentCacheID = this.element[PropertySymbol.ownerDocument][PropertySymbol.cacheID]; + if (cache?.result) { + const result = cache.result.deref(); + if (result) { + return result; + } + } // Walks through all parent elements and stores them in an array with element and matching CSS text. while (styleAndElement.element) { @@ -302,7 +299,14 @@ export default class CSSStyleDeclarationElementStyle { } } - this.cache.propertyManager = propertyManager; + const cachedResult = { + result: new WeakRef(propertyManager) + }; + + this.element[PropertySymbol.computedStyleCache] = cachedResult; + this.element[PropertySymbol.ownerDocument][PropertySymbol.computedStyleCache].push( + cachedResult + ); return propertyManager; } diff --git a/packages/happy-dom/src/mutation-observer/IMutationListener.ts b/packages/happy-dom/src/mutation-observer/IMutationListener.ts new file mode 100644 index 000000000..f1f979863 --- /dev/null +++ b/packages/happy-dom/src/mutation-observer/IMutationListener.ts @@ -0,0 +1,7 @@ +import IMutationObserverInit from './IMutationObserverInit.js'; +import MutationRecord from './MutationRecord.js'; + +export default interface IMutationListener { + options: IMutationObserverInit; + callback: WeakRef<(record: MutationRecord) => void>; +} diff --git a/packages/happy-dom/src/mutation-observer/MutationObserver.ts b/packages/happy-dom/src/mutation-observer/MutationObserver.ts index e104908f4..a3d2daf3f 100644 --- a/packages/happy-dom/src/mutation-observer/MutationObserver.ts +++ b/packages/happy-dom/src/mutation-observer/MutationObserver.ts @@ -1,7 +1,7 @@ import * as PropertySymbol from '../PropertySymbol.js'; import Node from '../nodes/node/Node.js'; import IMutationObserverInit from './IMutationObserverInit.js'; -import MutationListener from './MutationListener.js'; +import MutationObserverListener from './MutationObserverListener.js'; import MutationRecord from './MutationRecord.js'; import BrowserWindow from '../window/BrowserWindow.js'; @@ -12,7 +12,7 @@ import BrowserWindow from '../window/BrowserWindow.js'; */ export default class MutationObserver { #callback: (records: MutationRecord[], observer: MutationObserver) => void; - #listeners: MutationListener[] = []; + #listeners: MutationObserverListener[] = []; #window: BrowserWindow | null = null; /** @@ -104,7 +104,7 @@ export default class MutationObserver { } } - const listener = new MutationListener({ + const listener = new MutationObserverListener({ window: this.#window, options, callback: this.#callback.bind(this), @@ -118,7 +118,7 @@ export default class MutationObserver { this.#window[PropertySymbol.mutationObservers].push(this); // Starts observing target node. - (target)[PropertySymbol.observe](listener); + (target)[PropertySymbol.observeMutations](listener.mutationListener); } /** @@ -137,7 +137,7 @@ export default class MutationObserver { } for (const listener of this.#listeners) { - (listener.target)[PropertySymbol.unobserve](listener); + (listener.target)[PropertySymbol.unobserveMutations](listener.mutationListener); listener.destroy(); } diff --git a/packages/happy-dom/src/mutation-observer/MutationListener.ts b/packages/happy-dom/src/mutation-observer/MutationObserverListener.ts similarity index 86% rename from packages/happy-dom/src/mutation-observer/MutationListener.ts rename to packages/happy-dom/src/mutation-observer/MutationObserverListener.ts index 6a3fdeb0a..8f2e43c27 100644 --- a/packages/happy-dom/src/mutation-observer/MutationListener.ts +++ b/packages/happy-dom/src/mutation-observer/MutationObserverListener.ts @@ -3,13 +3,15 @@ import MutationObserver from './MutationObserver.js'; import MutationRecord from './MutationRecord.js'; import Node from '../nodes/node/Node.js'; import BrowserWindow from '../window/BrowserWindow.js'; +import IMutationListener from './IMutationListener.js'; /** * Mutation Observer Listener. */ -export default class MutationListener { +export default class MutationObserverListener { public readonly target: Node; public options: IMutationObserverInit; + public mutationListener: IMutationListener; #window: BrowserWindow; #observer: MutationObserver; #callback: (record: MutationRecord[], observer: MutationObserver) => void; @@ -35,6 +37,10 @@ export default class MutationListener { }) { this.options = init.options; this.target = init.target; + this.mutationListener = { + options: init.options, + callback: new WeakRef((record: MutationRecord) => this.report(record)) + }; this.#window = init.window; this.#observer = init.observer; this.#callback = init.callback; @@ -86,6 +92,8 @@ export default class MutationListener { } (this.options) = null; (this.target) = null; + (this.mutationListener) = null; + (this.#window) = null; (this.#observer) = null; (this.#callback) = null; (this.#immediate) = null; diff --git a/packages/happy-dom/src/nodes/character-data/CharacterData.ts b/packages/happy-dom/src/nodes/character-data/CharacterData.ts index 1bb41d851..db3056a3e 100644 --- a/packages/happy-dom/src/nodes/character-data/CharacterData.ts +++ b/packages/happy-dom/src/nodes/character-data/CharacterData.ts @@ -62,26 +62,13 @@ export default abstract class CharacterData const oldValue = this[PropertySymbol.data]; this[PropertySymbol.data] = String(data); - let parent: Node = this; - while (parent) { - parent[PropertySymbol.mutationCacheID].characterData++; - parent = parent[PropertySymbol.parentNode]; - } - - // MutationObserver - if (this[PropertySymbol.observers].length > 0) { - for (const observer of this[PropertySymbol.observers]) { - if (observer.options?.characterData) { - observer.report( - new MutationRecord({ - target: this, - type: MutationTypeEnum.characterData, - oldValue: observer.options.characterDataOldValue ? oldValue : null - }) - ); - } - } - } + this[PropertySymbol.reportMutation]( + new MutationRecord({ + target: this, + type: MutationTypeEnum.characterData, + oldValue + }) + ); } /** diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index 74f3bd70e..8697cb9c3 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -24,20 +24,8 @@ export default class DocumentFragment extends Node { */ constructor() { super(); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('add', (item: Node) => - this[PropertySymbol.children][PropertySymbol.addItem](item) - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( - 'insert', - (item: Node, referenceItem?: Node) => - this[PropertySymbol.children][PropertySymbol.insertItem]( - item, - referenceItem - ) - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('remove', (item: Node) => - this[PropertySymbol.children][PropertySymbol.removeItem](item) - ); + this[PropertySymbol.childNodes][PropertySymbol.attachedHTMLCollection] = + this[PropertySymbol.children]; } /** diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 4d8510aa9..ed7315c32 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -30,7 +30,6 @@ import Location from '../../location/Location.js'; import Selection from '../../selection/Selection.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; import Range from '../../range/Range.js'; -import HTMLBaseElement from '../html-base-element/HTMLBaseElement.js'; import Attr from '../attr/Attr.js'; import ProcessingInstruction from '../processing-instruction/ProcessingInstruction.js'; import VisibilityStateEnum from './VisibilityStateEnum.js'; @@ -45,6 +44,10 @@ import SVGElement from '../svg-element/SVGElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLAnchorElement from '../html-anchor-element/HTMLAnchorElement.js'; import HTMLElementConfig from '../../config/HTMLElementConfig.js'; +import CSSStyleDeclarationPropertyManager from '../../css/declaration/property-manager/CSSStyleDeclarationPropertyManager.js'; +import HTMLHtmlElement from '../html-html-element/HTMLHtmlElement.js'; +import HTMLBodyElement from '../html-body-element/HTMLBodyElement.js'; +import HTMLHeadElement from '../html-head-element/HTMLHeadElement.js'; const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; @@ -68,6 +71,9 @@ export default class Document extends Node { public [PropertySymbol.referrer] = ''; public [PropertySymbol.defaultView]: BrowserWindow | null = null; public [PropertySymbol.ownerWindow]: BrowserWindow; + public [PropertySymbol.computedStyleCache]: Array<{ + result: WeakRef; + }> = []; public declare cloneNode: (deep?: boolean) => Document; // Private properties @@ -196,20 +202,8 @@ export default class Document extends Node { super(); this.#browserFrame = injected.browserFrame; this[PropertySymbol.ownerWindow] = injected.window; - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('add', (item: Node) => - this[PropertySymbol.children][PropertySymbol.addItem](item) - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( - 'insert', - (item: Node, referenceItem?: Node) => - this[PropertySymbol.children][PropertySymbol.insertItem]( - item, - referenceItem - ) - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]('remove', (item: Node) => - this[PropertySymbol.children][PropertySymbol.removeItem](item) - ); + this[PropertySymbol.childNodes][PropertySymbol.attachedHTMLCollection] = + this[PropertySymbol.children]; } /** @@ -299,7 +293,7 @@ export default class Document extends Node { * @returns Title. */ public get title(): string { - const element = ParentNodeUtility.getElementByTagName(this, 'title'); + const element = QuerySelector.querySelector(this, 'title'); if (element) { return element.textContent; } @@ -311,7 +305,7 @@ export default class Document extends Node { * */ public set title(title: string) { - const element = ParentNodeUtility.getElementByTagName(this, 'title'); + const element = QuerySelector.querySelector(this, 'title'); if (element) { element.textContent = title; } else { @@ -405,8 +399,8 @@ export default class Document extends Node { * * @returns Element. */ - public get documentElement(): HTMLElement { - return ParentNodeUtility.getElementByTagName(this, 'html'); + public get documentElement(): HTMLHtmlElement { + return QuerySelector.querySelector(this, 'html'); } /** @@ -428,8 +422,8 @@ export default class Document extends Node { * * @returns Element. */ - public get body(): HTMLElement { - return ParentNodeUtility.getElementByTagName(this, 'body'); + public get body(): HTMLBodyElement { + return QuerySelector.querySelector(this, 'body'); } /** @@ -437,8 +431,8 @@ export default class Document extends Node { * * @returns Element. */ - public get head(): HTMLElement { - return ParentNodeUtility.getElementByTagName(this, 'head'); + public get head(): HTMLHeadElement { + return QuerySelector.querySelector(this, 'head'); } /** @@ -524,7 +518,7 @@ export default class Document extends Node { * @returns Base URI. */ public get baseURI(): string { - const element = ParentNodeUtility.getElementByTagName(this, 'base'); + const element = QuerySelector.querySelector(this, 'base'); if (element) { return element.href; } @@ -869,8 +863,8 @@ export default class Document extends Node { this.appendChild(documentElement); - const head = ParentNodeUtility.getElementByTagName(this, 'head'); - let body = ParentNodeUtility.getElementByTagName(this, 'body'); + const head = QuerySelector.querySelector(this, 'head'); + let body = QuerySelector.querySelector(this, 'body'); if (!body) { body = this.createElement('body'); @@ -881,8 +875,8 @@ export default class Document extends Node { documentElement.insertBefore(this.createElement('head'), body); } } else { - const rootBody = ParentNodeUtility.getElementByTagName(root, 'body'); - const body = ParentNodeUtility.getElementByTagName(this, 'body'); + const rootBody = QuerySelector.querySelector(root, 'body'); + const body = QuerySelector.querySelector(this, 'body'); if (rootBody && body) { const childNodes = rootBody[PropertySymbol.childNodes]; while (childNodes.length) { @@ -892,7 +886,7 @@ export default class Document extends Node { } // Remaining nodes outside the element are added to the element. - const body = ParentNodeUtility.getElementByTagName(this, 'body'); + const body = QuerySelector.querySelector(this, 'body'); if (body) { const childNodes = root[PropertySymbol.childNodes]; while (childNodes.length) { @@ -921,8 +915,8 @@ export default class Document extends Node { this.appendChild(documentElement); } } else { - const bodyNode = ParentNodeUtility.getElementByTagName(root, 'body'); - const body = ParentNodeUtility.getElementByTagName(this, 'body'); + const bodyNode = QuerySelector.querySelector(root, 'body'); + const body = QuerySelector.querySelector(this, 'body'); const childNodes = ((bodyNode || root))[PropertySymbol.childNodes][ PropertySymbol.items ]; @@ -1341,6 +1335,16 @@ export default class Document extends Node { return null; } + /** + * Clears computed style cache. + */ + public [PropertySymbol.clearComputedStyleCache](): void { + for (const item of this[PropertySymbol.computedStyleCache]) { + item.result = null; + } + this[PropertySymbol.computedStyleCache] = []; + } + /** * Imports a node. * diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index d11c33849..386cfb5f1 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -30,10 +30,10 @@ import ISVGElementTagNameMap from '../../config/ISVGElementTagNameMap.js'; import IChildNode from '../child-node/IChildNode.js'; import INonDocumentTypeChildNode from '../child-node/INonDocumentTypeChildNode.js'; import IParentNode from '../parent-node/IParentNode.js'; -import MutationListener from '../../mutation-observer/MutationListener.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import INodeList from '../node/INodeList.js'; +import CSSStyleDeclarationPropertyManager from '../../css/declaration/property-manager/CSSStyleDeclarationPropertyManager.js'; type InsertAdjacentPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'; @@ -107,6 +107,9 @@ export default class Element public [PropertySymbol.namespaceURI]: string | null = this.constructor[PropertySymbol.namespaceURI] || null; public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); + public [PropertySymbol.computedStyleCache]: { + result: WeakRef | null; + } | null = null; /** * Constructor. @@ -117,24 +120,8 @@ export default class Element attributes[PropertySymbol.addEventListener]('set', this.#onSetAttribute.bind(this)); attributes[PropertySymbol.addEventListener]('remove', this.#onRemoveAttribute.bind(this)); - // Use variable here instead of referencing the property to work with HTMLElement[PropertySymbol.connectedToDocument] (when it defines custom element) - // Otherwise it will be connected to the new children collection set on the children property - const children = this[PropertySymbol.children]; - const childNodes = this[PropertySymbol.childNodes]; - - childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { - children[PropertySymbol.addItem](item); - this.#onNodeListChange(item); - }); - - childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { - children[PropertySymbol.insertItem](item, referenceItem); - this.#onNodeListChange(item); - }); - childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { - children[PropertySymbol.removeItem](item); - this.#onNodeListChange(item); - }); + this[PropertySymbol.childNodes][PropertySymbol.attachedHTMLCollection] = + this[PropertySymbol.children]; } /** @@ -1194,6 +1181,43 @@ export default class Element return returnValue; } + /** + * Append a child node to childNodes. + * + * @param node Node to append. + * @returns Appended node. + */ + public [PropertySymbol.appendChild](node: Node): Node { + const returnValue = super[PropertySymbol.appendChild](node); + this.#onNodeListChange(node); + return returnValue; + } + + /** + * Remove Child element from childNodes array. + * + * @param node Node to remove. + * @returns Removed node. + */ + public [PropertySymbol.removeChild](node: Node): Node { + const returnValue = super[PropertySymbol.removeChild](node); + this.#onNodeListChange(node); + return returnValue; + } + + /** + * Inserts a node before another. + * + * @param newNode Node to insert. + * @param referenceNode Node to insert before. + * @returns Inserted node. + */ + public [PropertySymbol.insertBefore](newNode: Node, referenceNode: Node | null): Node { + const returnValue = super[PropertySymbol.insertBefore](newNode, referenceNode); + this.#onNodeListChange(newNode); + return returnValue; + } + /** * Triggered when an attribute is set. * @@ -1205,12 +1229,6 @@ export default class Element return null; } - let parent: Node = this; - while (parent) { - parent[PropertySymbol.mutationCacheID].attributes++; - parent = parent[PropertySymbol.parentNode]; - } - const oldValue = replacedAttribute ? replacedAttribute[PropertySymbol.value] : null; if (attribute[PropertySymbol.name] === 'class' && this[PropertySymbol.classList]) { @@ -1239,6 +1257,11 @@ export default class Element } } + if (this[PropertySymbol.computedStyleCache]) { + this[PropertySymbol.computedStyleCache].result = null; + this[PropertySymbol.computedStyleCache] = null; + } + if ( this.attributeChangedCallback && (this.constructor)[PropertySymbol.observedAttributes] && @@ -1253,25 +1276,14 @@ export default class Element ); } - // MutationObserver - if (this[PropertySymbol.observers].length > 0) { - for (const observer of this[PropertySymbol.observers]) { - if ( - observer.options?.attributes && - (!observer.options.attributeFilter || - observer.options.attributeFilter.includes(attribute[PropertySymbol.name])) - ) { - observer.report( - new MutationRecord({ - target: this, - type: MutationTypeEnum.attributes, - attributeName: attribute[PropertySymbol.name], - oldValue: observer.options.attributeOldValue ? oldValue : null - }) - ); - } - } - } + this[PropertySymbol.reportMutation]( + new MutationRecord({ + target: this, + type: MutationTypeEnum.attributes, + attributeName: attribute[PropertySymbol.name], + oldValue + }) + ); } /** @@ -1280,12 +1292,6 @@ export default class Element * @param removedAttribute Attribute. */ #onRemoveAttribute(removedAttribute: Attr): void { - let parent: Node = this; - while (parent) { - parent[PropertySymbol.mutationCacheID].attributes++; - parent = parent[PropertySymbol.parentNode]; - } - if (removedAttribute[PropertySymbol.name] === 'class' && this[PropertySymbol.classList]) { this[PropertySymbol.classList][PropertySymbol.updateIndices](); } @@ -1302,27 +1308,33 @@ export default class Element } } - // MutationObserver - if (this[PropertySymbol.observers].length > 0) { - for (const observer of this[PropertySymbol.observers]) { - if ( - observer.options?.attributes && - (!observer.options.attributeFilter || - observer.options.attributeFilter.includes(removedAttribute[PropertySymbol.name])) - ) { - observer.report( - new MutationRecord({ - target: this, - type: MutationTypeEnum.attributes, - attributeName: removedAttribute[PropertySymbol.name], - oldValue: observer.options.attributeOldValue - ? removedAttribute[PropertySymbol.value] - : null - }) - ); - } - } + if (this[PropertySymbol.computedStyleCache]) { + this[PropertySymbol.computedStyleCache].result = null; + this[PropertySymbol.computedStyleCache] = null; + } + + if ( + this.attributeChangedCallback && + (this.constructor)[PropertySymbol.observedAttributes] && + (this.constructor)[PropertySymbol.observedAttributes].includes( + removedAttribute[PropertySymbol.name] + ) + ) { + this.attributeChangedCallback( + removedAttribute[PropertySymbol.name], + removedAttribute[PropertySymbol.value], + null + ); } + + this[PropertySymbol.reportMutation]( + new MutationRecord({ + type: MutationTypeEnum.attributes, + target: this, + attributeName: removedAttribute[PropertySymbol.name], + oldValue: removedAttribute[PropertySymbol.value] + }) + ); } /** diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index efece61ed..d871eb73c 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -1,10 +1,14 @@ import * as PropertySymbol from '../../PropertySymbol.js'; +import EventTarget from '../../event/EventTarget.js'; +import MutationRecord from '../../mutation-observer/MutationRecord.js'; import Attr from '../attr/Attr.js'; +import DocumentFragment from '../document-fragment/DocumentFragment.js'; +import Document from '../document/Document.js'; +import HTMLElement from '../html-element/HTMLElement.js'; import Node from '../node/Node.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import Element from './Element.js'; import IHTMLCollection from './IHTMLCollection.js'; -import THTMLCollectionListener from './THTMLCollectionListener.js'; import TNamedNodeMapListener from './TNamedNodeMapListener.js'; const NAMED_ITEM_ATTRIBUTES = ['id', 'name']; @@ -17,38 +21,44 @@ const NAMED_ITEM_ATTRIBUTES = ['id', 'name']; * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection */ -class HTMLCollection extends Array implements IHTMLCollection { +export default class HTMLCollection + extends Array + implements IHTMLCollection +{ public [PropertySymbol.namedItems] = new Map>(); #namedNodeMapListeners = new Map< T, { set: TNamedNodeMapListener; remove: TNamedNodeMapListener } >(); - #eventListeners: { - indexChange: WeakRef>[]; - propertyChange: WeakRef>[]; - } = { - indexChange: [], - propertyChange: [] - }; #filter: (item: T) => boolean | null; + #synchronizedPropertiesElement: Element; /** * Constructor. * - * @param [filter] Filter. - * @param items + * @param [options] Options. + * @param [options.filter] Filter. + * @param [options.observeNode] Observe node. + * @param [options.synchronizedPropertiesElement] Synchronized properties element. */ - constructor( - filter?: (item: T) => boolean, - items?: Array<{ [index: number]: T; length: number }> - ) { + constructor(options?: { + filter?: (item: T) => boolean; + observeNode?: Element | DocumentFragment | Document; + synchronizedPropertiesElement?: Element; + }) { super(); - this.#filter = filter || null; + if (options) { + if (options.filter) { + this.#filter = options.filter; + } + + if (options.synchronizedPropertiesElement) { + this.#synchronizedPropertiesElement = options.synchronizedPropertiesElement; + } - if (items) { - for (let i = 0, max = items.length; i < max; i++) { - this[PropertySymbol.addItem](items[i]); + if (options.observeNode) { + this.#observeNode(options.observeNode); } } } @@ -121,7 +131,10 @@ class HTMLCollection extends Array implements IHTMLCollecti super.push(item); this.#addNamedItem(item); - this[PropertySymbol.dispatchEvent]('indexChange', { index: this.length - 1, item }); + + if (this.#synchronizedPropertiesElement) { + this.#synchronizedPropertiesElement[this.length - 1] = item; + } return true; } @@ -134,10 +147,6 @@ class HTMLCollection extends Array implements IHTMLCollecti * @returns True if the item was added. */ public [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean { - if (!referenceItem) { - return this[PropertySymbol.addItem](newItem); - } - const filter = this.#filter; if ( @@ -151,6 +160,19 @@ class HTMLCollection extends Array implements IHTMLCollecti this[PropertySymbol.removeItem](newItem); } + // We should not call addItem() here, as we don't want HTMLOptionsCollection to run updateSelectedness() twice. + if (!referenceItem) { + super.push(newItem); + + this.#addNamedItem(newItem); + + if (this.#synchronizedPropertiesElement) { + this.#synchronizedPropertiesElement[this.length - 1] = newItem; + } + + return true; + } + if (!referenceItem) { return this[PropertySymbol.addItem](newItem); } @@ -186,7 +208,14 @@ class HTMLCollection extends Array implements IHTMLCollecti super.splice(referenceItemIndex, 0, newItem); this.#addNamedItem(newItem); - this[PropertySymbol.dispatchEvent]('indexChange', { index: referenceItemIndex, item: newItem }); + + if (this.#synchronizedPropertiesElement) { + this.#synchronizedPropertiesElement[referenceItemIndex] = newItem; + + for (let i = referenceItemIndex + 1, max = this.length; i < max; i++) { + this.#synchronizedPropertiesElement[i] = this[i]; + } + } return true; } @@ -207,7 +236,14 @@ class HTMLCollection extends Array implements IHTMLCollecti super.splice(index, 1); this.#removeNamedItem(item); - this[PropertySymbol.dispatchEvent]('indexChange', { index: 0, item: null }); + + if (this.#synchronizedPropertiesElement) { + for (let i = index, max = this.length; i < max; i++) { + this.#synchronizedPropertiesElement[i] = this[i]; + } + + delete this.#synchronizedPropertiesElement[this.length]; + } return true; } @@ -232,70 +268,6 @@ class HTMLCollection extends Array implements IHTMLCollecti return super.includes(item); } - /** - * Adds event listener. - * - * @param type Type. - * @param listener Listener. - */ - public [PropertySymbol.addEventListener]( - type: 'indexChange' | 'propertyChange', - listener: THTMLCollectionListener - ): void { - this.#eventListeners[type].push(new WeakRef(listener)); - } - - /** - * Removes event listener. - * - * @param type Type. - * @param listener Listener. - */ - public [PropertySymbol.removeEventListener]( - type: 'indexChange' | 'propertyChange', - listener: THTMLCollectionListener - ): void { - const listeners = this.#eventListeners[type]; - for (let i = 0, max = listeners.length; i < max; i++) { - if (listeners[i].deref() === listener) { - listeners.splice(i, 1); - return; - } - } - } - - /** - * Dispatches event. - * - * @param type Type. - * @param details Options. - * @param [details.index] Index. - * @param [details.item] Item. - * @param [details.propertyName] Property name. - * @param [details.propertyValue] Property value. - */ - public [PropertySymbol.dispatchEvent]( - type: 'indexChange' | 'propertyChange', - details: { - index?: number; - item?: T; - propertyName?: string; - propertyValue?: any; - } - ): void { - const listeners = this.#eventListeners[type]; - for (let i = 0, max = listeners.length; i < max; i++) { - const listener = listeners[i].deref(); - if (listener) { - listener(details); - } else { - listeners.splice(i, 1); - i--; - max--; - } - } - } - /** * Returns named items. * @@ -326,15 +298,22 @@ class HTMLCollection extends Array implements IHTMLCollecti enumerable: true, configurable: true }); + + if (this.#synchronizedPropertiesElement) { + Object.defineProperty(this.#synchronizedPropertiesElement, name, { + value: namedItems[0], + writable: false, + enumerable: true, + configurable: true + }); + } } } else { delete this[name]; + if (this.#synchronizedPropertiesElement) { + delete this.#synchronizedPropertiesElement[name]; + } } - - this[PropertySymbol.dispatchEvent]('propertyChange', { - propertyName: name, - propertyValue: this[name] ?? null - }); } /** @@ -347,6 +326,12 @@ class HTMLCollection extends Array implements IHTMLCollecti return ( !!name && !this.constructor.prototype.hasOwnProperty(name) && + (!this.#synchronizedPropertiesElement || + (!this.#synchronizedPropertiesElement.constructor.prototype.hasOwnProperty(name) && + !HTMLElement.constructor.prototype.hasOwnProperty(name) && + !Element.constructor.prototype.hasOwnProperty(name) && + !Node.constructor.hasOwnProperty(name) && + !EventTarget.constructor.hasOwnProperty(name))) && (isNaN(Number(name)) || name.includes('.')) ); } @@ -364,7 +349,7 @@ class HTMLCollection extends Array implements IHTMLCollecti return; } - const name = (item)[PropertySymbol.attributes][attributeName]?.value; + const name = item[PropertySymbol.attributes][attributeName]?.value; if (name) { const namedItems = this[PropertySymbol.getNamedItems](name); @@ -456,6 +441,111 @@ class HTMLCollection extends Array implements IHTMLCollecti } } } + + /** + * Observes node. + * + * @param parentNode Parent node. + */ + #observeNode(parentNode: Element | DocumentFragment | Document): void { + const filter = this.#filter; + + this.#loadObservedItems(parentNode); + + parentNode[PropertySymbol.observeMutations]({ + options: { childList: true }, + callback: new WeakRef((record: MutationRecord) => { + if (record.addedNodes.length) { + // There is always only one added node. + const addedNode = record.addedNodes[0]; + + if ( + addedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(addedNode)) + ) { + const index = this.#getObservedItemIndex(parentNode, addedNode); + + if (index === -1) { + throw new Error( + `Failed to update observed HTMLCollection after a mutation. Added element "${ + addedNode[PropertySymbol.tagName] + }" could not be found.` + ); + } + + this[PropertySymbol.insertItem](addedNode, this[index] || null); + } + } else { + for (const node of record.removedNodes) { + if ( + node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(node)) + ) { + this[PropertySymbol.removeItem](node); + } + } + } + }) + }); + } + + /** + * Loads initial observed items. + * + * @param parentNode Parent node. + */ + #loadObservedItems(parentNode: Element | DocumentFragment | Document): void { + const filter = this.#filter; + const children = parentNode[PropertySymbol.children]; + + for (let i = 0, max = children.length; i < max; i++) { + if ( + children[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(children[i])) + ) { + this[PropertySymbol.addItem](children[i]); + } + + this.#loadObservedItems(children[i]); + } + } + + /** + * Returns the index for the first element matching the filter inside the parent parent element. + * + * @param parentNode Parent node. + * @param item Item. + * @param [indexContainer] Index container. + */ + #getObservedItemIndex( + parentNode: Element | DocumentFragment | Document, + item: T, + indexContainer = { index: 0 } + ): number { + const filter = this.#filter; + const children = parentNode[PropertySymbol.children]; + + for (let i = 0, max = children.length; i < max; i++) { + if ( + children[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(children[i])) + ) { + if (children[i] === item) { + return indexContainer.index; + } + + const returnValue = this.#getObservedItemIndex(children[i], item, indexContainer); + + if (returnValue !== -1) { + return returnValue; + } + + indexContainer.index++; + } + } + + return -1; + } } // Removes Array methods from HTMLCollection. @@ -473,6 +563,3 @@ for (const key of Object.keys(descriptors)) { } } } - -// Forces the type to be an interface to hide Array methods from the outside. -export default HTMLCollection; diff --git a/packages/happy-dom/src/nodes/element/IHTMLCollection.ts b/packages/happy-dom/src/nodes/element/IHTMLCollection.ts index 49da2a930..4efed878f 100644 --- a/packages/happy-dom/src/nodes/element/IHTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/IHTMLCollection.ts @@ -2,7 +2,7 @@ /* eslint-disable filenames/match-exported */ import * as PropertySymbol from '../../PropertySymbol.js'; -import THTMLCollectionListener from './THTMLCollectionListener.js'; +import Element from './Element.js'; /** * HTMLCollection. @@ -11,7 +11,7 @@ import THTMLCollectionListener from './THTMLCollectionListener.js'; * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection */ -export default interface IHTMLCollection { +export default interface IHTMLCollection { [index: number]: T; /** @@ -62,48 +62,6 @@ export default interface IHTMLCollection { */ [PropertySymbol.removeItem](item: T): boolean; - /** - * Adds event listener. - * - * @param type Type. - * @param listener Listener. - */ - [PropertySymbol.addEventListener]( - type: 'indexChange' | 'propertyChange', - listener: THTMLCollectionListener - ): void; - - /** - * Removes event listener. - * - * @param type Type. - * @param listener Listener. - */ - [PropertySymbol.removeEventListener]( - type: 'indexChange' | 'propertyChange', - listener: THTMLCollectionListener - ): void; - - /** - * Dispatches event. - * - * @param type Type. - * @param details Options. - * @param [details.index] Index. - * @param [details.item] Item. - * @param [details.propertyName] Property name. - * @param [details.propertyValue] Property value. - */ - [PropertySymbol.dispatchEvent]( - type: 'indexChange' | 'propertyChange', - details: { - index?: number; - item?: T; - propertyName?: string; - propertyValue?: any; - } - ): void; - /** * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. * diff --git a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts index 5c247f5f9..0659a3138 100644 --- a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts @@ -227,10 +227,6 @@ export default class NamedNodeMap { item[PropertySymbol.name] = this.#getAttributeName(item[PropertySymbol.name]); (item[PropertySymbol.ownerElement]) = this[PropertySymbol.ownerElement]; - if (this[PropertySymbol.ownerElement][PropertySymbol.isConnected]) { - this[PropertySymbol.ownerElement][PropertySymbol.ownerDocument][PropertySymbol.cacheID]++; - } - const name = item[PropertySymbol.name]; const replacedItem = this[PropertySymbol.namedItems].get(name) || null; diff --git a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts index 4904a4ca1..fd111f978 100644 --- a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts +++ b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts @@ -1,9 +1,8 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLCollection from '../element/HTMLCollection.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; -import Node from '../node/Node.js'; +import HTMLCollection from '../element/HTMLCollection.js'; /** * HTMLDataListElement @@ -11,38 +10,7 @@ import Node from '../node/Node.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataListElement */ export default class HTMLDataListElement extends HTMLElement { - public [PropertySymbol.options] = new HTMLCollection( - (item: Node) => item[PropertySymbol.tagName] === 'OPTION' - ); - - /** - * Constructor. - * - * @param browserFrame Browser frame. - */ - constructor() { - super(); - // Child nodes listeners - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { - this[PropertySymbol.elements][PropertySymbol.addItem](item); - }); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'insert', - (newItem: Node, referenceItem: Node | null) => { - this[PropertySymbol.elements][PropertySymbol.insertItem]( - newItem, - referenceItem - ); - } - ); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'remove', - (item: Node) => { - (item)[PropertySymbol.formNode] = null; - this[PropertySymbol.elements][PropertySymbol.removeItem](item); - } - ); - } + public [PropertySymbol.options]: IHTMLCollection | null = null; /** * Returns options. @@ -50,6 +18,12 @@ export default class HTMLDataListElement extends HTMLElement { * @returns Options. */ public get options(): IHTMLCollection { + if (!this[PropertySymbol.options]) { + this[PropertySymbol.options] = new HTMLCollection({ + filter: (item) => item[PropertySymbol.tagName] === 'OPTION', + observeNode: this + }); + } return this[PropertySymbol.options]; } } diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 3fb05954a..9de131f7a 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -553,9 +553,6 @@ export default class HTMLElement extends Element { (>newElement[PropertySymbol.childNodes]) = >( this[PropertySymbol.childNodes] ); - (>newElement[PropertySymbol.childNodesFlatten]) = >( - this[PropertySymbol.childNodesFlatten] - ); (>newElement[PropertySymbol.children]) = this[PropertySymbol.children]; (newElement[PropertySymbol.isConnected]) = this[PropertySymbol.isConnected]; @@ -564,7 +561,7 @@ export default class HTMLElement extends Element { newElement[PropertySymbol.formNode] = this[PropertySymbol.formNode]; newElement[PropertySymbol.selectNode] = this[PropertySymbol.selectNode]; newElement[PropertySymbol.textAreaNode] = this[PropertySymbol.textAreaNode]; - newElement[PropertySymbol.observers] = this[PropertySymbol.observers]; + newElement[PropertySymbol.mutationListeners] = this[PropertySymbol.mutationListeners]; newElement[PropertySymbol.isValue] = this[PropertySymbol.isValue]; for (let i = 0, max = this[PropertySymbol.attributes].length; i < max; i++) { @@ -573,81 +570,21 @@ export default class HTMLElement extends Element { ); } - const children = new HTMLCollection(); - const childNodes = new NodeList(); - const childNodesFlatten = new NodeList(); + (>this[PropertySymbol.childNodes]) = new NodeList(); + (>this[PropertySymbol.children]) = + new HTMLCollection(); + + this[PropertySymbol.childNodes][PropertySymbol.attachedHTMLCollection] = + this[PropertySymbol.children]; - (>this[PropertySymbol.childNodes]) = childNodes; - (>this[PropertySymbol.childNodesFlatten]) = childNodesFlatten; - (>this[PropertySymbol.children]) = >( - children - ); this[PropertySymbol.rootNode] = null; this[PropertySymbol.formNode] = null; this[PropertySymbol.selectNode] = null; this[PropertySymbol.textAreaNode] = null; - this[PropertySymbol.observers] = []; + this[PropertySymbol.mutationListeners] = []; this[PropertySymbol.isValue] = null; (this[PropertySymbol.attributes]) = new NamedNodeMap(this); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( - 'add', - (item: Node) => { - let parent: Node = this; - while (parent) { - const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; - - childNodesFlatten[PropertySymbol.addItem](item); - - for (const child of item[PropertySymbol.childNodesFlatten]) { - childNodesFlatten[PropertySymbol.addItem](child); - } - - parent = parent[PropertySymbol.parentNode]; - } - - children[PropertySymbol.addItem](item); - } - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( - 'insert', - (item: Node, referenceItem?: Node) => { - let parent: Node = this; - while (parent) { - const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; - - childNodesFlatten[PropertySymbol.insertItem](item, referenceItem); - - for (const child of item[PropertySymbol.childNodesFlatten]) { - childNodesFlatten[PropertySymbol.insertItem](child, referenceItem); - } - - parent = parent[PropertySymbol.parentNode]; - } - - children[PropertySymbol.insertItem](item, referenceItem); - } - ); - this[PropertySymbol.childNodes][PropertySymbol.addEventListener]( - 'remove', - (item: Node) => { - let parent: Node = this; - while (parent) { - const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; - - childNodesFlatten[PropertySymbol.removeItem](item); - - for (const child of item[PropertySymbol.childNodesFlatten]) { - childNodesFlatten[PropertySymbol.removeItem](child); - } - - parent = parent[PropertySymbol.parentNode]; - } - - children[PropertySymbol.removeItem](item); - } - ); - const parentChildNodes = (this[PropertySymbol.parentNode])[ PropertySymbol.childNodes ]; diff --git a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts index 0cba6cbdc..00e9d300e 100644 --- a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts +++ b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts @@ -7,7 +7,6 @@ import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.j import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; -import Node from '../node/Node.js'; import Element from '../element/Element.js'; type THTMLFieldSetElement = @@ -26,44 +25,16 @@ export default class HTMLFieldSetElement extends HTMLElement { public declare cloneNode: (deep?: boolean) => HTMLFieldSetElement; // Internal properties - public [PropertySymbol.elements] = new HTMLCollection( - (item: Element) => + public [PropertySymbol.elements] = new HTMLCollection({ + filter: (item: Element) => item.tagName === 'INPUT' || item.tagName === 'BUTTON' || item.tagName === 'TEXTAREA' || - item.tagName === 'SELECT' - ); + item.tagName === 'SELECT', + observeNode: this + }); public [PropertySymbol.formNode]: HTMLFormElement | null = null; - /** - * Constructor. - * - * @param browserFrame Browser frame. - */ - constructor() { - super(); - // Child nodes listeners - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { - this[PropertySymbol.elements][PropertySymbol.addItem](item); - }); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'insert', - (newItem: Node, referenceItem: Node | null) => { - this[PropertySymbol.elements][PropertySymbol.insertItem]( - newItem, - referenceItem - ); - } - ); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'remove', - (item: Node) => { - (item)[PropertySymbol.formNode] = null; - this[PropertySymbol.elements][PropertySymbol.removeItem](item); - } - ); - } - /** * Returns elements. * diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index 552af4a94..43b7dd94f 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -1,8 +1,5 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import Attr from '../attr/Attr.js'; -import Element from '../element/Element.js'; import HTMLCollection from '../element/HTMLCollection.js'; -import TNamedNodeMapListener from '../element/TNamedNodeMapListener.js'; import HTMLFormElement from './HTMLFormElement.js'; import RadioNodeList from './RadioNodeList.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; @@ -17,37 +14,26 @@ export default class HTMLFormControlsCollection extends HTMLCollection< THTMLFormControlElement | RadioNodeList > { public [PropertySymbol.namedItems] = new Map(); - #namedNodeMapListeners = new Map(); + #formElement: HTMLFormElement; /** * Constructor. - * @param formElement + * + * @param formElement Form element. */ constructor(formElement: HTMLFormElement) { - super((item: Element) => { - if ( - item[PropertySymbol.tagName] !== 'INPUT' && - item[PropertySymbol.tagName] !== 'SELECT' && - item[PropertySymbol.tagName] !== 'TEXTAREA' && - item[PropertySymbol.tagName] !== 'BUTTON' && - item[PropertySymbol.tagName] !== 'FIELDSET' - ) { - return false; - } - if (formElement[PropertySymbol.childNodesFlatten][PropertySymbol.includes](item)) { - return true; - } - if ( - !item[PropertySymbol.attributes]['form'] || - !formElement[PropertySymbol.attributes]['id'] - ) { - return false; - } - return ( - item[PropertySymbol.attributes]['form'].value === - formElement[PropertySymbol.attributes]['id'].value - ); + super({ + filter: (item: THTMLFormControlElement) => + item[PropertySymbol.tagName] === 'INPUT' || + item[PropertySymbol.tagName] === 'SELECT' || + item[PropertySymbol.tagName] === 'TEXTAREA' || + item[PropertySymbol.tagName] === 'BUTTON' || + item[PropertySymbol.tagName] === 'FIELDSET', + // Array.splice() method creates a new instance of HTMLOptionsCollection with a number sent as the first argument. + observeNode: formElement instanceof HTMLFormElement ? formElement : null, + synchronizedPropertiesElement: formElement }); + this.#formElement = formElement; } /** @@ -80,16 +66,7 @@ export default class HTMLFormControlsCollection extends HTMLCollection< return false; } - const listener = (attribute: Attr): void => { - if (attribute.name === 'form') { - this[PropertySymbol.removeItem](item); - this[PropertySymbol.addItem](item); - } - }; - - this.#namedNodeMapListeners.set(item, listener); - item[PropertySymbol.attributes][PropertySymbol.addEventListener]('set', listener); - item[PropertySymbol.attributes][PropertySymbol.addEventListener]('remove', listener); + item[PropertySymbol.formNode] = this.#formElement; return true; } @@ -111,16 +88,7 @@ export default class HTMLFormControlsCollection extends HTMLCollection< return false; } - const listener = (attribute: Attr): void => { - if (attribute.name === 'form') { - this[PropertySymbol.removeItem](newItem); - this[PropertySymbol.insertItem](newItem, referenceItem); - } - }; - - this.#namedNodeMapListeners.set(newItem, listener); - newItem[PropertySymbol.attributes][PropertySymbol.addEventListener]('set', listener); - newItem[PropertySymbol.attributes][PropertySymbol.addEventListener]('remove', listener); + newItem[PropertySymbol.formNode] = this.#formElement; return true; } @@ -138,10 +106,7 @@ export default class HTMLFormControlsCollection extends HTMLCollection< return false; } - const listener = this.#namedNodeMapListeners.get(item); - - item[PropertySymbol.attributes][PropertySymbol.removeEventListener]('set', listener); - item[PropertySymbol.attributes][PropertySymbol.removeEventListener]('remove', listener); + item[PropertySymbol.formNode] = null; return true; } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index f2f72ff9c..438dccb70 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -10,11 +10,13 @@ import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import BrowserFrameNavigator from '../../browser/utilities/BrowserFrameNavigator.js'; import FormData from '../../form-data/FormData.js'; -import Element from '../element/Element.js'; import BrowserWindow from '../../window/BrowserWindow.js'; import Attr from '../attr/Attr.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; import IHTMLFormControlsCollection from './IHTMLFormControlsCollection.js'; +import IMutationListener from '../../mutation-observer/IMutationListener.js'; +import MutationRecord from '../../mutation-observer/MutationRecord.js'; +import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; /** * HTML Form Element. @@ -39,11 +41,7 @@ export default class HTMLFormElement extends HTMLElement { // Private properties #browserFrame: IBrowserFrame; - #documentChildNodeListeners: { - add: (item: Node) => void; - insert: (newItem: Node, referenceItem: Node | null) => void; - remove: (item: Node) => void; - } | null = null; + #documentMutationListener: IMutationListener | null = null; /** * Constructor. @@ -61,56 +59,6 @@ export default class HTMLFormElement extends HTMLElement { 'remove', this.#onRemoveAttribute.bind(this) ); - - // Child nodes listeners - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { - (item)[PropertySymbol.formNode] = this; - this[PropertySymbol.elements][PropertySymbol.addItem](item); - }); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'insert', - (newItem: Node, referenceItem: Node | null) => { - (newItem)[PropertySymbol.formNode] = this; - this[PropertySymbol.elements][PropertySymbol.insertItem]( - newItem, - referenceItem - ); - } - ); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'remove', - (item: Node) => { - (item)[PropertySymbol.formNode] = null; - this[PropertySymbol.elements][PropertySymbol.removeItem](item); - } - ); - - // HTMLFormControlsCollection listeners - this[PropertySymbol.elements][PropertySymbol.addEventListener]('indexChange', (details) => { - const length = this[PropertySymbol.elements].length; - for (let i = details.index; i < length; i++) { - this[i] = this[PropertySymbol.elements][i]; - } - // Item removed - if (!details.item) { - delete this[length]; - } - }); - this[PropertySymbol.elements][PropertySymbol.addEventListener]('propertyChange', (details) => { - if (!this[PropertySymbol.isValidPropertyName](details.propertyName)) { - return; - } - if (details.propertyValue) { - Object.defineProperty(this, details.propertyName, { - value: details.propertyValue, - writable: false, - enumerable: true, - configurable: true - }); - } else { - delete this[details.propertyName]; - } - }); } /** @@ -406,57 +354,96 @@ export default class HTMLFormElement extends HTMLElement { public override [PropertySymbol.connectedToDocument](): void { super[PropertySymbol.connectedToDocument](); - // Document child nodes listeners - this.#documentChildNodeListeners = { - add: (item: Node) => { - if (!this[PropertySymbol.isConnected]) { - return; - } - (item)[PropertySymbol.formNode] = this; - this[PropertySymbol.elements][PropertySymbol.addItem](item); + /** + * It is possible to associate a form control element by setting the "form" attribute to the form's id. + * + * We need to listen for changes to all elements in the document to detect when a form control element is added or removed. + */ + this.#documentMutationListener = { + options: { + childList: true, + subtree: true, + attributes: true }, - insert: (newItem: Node, referenceItem: Node | null) => { - if (!this[PropertySymbol.isConnected]) { + callback: new WeakRef((record: MutationRecord) => { + const id = this[PropertySymbol.attributes]?.['id']?.value; + if (!id) { return; } - (newItem)[PropertySymbol.formNode] = this; - this[PropertySymbol.elements][PropertySymbol.insertItem]( - newItem, - referenceItem - ); - }, - remove: (item: Node) => { - if (!this[PropertySymbol.isConnected]) { - return; + switch (record.type) { + case MutationTypeEnum.childList: + const addedNode = record.addedNodes[0]; + const removedNode = record.removedNodes[0]; + if ( + addedNode && + (addedNode[PropertySymbol.tagName] === 'INPUT' || + addedNode[PropertySymbol.tagName] === 'SELECT' || + addedNode[PropertySymbol.tagName] === 'TEXTAREA' || + addedNode[PropertySymbol.tagName] === 'BUTTON' || + addedNode[PropertySymbol.tagName] === 'FIELDSET') && + addedNode[PropertySymbol.attributes]?.['form']?.value === id && + addedNode[PropertySymbol.formNode] !== this + ) { + addedNode[PropertySymbol.formNode] = this; + this[PropertySymbol.elements][PropertySymbol.addItem]( + addedNode + ); + } else if ( + (removedNode[PropertySymbol.tagName] === 'INPUT' || + removedNode[PropertySymbol.tagName] === 'SELECT' || + removedNode[PropertySymbol.tagName] === 'TEXTAREA' || + removedNode[PropertySymbol.tagName] === 'BUTTON' || + removedNode[PropertySymbol.tagName] === 'FIELDSET') && + removedNode[PropertySymbol.attributes]?.['form']?.value === id + ) { + this[PropertySymbol.elements][PropertySymbol.removeItem]( + removedNode + ); + } + break; + case MutationTypeEnum.attributes: + if ( + record.attributeName === 'form' && + (record.target[PropertySymbol.tagName] === 'INPUT' || + record.target[PropertySymbol.tagName] === 'SELECT' || + record.target[PropertySymbol.tagName] === 'TEXTAREA' || + record.target[PropertySymbol.tagName] === 'BUTTON' || + record.target[PropertySymbol.tagName] === 'FIELDSET') + ) { + if ( + record.target[PropertySymbol.attributes]?.['form']?.[PropertySymbol.value] === + this[PropertySymbol.attributes]?.['id']?.[PropertySymbol.value] && + record.target[PropertySymbol.formNode] !== this + ) { + this[PropertySymbol.elements][PropertySymbol.addItem]( + record.target + ); + } else { + this[PropertySymbol.elements][PropertySymbol.removeItem]( + record.target + ); + } + } + break; } - (item)[PropertySymbol.formNode] = null; - this[PropertySymbol.elements][PropertySymbol.removeItem](item); - } + }) }; - this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ - PropertySymbol.addEventListener - ]('add', this.#documentChildNodeListeners.add); - this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ - PropertySymbol.addEventListener - ]('insert', this.#documentChildNodeListeners.insert); - this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ - PropertySymbol.addEventListener - ]('remove', this.#documentChildNodeListeners.remove); + this[PropertySymbol.ownerDocument][PropertySymbol.observeMutations]( + this.#documentMutationListener + ); - const id = this.id; + const id = this[PropertySymbol.attributes]?.['id']?.value; if (!id) { return; } - for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { - if ( - node[PropertySymbol.attributes]?.['form']?.value === id && - node[PropertySymbol.formNode] !== this - ) { - node[PropertySymbol.formNode] = this; - this[PropertySymbol.elements][PropertySymbol.addItem](node); + for (const element of this[PropertySymbol.ownerDocument].querySelectorAll( + `INPUT[form="${id}"], SELECT[form="${id}"], TEXTAREA[form="${id}"], BUTTON[form="${id}"], FIELDSET[form="${id}"]` + )) { + if (element[PropertySymbol.formNode] !== this) { + this[PropertySymbol.elements][PropertySymbol.addItem](element); } } } @@ -467,22 +454,11 @@ export default class HTMLFormElement extends HTMLElement { public override [PropertySymbol.disconnectedFromDocument](): void { super[PropertySymbol.disconnectedFromDocument](); - if (!this.#documentChildNodeListeners) { - return; - } - - // Document child nodes listeners - this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ - PropertySymbol.removeEventListener - ]('add', this.#documentChildNodeListeners.add); - this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ - PropertySymbol.removeEventListener - ]('insert', this.#documentChildNodeListeners.insert); - this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten][ - PropertySymbol.removeEventListener - ]('remove', this.#documentChildNodeListeners.remove); + this[PropertySymbol.ownerDocument][PropertySymbol.unobserveMutations]( + this.#documentMutationListener + ); - this.#documentChildNodeListeners = null; + this.#documentMutationListener = null; const id = this.id; @@ -490,34 +466,13 @@ export default class HTMLFormElement extends HTMLElement { return; } - for (const node of this[PropertySymbol.elements]) { - if ( - node[PropertySymbol.attributes]?.['form']?.value === id && - !this[PropertySymbol.childNodesFlatten][PropertySymbol.includes](node) - ) { - node[PropertySymbol.formNode] = null; - this[PropertySymbol.elements][PropertySymbol.removeItem](node); + for (const element of this[PropertySymbol.elements]) { + if (element[PropertySymbol.attributes]?.['form']?.value === id && !this.contains(element)) { + this[PropertySymbol.elements][PropertySymbol.removeItem](element); } } } - /** - * Returns "true" if the property name is valid. - * - * @param name Name. - * @returns True if the property name is valid. - */ - protected [PropertySymbol.isValidPropertyName](name: string): boolean { - return ( - !!name && - !HTMLFormElement.prototype.hasOwnProperty(name) && - !HTMLElement.prototype.hasOwnProperty(name) && - !Element.prototype.hasOwnProperty(name) && - !Node.prototype.hasOwnProperty(name) && - (isNaN(Number(name)) || name.includes('.')) - ); - } - /** * Submits form. * @@ -606,25 +561,24 @@ export default class HTMLFormElement extends HTMLElement { } if (replacedAttribute[PropertySymbol.value]) { - for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { - if ( - node[PropertySymbol.attributes]?.['form']?.value === replacedAttribute.value && - !this[PropertySymbol.childNodesFlatten][PropertySymbol.includes](node) - ) { - node[PropertySymbol.formNode] = null; - this[PropertySymbol.elements][PropertySymbol.removeItem](node); + const id = replacedAttribute[PropertySymbol.value]; + for (const element of this[PropertySymbol.elements]) { + if (element[PropertySymbol.attributes]?.['form']?.value === id && !this.contains(element)) { + this[PropertySymbol.elements][PropertySymbol.removeItem]( + element + ); } } } if (attribute[PropertySymbol.value]) { - for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { + const id = attribute[PropertySymbol.value]; + for (const element of this[PropertySymbol.elements]) { if ( - node[PropertySymbol.attributes]?.['form']?.value === attribute[PropertySymbol.value] && - node[PropertySymbol.formNode] !== this + element[PropertySymbol.attributes]?.['form']?.value === id && + element[PropertySymbol.formNode] !== this ) { - node[PropertySymbol.formNode] = this; - this[PropertySymbol.elements][PropertySymbol.addItem](node); + this[PropertySymbol.elements][PropertySymbol.addItem](element); } } } @@ -637,19 +591,17 @@ export default class HTMLFormElement extends HTMLElement { */ #onRemoveAttribute(removedAttribute: Attr): void { if ( - removedAttribute.name === 'id' && - removedAttribute[PropertySymbol.value] && - this[PropertySymbol.isConnected] + removedAttribute.name !== 'id' || + !removedAttribute[PropertySymbol.value] || + !this[PropertySymbol.isConnected] ) { - for (const node of this[PropertySymbol.ownerDocument][PropertySymbol.childNodesFlatten]) { - if ( - node[PropertySymbol.attributes]?.['form']?.value === - removedAttribute[PropertySymbol.value] && - !this[PropertySymbol.childNodesFlatten][PropertySymbol.includes](node) - ) { - node[PropertySymbol.formNode] = null; - this[PropertySymbol.elements][PropertySymbol.removeItem](node); - } + return; + } + + const id = removedAttribute[PropertySymbol.value]; + for (const element of this[PropertySymbol.elements]) { + if (element[PropertySymbol.attributes]?.['form']?.value === id && !this.contains(element)) { + this[PropertySymbol.elements][PropertySymbol.removeItem](element); } } } diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index 9d99c3ff2..a1ee8d1d0 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -328,6 +328,7 @@ export default class HTMLLinkElement extends HTMLElement { const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(code); this[PropertySymbol.sheet] = styleSheet; + this[PropertySymbol.ownerDocument][PropertySymbol.clearComputedStyleCache](); this.dispatchEvent(new Event('load')); } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts index 794b1fa0c..7585dc1a2 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts @@ -18,11 +18,17 @@ export default class HTMLOptionsCollection extends HTMLCollection element[PropertySymbol.tagName] === 'OPTION'); + super({ + filter: (element: Element) => element[PropertySymbol.tagName] === 'OPTION', + // Array.splice() method creates a new instance of HTMLOptionsCollection with a number sent as the first argument. + observeNode: selectElement instanceof HTMLSelectElement ? selectElement : null, + synchronizedPropertiesElement: selectElement + }); this.#selectElement = selectElement; } @@ -46,13 +52,19 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[selectedIndex]; - - if (!selectedOption) { + if (selectedIndex < 0 || !this[selectedIndex]) { + const selectedOptions = this.#selectElement[PropertySymbol.selectedOptions]; + for (let i = 0, max = this.length; i < max; i++) { + const option = this[i]; + option[PropertySymbol.selectedness] = false; + selectedOptions?.[PropertySymbol.removeItem](option); + } this.#selectedIndex = -1; return; } + const selectedOption = this[selectedIndex]; + selectedOption[PropertySymbol.selectedness] = true; selectedOption[PropertySymbol.dirtyness] = true; @@ -125,7 +137,7 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[i]; - if (!isMultiple) { + if (!isMultiple) { + for (let i = 0, max = this.length; i < max; i++) { + const option = this[i]; if (selectedOption) { option[PropertySymbol.selectedness] = option === selectedOption; } @@ -183,15 +197,22 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[i]; + if (option[PropertySymbol.selectedness]) { + selectedOptions?.[PropertySymbol.addItem](option); } else { - selectedOptions[PropertySymbol.removeItem](option); + selectedOptions?.[PropertySymbol.removeItem](option); } } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 2ba19d63d..8d4a7ae0e 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -6,7 +6,6 @@ import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; import HTMLOptionsCollection from './HTMLOptionsCollection.js'; import Event from '../../event/Event.js'; -import Node from '../node/Node.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import HTMLCollection from '../element/HTMLCollection.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; @@ -25,70 +24,12 @@ export default class HTMLSelectElement extends HTMLElement { public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.options]: HTMLOptionsCollection = new HTMLOptionsCollection(this); public [PropertySymbol.formNode]: HTMLFormElement | null = null; - public [PropertySymbol.selectedOptions]: IHTMLCollection = - new HTMLCollection( - (element: Element) => - element[PropertySymbol.tagName] === 'OPTION' && element[PropertySymbol.selectedness] - ); + public [PropertySymbol.selectedOptions]: IHTMLCollection | null = null; // Events public onchange: (event: Event) => void | null = null; public oninput: (event: Event) => void | null = null; - /** - * Constructor. - */ - constructor() { - super(); - - // Child nodes listeners - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { - this[PropertySymbol.options][PropertySymbol.addItem](item); - }); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'insert', - (newItem: Node, referenceItem: Node | null) => { - this[PropertySymbol.options][PropertySymbol.insertItem]( - newItem, - referenceItem - ); - } - ); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'remove', - (item: Node) => { - this[PropertySymbol.options][PropertySymbol.removeItem](item); - } - ); - - // HTMLOptionsCollection listeners - this[PropertySymbol.options][PropertySymbol.addEventListener]('indexChange', (details) => { - const length = this[PropertySymbol.options].length; - for (let i = details.index; i < length; i++) { - this[i] = this[PropertySymbol.options][i]; - } - // Item removed - if (!details.item) { - delete this[length]; - } - }); - this[PropertySymbol.options][PropertySymbol.addEventListener]('propertyChange', (details) => { - if (!this[PropertySymbol.isValidPropertyName](details.propertyName)) { - return; - } - if (details.propertyValue) { - Object.defineProperty(this, details.propertyName, { - value: details.propertyValue, - writable: false, - enumerable: true, - configurable: true - }); - } else { - delete this[details.propertyName]; - } - }); - } - /** * Returns length. * @@ -297,6 +238,17 @@ export default class HTMLSelectElement extends HTMLElement { * @returns HTMLCollection. */ public get selectedOptions(): IHTMLCollection { + if (!this[PropertySymbol.selectedOptions]) { + this[PropertySymbol.selectedOptions] = new HTMLCollection({ + filter: (element: Element) => + element[PropertySymbol.tagName] === 'OPTION' && element[PropertySymbol.selectedness] + }); + for (const option of this[PropertySymbol.options]) { + if (option[PropertySymbol.selectedness]) { + this[PropertySymbol.selectedOptions][PropertySymbol.addItem](option); + } + } + } return this[PropertySymbol.selectedOptions]; } diff --git a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts index f5706a7d4..071e06d6e 100644 --- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts +++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts @@ -1,7 +1,7 @@ import CSSStyleSheet from '../../css/CSSStyleSheet.js'; +import MutationRecord from '../../mutation-observer/MutationRecord.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElement from '../html-element/HTMLElement.js'; -import Node from '../node/Node.js'; import Text from '../text/Text.js'; /** @@ -19,32 +19,19 @@ export default class HTMLStyleElement extends HTMLElement { constructor() { super(); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { - if (item instanceof Text) { - item[PropertySymbol.styleNode] = this; - this[PropertySymbol.updateSheet](); - } - }); - - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'insert', - (item: Node) => { - if (item instanceof Text) { - item[PropertySymbol.styleNode] = this; - this[PropertySymbol.updateSheet](); - } - } - ); - - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'remove', - (item: Node) => { - if (item instanceof Text) { - item[PropertySymbol.styleNode] = null; + this[PropertySymbol.observeMutations]({ + options: { + childList: true, + subtree: true + }, + callback: new WeakRef((record: MutationRecord) => { + const node = record.addedNodes[0] || record.removedNodes[0]; + if (node instanceof Text) { + node[PropertySymbol.styleNode] = record.addedNodes[0] ? this : null; this[PropertySymbol.updateSheet](); } - } - ); + }) + }); } /** @@ -136,7 +123,6 @@ export default class HTMLStyleElement extends HTMLElement { */ public [PropertySymbol.updateSheet](): void { if (this[PropertySymbol.sheet]) { - this[PropertySymbol.ownerDocument][PropertySymbol.cacheID]++; this[PropertySymbol.sheet].replaceSync(this.textContent); } } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index 24a8b6e95..ca9377956 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -7,11 +7,11 @@ import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLInputElementSelectionDirectionEnum from '../html-input-element/HTMLInputElementSelectionDirectionEnum.js'; import HTMLInputElementSelectionModeEnum from '../html-input-element/HTMLInputElementSelectionModeEnum.js'; import ValidityState from '../../validity-state/ValidityState.js'; -import NodeList from '../node/NodeList.js'; import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import Text from '../text/Text.js'; -import Node from '../node/Node.js'; +import INodeList from '../node/INodeList.js'; +import MutationRecord from '../../mutation-observer/MutationRecord.js'; /** * HTML Text Area Element. @@ -45,30 +45,20 @@ export default class HTMLTextAreaElement extends HTMLElement { */ constructor() { super(); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]('add', (item: Node) => { - if (item instanceof Text) { - item[PropertySymbol.textAreaNode] = this; - this[PropertySymbol.resetSelection](); - } - }); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'insert', - (item: Node) => { - if (item instanceof Text) { - item[PropertySymbol.textAreaNode] = this; - this[PropertySymbol.resetSelection](); - } - } - ); - this[PropertySymbol.childNodesFlatten][PropertySymbol.addEventListener]( - 'remove', - (item: Node) => { - if (item instanceof Text) { - item[PropertySymbol.textAreaNode] = null; + + this[PropertySymbol.observeMutations]({ + options: { + childList: true, + subtree: true + }, + callback: new WeakRef((record: MutationRecord) => { + const node = record.addedNodes[0] || record.removedNodes[0]; + if (node instanceof Text) { + node[PropertySymbol.textAreaNode] = record.addedNodes[0] ? this : null; this[PropertySymbol.resetSelection](); } - } - ); + }) + }); } /** @@ -461,7 +451,7 @@ export default class HTMLTextAreaElement extends HTMLElement { * * @returns Label elements. */ - public get labels(): NodeList { + public get labels(): INodeList { return HTMLLabelElementUtility.getAssociatedLabelElements(this); } diff --git a/packages/happy-dom/src/nodes/node/ICachedMatchesItem.ts b/packages/happy-dom/src/nodes/node/ICachedMatchesItem.ts new file mode 100644 index 000000000..efbfe439e --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedMatchesItem.ts @@ -0,0 +1,5 @@ +import ISelectorMatch from '../../query-selector/ISelectorMatch.js'; + +export default interface ICachedMatchesItem { + result: { match: ISelectorMatch | null } | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllItem.ts b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllItem.ts new file mode 100644 index 000000000..76f1f20ec --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllItem.ts @@ -0,0 +1,6 @@ +import Element from '../element/Element.js'; +import INodeList from './INodeList.js'; + +export default interface ICachedQuerySelectorAllItem { + result: WeakRef> | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedQuerySelectorItem.ts b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorItem.ts new file mode 100644 index 000000000..5d064cf4d --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorItem.ts @@ -0,0 +1,5 @@ +import Element from '../element/Element.js'; + +export default interface ICachedQuerySelectorItem { + result: WeakRef | null; +} diff --git a/packages/happy-dom/src/nodes/node/INodeList.ts b/packages/happy-dom/src/nodes/node/INodeList.ts index e947341ee..0c3d7cfb1 100644 --- a/packages/happy-dom/src/nodes/node/INodeList.ts +++ b/packages/happy-dom/src/nodes/node/INodeList.ts @@ -1,5 +1,6 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import TNodeListListener from './TNodeListListener.js'; +import Element from '../element/Element.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; /** * NodeList. @@ -10,6 +11,7 @@ import TNodeListListener from './TNodeListListener.js'; */ export default interface INodeList { readonly [index: number]: T; + [PropertySymbol.attachedHTMLCollection]: IHTMLCollection | null; /** * The number of items in the NodeList. @@ -69,41 +71,6 @@ export default interface INodeList { */ [PropertySymbol.removeItem](item: T): boolean; - /** - * Adds event listener. - * - * @param type Type. - * @param listener Listener. - */ - [PropertySymbol.addEventListener]( - type: 'add' | 'insert' | 'remove', - listener: TNodeListListener - ): void; - - /** - * Removes event listener. - * - * @param type Type. - * @param listener Listener. - */ - [PropertySymbol.removeEventListener]( - type: 'add' | 'insert' | 'remove', - listener: TNodeListListener - ): void; - - /** - * Dispatches event. - * - * @param type Type. - * @param item Item. - * @param referenceItem Reference item. - */ - [PropertySymbol.dispatchEvent]( - type: 'add' | 'insert' | 'remove', - item: T, - referenceItem?: T | null - ): void; - /** * Index of item. * diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 67e7fc72c..62259ca8d 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -1,6 +1,5 @@ import EventTarget from '../../event/EventTarget.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import MutationListener from '../../mutation-observer/MutationListener.js'; import Document from '../document/Document.js'; import Element from '../element/Element.js'; import NodeTypeEnum from './NodeTypeEnum.js'; @@ -14,6 +13,10 @@ import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import INodeList from './INodeList.js'; +import IMutationListener from '../../mutation-observer/IMutationListener.js'; +import ICachedQuerySelectorAllItem from './ICachedQuerySelectorAllItem.js'; +import ICachedQuerySelectorItem from './ICachedQuerySelectorItem.js'; +import ICachedMatchesItem from './ICachedMatchesItem.js'; /** * Node. @@ -63,19 +66,28 @@ export default class Node extends EventTarget { public [PropertySymbol.parentNode]: Node | null = null; public [PropertySymbol.nodeType]: NodeTypeEnum; public [PropertySymbol.rootNode]: Node = null; - public [PropertySymbol.observers]: MutationListener[] = []; + public [PropertySymbol.mutationListeners]: IMutationListener[] = []; public [PropertySymbol.childNodes]: INodeList = new NodeList(); - public [PropertySymbol.childNodesFlatten]: INodeList = new NodeList(); - public [PropertySymbol.mutationCacheID]: { - attributes: number; - childNodes: number; - children: number; - characterData: number; + public [PropertySymbol.querySelectorCache]: { + items: Map; + affectedItems: ICachedQuerySelectorItem[]; } = { - attributes: 0, - childNodes: 0, - children: 0, - characterData: 0 + items: new Map(), + affectedItems: [] + }; + public [PropertySymbol.querySelectorAllCache]: { + items: Map; + affectedItems: ICachedQuerySelectorAllItem[]; + } = { + items: new Map(), + affectedItems: [] + }; + public [PropertySymbol.matchesCache]: { + items: Map; + affectedItems: ICachedMatchesItem[]; + } = { + items: new Map(), + affectedItems: [] }; /** @@ -96,70 +108,6 @@ export default class Node extends EventTarget { } this[PropertySymbol.ownerDocument] = ownerDocument; } - - const childNodes = this[PropertySymbol.childNodes]; - - childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { - let parent: Node = this; - while (parent) { - const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; - - childNodesFlatten[PropertySymbol.addItem](item); - - for (const child of item[PropertySymbol.childNodesFlatten]) { - childNodesFlatten[PropertySymbol.addItem](child); - } - - parent[PropertySymbol.mutationCacheID].childNodes++; - - if (parent[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - parent[PropertySymbol.mutationCacheID].children++; - } - - parent = parent[PropertySymbol.parentNode]; - } - }); - - childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { - let parent: Node = this; - while (parent) { - const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; - - childNodesFlatten[PropertySymbol.insertItem](item, referenceItem); - - for (const child of item[PropertySymbol.childNodesFlatten]) { - childNodesFlatten[PropertySymbol.insertItem](child, referenceItem); - } - - parent[PropertySymbol.mutationCacheID].childNodes++; - - if (parent[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - parent[PropertySymbol.mutationCacheID].children++; - } - - parent = parent[PropertySymbol.parentNode]; - } - }); - childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { - let parent: Node = this; - while (parent) { - const childNodesFlatten = parent[PropertySymbol.childNodesFlatten]; - - childNodesFlatten[PropertySymbol.removeItem](item); - - for (const child of item[PropertySymbol.childNodesFlatten]) { - childNodesFlatten[PropertySymbol.removeItem](child); - } - - parent[PropertySymbol.mutationCacheID].childNodes++; - - if (parent[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - parent[PropertySymbol.mutationCacheID].children++; - } - - parent = parent[PropertySymbol.parentNode]; - } - }); } /** @@ -537,12 +485,10 @@ export default class Node extends EventTarget { node[PropertySymbol.parentNode][PropertySymbol.childNodes][PropertySymbol.removeItem](node); } - if (this[PropertySymbol.isConnected]) { - (this[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; - } - node[PropertySymbol.parentNode] = this; + node[PropertySymbol.clearCache](); + this[PropertySymbol.childNodes][PropertySymbol.addItem](node); if (this[PropertySymbol.isConnected] && !node[PropertySymbol.isConnected]) { @@ -551,23 +497,19 @@ export default class Node extends EventTarget { node[PropertySymbol.disconnectedFromDocument](); } - // MutationObserver - if ((this)[PropertySymbol.observers].length > 0) { - const record = new MutationRecord({ + // Mutation listeners + for (const mutationListener of this[PropertySymbol.mutationListeners]) { + if (mutationListener.options?.subtree && mutationListener.callback.deref()) { + (node)[PropertySymbol.observeMutations](mutationListener); + } + } + this[PropertySymbol.reportMutation]( + new MutationRecord({ target: this, type: MutationTypeEnum.childList, addedNodes: [node] - }); - - for (const observer of (this)[PropertySymbol.observers]) { - if (observer.options?.subtree) { - (node)[PropertySymbol.observe](observer); - } - if (observer.options?.childList) { - observer.report(record); - } - } - } + }) + ); return node; } @@ -579,35 +521,29 @@ export default class Node extends EventTarget { * @returns Removed node. */ public [PropertySymbol.removeChild](node: Node): Node { - if (this[PropertySymbol.isConnected]) { - (this[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; - } - node[PropertySymbol.parentNode] = null; + node[PropertySymbol.clearCache](); + this[PropertySymbol.childNodes][PropertySymbol.removeItem](node); if (node[PropertySymbol.isConnected]) { node[PropertySymbol.disconnectedFromDocument](); } - // MutationObserver - if (this[PropertySymbol.observers].length > 0) { - const record = new MutationRecord({ + // Mutation listeners + for (const mutationListener of this[PropertySymbol.mutationListeners]) { + if (mutationListener.options?.subtree && mutationListener.callback.deref()) { + (node)[PropertySymbol.unobserveMutations](mutationListener); + } + } + this[PropertySymbol.reportMutation]( + new MutationRecord({ target: this, type: MutationTypeEnum.childList, removedNodes: [node] - }); - - for (const observer of this[PropertySymbol.observers]) { - if (observer.options?.subtree) { - (node)[PropertySymbol.unobserve](observer); - } - if (observer.options?.childList) { - observer.report(record); - } - } - } + }) + ); return node; } @@ -650,10 +586,6 @@ export default class Node extends EventTarget { ); } - if (this[PropertySymbol.isConnected]) { - (this[PropertySymbol.ownerDocument] || this)[PropertySymbol.cacheID]++; - } - if (newNode[PropertySymbol.parentNode]) { newNode[PropertySymbol.parentNode][PropertySymbol.childNodes][PropertySymbol.removeItem]( newNode @@ -662,6 +594,8 @@ export default class Node extends EventTarget { newNode[PropertySymbol.parentNode] = this; + newNode[PropertySymbol.clearCache](); + this[PropertySymbol.childNodes][PropertySymbol.insertItem](newNode, referenceNode); if (this[PropertySymbol.isConnected] && !newNode[PropertySymbol.isConnected]) { @@ -670,23 +604,20 @@ export default class Node extends EventTarget { newNode[PropertySymbol.disconnectedFromDocument](); } - // MutationObserver - if ((this)[PropertySymbol.observers].length > 0) { - const record = new MutationRecord({ + // Mutation listeners + for (const mutationListener of this[PropertySymbol.mutationListeners]) { + if (mutationListener.options?.subtree && mutationListener.callback.deref()) { + newNode[PropertySymbol.observeMutations](mutationListener); + } + } + + this[PropertySymbol.reportMutation]( + new MutationRecord({ target: this, type: MutationTypeEnum.childList, addedNodes: [newNode] - }); - - for (const observer of (this)[PropertySymbol.observers]) { - if (observer.options?.subtree) { - (newNode)[PropertySymbol.observe](observer); - } - if (observer.options?.childList) { - observer.report(record); - } - } - } + }) + ); return newNode; } @@ -726,38 +657,137 @@ export default class Node extends EventTarget { } /** - * Observeres the node. - * Used by MutationObserver, but it is not part of the HTML standard. + * Observeres mutations on the node. + * + * Used by MutationObserver and internal logic. * * @param listener Listener. */ - public [PropertySymbol.observe](listener: MutationListener): void { - this[PropertySymbol.observers].push(listener); + public [PropertySymbol.observeMutations](listener: IMutationListener): void { + this[PropertySymbol.mutationListeners].push(listener); if (listener.options.subtree) { for (const node of this[PropertySymbol.childNodes]) { - (node)[PropertySymbol.observe](listener); + (node)[PropertySymbol.observeMutations](listener); } } } /** - * Stops observing the node. - * Used by MutationObserver, but it is not part of the HTML standard. + * Observeres mutations on the node once. + * + * Used by MutationObserver and internal logic. * * @param listener Listener. */ - public [PropertySymbol.unobserve](listener: MutationListener): void { - const index = this[PropertySymbol.observers].indexOf(listener); + public [PropertySymbol.observeMutationsOnce](listener: IMutationListener): void { + const callback = listener.callback.deref(); + const wrapperListener = { + options: listener.options, + callback: new WeakRef((record: MutationRecord) => { + callback(record); + this[PropertySymbol.unobserveMutations](wrapperListener); + }) + }; + this[PropertySymbol.observeMutations](wrapperListener); + } + + /** + * Stops observing mutations on the node. + * + * Used by MutationObserver and internal logic. + * + * @param listener Listener. + */ + public [PropertySymbol.unobserveMutations](listener: IMutationListener): void { + const index = this[PropertySymbol.mutationListeners].indexOf(listener); if (index !== -1) { - this[PropertySymbol.observers].splice(index, 1); + this[PropertySymbol.mutationListeners].splice(index, 1); } if (listener.options.subtree) { for (const node of this[PropertySymbol.childNodes]) { - (node)[PropertySymbol.unobserve](listener); + node[PropertySymbol.unobserveMutations](listener); } } } + /** + * Reports a mutation on the node. + * + * Used by MutationObserver and internal logic. + * + * @param record Mutation record. + */ + public [PropertySymbol.reportMutation](record: MutationRecord): void { + this[PropertySymbol.clearCache](); + + const mutationListeners = this[PropertySymbol.mutationListeners]; + + if (!mutationListeners.length) { + return; + } + + for (let i = 0, max = mutationListeners.length; i < max; i++) { + const mutationListener = mutationListeners[i]; + const callback = mutationListener.callback.deref(); + if (callback) { + switch (record.type) { + case MutationTypeEnum.childList: + if (mutationListener.options.childList) { + callback(record); + } + break; + case MutationTypeEnum.attributes: + if ( + mutationListener.options.attributes && + (!mutationListener.options.attributeFilter || + mutationListener.options.attributeFilter.includes(record.attributeName)) + ) { + callback(record); + } + break; + case MutationTypeEnum.characterData: + if (mutationListener.options?.characterData) { + callback(record); + } + break; + } + } else { + mutationListeners.splice(i, 1); + i--; + max--; + } + } + } + + /** + * Clears query selector cache. + */ + public [PropertySymbol.clearCache](): void { + for (const item of this[PropertySymbol.querySelectorCache].affectedItems) { + if (item.result) { + item.result = null; + } + } + + for (const item of this[PropertySymbol.querySelectorAllCache].affectedItems) { + if (item.result) { + item.result = null; + } + } + + for (const item of this[PropertySymbol.matchesCache].affectedItems) { + if (item.result) { + item.result = null; + } + } + + this[PropertySymbol.querySelectorCache].affectedItems = []; + this[PropertySymbol.querySelectorAllCache].affectedItems = []; + this[PropertySymbol.matchesCache].affectedItems = []; + + this[PropertySymbol.ownerDocument]?.[PropertySymbol.clearComputedStyleCache](); + } + /** * Called when connected to document. */ diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts index 68c16a13d..03ac5cd52 100644 --- a/packages/happy-dom/src/nodes/node/NodeList.ts +++ b/packages/happy-dom/src/nodes/node/NodeList.ts @@ -1,8 +1,9 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import Element from '../element/Element.js'; +import IHTMLCollection from '../element/IHTMLCollection.js'; import INodeList from './INodeList.js'; -import TNodeListListener from './TNodeListListener.js'; /** * NodeList. @@ -10,15 +11,7 @@ import TNodeListListener from './TNodeListListener.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList */ class NodeList extends Array implements INodeList { - #eventListeners: { - add: WeakRef>[]; - insert: WeakRef>[]; - remove: WeakRef>[]; - } = { - add: [], - insert: [], - remove: [] - }; + public [PropertySymbol.attachedHTMLCollection]: IHTMLCollection | null = null; /** * Returns `Symbol.toStringTag`. @@ -69,7 +62,11 @@ class NodeList extends Array implements INodeList { super.push(item); - this[PropertySymbol.dispatchEvent]('add', item); + const htmlCollection = this[PropertySymbol.attachedHTMLCollection]; + + if (htmlCollection) { + htmlCollection[PropertySymbol.addItem](item); + } return true; } @@ -101,7 +98,11 @@ class NodeList extends Array implements INodeList { super.splice(index, 0, newItem); - this[PropertySymbol.dispatchEvent]('insert', newItem, referenceItem); + const htmlCollection = this[PropertySymbol.attachedHTMLCollection]; + + if (htmlCollection) { + htmlCollection[PropertySymbol.insertItem](newItem, referenceItem); + } return true; } @@ -124,66 +125,13 @@ class NodeList extends Array implements INodeList { super.splice(index, 1); - this[PropertySymbol.dispatchEvent]('remove', item); - - return true; - } - - /** - * Adds event listener. - * - * @param type Type. - * @param listener Listener. - */ - public [PropertySymbol.addEventListener]( - type: 'add' | 'insert' | 'remove', - listener: TNodeListListener - ): void { - this.#eventListeners[type].push(new WeakRef(listener)); - } + const htmlCollection = this[PropertySymbol.attachedHTMLCollection]; - /** - * Removes event listener. - * - * @param type Type. - * @param listener Listener. - */ - public [PropertySymbol.removeEventListener]( - type: 'add' | 'insert' | 'remove', - listener: TNodeListListener - ): void { - const listeners = this.#eventListeners[type]; - for (let i = 0, max = listeners.length; i < max; i++) { - if (listeners[i].deref() === listener) { - listeners.splice(i, 1); - return; - } + if (htmlCollection) { + htmlCollection[PropertySymbol.removeItem](item); } - } - /** - * Dispatches event. - * - * @param type Type. - * @param item Item. - * @param referenceItem Reference item. - */ - public [PropertySymbol.dispatchEvent]( - type: 'add' | 'insert' | 'remove', - item: T, - referenceItem?: T | null - ): void { - const listeners = this.#eventListeners[type]; - for (let i = 0, max = listeners.length; i < max; i++) { - const listener = listeners[i].deref(); - if (listener) { - listener(item, referenceItem); - } else { - listeners.splice(i, 1); - i--; - max--; - } - } + return true; } /** diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index 5b86c7724..4c63b03c7 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -3,11 +3,10 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import Document from '../document/Document.js'; import Element from '../element/Element.js'; -import HTMLCollection from '../element/HTMLCollection.js'; import Node from '../node/Node.js'; import NamespaceURI from '../../config/NamespaceURI.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; -import NodeTypeEnum from '../node/NodeTypeEnum.js'; +import HTMLCollection from '../element/HTMLCollection.js'; /** * Parent node utility. @@ -91,32 +90,10 @@ export default class ParentNodeUtility { parentNode: Element | DocumentFragment | Document, className: string ): IHTMLCollection { - const childNodes = parentNode[PropertySymbol.childNodesFlatten]; - const matches: IHTMLCollection = new HTMLCollection((item) => - (item).className.split(' ').includes(className) - ); - - childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { - matches[PropertySymbol.addItem](item); - }); - - childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { - matches[PropertySymbol.insertItem](item, referenceItem); + return new HTMLCollection({ + filter: (item: Element) => (item).className.split(' ').includes(className), + observeNode: parentNode }); - childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { - matches[PropertySymbol.removeItem](item); - }); - - for (const childNode of childNodes) { - if ( - childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (childNode).className.split(' ').includes(className) - ) { - matches[PropertySymbol.addItem](childNode); - } - } - - return matches; } /** @@ -132,33 +109,11 @@ export default class ParentNodeUtility { ): IHTMLCollection { const upperTagName = tagName.toUpperCase(); const includeAll = tagName === '*'; - const childNodes = parentNode[PropertySymbol.childNodesFlatten]; - const matches: IHTMLCollection = new HTMLCollection( - !includeAll && ((item) => item[PropertySymbol.tagName] === upperTagName) - ); - childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { - matches[PropertySymbol.addItem](item); + return new HTMLCollection({ + filter: (item: Element) => includeAll || item[PropertySymbol.tagName] === upperTagName, + observeNode: parentNode }); - - childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { - matches[PropertySymbol.insertItem](item, referenceItem); - }); - - childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { - matches[PropertySymbol.removeItem](item); - }); - - for (const childNode of childNodes) { - if ( - (includeAll || childNode[PropertySymbol.tagName] === upperTagName) && - childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode - ) { - matches[PropertySymbol.addItem](childNode); - } - } - - return matches; } /** @@ -177,37 +132,14 @@ export default class ParentNodeUtility { // When the namespace is HTML, the tag name is case-insensitive. const formattedTagName = namespaceURI === NamespaceURI.html ? tagName.toUpperCase() : tagName; const includeAll = tagName === '*'; - const childNodes = parentNode[PropertySymbol.childNodesFlatten]; - const matches: IHTMLCollection = new HTMLCollection( - !includeAll && - ((item) => - item[PropertySymbol.tagName] === formattedTagName && - item[PropertySymbol.namespaceURI] === namespaceURI) - ); - - childNodes[PropertySymbol.addEventListener]('add', (item: Node) => { - matches[PropertySymbol.addItem](item); - }); - childNodes[PropertySymbol.addEventListener]('insert', (item: Node, referenceItem?: Node) => { - matches[PropertySymbol.insertItem](item, referenceItem); + return new HTMLCollection({ + filter: (item: Element) => + includeAll || + (item[PropertySymbol.tagName] === formattedTagName && + item[PropertySymbol.namespaceURI] === namespaceURI), + observeNode: parentNode }); - - childNodes[PropertySymbol.addEventListener]('remove', (item: Node) => { - matches[PropertySymbol.removeItem](item); - }); - - for (const childNode of childNodes) { - if ( - (includeAll || childNode[PropertySymbol.tagName] === formattedTagName) && - childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - childNode[PropertySymbol.namespaceURI] === namespaceURI - ) { - matches[PropertySymbol.addItem](childNode); - } - } - - return matches; } /** @@ -218,24 +150,24 @@ export default class ParentNodeUtility { * @param tagName Tag name. * @returns Matching element. */ - public static getElementByTagName( - parentNode: Element | DocumentFragment | Document, - tagName: string - ): Element { - const upperTagName = tagName.toUpperCase(); - - for (const child of (parentNode)[PropertySymbol.children]) { - if (child[PropertySymbol.tagName] === upperTagName) { - return child; - } - const match = this.getElementByTagName(child, tagName); - if (match) { - return match; - } - } - - return null; - } + // public static getElementByTagName( + // parentNode: Element | DocumentFragment | Document, + // tagName: string + // ): Element { + // const upperTagName = tagName.toUpperCase(); + + // for (const child of (parentNode)[PropertySymbol.children]) { + // if (child[PropertySymbol.tagName] === upperTagName) { + // return child; + // } + // const match = this.getElementByTagName(child, tagName); + // if (match) { + // return match; + // } + // } + + // return null; + // } /** * Returns an element by ID. diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index 2214d2212..806bab04c 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -12,6 +12,9 @@ import IHTMLElementTagNameMap from '../config/IHTMLElementTagNameMap.js'; import ISVGElementTagNameMap from '../config/ISVGElementTagNameMap.js'; import IHTMLCollection from '../nodes/element/IHTMLCollection.js'; import INodeList from '../nodes/node/INodeList.js'; +import ICachedQuerySelectorAllItem from '../nodes/node/ICachedQuerySelectorAllItem.js'; +import ICachedQuerySelectorItem from '../nodes/node/ICachedQuerySelectorItem.js'; +import ICachedMatchesItem from '../nodes/node/ICachedMatchesItem.js'; type DocumentPositionAndElement = { documentPosition: string; @@ -86,6 +89,16 @@ export default class QuerySelector { return new NodeList(); } + const cache = node[PropertySymbol.querySelectorAllCache]; + const cachedResult = cache.items.get(selector); + + if (cachedResult?.result) { + const result = cachedResult.result.deref(); + if (result) { + return result; + } + } + if (INVALID_SELECTOR_REGEXP.test(selector)) { throw new Error( `Failed to execute 'querySelectorAll' on '${node.constructor.name}': '${selector}' is not a valid selector.` @@ -93,21 +106,21 @@ export default class QuerySelector { } const groups = SelectorParser.getSelectorGroups(selector); - let matches: DocumentPositionAndElement[] = []; - - for (const items of groups) { - matches = matches.concat( - node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode - ? this.findAll(node, [node], items) - : this.findAll(null, (node)[PropertySymbol.children], items) - ); - } - const nodeList: INodeList = new NodeList(); const matchesMap: { [position: string]: Element } = {}; + const cachedItem = { + result: new WeakRef(nodeList) + }; + node[PropertySymbol.querySelectorAllCache].items.set(selector, cachedItem); - for (let i = 0, max = matches.length; i < max; i++) { - matchesMap[matches[i].documentPosition] = matches[i].element; + for (const items of groups) { + const matches = + node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode + ? this.findAll(node, [node], items, cachedItem) + : this.findAll(null, (node)[PropertySymbol.children], items, cachedItem); + for (const match of matches) { + matchesMap[match.documentPosition] = match.element; + } } const keys = Object.keys(matchesMap).sort(); @@ -175,19 +188,37 @@ export default class QuerySelector { return null; } + const cachedResult = node[PropertySymbol.querySelectorCache].items.get(selector); + + if (cachedResult?.result) { + const result = cachedResult.result.deref(); + if (result) { + return result; + } + } + if (INVALID_SELECTOR_REGEXP.test(selector)) { throw new Error( `Failed to execute 'querySelector' on '${node.constructor.name}': '${selector}' is not a valid selector.` ); } + const cachedItem: ICachedQuerySelectorItem = { + result: >{ + deref: () => null + } + }; + + node[PropertySymbol.querySelectorCache].items.set(selector, cachedItem); + for (const items of SelectorParser.getSelectorGroups(selector)) { const match = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode - ? this.findFirst(node, [node], items) - : this.findFirst(null, (node)[PropertySymbol.children], items); + ? this.findFirst(node, [node], items, cachedItem) + : this.findFirst(null, (node)[PropertySymbol.children], items, cachedItem); if (match) { + cachedItem.result = new WeakRef(match); return match; } } @@ -220,6 +251,11 @@ export default class QuerySelector { } const ignoreErrors = options?.ignoreErrors; + const cachedResult = element[PropertySymbol.matchesCache].items.get(selector); + + if (cachedResult?.result) { + return cachedResult.result.match; + } if (INVALID_SELECTOR_REGEXP.test(selector)) { if (ignoreErrors) { @@ -230,10 +266,17 @@ export default class QuerySelector { ); } + const cachedItem: ICachedMatchesItem = { + result: { match: null } + }; + + element[PropertySymbol.matchesCache].items.set(selector, cachedItem); + for (const items of SelectorParser.getSelectorGroups(selector, options)) { - const result = this.matchSelector(element, element, items.reverse(), 0); + const result = this.matchSelector(element, element, items.reverse(), cachedItem); if (result) { + cachedItem.result.match = result; return result; } } @@ -247,6 +290,7 @@ export default class QuerySelector { * @param targetElement Target element. * @param currentElement Current element. * @param selectorItems Selector items. + * @param cachedItem Cached item. * @param [priorityWeight] Priority weight. * @returns Result. */ @@ -254,6 +298,7 @@ export default class QuerySelector { targetElement: Element, currentElement: Element, selectorItems: SelectorItem[], + cachedItem: ICachedMatchesItem, priorityWeight = 0 ): ISelectorMatch | null { const selectorItem = selectorItems[0]; @@ -268,11 +313,14 @@ export default class QuerySelector { switch (selectorItem.combinator) { case SelectorCombinatorEnum.adjacentSibling: - if (currentElement.previousElementSibling) { + const previousElementSibling = currentElement.previousElementSibling; + if (previousElementSibling) { + previousElementSibling[PropertySymbol.matchesCache].affectedItems.push(cachedItem); const match = this.matchSelector( targetElement, - currentElement.previousElementSibling, + previousElementSibling, selectorItems.slice(1), + cachedItem, priorityWeight + result.priorityWeight ); @@ -283,11 +331,14 @@ export default class QuerySelector { break; case SelectorCombinatorEnum.child: case SelectorCombinatorEnum.descendant: - if (currentElement.parentElement) { + const parentElement = currentElement.parentElement; + if (parentElement) { + parentElement[PropertySymbol.matchesCache].affectedItems.push(cachedItem); const match = this.matchSelector( targetElement, - currentElement.parentElement, + parentElement, selectorItems.slice(1), + cachedItem, priorityWeight + result.priorityWeight ); if (match) { @@ -298,15 +349,17 @@ export default class QuerySelector { } } + const parentElement = currentElement.parentElement; if ( selectorItem.combinator === SelectorCombinatorEnum.descendant && targetElement !== currentElement && - currentElement.parentElement + parentElement ) { return this.matchSelector( targetElement, - currentElement.parentElement, + parentElement, selectorItems, + cachedItem, priorityWeight ); } @@ -320,6 +373,7 @@ export default class QuerySelector { * @param rootElement Root element. * @param children Child elements. * @param selectorItems Selector items. + * @param cachedItem Cached item. * @param [documentPosition] Document position of the element. * @returns Document position and element map. */ @@ -327,6 +381,7 @@ export default class QuerySelector { rootElement: Element, children: Element[] | IHTMLCollection, selectorItems: SelectorItem[], + cachedItem: ICachedQuerySelectorAllItem, documentPosition?: string ): DocumentPositionAndElement[] { const selectorItem = selectorItems[0]; @@ -338,6 +393,8 @@ export default class QuerySelector { const childrenOfChild = (child)[PropertySymbol.children]; const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i); + child[PropertySymbol.querySelectorAllCache].affectedItems.push(cachedItem); + if (selectorItem.match(child)) { if (!nextSelectorItem) { if (rootElement !== child) { @@ -355,6 +412,7 @@ export default class QuerySelector { rootElement, [child.nextElementSibling], selectorItems.slice(1), + cachedItem, position ) ); @@ -363,7 +421,13 @@ export default class QuerySelector { case SelectorCombinatorEnum.descendant: case SelectorCombinatorEnum.child: matched = matched.concat( - this.findAll(rootElement, childrenOfChild, selectorItems.slice(1), position) + this.findAll( + rootElement, + childrenOfChild, + selectorItems.slice(1), + cachedItem, + position + ) ); break; } @@ -372,7 +436,7 @@ export default class QuerySelector { if (selectorItem.combinator === SelectorCombinatorEnum.descendant && childrenOfChild.length) { matched = matched.concat( - this.findAll(rootElement, childrenOfChild, selectorItems, position) + this.findAll(rootElement, childrenOfChild, selectorItems, cachedItem, position) ); } } @@ -386,12 +450,14 @@ export default class QuerySelector { * @param rootElement Root element. * @param children Child elements. * @param selectorItems Selector items. + * @param cachedItem Cached item. * @returns HTML element. */ private static findFirst( rootElement: Element, children: Element[] | IHTMLCollection, - selectorItems: SelectorItem[] + selectorItems: SelectorItem[], + cachedItem: ICachedQuerySelectorItem ): Element { const selectorItem = selectorItems[0]; const nextSelectorItem = selectorItems[1]; @@ -399,6 +465,8 @@ export default class QuerySelector { for (const child of children) { const childrenOfChild = (child)[PropertySymbol.children]; + child[PropertySymbol.querySelectorCache].affectedItems.push(cachedItem); + if (selectorItem.match(child)) { if (!nextSelectorItem) { if (rootElement !== child) { @@ -411,7 +479,8 @@ export default class QuerySelector { const match = this.findFirst( rootElement, [child.nextElementSibling], - selectorItems.slice(1) + selectorItems.slice(1), + cachedItem ); if (match) { return match; @@ -420,7 +489,12 @@ export default class QuerySelector { break; case SelectorCombinatorEnum.descendant: case SelectorCombinatorEnum.child: - const match = this.findFirst(rootElement, childrenOfChild, selectorItems.slice(1)); + const match = this.findFirst( + rootElement, + childrenOfChild, + selectorItems.slice(1), + cachedItem + ); if (match) { return match; } @@ -430,7 +504,7 @@ export default class QuerySelector { } if (selectorItem.combinator === SelectorCombinatorEnum.descendant && childrenOfChild.length) { - const match = this.findFirst(rootElement, childrenOfChild, selectorItems); + const match = this.findFirst(rootElement, childrenOfChild, selectorItems, cachedItem); if (match) { return match; diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts index c59c27d6d..7b41dd879 100644 --- a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts +++ b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts @@ -516,7 +516,7 @@ describe('HTMLElement', () => { const formNode = (element[PropertySymbol.formNode] = document.createElement('div')); const selectNode = (element[PropertySymbol.selectNode] = document.createElement('div')); const textAreaNode = (element[PropertySymbol.textAreaNode] = document.createElement('div')); - const observers = element[PropertySymbol.observers]; + const mutationListeners = element[PropertySymbol.mutationListeners]; const isValue = (element[PropertySymbol.isValue] = 'test'); window.customElements.define('custom-element', CustomElement); @@ -535,7 +535,7 @@ describe('HTMLElement', () => { expect(customElement[PropertySymbol.formNode] === formNode).toBe(true); expect(customElement[PropertySymbol.selectNode] === selectNode).toBe(true); expect(customElement[PropertySymbol.textAreaNode] === textAreaNode).toBe(true); - expect(customElement[PropertySymbol.observers] === observers).toBe(true); + expect(customElement[PropertySymbol.mutationListeners] === mutationListeners).toBe(true); expect(customElement[PropertySymbol.isValue] === isValue).toBe(true); expect(customElement.attributes.length).toBe(1); expect(customElement.attributes[0] === attribute1).toBe(true); diff --git a/packages/happy-dom/test/nodes/parent-node/ParentNodeUtility.test.ts b/packages/happy-dom/test/nodes/parent-node/ParentNodeUtility.test.ts index 95e359719..af839dcb7 100644 --- a/packages/happy-dom/test/nodes/parent-node/ParentNodeUtility.test.ts +++ b/packages/happy-dom/test/nodes/parent-node/ParentNodeUtility.test.ts @@ -264,28 +264,28 @@ describe('ParentNodeUtility', () => { }); }); - describe('getElementByTagName()', () => { - it('Returns the first element matching a tag name.', () => { - const parent = document.createElement('div'); - const div1 = document.createElement('div'); - const div2 = document.createElement('div'); - const div3 = document.createElement('div'); - const div4 = document.createElement('div'); - const span1 = document.createElement('span'); - const span2 = document.createElement('span'); - const span3 = document.createElement('span'); - - parent.appendChild(div1); - div1.appendChild(div2); - div2.appendChild(span1); - span1.appendChild(div3); - div3.appendChild(span2); - div3.appendChild(span3); - span3.appendChild(div4); - - expect(ParentNodeUtility.getElementByTagName(parent, 'div') === div1).toBe(true); - }); - }); + // describe('getElementByTagName()', () => { + // it('Returns the first element matching a tag name.', () => { + // const parent = document.createElement('div'); + // const div1 = document.createElement('div'); + // const div2 = document.createElement('div'); + // const div3 = document.createElement('div'); + // const div4 = document.createElement('div'); + // const span1 = document.createElement('span'); + // const span2 = document.createElement('span'); + // const span3 = document.createElement('span'); + + // parent.appendChild(div1); + // div1.appendChild(div2); + // div2.appendChild(span1); + // span1.appendChild(div3); + // div3.appendChild(span2); + // div3.appendChild(span3); + // span3.appendChild(div4); + + // expect(ParentNodeUtility.getElementByTagName(parent, 'div') === div1).toBe(true); + // }); + // }); describe('getElementById()', () => { it('Returns the first element matching an id.', () => { From ecb518ec24333e9c997c42341e55d3eceeac2f79 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 21 Jun 2024 01:51:52 +0200 Subject: [PATCH 16/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 3 +- .../src/nodes/element/HTMLCollection.ts | 113 ++++++++++++------ .../html-button-element/HTMLButtonElement.ts | 1 + .../HTMLFieldSetElement.ts | 1 + .../HTMLFormControlsCollection.ts | 18 +-- .../html-form-element/HTMLFormElement.ts | 106 ++++++++++------ .../IHTMLFormControlsCollection.ts | 4 +- .../nodes/html-form-element/IRadioNodeList.ts | 11 ++ .../html-input-element/HTMLInputElement.ts | 1 + .../html-label-element/HTMLLabelElement.ts | 27 +++-- .../html-select-element/HTMLSelectElement.ts | 1 + .../html-style-element/HTMLStyleElement.ts | 30 +++++ .../HTMLTextAreaElement.ts | 31 +++++ packages/happy-dom/src/nodes/node/Node.ts | 10 ++ .../nodes/parent-node/ParentNodeUtility.ts | 5 +- .../HTMLLabelElement.test.ts | 2 + 16 files changed, 257 insertions(+), 107 deletions(-) create mode 100644 packages/happy-dom/src/nodes/html-form-element/IRadioNodeList.ts diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 1852eeee2..5f384bf31 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -188,7 +188,7 @@ export const removeEventListener = Symbol('removeEventListener'); export const htmlCollections = Symbol('htmlCollections'); export const namedItemListeners = Symbol('namedItemListeners'); export const dispatchEvent = Symbol('dispatchEvent'); -export const getNamedItems = Symbol('getNamedItems'); +export const createNamedItemsNodeList = Symbol('createNamedItemsNodeList'); export const setNamedItemProperty = Symbol('setNamedItemProperty'); export const selectedOptions = Symbol('selectedOptions'); export const styleNode = Symbol('styleNode'); @@ -204,3 +204,4 @@ export const matchesCache = Symbol('matchesCache'); export const computedStyleCache = Symbol('computedStyleCache'); export const clearComputedStyleCache = Symbol('clearComputedStyleCache'); export const clearCache = Symbol('clearCache'); +export const isInsideObservedFormNode = Symbol('isInsideObservedFormNode'); diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index d871eb73c..72bd4541e 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -5,7 +5,9 @@ import Attr from '../attr/Attr.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import Document from '../document/Document.js'; import HTMLElement from '../html-element/HTMLElement.js'; +import INodeList from '../node/INodeList.js'; import Node from '../node/Node.js'; +import NodeList from '../node/NodeList.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import Element from './Element.js'; import IHTMLCollection from './IHTMLCollection.js'; @@ -25,7 +27,7 @@ export default class HTMLCollection extends Array implements IHTMLCollection { - public [PropertySymbol.namedItems] = new Map>(); + public [PropertySymbol.namedItems] = new Map>(); #namedNodeMapListeners = new Map< T, { set: TNamedNodeMapListener; remove: TNamedNodeMapListener } @@ -268,16 +270,6 @@ export default class HTMLCollection return super.includes(item); } - /** - * Returns named items. - * - * @param name Name. - * @returns Named items. - */ - protected [PropertySymbol.getNamedItems](name: string): T[] { - return this[PropertySymbol.namedItems].get(name) || []; - } - /** * Sets named item property. * @@ -316,6 +308,15 @@ export default class HTMLCollection } } + /** + * Creates a new NodeList to be used as a named item. + * + * @returns NodeList. + */ + protected [PropertySymbol.createNamedItemsNodeList](): INodeList { + return new NodeList(); + } + /** * Returns "true" if the property name is valid. * @@ -352,18 +353,17 @@ export default class HTMLCollection const name = item[PropertySymbol.attributes][attributeName]?.value; if (name) { - const namedItems = this[PropertySymbol.getNamedItems](name); + const namedItems = this[PropertySymbol.namedItems].get(name); - if (!namedItems.includes(item)) { + if (!namedItems?.[PropertySymbol.includes](item)) { this[PropertySymbol.namedItems].set(name, namedItems); this[PropertySymbol.setNamedItemProperty](name); } } else { - const namedItems = this[PropertySymbol.getNamedItems](name); - const index = namedItems.indexOf(item); + const namedItems = this[PropertySymbol.namedItems].get(name); - if (index !== -1) { - namedItems.splice(index, 1); + if (namedItems) { + namedItems[PropertySymbol.removeItem](item); } this[PropertySymbol.setNamedItemProperty](name); @@ -395,9 +395,11 @@ export default class HTMLCollection for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const name = (item)[PropertySymbol.attributes][attributeName]?.value; if (name) { - const namedItems = this[PropertySymbol.getNamedItems](name); + const namedItems = + this[PropertySymbol.namedItems].get(name) || + this[PropertySymbol.createNamedItemsNodeList](); - if (namedItems.includes(item)) { + if (namedItems[PropertySymbol.includes](item)) { return; } @@ -427,15 +429,13 @@ export default class HTMLCollection for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const name = (item)[PropertySymbol.attributes][attributeName]?.value; if (name) { - const namedItems = this[PropertySymbol.getNamedItems](name); + const namedItems = this[PropertySymbol.namedItems].get(name); - const index = namedItems.indexOf(item); - - if (index === -1) { + if (!namedItems) { return; } - namedItems.splice(index, 1); + namedItems[PropertySymbol.removeItem](item); this[PropertySymbol.setNamedItemProperty](name); } @@ -464,24 +464,31 @@ export default class HTMLCollection (!filter || filter(addedNode)) ) { const index = this.#getObservedItemIndex(parentNode, addedNode); - - if (index === -1) { - throw new Error( - `Failed to update observed HTMLCollection after a mutation. Added element "${ - addedNode[PropertySymbol.tagName] - }" could not be found.` - ); - } - + addedNode[PropertySymbol.isInsideObservedFormNode] = true; this[PropertySymbol.insertItem](addedNode, this[index] || null); + } else if (addedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + const items = this.#getItemsInNode(addedNode); + const index = this.#getObservedItemIndex(parentNode, items[items.length - 1]); + const referenceItem = this[index]; + + for (let i = items.length - 1; i >= 0; i--) { + items[i][PropertySymbol.isInsideObservedFormNode] = true; + this[PropertySymbol.insertItem](items[i], referenceItem); + } } } else { - for (const node of record.removedNodes) { - if ( - node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(node)) - ) { - this[PropertySymbol.removeItem](node); + const removedNode = record.removedNodes[0]; + if ( + removedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(removedNode)) + ) { + removedNode[PropertySymbol.isInsideObservedFormNode] = true; + this[PropertySymbol.removeItem](removedNode); + } else { + const items = this.#getItemsInNode(removedNode); + for (let i = items.length - 1; i >= 0; i--) { + items[i][PropertySymbol.isInsideObservedFormNode] = true; + this[PropertySymbol.removeItem](items[i]); } } } @@ -489,6 +496,33 @@ export default class HTMLCollection }); } + /** + * Returns items in node. + * + * @param parentNode Parent node. + */ + #getItemsInNode(parentNode: Element | DocumentFragment | Document): T[] { + const filter = this.#filter; + const items: T[] = []; + + if ( + parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(parentNode)) + ) { + items.push(parentNode); + } else { + const children = parentNode[PropertySymbol.children]; + for (let a = 0; a < children.length; a++) { + const childrenOfChild = children[a][PropertySymbol.children]; + for (let b = 0; b < childrenOfChild.length; b++) { + items.push(childrenOfChild[b]); + } + } + } + + return items; + } + /** * Loads initial observed items. * @@ -503,6 +537,7 @@ export default class HTMLCollection children[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && (!filter || filter(children[i])) ) { + children[i][PropertySymbol.isInsideObservedFormNode] = true; this[PropertySymbol.addItem](children[i]); } diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index 6bbf984c6..5f855e88e 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -22,6 +22,7 @@ export default class HTMLButtonElement extends HTMLElement { public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.formNode]: HTMLFormElement | null = null; + public [PropertySymbol.isInsideObservedFormNode] = false; /** * Returns validation message. diff --git a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts index 00e9d300e..3292ae4b2 100644 --- a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts +++ b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts @@ -34,6 +34,7 @@ export default class HTMLFieldSetElement extends HTMLElement { observeNode: this }); public [PropertySymbol.formNode]: HTMLFormElement | null = null; + public [PropertySymbol.isInsideObservedFormNode] = false; /** * Returns elements. diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index 43b7dd94f..ac4ee11ec 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -1,7 +1,7 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLCollection from '../element/HTMLCollection.js'; import HTMLFormElement from './HTMLFormElement.js'; -import RadioNodeList from './RadioNodeList.js'; +import IRadioNodeList from './IRadioNodeList.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; /** @@ -11,9 +11,9 @@ import THTMLFormControlElement from './THTMLFormControlElement.js'; */ export default class HTMLFormControlsCollection extends HTMLCollection< THTMLFormControlElement, - THTMLFormControlElement | RadioNodeList + THTMLFormControlElement | IRadioNodeList > { - public [PropertySymbol.namedItems] = new Map(); + public [PropertySymbol.namedItems] = new Map(); #formElement: HTMLFormElement; /** @@ -39,7 +39,7 @@ export default class HTMLFormControlsCollection extends HTMLCollection< /** * @override */ - public namedItem(name: string): THTMLFormControlElement | RadioNodeList | null { + public namedItem(name: string): THTMLFormControlElement | IRadioNodeList | null { const namedItems = this[PropertySymbol.namedItems].get(name); if (!namedItems?.length) { @@ -111,16 +111,6 @@ export default class HTMLFormControlsCollection extends HTMLCollection< return true; } - /** - * Returns named items. - * - * @param name Name. - * @returns Named items. - */ - protected [PropertySymbol.getNamedItems](name: string): RadioNodeList { - return this[PropertySymbol.namedItems].get(name) || new RadioNodeList(); - } - /** * Sets named item property. * diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index 438dccb70..7f69e1325 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -17,6 +17,7 @@ import IHTMLFormControlsCollection from './IHTMLFormControlsCollection.js'; import IMutationListener from '../../mutation-observer/IMutationListener.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; +import NodeTypeEnum from '../node/NodeTypeEnum.js'; /** * HTML Form Element. @@ -370,55 +371,74 @@ export default class HTMLFormElement extends HTMLElement { if (!id) { return; } + switch (record.type) { case MutationTypeEnum.childList: - const addedNode = record.addedNodes[0]; - const removedNode = record.removedNodes[0]; - if ( - addedNode && - (addedNode[PropertySymbol.tagName] === 'INPUT' || - addedNode[PropertySymbol.tagName] === 'SELECT' || - addedNode[PropertySymbol.tagName] === 'TEXTAREA' || - addedNode[PropertySymbol.tagName] === 'BUTTON' || - addedNode[PropertySymbol.tagName] === 'FIELDSET') && - addedNode[PropertySymbol.attributes]?.['form']?.value === id && - addedNode[PropertySymbol.formNode] !== this - ) { - addedNode[PropertySymbol.formNode] = this; - this[PropertySymbol.elements][PropertySymbol.addItem]( - addedNode - ); - } else if ( - (removedNode[PropertySymbol.tagName] === 'INPUT' || - removedNode[PropertySymbol.tagName] === 'SELECT' || - removedNode[PropertySymbol.tagName] === 'TEXTAREA' || - removedNode[PropertySymbol.tagName] === 'BUTTON' || - removedNode[PropertySymbol.tagName] === 'FIELDSET') && - removedNode[PropertySymbol.attributes]?.['form']?.value === id - ) { - this[PropertySymbol.elements][PropertySymbol.removeItem]( - removedNode - ); + if (record.addedNodes.length) { + const addedNode = record.addedNodes[0]; + + if ( + addedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + addedNode[PropertySymbol.attributes]?.['form']?.value === id && + !addedNode[PropertySymbol.isInsideObservedFormNode] && + this.#isFormControlElement(addedNode) + ) { + addedNode[PropertySymbol.formNode] = this; + this[PropertySymbol.elements][PropertySymbol.addItem]( + addedNode + ); + } else if (addedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + const items = this.querySelectorAll( + `INPUT['form="${id}"], SELECT['form="${id}"], TEXTAREA['form="${id}"], BUTTON['form="${id}"], FIELDSET['form="${id}"]` + ); + for (const item of items) { + if (!item[PropertySymbol.isInsideObservedFormNode]) { + this[PropertySymbol.elements][PropertySymbol.addItem]( + item + ); + } + } + } + } else { + const removedNode = record.removedNodes[0]; + + if ( + removedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + removedNode[PropertySymbol.attributes]?.['form']?.value === id && + !removedNode[PropertySymbol.isInsideObservedFormNode] && + this.#isFormControlElement(removedNode) + ) { + this[PropertySymbol.elements][PropertySymbol.removeItem]( + removedNode + ); + } else if (removedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + const items = this.querySelectorAll( + `INPUT['form="${id}"], SELECT['form="${id}"], TEXTAREA['form="${id}"], BUTTON['form="${id}"], FIELDSET['form="${id}"]` + ); + for (const item of items) { + if (!item[PropertySymbol.isInsideObservedFormNode]) { + this[PropertySymbol.elements][PropertySymbol.removeItem]( + item + ); + } + } + } } break; case MutationTypeEnum.attributes: if ( record.attributeName === 'form' && - (record.target[PropertySymbol.tagName] === 'INPUT' || - record.target[PropertySymbol.tagName] === 'SELECT' || - record.target[PropertySymbol.tagName] === 'TEXTAREA' || - record.target[PropertySymbol.tagName] === 'BUTTON' || - record.target[PropertySymbol.tagName] === 'FIELDSET') + this.#isFormControlElement(record.target) ) { if ( record.target[PropertySymbol.attributes]?.['form']?.[PropertySymbol.value] === this[PropertySymbol.attributes]?.['id']?.[PropertySymbol.value] && - record.target[PropertySymbol.formNode] !== this + !record.target[PropertySymbol.isInsideObservedFormNode] ) { this[PropertySymbol.elements][PropertySymbol.addItem]( record.target ); - } else { + } else if (!record.target[PropertySymbol.isInsideObservedFormNode]) { this[PropertySymbol.elements][PropertySymbol.removeItem]( record.target ); @@ -442,7 +462,7 @@ export default class HTMLFormElement extends HTMLElement { for (const element of this[PropertySymbol.ownerDocument].querySelectorAll( `INPUT[form="${id}"], SELECT[form="${id}"], TEXTAREA[form="${id}"], BUTTON[form="${id}"], FIELDSET[form="${id}"]` )) { - if (element[PropertySymbol.formNode] !== this) { + if (!element[PropertySymbol.isInsideObservedFormNode]) { this[PropertySymbol.elements][PropertySymbol.addItem](element); } } @@ -549,6 +569,22 @@ export default class HTMLFormElement extends HTMLElement { }); } + /** + * Checks if an element is a form control element. + * + * @param item Item. + * @returns True if the item is a form control element. + */ + #isFormControlElement(item: THTMLFormControlElement): boolean { + return ( + item[PropertySymbol.tagName] === 'INPUT' || + item[PropertySymbol.tagName] === 'SELECT' || + item[PropertySymbol.tagName] === 'TEXTAREA' || + item[PropertySymbol.tagName] === 'BUTTON' || + item[PropertySymbol.tagName] === 'FIELDSET' + ); + } + /** * Triggered when an attribute is set. * diff --git a/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts index a41df25b9..36354571c 100644 --- a/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts @@ -1,6 +1,6 @@ import IHTMLCollection from '../element/IHTMLCollection.js'; -import RadioNodeList from './RadioNodeList.js'; +import IRadioNodeList from './IRadioNodeList.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; export default interface IHTMLFormControlsCollection - extends IHTMLCollection {} + extends IHTMLCollection {} diff --git a/packages/happy-dom/src/nodes/html-form-element/IRadioNodeList.ts b/packages/happy-dom/src/nodes/html-form-element/IRadioNodeList.ts new file mode 100644 index 000000000..9c6b9cf40 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-form-element/IRadioNodeList.ts @@ -0,0 +1,11 @@ +import INodeList from '../node/INodeList.js'; +import THTMLFormControlElement from './THTMLFormControlElement.js'; + +export default interface IRadioNodeList extends INodeList { + /** + * Returns value. + * + * @returns Value. + */ + readonly value: string; +} diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 30a8a487a..3b5604fb4 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -48,6 +48,7 @@ export default class HTMLInputElement extends HTMLElement { public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.files]: FileList = new FileList(); public [PropertySymbol.formNode]: HTMLFormElement | null = null; + public [PropertySymbol.isInsideObservedFormNode] = false; // Private properties #selectionStart: number = null; diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts index 6ecf213df..1021413e1 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts @@ -53,19 +53,20 @@ export default class HTMLLabelElement extends HTMLElement { const control = ( (this[PropertySymbol.rootNode]).getElementById(htmlFor) ); - switch (control.tagName) { - case 'input': - return (control).type !== 'hidden' ? control : null; - case 'button': - case 'meter': - case 'output': - case 'progress': - case 'select': - case 'textarea': - case 'textarea': - return control; - default: - return null; + if (control) { + switch (control[PropertySymbol.tagName]) { + case 'INPUT': + return (control).type !== 'hidden' ? control : null; + case 'BUTTON': + case 'METER': + case 'OUTPUT': + case 'PROGRESS': + case 'SELECT': + case 'TEXTAREA': + return control; + default: + return null; + } } } return ( diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 8d4a7ae0e..4cd1a085b 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -25,6 +25,7 @@ export default class HTMLSelectElement extends HTMLElement { public [PropertySymbol.options]: HTMLOptionsCollection = new HTMLOptionsCollection(this); public [PropertySymbol.formNode]: HTMLFormElement | null = null; public [PropertySymbol.selectedOptions]: IHTMLCollection | null = null; + public [PropertySymbol.isInsideObservedFormNode] = false; // Events public onchange: (event: Event) => void | null = null; diff --git a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts index 071e06d6e..b96db9479 100644 --- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts +++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts @@ -1,7 +1,9 @@ import CSSStyleSheet from '../../css/CSSStyleSheet.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; import * as PropertySymbol from '../../PropertySymbol.js'; +import Element from '../element/Element.js'; import HTMLElement from '../html-element/HTMLElement.js'; +import NodeTypeEnum from '../node/NodeTypeEnum.js'; import Text from '../text/Text.js'; /** @@ -29,6 +31,14 @@ export default class HTMLStyleElement extends HTMLElement { if (node instanceof Text) { node[PropertySymbol.styleNode] = record.addedNodes[0] ? this : null; this[PropertySymbol.updateSheet](); + } else { + const textNodes = this.#findTextNodes(node); + if (textNodes.length) { + for (const textNode of textNodes) { + textNode[PropertySymbol.styleNode] = record.addedNodes[0] ? this : null; + } + this[PropertySymbol.updateSheet](); + } } }) }); @@ -126,4 +136,24 @@ export default class HTMLStyleElement extends HTMLElement { this[PropertySymbol.sheet].replaceSync(this.textContent); } } + + /** + * Finds all text nodes in the element. + * + * @param parentElement Parent element. + * @returns Text nodes. + */ + #findTextNodes(parentElement: Element): Text[] { + const textNodes: Text[] = []; + for (const childNode of parentElement[PropertySymbol.childNodes]) { + if (childNode instanceof Text) { + textNodes.push(childNode); + } else if (childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + for (const textNode of this.#findTextNodes(childNode)) { + textNodes.push(textNode); + } + } + } + return textNodes; + } } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index ca9377956..a659716ad 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -12,6 +12,8 @@ import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtili import Text from '../text/Text.js'; import INodeList from '../node/INodeList.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; +import Element from '../element/Element.js'; +import NodeTypeEnum from '../node/NodeTypeEnum.js'; /** * HTML Text Area Element. @@ -34,6 +36,7 @@ export default class HTMLTextAreaElement extends HTMLElement { public [PropertySymbol.value] = null; public [PropertySymbol.textAreaNode]: HTMLTextAreaElement = this; public [PropertySymbol.formNode]: HTMLFormElement | null = null; + public [PropertySymbol.isInsideObservedFormNode] = false; // Private properties #selectionStart = null; @@ -56,6 +59,14 @@ export default class HTMLTextAreaElement extends HTMLElement { if (node instanceof Text) { node[PropertySymbol.textAreaNode] = record.addedNodes[0] ? this : null; this[PropertySymbol.resetSelection](); + } else if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + const textNodes = this.#findTextNodes(node); + if (textNodes.length) { + for (const textNode of textNodes) { + textNode[PropertySymbol.textAreaNode] = record.addedNodes[0] ? this : null; + } + this[PropertySymbol.resetSelection](); + } } }) }); @@ -609,4 +620,24 @@ export default class HTMLTextAreaElement extends HTMLElement { this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; } } + + /** + * Finds all text nodes in the element. + * + * @param parentElement Parent element. + * @returns Text nodes. + */ + #findTextNodes(parentElement: Element): Text[] { + const textNodes: Text[] = []; + for (const childNode of parentElement[PropertySymbol.childNodes]) { + if (childNode instanceof Text) { + textNodes.push(childNode); + } else if (childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + for (const textNode of this.#findTextNodes(childNode)) { + textNodes.push(textNode); + } + } + } + return textNodes; + } } diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 62259ca8d..0b88f1c5f 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -763,6 +763,12 @@ export default class Node extends EventTarget { * Clears query selector cache. */ public [PropertySymbol.clearCache](): void { + for (const item of this[PropertySymbol.querySelectorCache].items.values()) { + if (item.result) { + item.result = null; + } + } + for (const item of this[PropertySymbol.querySelectorCache].affectedItems) { if (item.result) { item.result = null; @@ -781,6 +787,10 @@ export default class Node extends EventTarget { } } + this[PropertySymbol.querySelectorCache].items = new Map(); + this[PropertySymbol.querySelectorAllCache].items = new Map(); + this[PropertySymbol.matchesCache].items = new Map(); + this[PropertySymbol.querySelectorCache].affectedItems = []; this[PropertySymbol.querySelectorAllCache].affectedItems = []; this[PropertySymbol.matchesCache].affectedItems = []; diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index 4c63b03c7..fc8e93488 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -135,9 +135,8 @@ export default class ParentNodeUtility { return new HTMLCollection({ filter: (item: Element) => - includeAll || - (item[PropertySymbol.tagName] === formattedTagName && - item[PropertySymbol.namespaceURI] === namespaceURI), + (includeAll || item[PropertySymbol.tagName] === formattedTagName) && + item[PropertySymbol.namespaceURI] === namespaceURI, observeNode: parentNode }); } diff --git a/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts b/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts index 850c938cd..bde314c9e 100644 --- a/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts +++ b/packages/happy-dom/test/nodes/html-label-element/HTMLLabelElement.test.ts @@ -78,6 +78,8 @@ describe('HTMLLabelElement', () => { it('Returns parent form element.', () => { const form = document.createElement('form'); const div = document.createElement('div'); + const input = document.createElement('input'); + element.appendChild(input); div.appendChild(element); form.appendChild(div); expect(element.form).toBe(form); From 44e6748dee483af2371ff3b7f162d89e90bc5886 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 28 Jun 2024 08:50:08 +0200 Subject: [PATCH 17/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 17 +- packages/happy-dom/src/form-data/FormData.ts | 59 +- .../document-fragment/DocumentFragment.ts | 4 +- .../happy-dom/src/nodes/document/Document.ts | 4 +- .../happy-dom/src/nodes/element/Element.ts | 16 +- .../src/nodes/element/HTMLCollection.ts | 719 ++++++++++-------- .../element/IHTMLCollectionObservedNode.ts | 10 + .../html-button-element/HTMLButtonElement.ts | 1 - .../HTMLDataListElement.ts | 7 +- .../src/nodes/html-element/HTMLElement.ts | 2 +- .../HTMLFieldSetElement.ts | 25 +- .../HTMLFormControlsCollection.ts | 270 ++++++- .../html-form-element/HTMLFormElement.ts | 221 +----- .../html-input-element/HTMLInputElement.ts | 1 - .../HTMLOptionsCollection.ts | 165 +++- .../html-select-element/HTMLSelectElement.ts | 15 +- .../HTMLTextAreaElement.ts | 1 - .../happy-dom/src/nodes/node/INodeList.ts | 3 - packages/happy-dom/src/nodes/node/Node.ts | 79 +- packages/happy-dom/src/nodes/node/NodeList.ts | 53 +- .../nodes/parent-node/ParentNodeUtility.ts | 28 +- .../HTMLButtonElement.test.ts | 4 +- .../HTMLFieldSetElement.test.ts | 4 +- .../html-form-element/HTMLFormElement.test.ts | 75 +- .../HTMLInputElement.test.ts | 4 +- .../HTMLSelectElement.test.ts | 9 +- .../happy-dom/test/nodes/text/Text.test.ts | 1 + 27 files changed, 1015 insertions(+), 782 deletions(-) create mode 100644 packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 5f384bf31..4b5d38ddb 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -42,7 +42,6 @@ export const listeners = Symbol('listeners'); export const namedItems = Symbol('namedItems'); export const nextActiveElement = Symbol('nextActiveElement'); export const observeMutations = Symbol('observeMutations'); -export const observeMutationsOnce = Symbol('observeMutationsOnce'); export const observedAttributes = Symbol('observedAttributes'); export const mutationListeners = Symbol('mutationListeners'); export const ownerDocument = Symbol('ownerDocument'); @@ -185,11 +184,10 @@ export const includes = Symbol('includes'); export const insertItem = Symbol('insertItem'); export const addEventListener = Symbol('addEventListener'); export const removeEventListener = Symbol('removeEventListener'); -export const htmlCollections = Symbol('htmlCollections'); export const namedItemListeners = Symbol('namedItemListeners'); export const dispatchEvent = Symbol('dispatchEvent'); export const createNamedItemsNodeList = Symbol('createNamedItemsNodeList'); -export const setNamedItemProperty = Symbol('setNamedItemProperty'); +export const updateNamedItemProperty = Symbol('updateNamedItemProperty'); export const selectedOptions = Symbol('selectedOptions'); export const styleNode = Symbol('styleNode'); export const updateSheet = Symbol('updateSheet'); @@ -197,11 +195,20 @@ export const slice = Symbol('slice'); export const replaceItems = Symbol('replaceItems'); export const clearItems = Symbol('clearItems'); export const addItems = Symbol('addItems'); -export const attachedHTMLCollection = Symbol('attachedHTMLCollection'); export const querySelectorCache = Symbol('querySelectorCache'); export const querySelectorAllCache = Symbol('querySelectorAllCache'); export const matchesCache = Symbol('matchesCache'); export const computedStyleCache = Symbol('computedStyleCache'); export const clearComputedStyleCache = Symbol('clearComputedStyleCache'); export const clearCache = Symbol('clearCache'); -export const isInsideObservedFormNode = Symbol('isInsideObservedFormNode'); +export const insertObservedItem = Symbol('insertObservedItem'); +export const removeObservedItem = Symbol('removeObservedItem'); +export const observe = Symbol('observe'); +export const unobserve = Symbol('unobserve'); +export const loadObservedNodes = Symbol('loadObservedNodes'); +export const unloadObservedNodes = Symbol('unloadObservedNodes'); +export const attributeFilter = Symbol('attributeFilter'); +export const onObservedItemAttributeChange = Symbol('onObservedItemAttributeChange'); +export const observeDocument = Symbol('observeDocument'); +export const unobserveDocument = Symbol('unobserveDocument'); +export const htmlCollection = Symbol('htmlCollection'); diff --git a/packages/happy-dom/src/form-data/FormData.ts b/packages/happy-dom/src/form-data/FormData.ts index 5a6feb42d..50414e6da 100644 --- a/packages/happy-dom/src/form-data/FormData.ts +++ b/packages/happy-dom/src/form-data/FormData.ts @@ -5,6 +5,7 @@ import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; +import IRadioNodeList from '../nodes/html-form-element/IRadioNodeList.js'; type FormDataEntry = { name: string; @@ -27,41 +28,39 @@ export default class FormData implements Iterable<[string, string | File]> { * @param [form] Form. */ constructor(form?: HTMLFormElement) { - if (form) { - for (const name of Object.keys( - (form[PropertySymbol.elements])[PropertySymbol.namedItems] - )) { - let radioNodeList = (form[PropertySymbol.elements])[ - PropertySymbol.namedItems - ][name]; - - if ( - radioNodeList[0][PropertySymbol.tagName] === 'INPUT' && - (radioNodeList[0].type === 'checkbox' || radioNodeList[0].type === 'radio') - ) { - const newRadioNodeList = new RadioNodeList(); - for (const node of radioNodeList) { - if ((node).checked) { - newRadioNodeList.push(node); - break; - } + if (!form) { + return; + } + + for (let radioNodeList of (form[PropertySymbol.elements])[ + PropertySymbol.namedItems + ].values()) { + if ( + radioNodeList[0][PropertySymbol.tagName] === 'INPUT' && + (radioNodeList[0].type === 'checkbox' || radioNodeList[0].type === 'radio') + ) { + const newRadioNodeList: IRadioNodeList = new RadioNodeList(); + for (const node of radioNodeList) { + if ((node).checked) { + newRadioNodeList[PropertySymbol.addItem](node); + break; } - radioNodeList = newRadioNodeList; } + radioNodeList = newRadioNodeList; + } - for (const node of radioNodeList) { - if (node.name && SUBMITTABLE_ELEMENTS.includes(node[PropertySymbol.tagName])) { - if (node[PropertySymbol.tagName] === 'INPUT' && node.type === 'file') { - if ((node)[PropertySymbol.files].length === 0) { - this.append(node.name, new File([], '', { type: 'application/octet-stream' })); - } else { - for (const file of (node)[PropertySymbol.files]) { - this.append(node.name, file); - } + for (const node of radioNodeList) { + if (node.name && SUBMITTABLE_ELEMENTS.includes(node[PropertySymbol.tagName])) { + if (node[PropertySymbol.tagName] === 'INPUT' && node.type === 'file') { + if ((node)[PropertySymbol.files].length === 0) { + this.append(node.name, new File([], '', { type: 'application/octet-stream' })); + } else { + for (const file of (node)[PropertySymbol.files]) { + this.append(node.name, file); } - } else if ((node).value) { - this.append(node.name, (node).value); } + } else if ((node).value) { + this.append(node.name, (node).value); } } } diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index 8697cb9c3..8a04f1bb9 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -24,8 +24,8 @@ export default class DocumentFragment extends Node { */ constructor() { super(); - this[PropertySymbol.childNodes][PropertySymbol.attachedHTMLCollection] = - this[PropertySymbol.children]; + + this[PropertySymbol.childNodes][PropertySymbol.htmlCollection] = this[PropertySymbol.children]; } /** diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index ed7315c32..d344ef7f9 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -202,8 +202,8 @@ export default class Document extends Node { super(); this.#browserFrame = injected.browserFrame; this[PropertySymbol.ownerWindow] = injected.window; - this[PropertySymbol.childNodes][PropertySymbol.attachedHTMLCollection] = - this[PropertySymbol.children]; + + this[PropertySymbol.childNodes][PropertySymbol.htmlCollection] = this[PropertySymbol.children]; } /** diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 386cfb5f1..32ac41571 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -120,8 +120,7 @@ export default class Element attributes[PropertySymbol.addEventListener]('set', this.#onSetAttribute.bind(this)); attributes[PropertySymbol.addEventListener]('remove', this.#onRemoveAttribute.bind(this)); - this[PropertySymbol.childNodes][PropertySymbol.attachedHTMLCollection] = - this[PropertySymbol.children]; + this[PropertySymbol.childNodes][PropertySymbol.htmlCollection] = this[PropertySymbol.children]; } /** @@ -496,6 +495,19 @@ export default class Element clone[PropertySymbol.localName] = this[PropertySymbol.localName]; clone[PropertySymbol.namespaceURI] = this[PropertySymbol.namespaceURI]; + for (let i = 0, max = this[PropertySymbol.attributes].length; i < max; i++) { + const attribute = this[PropertySymbol.attributes][i]; + clone[PropertySymbol.attributes].setNamedItem( + Object.assign( + this[PropertySymbol.ownerDocument].createAttributeNS( + attribute[PropertySymbol.namespaceURI], + attribute[PropertySymbol.name] + ), + attribute + ) + ); + } + return clone; } diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index 72bd4541e..8cc07a4b2 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -1,17 +1,17 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import EventTarget from '../../event/EventTarget.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; -import Attr from '../attr/Attr.js'; +import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; +import NodeFilter from '../../tree-walker/NodeFilter.js'; +import TreeWalker from '../../tree-walker/TreeWalker.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import Document from '../document/Document.js'; -import HTMLElement from '../html-element/HTMLElement.js'; import INodeList from '../node/INodeList.js'; import Node from '../node/Node.js'; import NodeList from '../node/NodeList.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import Element from './Element.js'; import IHTMLCollection from './IHTMLCollection.js'; -import TNamedNodeMapListener from './TNamedNodeMapListener.js'; +import IHTMLCollectionObservedNode from './IHTMLCollectionObservedNode.js'; const NAMED_ITEM_ATTRIBUTES = ['id', 'name']; @@ -28,42 +28,8 @@ export default class HTMLCollection implements IHTMLCollection { public [PropertySymbol.namedItems] = new Map>(); - #namedNodeMapListeners = new Map< - T, - { set: TNamedNodeMapListener; remove: TNamedNodeMapListener } - >(); - #filter: (item: T) => boolean | null; - #synchronizedPropertiesElement: Element; - - /** - * Constructor. - * - * @param [options] Options. - * @param [options.filter] Filter. - * @param [options.observeNode] Observe node. - * @param [options.synchronizedPropertiesElement] Synchronized properties element. - */ - constructor(options?: { - filter?: (item: T) => boolean; - observeNode?: Element | DocumentFragment | Document; - synchronizedPropertiesElement?: Element; - }) { - super(); - - if (options) { - if (options.filter) { - this.#filter = options.filter; - } - - if (options.synchronizedPropertiesElement) { - this.#synchronizedPropertiesElement = options.synchronizedPropertiesElement; - } - - if (options.observeNode) { - this.#observeNode(options.observeNode); - } - } - } + protected [PropertySymbol.attributeFilter]: string[] = ['id', 'name']; + #observedNodes: IHTMLCollectionObservedNode[] = []; /** * Returns `Symbol.toStringTag`. @@ -120,24 +86,14 @@ export default class HTMLCollection * @returns True if the item was added. */ public [PropertySymbol.addItem](item: T): boolean { - const filter = this.#filter; - - if (item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || (filter && !filter?.(item))) { + if (item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || super.includes(item)) { return false; } - if (super.includes(item)) { - this[PropertySymbol.removeItem](item); - } - super.push(item); this.#addNamedItem(item); - if (this.#synchronizedPropertiesElement) { - this.#synchronizedPropertiesElement[this.length - 1] = item; - } - return true; } @@ -149,58 +105,16 @@ export default class HTMLCollection * @returns True if the item was added. */ public [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean { - const filter = this.#filter; - - if ( - newItem[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || - (filter && !filter?.(newItem)) - ) { - return false; - } - - if (super.includes(newItem)) { - this[PropertySymbol.removeItem](newItem); - } - - // We should not call addItem() here, as we don't want HTMLOptionsCollection to run updateSelectedness() twice. - if (!referenceItem) { - super.push(newItem); - - this.#addNamedItem(newItem); - - if (this.#synchronizedPropertiesElement) { - this.#synchronizedPropertiesElement[this.length - 1] = newItem; - } - - return true; - } - if (!referenceItem) { return this[PropertySymbol.addItem](newItem); } - let referenceItemIndex: number = -1; - - if (referenceItem[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - referenceItemIndex = this[PropertySymbol.indexOf](referenceItem); - } else { - const parentChildNodes = (referenceItem).parentNode?.[PropertySymbol.childNodes]; - for ( - let i = parentChildNodes[PropertySymbol.indexOf](referenceItem), - max = parentChildNodes.length; - i < max; - i++ - ) { - if ( - parentChildNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(parentChildNodes[i])) - ) { - referenceItemIndex = this[PropertySymbol.indexOf](parentChildNodes[i]); - break; - } - } + if (newItem[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || super.includes(newItem)) { + return false; } + const referenceItemIndex = this[PropertySymbol.indexOf](referenceItem); + if (referenceItemIndex === -1) { throw new Error( 'Failed to execute "insertItem" on "HTMLCollection": The node before which the new node is to be inserted is not an item of this collection.' @@ -211,14 +125,6 @@ export default class HTMLCollection this.#addNamedItem(newItem); - if (this.#synchronizedPropertiesElement) { - this.#synchronizedPropertiesElement[referenceItemIndex] = newItem; - - for (let i = referenceItemIndex + 1, max = this.length; i < max; i++) { - this.#synchronizedPropertiesElement[i] = this[i]; - } - } - return true; } @@ -239,15 +145,104 @@ export default class HTMLCollection this.#removeNamedItem(item); - if (this.#synchronizedPropertiesElement) { - for (let i = index, max = this.length; i < max; i++) { - this.#synchronizedPropertiesElement[i] = this[i]; - } + return true; + } + + /** + * Destroys the collection. + */ + public [PropertySymbol.destroy](): void { + const observedNodes = this.#observedNodes; - delete this.#synchronizedPropertiesElement[this.length]; + while (observedNodes.length) { + this[PropertySymbol.unobserve](observedNodes[observedNodes.length - 1]); } - return true; + while (this.length) { + this[PropertySymbol.removeItem](this[this.length - 1]); + } + } + + /** + * Observes node. + * + * @param node Root node. + * @param filter Filter. + * @returns Observed node. + */ + public [PropertySymbol.observe]( + node: Element | DocumentFragment | Document, + filter?: (item: T) => boolean + ): IHTMLCollectionObservedNode { + const observedNode: IHTMLCollectionObservedNode = { + node, + filter, + mutationListener: null + }; + + observedNode.mutationListener = { + options: { + childList: true, + subtree: true, + attributes: true, + attributeOldValue: true, + attributeFilter: this[PropertySymbol.attributeFilter] + }, + callback: new WeakRef((record: MutationRecord) => { + switch (record.type) { + case MutationTypeEnum.childList: + if (record.addedNodes.length) { + this[PropertySymbol.insertObservedItem](observedNode, record.addedNodes[0]); + } else { + this[PropertySymbol.removeObservedItem](observedNode, record.removedNodes[0]); + } + break; + case MutationTypeEnum.attributes: + const newValue = record.target[PropertySymbol.attributes][record.attributeName]?.value; + if ( + record.target[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + record.oldValue !== newValue && + (!filter || filter(record.target)) && + (!observedNode.mutationListener.options.subtree || + this.#isObservedItem(observedNode, record.target)) + ) { + this[PropertySymbol.onObservedItemAttributeChange]( + record.target, + record.attributeName, + record.oldValue, + newValue + ); + } + } + }) + }; + + this.#observedNodes.push(observedNode); + + node[PropertySymbol.observeMutations](observedNode.mutationListener); + + this[PropertySymbol.loadObservedNodes](observedNode, node); + + return observedNode; + } + + /** + * Unobserves node. + * + * @param observedNode Observed node. + */ + public [PropertySymbol.unobserve](observedNode: IHTMLCollectionObservedNode): void { + const index = this.#observedNodes.indexOf(observedNode); + + if (index === -1) { + return; + } + + this.#observedNodes.splice(index, 1); + + this[PropertySymbol.unloadObservedNodes](observedNode, observedNode.node); + + observedNode.node[PropertySymbol.unobserveMutations](observedNode.mutationListener); } /** @@ -271,11 +266,238 @@ export default class HTMLCollection } /** - * Sets named item property. + * Observes node. + * + * @param observedNode Observed node. + * @param parentNode Parent node. + */ + protected [PropertySymbol.loadObservedNodes]( + observedNode: IHTMLCollectionObservedNode, + parentNode: Element | DocumentFragment | Document + ): void { + const childNodes = parentNode[PropertySymbol.childNodes]; + + if (observedNode.mutationListener.options.subtree) { + for (let i = 0, max = childNodes.length; i < max; i++) { + if ( + childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + this.#isObservedItem(observedNode, childNodes[i]) + ) { + if (!observedNode.filter || observedNode.filter(childNodes[i])) { + this[PropertySymbol.addItem](childNodes[i]); + } + + this[PropertySymbol.loadObservedNodes](observedNode, childNodes[i]); + } + } + return; + } + + for (let i = 0, max = childNodes.length; i < max; i++) { + if ( + childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!observedNode.filter || observedNode.filter(childNodes[i])) + ) { + this[PropertySymbol.addItem](childNodes[i]); + } + } + } + + /** + * Unobserves node. + * + * @param observedNode Observed node. + * @param parentNode Parent node. + */ + protected [PropertySymbol.unloadObservedNodes]( + observedNode: IHTMLCollectionObservedNode, + parentNode: Element | DocumentFragment | Document + ): void { + const childNodes = parentNode[PropertySymbol.childNodes]; + + if (observedNode.mutationListener.options.subtree) { + for (let i = 0, max = childNodes.length; i < max; i++) { + if ( + childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + this.#isObservedItem(observedNode, childNodes[i]) + ) { + if (!observedNode.filter || observedNode.filter(childNodes[i])) { + this[PropertySymbol.removeItem](childNodes[i]); + } + + this[PropertySymbol.unloadObservedNodes](observedNode, childNodes[i]); + } + } + } + + for (let i = 0, max = childNodes.length; i < max; i++) { + if ( + childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!observedNode.filter || observedNode.filter(childNodes[i])) + ) { + this[PropertySymbol.removeItem](childNodes[i]); + } + } + } + + /** + * Inserts new observed item. + * + * @param observedNode Observed node. + * @param newItem New item. + */ + protected [PropertySymbol.insertObservedItem]( + observedNode: IHTMLCollectionObservedNode, + newItem: Element + ): void { + // Is part of a subtree. + if (observedNode.mutationListener.options.subtree) { + // Check if the item is observed by this listener + if (!this.#isObservedItem(observedNode, newItem)) { + return; + } + + // Find all children that pass the filter inside the new item. + const items = this.#getItemsInElement(observedNode, newItem); + + if (!items.length) { + return; + } + + // As the new item is part of a subtree, we need to walk the tree to find the first item that passes the filter. + // We start with the last item in the collection. + const treeWalker = new TreeWalker(observedNode.node, NodeFilter.SHOW_ELEMENT, (node) => + !observedNode.filter || observedNode.filter(node) + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP + ); + treeWalker.currentNode = items[items.length - 1]; + const referenceItem = treeWalker.nextNode() || null; + + for (const item of items) { + this[PropertySymbol.insertItem](item, referenceItem); + } + + return; + } + + // Check if the new item is an element node and passes the filter. + if ( + newItem[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || + (newItem[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + observedNode.filter && + !observedNode.filter(newItem)) + ) { + return; + } + + // Is not part of a subtree. + // We can therefore find the reference item by iterating over the child nodes to find the first element node that passes the filter. + const childNodes = newItem.parentNode[PropertySymbol.childNodes]; + let referenceItemIndex = -1; + for ( + let i = childNodes[PropertySymbol.indexOf](newItem) + 1, max = childNodes.length; + i < max; + i++ + ) { + if ( + childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!observedNode.filter || observedNode.filter(childNodes[i])) + ) { + referenceItemIndex = this[PropertySymbol.indexOf](childNodes[i]); + break; + } + } + + if (referenceItemIndex === -1) { + this[PropertySymbol.addItem](newItem); + return; + } + + this[PropertySymbol.insertItem](newItem, this[referenceItemIndex]); + } + + /** + * Removes observed item. + * + * @param observedNode Observed node. + * @param item Item. + */ + protected [PropertySymbol.removeObservedItem]( + observedNode: IHTMLCollectionObservedNode, + item: T + ): void { + // Is part of a subtree. + if (observedNode.mutationListener.options.subtree) { + // Find all children that pass the filter inside the item. + const items = this.#getItemsInElement(observedNode, item); + + for (let i = items.length - 1; i >= 0; i--) { + this[PropertySymbol.removeItem](items[i]); + } + + return; + } + + // Check if the item is an element node and passes the filter. + if ( + item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || + (item[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + observedNode.filter && + !observedNode.filter(item)) + ) { + return; + } + + this[PropertySymbol.removeItem](item); + } + + /** + * Triggered when an attribute changes. + * + * @param item Item. + * @param name Name. + * @param oldValue Old value. + * @param value Value. + */ + protected [PropertySymbol.onObservedItemAttributeChange]( + item: T, + name: string, + oldValue: string | null, + value: string | null + ): void { + if (name !== 'id' && name !== 'name') { + return; + } + if (oldValue) { + const namedItems = this[PropertySymbol.namedItems].get(oldValue); + + if (namedItems) { + namedItems[PropertySymbol.removeItem](item); + } + + this[PropertySymbol.updateNamedItemProperty](oldValue); + } + + if (value) { + const namedItems = + this[PropertySymbol.namedItems].get(value) || + this[PropertySymbol.createNamedItemsNodeList](); + + if (!namedItems[PropertySymbol.includes](item)) { + namedItems[PropertySymbol.addItem](item); + this[PropertySymbol.namedItems].set(value, namedItems); + this[PropertySymbol.updateNamedItemProperty](value); + } + } + } + + /** + * Updates named item property. * * @param name Name. */ - protected [PropertySymbol.setNamedItemProperty](name: string): void { + protected [PropertySymbol.updateNamedItemProperty](name: string): void { if (!this[PropertySymbol.isValidPropertyName](name)) { return; } @@ -290,21 +512,9 @@ export default class HTMLCollection enumerable: true, configurable: true }); - - if (this.#synchronizedPropertiesElement) { - Object.defineProperty(this.#synchronizedPropertiesElement, name, { - value: namedItems[0], - writable: false, - enumerable: true, - configurable: true - }); - } } } else { delete this[name]; - if (this.#synchronizedPropertiesElement) { - delete this.#synchronizedPropertiesElement[name]; - } } } @@ -327,47 +537,66 @@ export default class HTMLCollection return ( !!name && !this.constructor.prototype.hasOwnProperty(name) && - (!this.#synchronizedPropertiesElement || - (!this.#synchronizedPropertiesElement.constructor.prototype.hasOwnProperty(name) && - !HTMLElement.constructor.prototype.hasOwnProperty(name) && - !Element.constructor.prototype.hasOwnProperty(name) && - !Node.constructor.hasOwnProperty(name) && - !EventTarget.constructor.hasOwnProperty(name))) && (isNaN(Number(name)) || name.includes('.')) ); } /** - * Updates named item. + * Returns true if the item is observed by the observed node and no other observers in the collection also observe it. * - * @param item Item. - * @param attributeName Attribute name. + * @param observedNode Observed node. + * @param node Node. + * @returns True if the item is observed. */ - #updateNamedItem(item: T, attributeName: string): void { - const filter = this.#filter; + #isObservedItem(observedNode: IHTMLCollectionObservedNode, node: Node): boolean { + // This method should not be executed when not in a subtree + if (!observedNode.mutationListener.options.subtree) { + return true; + } - if (item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || (filter && !filter?.(item))) { - return; + if (!node[PropertySymbol.mutationListeners].includes(observedNode.mutationListener)) { + return false; } - const name = item[PropertySymbol.attributes][attributeName]?.value; + for (const observedNodeItem of this.#observedNodes) { + if ( + observedNodeItem !== observedNode && + node[PropertySymbol.mutationListeners].includes(observedNodeItem.mutationListener) + ) { + return false; + } + } - if (name) { - const namedItems = this[PropertySymbol.namedItems].get(name); + return true; + } - if (!namedItems?.[PropertySymbol.includes](item)) { - this[PropertySymbol.namedItems].set(name, namedItems); - this[PropertySymbol.setNamedItemProperty](name); - } - } else { - const namedItems = this[PropertySymbol.namedItems].get(name); + /** + * Returns items in element. + * + * @param observedNode Observed node. + * @param element Element. + * @param [items] Items. + * @returns Items. + */ + #getItemsInElement( + observedNode: IHTMLCollectionObservedNode, + element: Node, + items: T[] = [] + ): T[] { + if ( + element[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!observedNode.filter || observedNode.filter(element)) + ) { + items.push(element); + } - if (namedItems) { - namedItems[PropertySymbol.removeItem](item); + for (let i = 0, max = element[PropertySymbol.childNodes].length; i < max; i++) { + if (element[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + this.#getItemsInElement(observedNode, element[PropertySymbol.childNodes][i], items); } - - this[PropertySymbol.setNamedItemProperty](name); } + + return items; } /** @@ -376,22 +605,6 @@ export default class HTMLCollection * @param item Item. */ #addNamedItem(item: T): void { - const listeners = { - set: (attribute: Attr) => { - if (NAMED_ITEM_ATTRIBUTES.includes(attribute.name)) { - this.#updateNamedItem(item, attribute.name); - } - }, - remove: (attribute: Attr) => { - if (NAMED_ITEM_ATTRIBUTES.includes(attribute.name)) { - this.#updateNamedItem(item, attribute.name); - } - } - }; - - item[PropertySymbol.attributes][PropertySymbol.addEventListener]('set', listeners.set); - item[PropertySymbol.attributes][PropertySymbol.addEventListener]('remove', listeners.remove); - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const name = (item)[PropertySymbol.attributes][attributeName]?.value; if (name) { @@ -403,9 +616,11 @@ export default class HTMLCollection return; } + namedItems[PropertySymbol.addItem](item); + this[PropertySymbol.namedItems].set(name, namedItems); - this[PropertySymbol.setNamedItemProperty](name); + this[PropertySymbol.updateNamedItemProperty](name); } } } @@ -416,16 +631,6 @@ export default class HTMLCollection * @param item Item. */ #removeNamedItem(item: T): void { - const listeners = this.#namedNodeMapListeners.get(item); - - if (listeners) { - item[PropertySymbol.attributes][PropertySymbol.removeEventListener]('set', listeners.set); - item[PropertySymbol.attributes][PropertySymbol.removeEventListener]( - 'remove', - listeners.remove - ); - } - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const name = (item)[PropertySymbol.attributes][attributeName]?.value; if (name) { @@ -437,163 +642,23 @@ export default class HTMLCollection namedItems[PropertySymbol.removeItem](item); - this[PropertySymbol.setNamedItemProperty](name); + this[PropertySymbol.updateNamedItemProperty](name); } } } - - /** - * Observes node. - * - * @param parentNode Parent node. - */ - #observeNode(parentNode: Element | DocumentFragment | Document): void { - const filter = this.#filter; - - this.#loadObservedItems(parentNode); - - parentNode[PropertySymbol.observeMutations]({ - options: { childList: true }, - callback: new WeakRef((record: MutationRecord) => { - if (record.addedNodes.length) { - // There is always only one added node. - const addedNode = record.addedNodes[0]; - - if ( - addedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(addedNode)) - ) { - const index = this.#getObservedItemIndex(parentNode, addedNode); - addedNode[PropertySymbol.isInsideObservedFormNode] = true; - this[PropertySymbol.insertItem](addedNode, this[index] || null); - } else if (addedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - const items = this.#getItemsInNode(addedNode); - const index = this.#getObservedItemIndex(parentNode, items[items.length - 1]); - const referenceItem = this[index]; - - for (let i = items.length - 1; i >= 0; i--) { - items[i][PropertySymbol.isInsideObservedFormNode] = true; - this[PropertySymbol.insertItem](items[i], referenceItem); - } - } - } else { - const removedNode = record.removedNodes[0]; - if ( - removedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(removedNode)) - ) { - removedNode[PropertySymbol.isInsideObservedFormNode] = true; - this[PropertySymbol.removeItem](removedNode); - } else { - const items = this.#getItemsInNode(removedNode); - for (let i = items.length - 1; i >= 0; i--) { - items[i][PropertySymbol.isInsideObservedFormNode] = true; - this[PropertySymbol.removeItem](items[i]); - } - } - } - }) - }); - } - - /** - * Returns items in node. - * - * @param parentNode Parent node. - */ - #getItemsInNode(parentNode: Element | DocumentFragment | Document): T[] { - const filter = this.#filter; - const items: T[] = []; - - if ( - parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(parentNode)) - ) { - items.push(parentNode); - } else { - const children = parentNode[PropertySymbol.children]; - for (let a = 0; a < children.length; a++) { - const childrenOfChild = children[a][PropertySymbol.children]; - for (let b = 0; b < childrenOfChild.length; b++) { - items.push(childrenOfChild[b]); - } - } - } - - return items; - } - - /** - * Loads initial observed items. - * - * @param parentNode Parent node. - */ - #loadObservedItems(parentNode: Element | DocumentFragment | Document): void { - const filter = this.#filter; - const children = parentNode[PropertySymbol.children]; - - for (let i = 0, max = children.length; i < max; i++) { - if ( - children[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(children[i])) - ) { - children[i][PropertySymbol.isInsideObservedFormNode] = true; - this[PropertySymbol.addItem](children[i]); - } - - this.#loadObservedItems(children[i]); - } - } - - /** - * Returns the index for the first element matching the filter inside the parent parent element. - * - * @param parentNode Parent node. - * @param item Item. - * @param [indexContainer] Index container. - */ - #getObservedItemIndex( - parentNode: Element | DocumentFragment | Document, - item: T, - indexContainer = { index: 0 } - ): number { - const filter = this.#filter; - const children = parentNode[PropertySymbol.children]; - - for (let i = 0, max = children.length; i < max; i++) { - if ( - children[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(children[i])) - ) { - if (children[i] === item) { - return indexContainer.index; - } - - const returnValue = this.#getObservedItemIndex(children[i], item, indexContainer); - - if (returnValue !== -1) { - return returnValue; - } - - indexContainer.index++; - } - } - - return -1; - } } // Removes Array methods from HTMLCollection. const descriptors = Object.getOwnPropertyDescriptors(Array.prototype); for (const key of Object.keys(descriptors)) { - const descriptor = descriptors[key]; - if (key === 'length') { - Object.defineProperty(HTMLCollection.prototype, key, { - set: () => {}, - get: descriptor.get - }); - } else { - if (typeof descriptor.value === 'function') { + if (key !== 'item' && key !== 'constructor') { + const descriptor = descriptors[key]; + if (key === 'length') { + Object.defineProperty(HTMLCollection.prototype, key, { + set: () => {}, + get: descriptor.get + }); + } else if (typeof descriptor.value === 'function') { Object.defineProperty(HTMLCollection.prototype, key, {}); } } diff --git a/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts b/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts new file mode 100644 index 000000000..57853fd7e --- /dev/null +++ b/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts @@ -0,0 +1,10 @@ +import IMutationListener from '../../mutation-observer/IMutationListener.js'; +import DocumentFragment from '../document-fragment/DocumentFragment.js'; +import Document from '../document/Document.js'; +import Element from './Element.js'; + +export default interface IHTMLCollectionObservedNode { + node: Element | DocumentFragment | Document; + filter: (item: Element) => boolean | null; + mutationListener: IMutationListener; +} diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index 5f855e88e..6bbf984c6 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -22,7 +22,6 @@ export default class HTMLButtonElement extends HTMLElement { public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.formNode]: HTMLFormElement | null = null; - public [PropertySymbol.isInsideObservedFormNode] = false; /** * Returns validation message. diff --git a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts index fd111f978..aa525ae26 100644 --- a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts +++ b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts @@ -19,9 +19,10 @@ export default class HTMLDataListElement extends HTMLElement { */ public get options(): IHTMLCollection { if (!this[PropertySymbol.options]) { - this[PropertySymbol.options] = new HTMLCollection({ - filter: (item) => item[PropertySymbol.tagName] === 'OPTION', - observeNode: this + this[PropertySymbol.options] = new HTMLCollection(); + this[PropertySymbol.options][PropertySymbol.observe](this, { + subtree: true, + filter: (item) => item[PropertySymbol.tagName] === 'OPTION' }); } return this[PropertySymbol.options]; diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 9de131f7a..5d79a1640 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -574,7 +574,7 @@ export default class HTMLElement extends Element { (>this[PropertySymbol.children]) = new HTMLCollection(); - this[PropertySymbol.childNodes][PropertySymbol.attachedHTMLCollection] = + this[PropertySymbol.childNodes][PropertySymbol.htmlCollection] = this[PropertySymbol.children]; this[PropertySymbol.rootNode] = null; diff --git a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts index 3292ae4b2..ab4167c13 100644 --- a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts +++ b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts @@ -25,25 +25,26 @@ export default class HTMLFieldSetElement extends HTMLElement { public declare cloneNode: (deep?: boolean) => HTMLFieldSetElement; // Internal properties - public [PropertySymbol.elements] = new HTMLCollection({ - filter: (item: Element) => - item.tagName === 'INPUT' || - item.tagName === 'BUTTON' || - item.tagName === 'TEXTAREA' || - item.tagName === 'SELECT', - observeNode: this - }); + public [PropertySymbol.elements]: IHTMLCollection | null = null; public [PropertySymbol.formNode]: HTMLFormElement | null = null; - public [PropertySymbol.isInsideObservedFormNode] = false; /** * Returns elements. * * @returns Elements. */ - public get elements(): IHTMLCollection< - HTMLInputElement | HTMLButtonElement | HTMLTextAreaElement | HTMLSelectElement - > { + public get elements(): IHTMLCollection { + if (!this[PropertySymbol.elements]) { + this[PropertySymbol.elements] = new HTMLCollection(); + this[PropertySymbol.elements][PropertySymbol.observe](this, { + subtree: true, + filter: (item: Element) => + item.tagName === 'INPUT' || + item.tagName === 'BUTTON' || + item.tagName === 'TEXTAREA' || + item.tagName === 'SELECT' + }); + } return this[PropertySymbol.elements]; } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index ac4ee11ec..726382e81 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -1,7 +1,15 @@ import * as PropertySymbol from '../../PropertySymbol.js'; +import EventTarget from '../../event/EventTarget.js'; +import Attr from '../attr/Attr.js'; +import Element from '../element/Element.js'; import HTMLCollection from '../element/HTMLCollection.js'; +import IHTMLCollectionObservedNode from '../element/IHTMLCollectionObservedNode.js'; +import TNamedNodeMapListener from '../element/TNamedNodeMapListener.js'; +import HTMLElement from '../html-element/HTMLElement.js'; +import Node from '../node/Node.js'; import HTMLFormElement from './HTMLFormElement.js'; import IRadioNodeList from './IRadioNodeList.js'; +import RadioNodeList from './RadioNodeList.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; /** @@ -14,6 +22,15 @@ export default class HTMLFormControlsCollection extends HTMLCollection< THTMLFormControlElement | IRadioNodeList > { public [PropertySymbol.namedItems] = new Map(); + #observedFormElement: IHTMLCollectionObservedNode | null = null; + #observedDocument: IHTMLCollectionObservedNode | null = null; + #observedDocumentAttributeListeners: { + set: TNamedNodeMapListener | null; + remove: TNamedNodeMapListener | null; + } = { + set: null, + remove: null + }; #formElement: HTMLFormElement; /** @@ -22,17 +39,7 @@ export default class HTMLFormControlsCollection extends HTMLCollection< * @param formElement Form element. */ constructor(formElement: HTMLFormElement) { - super({ - filter: (item: THTMLFormControlElement) => - item[PropertySymbol.tagName] === 'INPUT' || - item[PropertySymbol.tagName] === 'SELECT' || - item[PropertySymbol.tagName] === 'TEXTAREA' || - item[PropertySymbol.tagName] === 'BUTTON' || - item[PropertySymbol.tagName] === 'FIELDSET', - // Array.splice() method creates a new instance of HTMLOptionsCollection with a number sent as the first argument. - observeNode: formElement instanceof HTMLFormElement ? formElement : null, - synchronizedPropertiesElement: formElement - }); + super(); this.#formElement = formElement; } @@ -53,6 +60,143 @@ export default class HTMLFormControlsCollection extends HTMLCollection< return namedItems; } + /** + * Observes node. + * + * @returns Observed node. + */ + public [PropertySymbol.observe](): IHTMLCollectionObservedNode { + if (this.#observedFormElement) { + return; + } + const observedNode = super[PropertySymbol.observe](this.#formElement, { + subtree: true, + filter: (item: THTMLFormControlElement) => + item[PropertySymbol.tagName] === 'INPUT' || + item[PropertySymbol.tagName] === 'SELECT' || + item[PropertySymbol.tagName] === 'TEXTAREA' || + item[PropertySymbol.tagName] === 'BUTTON' || + item[PropertySymbol.tagName] === 'FIELDSET' + }); + + this.#observedFormElement = observedNode; + + return observedNode; + } + + /** + * Unobserves node. + * + * @param observedNode Observed node. + */ + public [PropertySymbol.unobserve](): void { + if (!this.#observedFormElement) { + return; + } + super[PropertySymbol.unobserve](this.#observedFormElement); + } + + /** + * Observes node. + * + * @returns Observed node. + */ + public [PropertySymbol.observeDocument](): IHTMLCollectionObservedNode { + if (this.#observedDocumentAttributeListeners.set) { + return; + } + + const formElement = this.#formElement; + + if (!formElement[PropertySymbol.isConnected]) { + return; + } + + this.#observedDocumentAttributeListeners.set = (attribute: Attr, replacedAttribute?: Attr) => { + if (attribute.name === 'id') { + if (replacedAttribute[PropertySymbol.value]) { + super[PropertySymbol.unobserve](this.#observedDocument); + this.#observedDocument = null; + } + if (attribute[PropertySymbol.value]) { + this.#observedDocument = super[PropertySymbol.observe]( + formElement[PropertySymbol.ownerDocument] + ); + } + } + }; + this.#observedDocumentAttributeListeners.remove = (attribute: Attr) => { + if (attribute.name === 'id') { + super[PropertySymbol.unobserve](this.#observedDocument); + this.#observedDocument = null; + } + }; + + formElement[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'set', + this.#observedDocumentAttributeListeners.set + ); + formElement[PropertySymbol.attributes][PropertySymbol.addEventListener]( + 'remove', + this.#observedDocumentAttributeListeners.remove + ); + + const id = formElement[PropertySymbol.attributes]['id']?.value; + + if (!id) { + return; + } + + const observedNode = super[PropertySymbol.observe](formElement[PropertySymbol.ownerDocument], { + subtree: true, + filter: (item: THTMLFormControlElement) => { + if (!id) { + return false; + } + return ( + (item[PropertySymbol.tagName] === 'INPUT' || + item[PropertySymbol.tagName] === 'SELECT' || + item[PropertySymbol.tagName] === 'TEXTAREA' || + item[PropertySymbol.tagName] === 'BUTTON' || + item[PropertySymbol.tagName] === 'FIELDSET') && + item[PropertySymbol.attributes]['form']?.value === id + ); + } + }); + + this.#observedDocument = observedNode; + + return observedNode; + } + + /** + * Unobserves node. + * + * @param observedNode Observed node. + */ + public [PropertySymbol.unobserveDocument](): void { + if (!this.#observedDocumentAttributeListeners.set) { + return; + } + + const formElement = this.#formElement; + + formElement[PropertySymbol.attributes][PropertySymbol.removeEventListener]( + 'set', + this.#observedDocumentAttributeListeners.set + ); + formElement[PropertySymbol.attributes][PropertySymbol.removeEventListener]( + 'remove', + this.#observedDocumentAttributeListeners.remove + ); + + if (!this.#observedDocument) { + return; + } + + super[PropertySymbol.unobserve](this.#observedDocument); + } + /** * Appends item. * @@ -60,14 +204,14 @@ export default class HTMLFormControlsCollection extends HTMLCollection< * @returns True if added. */ public [PropertySymbol.addItem](item: THTMLFormControlElement): boolean { - const returnValue = super[PropertySymbol.addItem](item); - - if (!returnValue) { + if (!super[PropertySymbol.addItem](item)) { return false; } item[PropertySymbol.formNode] = this.#formElement; + this.#formElement[this.length - 1] = item; + return true; } @@ -82,14 +226,18 @@ export default class HTMLFormControlsCollection extends HTMLCollection< newItem: THTMLFormControlElement, referenceItem: THTMLFormControlElement | null ): boolean { - const returnValue = super[PropertySymbol.insertItem](newItem, referenceItem); - - if (!returnValue) { + if (!super[PropertySymbol.insertItem](newItem, referenceItem)) { return false; } newItem[PropertySymbol.formNode] = this.#formElement; + const index = this[PropertySymbol.indexOf](newItem); + + for (let i = index, max = this.length; i < max; i++) { + this.#formElement[i] = this[i]; + } + return true; } @@ -100,23 +248,67 @@ export default class HTMLFormControlsCollection extends HTMLCollection< * @returns True if removed. */ public [PropertySymbol.removeItem](item: THTMLFormControlElement): boolean { - const returnValue = super[PropertySymbol.removeItem](item); + const index = this[PropertySymbol.indexOf](item); - if (!returnValue) { + if (!super[PropertySymbol.removeItem](item)) { return false; } item[PropertySymbol.formNode] = null; + for (let i = index, max = this.length; i < max; i++) { + this.#formElement[i] = this[i]; + } + + delete this.#formElement[this.length]; + return true; } + /** + * Triggered when an attribute changes. + * + * @param item Item. + * @param name Name. + * @param oldValue Old value. + * @param value Value. + */ + protected [PropertySymbol.onObservedItemAttributeChange]( + item: THTMLFormControlElement, + name: string, + oldValue: string | null, + value: string | null + ): void { + if (name !== 'form') { + super[PropertySymbol.onObservedItemAttributeChange](item, name, oldValue, value); + return; + } + + if (!this.#formElement[PropertySymbol.isConnected]) { + return; + } + + const id = this.#formElement[PropertySymbol.attributes]['id']?.value; + + if (!id) { + return; + } + + if (oldValue === id) { + this.#formElement[PropertySymbol.removeItem](item); + } + + if (value === id) { + this.#formElement[PropertySymbol.addItem](item); + } + } + /** * Sets named item property. * * @param name Name. */ - protected [PropertySymbol.setNamedItemProperty](name: string): void { + protected [PropertySymbol.updateNamedItemProperty](name: string): void { if (!this[PropertySymbol.isValidPropertyName](name)) { return; } @@ -132,14 +324,44 @@ export default class HTMLFormControlsCollection extends HTMLCollection< enumerable: true, configurable: true }); + + Object.defineProperty(this.#formElement, name, { + value: newValue, + writable: false, + enumerable: true, + configurable: true + }); } } else { delete this[name]; + delete this.#formElement[name]; } + } - this[PropertySymbol.dispatchEvent]('propertyChange', { - propertyName: name, - propertyValue: this[name] ?? null - }); + /** + * Returns "true" if the property name is valid. + * + * @param name Name. + * @returns True if the property name is valid. + */ + protected [PropertySymbol.isValidPropertyName](name: string): boolean { + return ( + !HTMLCollection.prototype.hasOwnProperty(name) && + !this.#formElement.constructor.prototype.hasOwnProperty(name) && + !HTMLElement.constructor.prototype.hasOwnProperty(name) && + !Element.constructor.prototype.hasOwnProperty(name) && + !Node.constructor.hasOwnProperty(name) && + !EventTarget.constructor.hasOwnProperty(name) && + super[PropertySymbol.isValidPropertyName](name) + ); + } + + /** + * Creates a new NodeList to be used as a named item. + * + * @returns NodeList. + */ + protected [PropertySymbol.createNamedItemsNodeList](): IRadioNodeList { + return new RadioNodeList(); } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index 7f69e1325..159837fff 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -11,13 +11,7 @@ import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import BrowserFrameNavigator from '../../browser/utilities/BrowserFrameNavigator.js'; import FormData from '../../form-data/FormData.js'; import BrowserWindow from '../../window/BrowserWindow.js'; -import Attr from '../attr/Attr.js'; -import THTMLFormControlElement from './THTMLFormControlElement.js'; import IHTMLFormControlsCollection from './IHTMLFormControlsCollection.js'; -import IMutationListener from '../../mutation-observer/IMutationListener.js'; -import MutationRecord from '../../mutation-observer/MutationRecord.js'; -import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; -import NodeTypeEnum from '../node/NodeTypeEnum.js'; /** * HTML Form Element. @@ -42,7 +36,6 @@ export default class HTMLFormElement extends HTMLElement { // Private properties #browserFrame: IBrowserFrame; - #documentMutationListener: IMutationListener | null = null; /** * Constructor. @@ -51,15 +44,10 @@ export default class HTMLFormElement extends HTMLElement { */ constructor(browserFrame: IBrowserFrame) { super(); + this.#browserFrame = browserFrame; - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); + + this[PropertySymbol.elements][PropertySymbol.observe](); } /** @@ -355,117 +343,7 @@ export default class HTMLFormElement extends HTMLElement { public override [PropertySymbol.connectedToDocument](): void { super[PropertySymbol.connectedToDocument](); - /** - * It is possible to associate a form control element by setting the "form" attribute to the form's id. - * - * We need to listen for changes to all elements in the document to detect when a form control element is added or removed. - */ - this.#documentMutationListener = { - options: { - childList: true, - subtree: true, - attributes: true - }, - callback: new WeakRef((record: MutationRecord) => { - const id = this[PropertySymbol.attributes]?.['id']?.value; - if (!id) { - return; - } - - switch (record.type) { - case MutationTypeEnum.childList: - if (record.addedNodes.length) { - const addedNode = record.addedNodes[0]; - - if ( - addedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - addedNode[PropertySymbol.attributes]?.['form']?.value === id && - !addedNode[PropertySymbol.isInsideObservedFormNode] && - this.#isFormControlElement(addedNode) - ) { - addedNode[PropertySymbol.formNode] = this; - this[PropertySymbol.elements][PropertySymbol.addItem]( - addedNode - ); - } else if (addedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - const items = this.querySelectorAll( - `INPUT['form="${id}"], SELECT['form="${id}"], TEXTAREA['form="${id}"], BUTTON['form="${id}"], FIELDSET['form="${id}"]` - ); - for (const item of items) { - if (!item[PropertySymbol.isInsideObservedFormNode]) { - this[PropertySymbol.elements][PropertySymbol.addItem]( - item - ); - } - } - } - } else { - const removedNode = record.removedNodes[0]; - - if ( - removedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - removedNode[PropertySymbol.attributes]?.['form']?.value === id && - !removedNode[PropertySymbol.isInsideObservedFormNode] && - this.#isFormControlElement(removedNode) - ) { - this[PropertySymbol.elements][PropertySymbol.removeItem]( - removedNode - ); - } else if (removedNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - const items = this.querySelectorAll( - `INPUT['form="${id}"], SELECT['form="${id}"], TEXTAREA['form="${id}"], BUTTON['form="${id}"], FIELDSET['form="${id}"]` - ); - for (const item of items) { - if (!item[PropertySymbol.isInsideObservedFormNode]) { - this[PropertySymbol.elements][PropertySymbol.removeItem]( - item - ); - } - } - } - } - break; - case MutationTypeEnum.attributes: - if ( - record.attributeName === 'form' && - this.#isFormControlElement(record.target) - ) { - if ( - record.target[PropertySymbol.attributes]?.['form']?.[PropertySymbol.value] === - this[PropertySymbol.attributes]?.['id']?.[PropertySymbol.value] && - !record.target[PropertySymbol.isInsideObservedFormNode] - ) { - this[PropertySymbol.elements][PropertySymbol.addItem]( - record.target - ); - } else if (!record.target[PropertySymbol.isInsideObservedFormNode]) { - this[PropertySymbol.elements][PropertySymbol.removeItem]( - record.target - ); - } - } - break; - } - }) - }; - - this[PropertySymbol.ownerDocument][PropertySymbol.observeMutations]( - this.#documentMutationListener - ); - - const id = this[PropertySymbol.attributes]?.['id']?.value; - - if (!id) { - return; - } - - for (const element of this[PropertySymbol.ownerDocument].querySelectorAll( - `INPUT[form="${id}"], SELECT[form="${id}"], TEXTAREA[form="${id}"], BUTTON[form="${id}"], FIELDSET[form="${id}"]` - )) { - if (!element[PropertySymbol.isInsideObservedFormNode]) { - this[PropertySymbol.elements][PropertySymbol.addItem](element); - } - } + this[PropertySymbol.elements][PropertySymbol.observeDocument](); } /** @@ -474,23 +352,7 @@ export default class HTMLFormElement extends HTMLElement { public override [PropertySymbol.disconnectedFromDocument](): void { super[PropertySymbol.disconnectedFromDocument](); - this[PropertySymbol.ownerDocument][PropertySymbol.unobserveMutations]( - this.#documentMutationListener - ); - - this.#documentMutationListener = null; - - const id = this.id; - - if (!id) { - return; - } - - for (const element of this[PropertySymbol.elements]) { - if (element[PropertySymbol.attributes]?.['form']?.value === id && !this.contains(element)) { - this[PropertySymbol.elements][PropertySymbol.removeItem](element); - } - } + this[PropertySymbol.elements][PropertySymbol.unobserveDocument](); } /** @@ -568,77 +430,4 @@ export default class HTMLFormElement extends HTMLElement { } }); } - - /** - * Checks if an element is a form control element. - * - * @param item Item. - * @returns True if the item is a form control element. - */ - #isFormControlElement(item: THTMLFormControlElement): boolean { - return ( - item[PropertySymbol.tagName] === 'INPUT' || - item[PropertySymbol.tagName] === 'SELECT' || - item[PropertySymbol.tagName] === 'TEXTAREA' || - item[PropertySymbol.tagName] === 'BUTTON' || - item[PropertySymbol.tagName] === 'FIELDSET' - ); - } - - /** - * Triggered when an attribute is set. - * - * @param attribute Attribute. - * @param replacedAttribute Replaced attribute. - */ - #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { - if (attribute.name !== 'id' || !this[PropertySymbol.isConnected]) { - return; - } - - if (replacedAttribute[PropertySymbol.value]) { - const id = replacedAttribute[PropertySymbol.value]; - for (const element of this[PropertySymbol.elements]) { - if (element[PropertySymbol.attributes]?.['form']?.value === id && !this.contains(element)) { - this[PropertySymbol.elements][PropertySymbol.removeItem]( - element - ); - } - } - } - - if (attribute[PropertySymbol.value]) { - const id = attribute[PropertySymbol.value]; - for (const element of this[PropertySymbol.elements]) { - if ( - element[PropertySymbol.attributes]?.['form']?.value === id && - element[PropertySymbol.formNode] !== this - ) { - this[PropertySymbol.elements][PropertySymbol.addItem](element); - } - } - } - } - - /** - * Triggered when an attribute is removed. - * - * @param removedAttribute Removed attribute. - */ - #onRemoveAttribute(removedAttribute: Attr): void { - if ( - removedAttribute.name !== 'id' || - !removedAttribute[PropertySymbol.value] || - !this[PropertySymbol.isConnected] - ) { - return; - } - - const id = removedAttribute[PropertySymbol.value]; - for (const element of this[PropertySymbol.elements]) { - if (element[PropertySymbol.attributes]?.['form']?.value === id && !this.contains(element)) { - this[PropertySymbol.elements][PropertySymbol.removeItem](element); - } - } - } } diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 3b5604fb4..30a8a487a 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -48,7 +48,6 @@ export default class HTMLInputElement extends HTMLElement { public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.files]: FileList = new FileList(); public [PropertySymbol.formNode]: HTMLFormElement | null = null; - public [PropertySymbol.isInsideObservedFormNode] = false; // Private properties #selectionStart: number = null; diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts index 7585dc1a2..2b694af7f 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts @@ -6,6 +6,9 @@ import Element from '../element/Element.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElement from '../html-element/HTMLElement.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; +import Node from '../node/Node.js'; +import EventTarget from '../../event/EventTarget.js'; +import IHTMLCollectionObservedNode from '../element/IHTMLCollectionObservedNode.js'; /** * HTML Options Collection. @@ -16,6 +19,8 @@ import NodeTypeEnum from '../node/NodeTypeEnum.js'; export default class HTMLOptionsCollection extends HTMLCollection { #selectedIndex: number = -1; #selectElement: HTMLSelectElement; + #observedSelectElement: IHTMLCollectionObservedNode | null = null; + protected [PropertySymbol.attributeFilter]: string[] = ['id', 'name', 'form']; /** * Constructor. @@ -23,12 +28,7 @@ export default class HTMLOptionsCollection extends HTMLCollection element[PropertySymbol.tagName] === 'OPTION', - // Array.splice() method creates a new instance of HTMLOptionsCollection with a number sent as the first argument. - observeNode: selectElement instanceof HTMLSelectElement ? selectElement : null, - synchronizedPropertiesElement: selectElement - }); + super(); this.#selectElement = selectElement; } @@ -134,12 +134,16 @@ export default class HTMLOptionsCollection extends HTMLCollection item[PropertySymbol.tagName] === 'OPTION' + }); + + this.#observedSelectElement = observedNode; + + return observedNode; + } + + /** + * Unobserves node. + * + * @param observedNode Observed node. + */ + public [PropertySymbol.unobserve](): void { + if (!this.#observedSelectElement) { + return; + } + super[PropertySymbol.unobserve](this.#observedSelectElement); + } + + /** + * Sets named item property. + * + * @param name Name. + */ + protected [PropertySymbol.updateNamedItemProperty](name: string): void { + super[PropertySymbol.updateNamedItemProperty](name); + + if (this[name]) { + Object.defineProperty(this.#selectElement, name, { + value: this[name], + writable: false, + enumerable: true, + configurable: true + }); + } else { + delete this.#selectElement[name]; + } + } + + /** + * Returns "true" if the property name is valid. + * + * @param name Name. + * @returns True if the property name is valid. + */ + protected [PropertySymbol.isValidPropertyName](name: string): boolean { + return ( + !HTMLCollection.prototype.hasOwnProperty(name) && + !this.#selectElement.constructor.prototype.hasOwnProperty(name) && + !HTMLElement.constructor.prototype.hasOwnProperty(name) && + !Element.constructor.prototype.hasOwnProperty(name) && + !Node.constructor.hasOwnProperty(name) && + !EventTarget.constructor.hasOwnProperty(name) && + super[PropertySymbol.isValidPropertyName](name) + ); } /** @@ -185,9 +276,24 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[i]; + if (option[PropertySymbol.selectedness]) { + selectedOptions[PropertySymbol.addItem](option); + } + } + } + } else { for (let i = 0, max = this.length; i < max; i++) { const option = this[i]; if (selectedOption) { @@ -202,17 +308,6 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[i]; - if (option[PropertySymbol.selectedness]) { - selectedOptions?.[PropertySymbol.addItem](option); - } else { - selectedOptions?.[PropertySymbol.removeItem](option); } } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 4cd1a085b..7fd587a72 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -9,7 +9,6 @@ import Event from '../../event/Event.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import HTMLCollection from '../element/HTMLCollection.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; -import Element from '../element/Element.js'; import NodeList from '../node/INodeList.js'; /** @@ -25,12 +24,19 @@ export default class HTMLSelectElement extends HTMLElement { public [PropertySymbol.options]: HTMLOptionsCollection = new HTMLOptionsCollection(this); public [PropertySymbol.formNode]: HTMLFormElement | null = null; public [PropertySymbol.selectedOptions]: IHTMLCollection | null = null; - public [PropertySymbol.isInsideObservedFormNode] = false; // Events public onchange: (event: Event) => void | null = null; public oninput: (event: Event) => void | null = null; + /** + * Constructor. + */ + constructor() { + super(); + this[PropertySymbol.options][PropertySymbol.observe](); + } + /** * Returns length. * @@ -240,10 +246,7 @@ export default class HTMLSelectElement extends HTMLElement { */ public get selectedOptions(): IHTMLCollection { if (!this[PropertySymbol.selectedOptions]) { - this[PropertySymbol.selectedOptions] = new HTMLCollection({ - filter: (element: Element) => - element[PropertySymbol.tagName] === 'OPTION' && element[PropertySymbol.selectedness] - }); + this[PropertySymbol.selectedOptions] = new HTMLCollection(); for (const option of this[PropertySymbol.options]) { if (option[PropertySymbol.selectedness]) { this[PropertySymbol.selectedOptions][PropertySymbol.addItem](option); diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index a659716ad..fa474ead8 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -36,7 +36,6 @@ export default class HTMLTextAreaElement extends HTMLElement { public [PropertySymbol.value] = null; public [PropertySymbol.textAreaNode]: HTMLTextAreaElement = this; public [PropertySymbol.formNode]: HTMLFormElement | null = null; - public [PropertySymbol.isInsideObservedFormNode] = false; // Private properties #selectionStart = null; diff --git a/packages/happy-dom/src/nodes/node/INodeList.ts b/packages/happy-dom/src/nodes/node/INodeList.ts index 0c3d7cfb1..b6c18daa3 100644 --- a/packages/happy-dom/src/nodes/node/INodeList.ts +++ b/packages/happy-dom/src/nodes/node/INodeList.ts @@ -1,6 +1,4 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import Element from '../element/Element.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; /** * NodeList. @@ -11,7 +9,6 @@ import IHTMLCollection from '../element/IHTMLCollection.js'; */ export default interface INodeList { readonly [index: number]: T; - [PropertySymbol.attachedHTMLCollection]: IHTMLCollection | null; /** * The number of items in the NodeList. diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 0b88f1c5f..64e978581 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -503,6 +503,7 @@ export default class Node extends EventTarget { (node)[PropertySymbol.observeMutations](mutationListener); } } + this[PropertySymbol.reportMutation]( new MutationRecord({ target: this, @@ -603,7 +604,6 @@ export default class Node extends EventTarget { } else if (!this[PropertySymbol.isConnected] && newNode[PropertySymbol.isConnected]) { newNode[PropertySymbol.disconnectedFromDocument](); } - // Mutation listeners for (const mutationListener of this[PropertySymbol.mutationListeners]) { if (mutationListener.options?.subtree && mutationListener.callback.deref()) { @@ -672,25 +672,6 @@ export default class Node extends EventTarget { } } - /** - * Observeres mutations on the node once. - * - * Used by MutationObserver and internal logic. - * - * @param listener Listener. - */ - public [PropertySymbol.observeMutationsOnce](listener: IMutationListener): void { - const callback = listener.callback.deref(); - const wrapperListener = { - options: listener.options, - callback: new WeakRef((record: MutationRecord) => { - callback(record); - this[PropertySymbol.unobserveMutations](wrapperListener); - }) - }; - this[PropertySymbol.observeMutations](wrapperListener); - } - /** * Stops observing mutations on the node. * @@ -763,37 +744,59 @@ export default class Node extends EventTarget { * Clears query selector cache. */ public [PropertySymbol.clearCache](): void { - for (const item of this[PropertySymbol.querySelectorCache].items.values()) { - if (item.result) { - item.result = null; + if (this[PropertySymbol.querySelectorCache].items.size) { + for (const item of this[PropertySymbol.querySelectorCache].items.values()) { + if (item.result) { + item.result = null; + } } + this[PropertySymbol.querySelectorCache].items = new Map(); } - for (const item of this[PropertySymbol.querySelectorCache].affectedItems) { - if (item.result) { - item.result = null; + if (this[PropertySymbol.querySelectorCache].affectedItems.length) { + for (const item of this[PropertySymbol.querySelectorCache].affectedItems) { + if (item.result) { + item.result = null; + } } + this[PropertySymbol.querySelectorCache].affectedItems = []; } - for (const item of this[PropertySymbol.querySelectorAllCache].affectedItems) { - if (item.result) { - item.result = null; + if (this[PropertySymbol.querySelectorAllCache].items.size) { + for (const item of this[PropertySymbol.querySelectorAllCache].items.values()) { + if (item.result) { + item.result = null; + } } + this[PropertySymbol.querySelectorAllCache].items = new Map(); } - for (const item of this[PropertySymbol.matchesCache].affectedItems) { - if (item.result) { - item.result = null; + if (this[PropertySymbol.querySelectorAllCache].affectedItems.length) { + for (const item of this[PropertySymbol.querySelectorAllCache].affectedItems) { + if (item.result) { + item.result = null; + } } + this[PropertySymbol.querySelectorAllCache].affectedItems = []; } - this[PropertySymbol.querySelectorCache].items = new Map(); - this[PropertySymbol.querySelectorAllCache].items = new Map(); - this[PropertySymbol.matchesCache].items = new Map(); + if (this[PropertySymbol.matchesCache].items.size) { + for (const item of this[PropertySymbol.matchesCache].items.values()) { + if (item.result) { + item.result = null; + } + } + this[PropertySymbol.matchesCache].items = new Map(); + } - this[PropertySymbol.querySelectorCache].affectedItems = []; - this[PropertySymbol.querySelectorAllCache].affectedItems = []; - this[PropertySymbol.matchesCache].affectedItems = []; + if (this[PropertySymbol.matchesCache].affectedItems.length) { + for (const item of this[PropertySymbol.matchesCache].affectedItems) { + if (item.result) { + item.result = null; + } + } + this[PropertySymbol.matchesCache].affectedItems = []; + } this[PropertySymbol.ownerDocument]?.[PropertySymbol.clearComputedStyleCache](); } diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts index 03ac5cd52..469c2a1e9 100644 --- a/packages/happy-dom/src/nodes/node/NodeList.ts +++ b/packages/happy-dom/src/nodes/node/NodeList.ts @@ -11,7 +11,7 @@ import INodeList from './INodeList.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList */ class NodeList extends Array implements INodeList { - public [PropertySymbol.attachedHTMLCollection]: IHTMLCollection | null = null; + public [PropertySymbol.htmlCollection]: IHTMLCollection | null = null; /** * Returns `Symbol.toStringTag`. @@ -57,15 +57,13 @@ class NodeList extends Array implements INodeList { */ public [PropertySymbol.addItem](item: T): boolean { if (super.includes(item)) { - this[PropertySymbol.removeItem](item); + return false; } super.push(item); - const htmlCollection = this[PropertySymbol.attachedHTMLCollection]; - - if (htmlCollection) { - htmlCollection[PropertySymbol.addItem](item); + if (this[PropertySymbol.htmlCollection]) { + this[PropertySymbol.htmlCollection][PropertySymbol.addItem](item); } return true; @@ -80,11 +78,11 @@ class NodeList extends Array implements INodeList { */ public [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean { if (!referenceItem) { - return this[PropertySymbol.appendChild](newItem); + return this[PropertySymbol.addItem](newItem); } if (super.includes(newItem)) { - this[PropertySymbol.removeItem](newItem); + return false; } const index = super.indexOf(referenceItem); @@ -98,10 +96,12 @@ class NodeList extends Array implements INodeList { super.splice(index, 0, newItem); - const htmlCollection = this[PropertySymbol.attachedHTMLCollection]; - - if (htmlCollection) { - htmlCollection[PropertySymbol.insertItem](newItem, referenceItem); + if (this[PropertySymbol.htmlCollection]) { + const htmlCollectionReferenceItem = this[PropertySymbol.htmlCollection][index] || null; + this[PropertySymbol.htmlCollection][PropertySymbol.insertItem]( + newItem, + htmlCollectionReferenceItem + ); } return true; @@ -125,10 +125,8 @@ class NodeList extends Array implements INodeList { super.splice(index, 1); - const htmlCollection = this[PropertySymbol.attachedHTMLCollection]; - - if (htmlCollection) { - htmlCollection[PropertySymbol.removeItem](item); + if (this[PropertySymbol.htmlCollection]) { + this[PropertySymbol.htmlCollection][PropertySymbol.removeItem](item); } return true; @@ -169,14 +167,21 @@ class NodeList extends Array implements INodeList { // Removes Array methods from NodeList. const descriptors = Object.getOwnPropertyDescriptors(Array.prototype); for (const key of Object.keys(descriptors)) { - const descriptor = descriptors[key]; - if (key === 'length') { - Object.defineProperty(NodeList.prototype, key, { - set: () => {}, - get: descriptor.get - }); - } else if (key !== 'values' && key !== 'keys' && key !== 'entries') { - if (typeof descriptor.value === 'function') { + if ( + typeof key !== 'symbol' && + key !== 'item' && + key !== 'entries' && + key !== 'values' && + key !== 'keys' && + key !== 'constructor' + ) { + const descriptor = descriptors[key]; + if (key === 'length') { + Object.defineProperty(NodeList.prototype, key, { + set: () => {}, + get: descriptor.get + }); + } else if (typeof descriptor.value === 'function') { Object.defineProperty(NodeList.prototype, key, {}); } } diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index fc8e93488..59b11d899 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -90,10 +90,14 @@ export default class ParentNodeUtility { parentNode: Element | DocumentFragment | Document, className: string ): IHTMLCollection { - return new HTMLCollection({ - filter: (item: Element) => (item).className.split(' ').includes(className), - observeNode: parentNode + const htmlCollection = new HTMLCollection(); + + htmlCollection[PropertySymbol.observe](parentNode, { + subtree: true, + filter: (item: Element) => (item).className.split(' ').includes(className) }); + + return htmlCollection; } /** @@ -109,11 +113,14 @@ export default class ParentNodeUtility { ): IHTMLCollection { const upperTagName = tagName.toUpperCase(); const includeAll = tagName === '*'; + const htmlCollection = new HTMLCollection(); - return new HTMLCollection({ - filter: (item: Element) => includeAll || item[PropertySymbol.tagName] === upperTagName, - observeNode: parentNode + htmlCollection[PropertySymbol.observe](parentNode, { + subtree: true, + filter: (item: Element) => includeAll || item[PropertySymbol.tagName] === upperTagName }); + + return htmlCollection; } /** @@ -132,13 +139,16 @@ export default class ParentNodeUtility { // When the namespace is HTML, the tag name is case-insensitive. const formattedTagName = namespaceURI === NamespaceURI.html ? tagName.toUpperCase() : tagName; const includeAll = tagName === '*'; + const htmlCollection = new HTMLCollection(); - return new HTMLCollection({ + htmlCollection[PropertySymbol.observe](parentNode, { + subtree: true, filter: (item: Element) => (includeAll || item[PropertySymbol.tagName] === formattedTagName) && - item[PropertySymbol.namespaceURI] === namespaceURI, - observeNode: parentNode + item[PropertySymbol.namespaceURI] === namespaceURI }); + + return htmlCollection; } /** diff --git a/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts index 3eb86b9da..73f824c38 100644 --- a/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts +++ b/packages/happy-dom/test/nodes/html-button-element/HTMLButtonElement.test.ts @@ -247,7 +247,7 @@ describe('HTMLButtonElement', () => { expect(element.form).toBe(null); document.body.appendChild(element); expect(element.form).toBe(form); - expect(form.elements.includes(element)).toBe(true); + expect(Array.from(form.elements).includes(element)).toBe(true); }); it('Returns form element by id if the form attribute is set when element is connected to DOM.', () => { @@ -257,7 +257,7 @@ describe('HTMLButtonElement', () => { document.body.appendChild(element); element.setAttribute('form', 'form'); expect(element.form).toBe(form); - expect(form.elements.includes(element)).toBe(true); + expect(Array.from(form.elements).includes(element)).toBe(true); }); }); diff --git a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts index 0fa997c08..977de8d8e 100644 --- a/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts +++ b/packages/happy-dom/test/nodes/html-field-set-element/HTMLFieldSetElement.test.ts @@ -43,7 +43,7 @@ describe('HTMLFieldSetElement', () => { expect(element.form).toBe(null); document.body.appendChild(element); expect(element.form).toBe(form); - expect(form.elements.includes(element)).toBe(true); + expect(Array.from(form.elements).includes(element)).toBe(true); }); it('Returns form element by id if the form attribute is set when element is connected to DOM.', () => { @@ -53,7 +53,7 @@ describe('HTMLFieldSetElement', () => { document.body.appendChild(element); element.setAttribute('form', 'form'); expect(element.form).toBe(form); - expect(form.elements.includes(element)).toBe(true); + expect(Array.from(form.elements).includes(element)).toBe(true); }); }); diff --git a/packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts b/packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts index 23d23349f..977b984c7 100644 --- a/packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts +++ b/packages/happy-dom/test/nodes/html-form-element/HTMLFormElement.test.ts @@ -17,6 +17,9 @@ import HTMLElement from '../../../src/nodes/html-element/HTMLElement.js'; import HTMLIFrameElement from '../../../src/nodes/html-iframe-element/HTMLIFrameElement.js'; import BrowserWindow from '../../../src/window/BrowserWindow.js'; import { beforeEach, describe, it, expect, vi } from 'vitest'; +import IRadioNodeList from '../../../src/nodes/html-form-element/IRadioNodeList.js'; +import * as PropertySymbol from '../../../src/PropertySymbol.js'; +import THTMLFormControlElement from '../../../src/nodes/html-form-element/THTMLFormControlElement.js'; describe('HTMLFormElement', () => { let window: Window; @@ -170,16 +173,16 @@ describe('HTMLFormElement', () => { expect(elements.item(7) === root.children[7]).toBe(true); expect(elements.item(8) === root.children[8]).toBe(true); - const radioNodeList1 = new RadioNodeList(); - const radioNodeList2 = new RadioNodeList(); + const radioNodeList1: IRadioNodeList = new RadioNodeList(); + const radioNodeList2: IRadioNodeList = new RadioNodeList(); - radioNodeList1.push(root.children[2]); - radioNodeList1.push(root.children[3]); - radioNodeList1.push(root.children[4]); + radioNodeList1[PropertySymbol.addItem](root.children[2]); + radioNodeList1[PropertySymbol.addItem](root.children[3]); + radioNodeList1[PropertySymbol.addItem](root.children[4]); - radioNodeList2.push(root.children[5]); - radioNodeList2.push(root.children[6]); - radioNodeList2.push(root.children[7]); + radioNodeList2[PropertySymbol.addItem](root.children[5]); + radioNodeList2[PropertySymbol.addItem](root.children[6]); + radioNodeList2[PropertySymbol.addItem](root.children[7]); expect(element['text1'] === root.children[0]).toBe(true); expect(element['button1'] === root.children[1]).toBe(true); @@ -194,10 +197,10 @@ describe('HTMLFormElement', () => { expect(elements.namedItem('text1') === root.children[0]).toBe(true); expect(elements.namedItem('button1') === root.children[1]).toBe(true); expect(elements.namedItem('checkbox1')).toEqual(radioNodeList1); - expect(elements.namedItem('checkbox1')?.value).toBe('value2'); + expect((elements.namedItem('checkbox1')).value).toBe('value2'); expect(elements.namedItem('radio1')).toEqual(radioNodeList2); - expect(elements.namedItem('radio1')?.value).toBe('value2'); - expect(elements.namedItem('1')?.value).toBe('value1'); + expect((elements.namedItem('radio1')).value).toBe('value2'); + expect((elements.namedItem('1')).value).toBe('value1'); (elements.namedItem('text1')).name = 'text2'; (elements.namedItem('text2')).id = 'text3'; @@ -247,16 +250,28 @@ describe('HTMLFormElement', () => { expect(elements[15] === anotherRoot.children[6]).toBe(true); expect(elements[16] === anotherRoot.children[7]).toBe(true); - const anotherRadioNodeList1 = new RadioNodeList(); - const anotherRadioNodeList2 = new RadioNodeList(); + const anotherRadioNodeList1: IRadioNodeList = new RadioNodeList(); + const anotherRadioNodeList2: IRadioNodeList = new RadioNodeList(); - anotherRadioNodeList1.push(anotherRoot.children[2]); - anotherRadioNodeList1.push(anotherRoot.children[3]); - anotherRadioNodeList1.push(anotherRoot.children[4]); + anotherRadioNodeList1[PropertySymbol.addItem]( + anotherRoot.children[2] + ); + anotherRadioNodeList1[PropertySymbol.addItem]( + anotherRoot.children[3] + ); + anotherRadioNodeList1[PropertySymbol.addItem]( + anotherRoot.children[4] + ); - anotherRadioNodeList2.push(anotherRoot.children[5]); - anotherRadioNodeList2.push(anotherRoot.children[6]); - anotherRadioNodeList2.push(anotherRoot.children[7]); + anotherRadioNodeList2[PropertySymbol.addItem]( + anotherRoot.children[5] + ); + anotherRadioNodeList2[PropertySymbol.addItem]( + anotherRoot.children[6] + ); + anotherRadioNodeList2[PropertySymbol.addItem]( + anotherRoot.children[7] + ); expect(element['anotherText1'] === anotherRoot.children[0]).toBe(true); expect(element['anotherButton1'] === anotherRoot.children[1]).toBe(true); @@ -268,7 +283,7 @@ describe('HTMLFormElement', () => { expect(elements['anotherCheckbox1']).toEqual(anotherRadioNodeList1); expect(elements['anotherRadio1']).toEqual(anotherRadioNodeList2); - for (const child of root.children.slice()) { + for (const child of Array.from(root.children)) { if (child !== anotherElement) { root.removeChild(child); } @@ -313,7 +328,7 @@ describe('HTMLFormElement', () => { - + `; const elements = element.elements; @@ -340,18 +355,18 @@ describe('HTMLFormElement', () => { expect(elements.item(3) === root.children[3]).toBe(true); expect(elements.item(4) === root.children[4]).toBe(true); - const radioNodeList = new RadioNodeList(); - radioNodeList.push(root.children[1]); - radioNodeList.push(root.children[2]); - radioNodeList.push(root.children[3]); + const radioNodeList: IRadioNodeList = new RadioNodeList(); + radioNodeList[PropertySymbol.addItem](root.children[1]); + radioNodeList[PropertySymbol.addItem](root.children[2]); + radioNodeList[PropertySymbol.addItem](root.children[3]); - expect(typeof elements.push).toBe('function'); + expect(typeof elements.item).toBe('function'); expect(typeof elements.namedItem).toBe('function'); expect(elements.namedItem('length') === root.children[0]).toBe(true); expect(elements.namedItem('namedItem')).toEqual(radioNodeList); - expect(elements.namedItem('push') === root.children[4]).toBe(true); + expect(elements.namedItem('item') === root.children[4]).toBe(true); - const children = root.children.slice(); + const children = Array.from(root.children); for (const child of children) { root.removeChild(child); @@ -372,11 +387,11 @@ describe('HTMLFormElement', () => { expect(elements[3] === root.children[3]).toBe(true); expect(elements[4] === root.children[4]).toBe(true); - expect(typeof elements.push).toBe('function'); + expect(typeof elements.item).toBe('function'); expect(typeof elements.namedItem).toBe('function'); expect(elements.namedItem('length') === root.children[0]).toBe(true); expect(elements.namedItem('namedItem')).toEqual(radioNodeList); - expect(elements.namedItem('push') === root.children[4]).toBe(true); + expect(elements.namedItem('item') === root.children[4]).toBe(true); }); }); diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts index e276b4e50..35a5ed471 100644 --- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts +++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts @@ -628,7 +628,7 @@ describe('HTMLInputElement', () => { expect(element.form).toBe(null); document.body.appendChild(element); expect(element.form).toBe(form); - expect(form.elements.includes(element)).toBe(true); + expect(Array.from(form.elements).includes(element)).toBe(true); }); it('Returns form element by id if the form attribute is set when element is connected to DOM.', () => { @@ -638,7 +638,7 @@ describe('HTMLInputElement', () => { document.body.appendChild(element); element.setAttribute('form', 'form'); expect(element.form).toBe(form); - expect(form.elements.includes(element)).toBe(true); + expect(Array.from(form.elements).includes(element)).toBe(true); }); }); diff --git a/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts b/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts index d8f443c5b..01c679c5e 100644 --- a/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts +++ b/packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts @@ -202,18 +202,18 @@ describe('HTMLSelectElement', () => { element.appendChild(option2); expect(element.selectedOptions.length).toBe(1); - expect(element.selectedOptions[0]).toBe(option2); + expect(element.selectedOptions[0] === option2).toBe(true); option1.setAttribute('selected', ''); expect(element.selectedOptions.length).toBe(2); - expect(element.selectedOptions[0]).toBe(option1); - expect(element.selectedOptions[1]).toBe(option2); + expect(element.selectedOptions[0] === option1).toBe(true); + expect(element.selectedOptions[1] === option2).toBe(true); option2.removeAttribute('selected'); expect(element.selectedOptions.length).toBe(1); - expect(element.selectedOptions[0]).toBe(option1); + expect(element.selectedOptions[0] === option1).toBe(true); }); }); @@ -490,6 +490,7 @@ describe('HTMLSelectElement', () => { element.appendChild(option2); element.appendChild(option3); + debugger; element.removeChild(option2); expect(element.length).toBe(2); diff --git a/packages/happy-dom/test/nodes/text/Text.test.ts b/packages/happy-dom/test/nodes/text/Text.test.ts index 584c1ef1a..ba3388a66 100644 --- a/packages/happy-dom/test/nodes/text/Text.test.ts +++ b/packages/happy-dom/test/nodes/text/Text.test.ts @@ -9,6 +9,7 @@ describe('Text', () => { let document: Document; beforeEach(() => { + debugger; window = new Window(); document = window.document; }); From f5f0a5ed350a27e9e2c423a7194242a1880914ea Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 9 Jul 2024 10:36:23 +0200 Subject: [PATCH 18/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 15 +- .../AbstractCSSStyleDeclaration.ts | 16 +- .../CSSStyleDeclarationElementStyle.ts | 6 +- .../custom-element/CustomElementRegistry.ts | 6 +- packages/happy-dom/src/nodes/attr/Attr.ts | 6 +- .../document-fragment/DocumentFragment.ts | 2 +- .../happy-dom/src/nodes/document/Document.ts | 15 +- .../src/nodes/element/DatasetFactory.ts | 13 +- .../happy-dom/src/nodes/element/Element.ts | 55 +++--- .../src/nodes/element/HTMLCollection.ts | 181 ++++++++++++------ .../element/IHTMLCollectionObservedNode.ts | 1 + .../src/nodes/element/NamedNodeMap.ts | 80 ++++---- .../nodes/element/NamedNodeMapProxyFactory.ts | 100 ++++++++++ .../src/nodes/html-element/HTMLElement.ts | 58 +----- .../HTMLFormControlsCollection.ts | 77 ++++---- .../HTMLOptionsCollection.ts | 1 - packages/happy-dom/src/nodes/node/Node.ts | 117 ++++++----- packages/happy-dom/src/nodes/node/NodeList.ts | 65 +++++-- .../src/nodes/svg-element/SVGElement.ts | 38 ---- .../src/query-selector/QuerySelector.ts | 12 +- .../src/query-selector/SelectorItem.ts | 68 ++++--- .../test/AdoptedStyleSheetCustomElement.ts | 6 +- .../nodes/child-node/ChildNodeUtility.test.ts | 42 +++- .../DocumentFragment.test.ts | 21 +- .../test/nodes/document/Document.test.ts | 32 ++-- .../test/nodes/element/Element.test.ts | 166 +++++++++------- .../test/nodes/element/HTMLCollection.test.ts | 14 +- .../nodes/html-element/HTMLElement.test.ts | 4 +- .../test/query-selector/QuerySelector.test.ts | 61 +++--- 29 files changed, 743 insertions(+), 535 deletions(-) create mode 100644 packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 4b5d38ddb..79fb5adfd 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -10,7 +10,8 @@ export const checked = Symbol('checked'); export const childNodes = Symbol('childNodes'); export const children = Symbol('children'); export const classList = Symbol('classList'); -export const computedStyle = Symbol('computedStyle'); +export const connectedToNode = Symbol('connectedToNode'); +export const disconnectedFromNode = Symbol('disconnectedFromNode'); export const connectedToDocument = Symbol('connectedToDocument'); export const disconnectedFromDocument = Symbol('disconnectedFromDocument'); export const contentLength = Symbol('contentLength'); @@ -85,6 +86,7 @@ export const scrollWidth = Symbol('scrollWidth'); export const scrollTop = Symbol('scrollTop'); export const scrollLeft = Symbol('scrollLeft'); export const attributes = Symbol('attributes'); +export const attributesProxy = Symbol('attributesProxy'); export const namespaceURI = Symbol('namespaceURI'); export const accessKey = Symbol('accessKey'); export const accessKeyLabel = Symbol('accessKeyLabel'); @@ -164,6 +166,7 @@ export const capabilities = Symbol('capabilities'); export const settings = Symbol('settings'); export const dataListNode = Symbol('dataListNode'); export const setNamedItem = Symbol('setNamedItem'); +export const setNamedItemNS = Symbol('setNamedItemNS'); export const fieldSetNode = Symbol('fieldSetNode'); export const addRemoveListener = Symbol('addRemoveListener'); export const addSetListener = Symbol('addSetListener'); @@ -175,6 +178,7 @@ export const clone = Symbol('clone'); export const addItem = Symbol('addItem'); export const addNamedItem = Symbol('addNamedItem'); export const removeItem = Symbol('removeItem'); +export const removeNamedItemNS = Symbol('removeNamedItemNS'); export const removeNamedItem = Symbol('removeNamedItem'); export const items = Symbol('items'); export const removeItemIndex = Symbol('removeItemIndex'); @@ -198,7 +202,9 @@ export const addItems = Symbol('addItems'); export const querySelectorCache = Symbol('querySelectorCache'); export const querySelectorAllCache = Symbol('querySelectorAllCache'); export const matchesCache = Symbol('matchesCache'); +export const styleCache = Symbol('styleCache'); export const computedStyleCache = Symbol('computedStyleCache'); +export const computedStyleCacheReferences = Symbol('computedStyleCacheReferences'); export const clearComputedStyleCache = Symbol('clearComputedStyleCache'); export const clearCache = Symbol('clearCache'); export const insertObservedItem = Symbol('insertObservedItem'); @@ -207,8 +213,9 @@ export const observe = Symbol('observe'); export const unobserve = Symbol('unobserve'); export const loadObservedNodes = Symbol('loadObservedNodes'); export const unloadObservedNodes = Symbol('unloadObservedNodes'); -export const attributeFilter = Symbol('attributeFilter'); -export const onObservedItemAttributeChange = Symbol('onObservedItemAttributeChange'); +export const onSetAttribute = Symbol('onSetAttribute'); +export const onRemoveAttribute = Symbol('onRemoveAttribute'); export const observeDocument = Symbol('observeDocument'); export const unobserveDocument = Symbol('unobserveDocument'); -export const htmlCollection = Symbol('htmlCollection'); +export const htmlCollections = Symbol('htmlCollections'); +export const removeNamedItemIndex = Symbol('removeNamedItemIndex'); diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 71f2b6dc6..31bcf7652 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -78,18 +78,10 @@ export default abstract class AbstractCSSStyleDeclaration { } if (this.#ownerElement) { - const style = new CSSStyleDeclarationPropertyManager({ cssText }); - let styleAttribute = this.#ownerElement[PropertySymbol.attributes]['style']; - - if (!styleAttribute) { - styleAttribute = this.#ownerElement[PropertySymbol.ownerDocument].createAttribute('style'); - (this.#ownerElement[PropertySymbol.attributes])[PropertySymbol.setNamedItem]( - styleAttribute, - true - ); - } - - styleAttribute[PropertySymbol.value] = style.toString(); + this.#ownerElement.setAttribute( + 'style', + new CSSStyleDeclarationPropertyManager({ cssText }).toString() + ); } else { this.#style = new CSSStyleDeclarationPropertyManager({ cssText }); } diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 2fe075607..1cbae19e4 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -55,7 +55,7 @@ export default class CSSStyleDeclarationElementStyle { return this.getComputedElementStyle(); } - const cache = this.element[PropertySymbol.computedStyleCache]; + const cache = this.element[PropertySymbol.styleCache]; if (cache?.result) { const result = cache.result.deref(); @@ -68,7 +68,7 @@ export default class CSSStyleDeclarationElementStyle { if (cssText) { const propertyManager = new CSSStyleDeclarationPropertyManager({ cssText }); - this.element[PropertySymbol.computedStyleCache] = { + this.element[PropertySymbol.styleCache] = { result: new WeakRef(propertyManager) }; return propertyManager; @@ -304,7 +304,7 @@ export default class CSSStyleDeclarationElementStyle { }; this.element[PropertySymbol.computedStyleCache] = cachedResult; - this.element[PropertySymbol.ownerDocument][PropertySymbol.computedStyleCache].push( + this.element[PropertySymbol.ownerDocument][PropertySymbol.computedStyleCacheReferences].push( cachedResult ); diff --git a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts index 475784603..bb07a6bc8 100644 --- a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts +++ b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts @@ -70,9 +70,9 @@ export default class CustomElementRegistry { this[PropertySymbol.registedClass].set(elementClass, name); // ObservedAttributes should only be called once by CustomElementRegistry (see #117) - if (elementClass.prototype.attributeChangedCallback) { - elementClass[PropertySymbol.observedAttributes] = elementClass.observedAttributes; - } + elementClass[PropertySymbol.observedAttributes] = (elementClass.observedAttributes || []).map( + (name) => String(name).toLowerCase() + ); if (this[PropertySymbol.callbacks][name]) { const callbacks = this[PropertySymbol.callbacks][name]; diff --git a/packages/happy-dom/src/nodes/attr/Attr.ts b/packages/happy-dom/src/nodes/attr/Attr.ts index c7a5b2a90..3e1d591d7 100644 --- a/packages/happy-dom/src/nodes/attr/Attr.ts +++ b/packages/happy-dom/src/nodes/attr/Attr.ts @@ -15,6 +15,8 @@ export default class Attr extends Node implements Attr { public [PropertySymbol.nodeType] = NodeTypeEnum.attributeNode; public [PropertySymbol.namespaceURI]: string | null = null; public [PropertySymbol.name]: string | null = null; + public [PropertySymbol.localName]: string | null = null; + public [PropertySymbol.prefix]: string | null = null; public [PropertySymbol.value]: string | null = null; public [PropertySymbol.specified] = true; public [PropertySymbol.ownerElement]: Element | null = null; @@ -70,7 +72,7 @@ export default class Attr extends Node implements Attr { * @returns Local name. */ public get localName(): string { - return this[PropertySymbol.name] ? this[PropertySymbol.name].split(':').reverse()[0] : null; + return this[PropertySymbol.localName]; } /** @@ -79,7 +81,7 @@ export default class Attr extends Node implements Attr { * @returns Prefix. */ public get prefix(): string { - return this[PropertySymbol.name] ? this[PropertySymbol.name].split(':')[0] : null; + return this[PropertySymbol.prefix]; } /** diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index 8a04f1bb9..f149f5b09 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -25,7 +25,7 @@ export default class DocumentFragment extends Node { constructor() { super(); - this[PropertySymbol.childNodes][PropertySymbol.htmlCollection] = this[PropertySymbol.children]; + this[PropertySymbol.children][PropertySymbol.observe](this); } /** diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index d344ef7f9..488a114d0 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -71,7 +71,7 @@ export default class Document extends Node { public [PropertySymbol.referrer] = ''; public [PropertySymbol.defaultView]: BrowserWindow | null = null; public [PropertySymbol.ownerWindow]: BrowserWindow; - public [PropertySymbol.computedStyleCache]: Array<{ + public [PropertySymbol.computedStyleCacheReferences]: Array<{ result: WeakRef; }> = []; public declare cloneNode: (deep?: boolean) => Document; @@ -203,7 +203,7 @@ export default class Document extends Node { this.#browserFrame = injected.browserFrame; this[PropertySymbol.ownerWindow] = injected.window; - this[PropertySymbol.childNodes][PropertySymbol.htmlCollection] = this[PropertySymbol.children]; + this[PropertySymbol.children][PropertySymbol.observe](this); } /** @@ -917,9 +917,7 @@ export default class Document extends Node { } else { const bodyNode = QuerySelector.querySelector(root, 'body'); const body = QuerySelector.querySelector(this, 'body'); - const childNodes = ((bodyNode || root))[PropertySymbol.childNodes][ - PropertySymbol.items - ]; + const childNodes = ((bodyNode || root))[PropertySymbol.childNodes]; while (childNodes.length) { body.appendChild(childNodes[0]); } @@ -1228,8 +1226,11 @@ export default class Document extends Node { */ public createAttributeNS(namespaceURI: string, qualifiedName: string): Attr { const attribute = NodeFactory.createNode(this, this[PropertySymbol.ownerWindow].Attr); + const parts = qualifiedName.split(':'); attribute[PropertySymbol.namespaceURI] = namespaceURI; attribute[PropertySymbol.name] = qualifiedName; + attribute[PropertySymbol.localName] = parts[1] ?? qualifiedName; + attribute[PropertySymbol.prefix] = parts[0] ?? null; return attribute; } @@ -1339,10 +1340,10 @@ export default class Document extends Node { * Clears computed style cache. */ public [PropertySymbol.clearComputedStyleCache](): void { - for (const item of this[PropertySymbol.computedStyleCache]) { + for (const item of this[PropertySymbol.computedStyleCacheReferences]) { item.result = null; } - this[PropertySymbol.computedStyleCache] = []; + this[PropertySymbol.computedStyleCacheReferences] = []; } /** diff --git a/packages/happy-dom/src/nodes/element/DatasetFactory.ts b/packages/happy-dom/src/nodes/element/DatasetFactory.ts index 1017968b8..0998fee94 100644 --- a/packages/happy-dom/src/nodes/element/DatasetFactory.ts +++ b/packages/happy-dom/src/nodes/element/DatasetFactory.ts @@ -46,10 +46,15 @@ export default class DatasetFactory { return true; }, deleteProperty(dataset: IDataset, key: string): boolean { - element[PropertySymbol.attributes][PropertySymbol.removeNamedItem]( - 'data-' + DatasetUtility.camelCaseToKebab(key) - ); - return delete dataset[key]; + const attributes = element[PropertySymbol.attributes]; + const dataKey = 'data-' + DatasetUtility.camelCaseToKebab(key); + const item = attributes.getNamedItem(dataKey); + delete dataset[key]; + if (!item) { + return true; + } + attributes[PropertySymbol.removeNamedItem](item); + return true; }, ownKeys(dataset: IDataset): string[] { // According to Mozilla we have to update the dataset object (target) to contain the same keys as what we return: diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 32ac41571..b619ff1e5 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -18,7 +18,6 @@ import Attr from '../attr/Attr.js'; import NamedNodeMap from './NamedNodeMap.js'; import Event from '../../event/Event.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; -import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; @@ -34,6 +33,8 @@ import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import INodeList from '../node/INodeList.js'; import CSSStyleDeclarationPropertyManager from '../../css/declaration/property-manager/CSSStyleDeclarationPropertyManager.js'; +import NamedNodeMapProxyFactory from './NamedNodeMapProxyFactory.js'; +import NamespaceURI from '../../config/NamespaceURI.js'; type InsertAdjacentPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'; @@ -92,7 +93,6 @@ export default class Element // Internal properties public [PropertySymbol.classList]: DOMTokenList = null; public [PropertySymbol.isValue]: string | null = null; - public [PropertySymbol.computedStyle]: CSSStyleDeclaration | null = null; public [PropertySymbol.nodeType] = NodeTypeEnum.elementNode; public [PropertySymbol.tagName]: string | null = this.constructor[PropertySymbol.tagName] || null; public [PropertySymbol.localName]: string | null = @@ -104,9 +104,13 @@ export default class Element public [PropertySymbol.scrollTop] = 0; public [PropertySymbol.scrollLeft] = 0; public [PropertySymbol.attributes] = new NamedNodeMap(this); + public [PropertySymbol.attributesProxy]: NamedNodeMap | null = null; public [PropertySymbol.namespaceURI]: string | null = this.constructor[PropertySymbol.namespaceURI] || null; public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); + public [PropertySymbol.styleCache]: { + result: WeakRef | null; + } | null = null; public [PropertySymbol.computedStyleCache]: { result: WeakRef | null; } | null = null; @@ -119,8 +123,7 @@ export default class Element const attributes = this[PropertySymbol.attributes]; attributes[PropertySymbol.addEventListener]('set', this.#onSetAttribute.bind(this)); attributes[PropertySymbol.addEventListener]('remove', this.#onRemoveAttribute.bind(this)); - - this[PropertySymbol.childNodes][PropertySymbol.htmlCollection] = this[PropertySymbol.children]; + this[PropertySymbol.children][PropertySymbol.observe](this); } /** @@ -211,7 +214,12 @@ export default class Element * @returns Attributes. */ public get attributes(): NamedNodeMap { - return this[PropertySymbol.attributes]; + if (!this[PropertySymbol.attributesProxy]) { + this[PropertySymbol.attributesProxy] = NamedNodeMapProxyFactory.createProxy( + this[PropertySymbol.attributes] + ); + } + return this[PropertySymbol.attributesProxy]; } /** @@ -637,7 +645,12 @@ export default class Element * @param value Value. */ public setAttribute(name: string, value: string): void { - const attribute = this[PropertySymbol.ownerDocument].createAttributeNS(null, name); + const namespaceURI = this[PropertySymbol.namespaceURI]; + // TODO: Is it correct to check for namespaceURI === NamespaceURI.svg? + const attribute = + namespaceURI === NamespaceURI.svg + ? this[PropertySymbol.ownerDocument].createAttributeNS(null, name) + : this[PropertySymbol.ownerDocument].createAttribute(name); attribute[PropertySymbol.value] = String(value); this.setAttributeNode(attribute); } @@ -793,7 +806,7 @@ export default class Element shadowRoot[PropertySymbol.host] = this; shadowRoot[PropertySymbol.mode] = init.mode; - (shadowRoot)[PropertySymbol.connectedToDocument](); + (shadowRoot)[PropertySymbol.connectedToNode](); return this[PropertySymbol.shadowRoot]; } @@ -1076,19 +1089,9 @@ export default class Element * @returns Removed attribute. */ public removeAttributeNode(attribute: Attr): Attr | null { - return this[PropertySymbol.attributes].removeNamedItem(attribute[PropertySymbol.name]); - } - - /** - * Removes an Attr node. - * - * @param attribute Attribute. - * @returns Removed attribute. - */ - public removeAttributeNodeNS(attribute: Attr): Attr | null { return this[PropertySymbol.attributes].removeNamedItemNS( attribute[PropertySymbol.namespaceURI], - attribute.localName + attribute[PropertySymbol.name] ); } @@ -1269,16 +1272,16 @@ export default class Element } } - if (this[PropertySymbol.computedStyleCache]) { - this[PropertySymbol.computedStyleCache].result = null; - this[PropertySymbol.computedStyleCache] = null; + if (this[PropertySymbol.styleCache]) { + this[PropertySymbol.styleCache].result = null; + this[PropertySymbol.styleCache] = null; } if ( this.attributeChangedCallback && (this.constructor)[PropertySymbol.observedAttributes] && (this.constructor)[PropertySymbol.observedAttributes].includes( - attribute[PropertySymbol.name] + attribute[PropertySymbol.name].toLowerCase() ) ) { this.attributeChangedCallback( @@ -1320,16 +1323,16 @@ export default class Element } } - if (this[PropertySymbol.computedStyleCache]) { - this[PropertySymbol.computedStyleCache].result = null; - this[PropertySymbol.computedStyleCache] = null; + if (this[PropertySymbol.styleCache]) { + this[PropertySymbol.styleCache].result = null; + this[PropertySymbol.styleCache] = null; } if ( this.attributeChangedCallback && (this.constructor)[PropertySymbol.observedAttributes] && (this.constructor)[PropertySymbol.observedAttributes].includes( - removedAttribute[PropertySymbol.name] + removedAttribute[PropertySymbol.name].toLowerCase() ) ) { this.attributeChangedCallback( diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index 8cc07a4b2..36dd173e7 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -1,8 +1,8 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; -import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import NodeFilter from '../../tree-walker/NodeFilter.js'; import TreeWalker from '../../tree-walker/TreeWalker.js'; +import Attr from '../attr/Attr.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import Document from '../document/Document.js'; import INodeList from '../node/INodeList.js'; @@ -12,6 +12,7 @@ import NodeTypeEnum from '../node/NodeTypeEnum.js'; import Element from './Element.js'; import IHTMLCollection from './IHTMLCollection.js'; import IHTMLCollectionObservedNode from './IHTMLCollectionObservedNode.js'; +import TNamedNodeMapListener from './TNamedNodeMapListener.js'; const NAMED_ITEM_ATTRIBUTES = ['id', 'name']; @@ -28,8 +29,22 @@ export default class HTMLCollection implements IHTMLCollection { public [PropertySymbol.namedItems] = new Map>(); - protected [PropertySymbol.attributeFilter]: string[] = ['id', 'name']; #observedNodes: IHTMLCollectionObservedNode[] = []; + #attributeListeners = new Map(); + + /** + * Constructor. + * + * @param items Items. + */ + constructor(items?: T[]) { + super(); + if (items && items instanceof Array) { + for (const item of items) { + this[PropertySymbol.addItem](item); + } + } + } /** * Returns `Symbol.toStringTag`. @@ -74,9 +89,7 @@ export default class HTMLCollection * @returns Node. */ public namedItem(name: string): NamedItem | null { - return this[PropertySymbol.namedItems][name] && this[PropertySymbol.namedItems][name].length - ? this[PropertySymbol.namedItems][name][0] - : null; + return (this[PropertySymbol.namedItems].get(name)?.[0]) ?? null; } /** @@ -167,59 +180,46 @@ export default class HTMLCollection * Observes node. * * @param node Root node. - * @param filter Filter. + * @param [options] Options. + * @param [options.subtree] Subtree. + * @param [options.filter] Filter. * @returns Observed node. */ public [PropertySymbol.observe]( node: Element | DocumentFragment | Document, - filter?: (item: T) => boolean + options?: { subtree: boolean; filter?: (item: T) => boolean } ): IHTMLCollectionObservedNode { const observedNode: IHTMLCollectionObservedNode = { node, - filter, + filter: options?.filter ?? null, + subtree: options?.subtree ?? false, mutationListener: null }; - observedNode.mutationListener = { - options: { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true, - attributeFilter: this[PropertySymbol.attributeFilter] - }, - callback: new WeakRef((record: MutationRecord) => { - switch (record.type) { - case MutationTypeEnum.childList: - if (record.addedNodes.length) { - this[PropertySymbol.insertObservedItem](observedNode, record.addedNodes[0]); - } else { - this[PropertySymbol.removeObservedItem](observedNode, record.removedNodes[0]); - } - break; - case MutationTypeEnum.attributes: - const newValue = record.target[PropertySymbol.attributes][record.attributeName]?.value; - if ( - record.target[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - record.oldValue !== newValue && - (!filter || filter(record.target)) && - (!observedNode.mutationListener.options.subtree || - this.#isObservedItem(observedNode, record.target)) - ) { - this[PropertySymbol.onObservedItemAttributeChange]( - record.target, - record.attributeName, - record.oldValue, - newValue - ); - } - } - }) - }; - this.#observedNodes.push(observedNode); - node[PropertySymbol.observeMutations](observedNode.mutationListener); + if (observedNode.subtree) { + observedNode.mutationListener = { + options: { + childList: true, + subtree: true + }, + callback: new WeakRef((record: MutationRecord) => { + if (record.addedNodes.length) { + this[PropertySymbol.insertObservedItem](observedNode, record.addedNodes[0]); + } else { + this[PropertySymbol.removeObservedItem](observedNode, record.removedNodes[0]); + } + }) + }; + + node[PropertySymbol.observeMutations](observedNode.mutationListener); + } else { + node[PropertySymbol.childNodes][PropertySymbol.htmlCollections].push({ + htmlCollection: this, + filter: observedNode.filter + }); + } this[PropertySymbol.loadObservedNodes](observedNode, node); @@ -242,7 +242,13 @@ export default class HTMLCollection this[PropertySymbol.unloadObservedNodes](observedNode, observedNode.node); - observedNode.node[PropertySymbol.unobserveMutations](observedNode.mutationListener); + if (observedNode.subtree) { + observedNode.node[PropertySymbol.unobserveMutations](observedNode.mutationListener); + } else { + const htmlCollections = + observedNode.node[PropertySymbol.childNodes][PropertySymbol.htmlCollections]; + htmlCollections.splice(htmlCollections.indexOf(this), 1); + } } /** @@ -277,7 +283,7 @@ export default class HTMLCollection ): void { const childNodes = parentNode[PropertySymbol.childNodes]; - if (observedNode.mutationListener.options.subtree) { + if (observedNode.subtree) { for (let i = 0, max = childNodes.length; i < max; i++) { if ( childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && @@ -315,7 +321,7 @@ export default class HTMLCollection ): void { const childNodes = parentNode[PropertySymbol.childNodes]; - if (observedNode.mutationListener.options.subtree) { + if (observedNode.subtree) { for (let i = 0, max = childNodes.length; i < max; i++) { if ( childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && @@ -328,6 +334,7 @@ export default class HTMLCollection this[PropertySymbol.unloadObservedNodes](observedNode, childNodes[i]); } } + return; } for (let i = 0, max = childNodes.length; i < max; i++) { @@ -351,7 +358,7 @@ export default class HTMLCollection newItem: Element ): void { // Is part of a subtree. - if (observedNode.mutationListener.options.subtree) { + if (observedNode.subtree) { // Check if the item is observed by this listener if (!this.#isObservedItem(observedNode, newItem)) { return; @@ -428,7 +435,7 @@ export default class HTMLCollection item: T ): void { // Is part of a subtree. - if (observedNode.mutationListener.options.subtree) { + if (observedNode.subtree) { // Find all children that pass the filter inside the item. const items = this.#getItemsInElement(observedNode, item); @@ -453,32 +460,37 @@ export default class HTMLCollection } /** - * Triggered when an attribute changes. + * Triggered when an attribute is set. * * @param item Item. - * @param name Name. - * @param oldValue Old value. - * @param value Value. + * @param attribute Attribute. + * @param replacedAttribute Replaced attribute. */ - protected [PropertySymbol.onObservedItemAttributeChange]( + protected [PropertySymbol.onSetAttribute]( item: T, - name: string, - oldValue: string | null, - value: string | null + attribute: Attr, + replacedAttribute: Attr | null ): void { + const name = attribute[PropertySymbol.name]; + if (name !== 'id' && name !== 'name') { return; } - if (oldValue) { - const namedItems = this[PropertySymbol.namedItems].get(oldValue); + + const replacedValue = replacedAttribute?.[PropertySymbol.value]; + + if (replacedValue) { + const namedItems = this[PropertySymbol.namedItems].get(replacedValue); if (namedItems) { namedItems[PropertySymbol.removeItem](item); } - this[PropertySymbol.updateNamedItemProperty](oldValue); + this[PropertySymbol.updateNamedItemProperty](replacedValue); } + const value = attribute.value; + if (value) { const namedItems = this[PropertySymbol.namedItems].get(value) || @@ -492,6 +504,28 @@ export default class HTMLCollection } } + /** + * Triggered when an attribute is set. + * + * @param item Item. + * @param removedAttribute Attribute. + */ + protected [PropertySymbol.onRemoveAttribute](item: T, removedAttribute: Attr): void { + if (removedAttribute.name !== 'id' && removedAttribute.name !== 'name') { + return; + } + + if (removedAttribute.value) { + const namedItems = this[PropertySymbol.namedItems].get(removedAttribute.value); + + if (namedItems) { + namedItems[PropertySymbol.removeItem](item); + } + + this[PropertySymbol.updateNamedItemProperty](removedAttribute.value); + } + } + /** * Updates named item property. * @@ -550,7 +584,7 @@ export default class HTMLCollection */ #isObservedItem(observedNode: IHTMLCollectionObservedNode, node: Node): boolean { // This method should not be executed when not in a subtree - if (!observedNode.mutationListener.options.subtree) { + if (!observedNode.subtree) { return true; } @@ -605,6 +639,17 @@ export default class HTMLCollection * @param item Item. */ #addNamedItem(item: T): void { + const listeners = { + set: (attribute: Attr, replacedAttribute: Attr) => + this[PropertySymbol.onSetAttribute](item, attribute, replacedAttribute), + remove: (attribute: Attr) => this[PropertySymbol.onRemoveAttribute](item, attribute) + }; + + item[PropertySymbol.attributes][PropertySymbol.addEventListener]('set', listeners.set); + item[PropertySymbol.attributes][PropertySymbol.addEventListener]('remove', listeners.remove); + + this.#attributeListeners.set(item, listeners); + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const name = (item)[PropertySymbol.attributes][attributeName]?.value; if (name) { @@ -631,6 +676,16 @@ export default class HTMLCollection * @param item Item. */ #removeNamedItem(item: T): void { + const listeners = this.#attributeListeners.get(item); + + if (listeners) { + item[PropertySymbol.attributes][PropertySymbol.removeEventListener]('set', listeners.set); + item[PropertySymbol.attributes][PropertySymbol.removeEventListener]( + 'remove', + listeners.remove + ); + } + for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const name = (item)[PropertySymbol.attributes][attributeName]?.value; if (name) { diff --git a/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts b/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts index 57853fd7e..e8ed01508 100644 --- a/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts +++ b/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts @@ -6,5 +6,6 @@ import Element from './Element.js'; export default interface IHTMLCollectionObservedNode { node: Element | DocumentFragment | Document; filter: (item: Element) => boolean | null; + subtree: boolean; mutationListener: IMutationListener; } diff --git a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts index 0659a3138..24b9c46d1 100644 --- a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts @@ -3,8 +3,8 @@ import Attr from '../attr/Attr.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import Element from './Element.js'; -import NamespaceURI from '../../config/NamespaceURI.js'; import TNamedNodeMapListener from './TNamedNodeMapListener.js'; +import NamespaceURI from '../../config/NamespaceURI.js'; /** * Named Node Map. @@ -71,7 +71,11 @@ export default class NamedNodeMap { * @returns Item. */ public getNamedItem(name: string): Attr | null { - return this[PropertySymbol.namedItems].get(this.#getAttributeName(name)) || null; + return ( + this[PropertySymbol.namedItems].get( + this.#getAttributeName(this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI], name) + ) || null + ); } /** @@ -82,22 +86,15 @@ export default class NamedNodeMap { * @returns Item. */ public getNamedItemNS(namespace: string, localName: string): Attr | null { - const attribute = this.getNamedItem(localName); - - if ( - attribute && - attribute[PropertySymbol.namespaceURI] === namespace && - attribute.localName === localName - ) { - return attribute; - } - - for (let i = 0, max = this.length; i < max; i++) { - if (this[i][PropertySymbol.namespaceURI] === namespace && this[i].localName === localName) { - return this[i]; + for (let i = 0; i < this.length; i++) { + const item = this[i]; + if ( + item[PropertySymbol.namespaceURI] === namespace && + item[PropertySymbol.localName] === localName + ) { + return item; } } - return null; } @@ -130,13 +127,14 @@ export default class NamedNodeMap { * @returns Removed item. */ public removeNamedItem(name: string): Attr { - const item = this[PropertySymbol.removeNamedItem](name); + const item = this.getNamedItem(name); if (!item) { throw new DOMException( `Failed to execute 'removeNamedItem' on 'NamedNodeMap': No item with name '${name}' was found.`, DOMExceptionNameEnum.notFoundError ); } + this[PropertySymbol.removeNamedItem](item); return item; } @@ -148,11 +146,15 @@ export default class NamedNodeMap { * @returns Removed item. */ public removeNamedItemNS(namespace: string, localName: string): Attr | null { - const attribute = this.getNamedItemNS(namespace, this.#getAttributeName(localName)); - if (attribute) { - return this.removeNamedItem(attribute[PropertySymbol.name]); + const item = this.getNamedItemNS(namespace, localName); + if (!item) { + throw new DOMException( + `Failed to execute 'removeNamedItemNS' on 'NamedNodeMap': No item with name '${localName}' in namespace '${namespace}' was found.`, + DOMExceptionNameEnum.notFoundError + ); } - return null; + this[PropertySymbol.removeNamedItem](item); + return item; } /** @@ -224,10 +226,12 @@ export default class NamedNodeMap { return null; } - item[PropertySymbol.name] = this.#getAttributeName(item[PropertySymbol.name]); (item[PropertySymbol.ownerElement]) = this[PropertySymbol.ownerElement]; - const name = item[PropertySymbol.name]; + const namespaceURI = + item[PropertySymbol.namespaceURI] ?? + this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI]; + const name = this.#getAttributeName(namespaceURI, item[PropertySymbol.name]); const replacedItem = this[PropertySymbol.namedItems].get(name) || null; this[PropertySymbol.namedItems].set(name, item); @@ -239,11 +243,11 @@ export default class NamedNodeMap { this[this.length] = item; this.length++; - if (this.#isValidPropertyName(name)) { + if (name === item[PropertySymbol.name] && this.#isValidPropertyName(name)) { this[name] = item; } - if (!ignoreListeners && replacedItem?.value !== item.value) { + if (!ignoreListeners) { this[PropertySymbol.dispatchEvent]('set', item, replacedItem); } @@ -251,32 +255,25 @@ export default class NamedNodeMap { } /** - * Removes an item without throwing if it doesn't exist. + * Removes an item. * - * @param name Name of item. + * @param item Item. * @param [ignoreListeners] Ignores listeners. - * @returns Removed item, or null if it didn't exist. */ - public [PropertySymbol.removeNamedItem](name: string, ignoreListeners = false): Attr | null { - const removedItem = this[PropertySymbol.namedItems].get(this.#getAttributeName(name)); + public [PropertySymbol.removeNamedItem](item: Attr, ignoreListeners = false): void { + this.#removeNamedItemIndex(item); - if (!removedItem) { - return null; - } - - this.#removeNamedItemIndex(removedItem); + const name = item[PropertySymbol.name]; - if (this[name] === removedItem) { + if (this[name] === item) { delete this[name]; } this[PropertySymbol.namedItems].delete(name); if (!ignoreListeners) { - this[PropertySymbol.dispatchEvent]('remove', removedItem); + this[PropertySymbol.dispatchEvent]('remove', item); } - - return removedItem; } /** @@ -317,11 +314,12 @@ export default class NamedNodeMap { /** * Returns attribute name. * + * @param namespace Namespace. * @param name Name. * @returns Attribute name based on namespace. */ - #getAttributeName(name): string { - if (this[PropertySymbol.ownerElement][PropertySymbol.namespaceURI] === NamespaceURI.svg) { + #getAttributeName(namespace: string, name: string): string { + if (namespace === NamespaceURI.svg) { return name; } return name.toLowerCase(); diff --git a/packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts b/packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts new file mode 100644 index 000000000..4d4fa0edb --- /dev/null +++ b/packages/happy-dom/src/nodes/element/NamedNodeMapProxyFactory.ts @@ -0,0 +1,100 @@ +/* eslint-disable filenames/match-exported */ + +import * as PropertySymbol from '../../PropertySymbol.js'; +import NamedNodeMap from './NamedNodeMap.js'; + +/** + * Named Node Map. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap + */ +export default class NamedNodeMapProxyFactory { + /** + * Constructor. + * + * @param namedNodeMap + */ + public static createProxy(namedNodeMap: NamedNodeMap): NamedNodeMap { + return new Proxy(namedNodeMap, { + get(attributes: NamedNodeMap, key: string): unknown { + const returnValue = attributes[key]; + if (typeof returnValue === 'function') { + return returnValue.bind(attributes); + } + if (returnValue !== undefined) { + return returnValue; + } + return attributes.getNamedItem(key) ?? undefined; + }, + set(): boolean { + return true; + }, + deleteProperty(): boolean { + return true; + }, + ownKeys(attributes: NamedNodeMap): string[] { + const keys = Array.from(attributes[PropertySymbol.namedItems].keys()); + for (let i = 0, max = attributes.length; i < max; i++) { + keys.push(String(i)); + } + return keys; + }, + has(attributes: NamedNodeMap, key: string): boolean { + if (attributes[PropertySymbol.namedItems].has(key)) { + return true; + } + const index = parseInt(key, 10); + return !isNaN(index) && index < attributes.length; + }, + defineProperty( + attributes: NamedNodeMap, + key: string, + descriptor: PropertyDescriptor + ): boolean { + if (!NamedNodeMap.prototype.hasOwnProperty(key)) { + return false; + } + if (descriptor.get || descriptor.set) { + Object.defineProperty(attributes, key, { + ...descriptor, + get: descriptor.get ? descriptor.get.bind(attributes) : undefined, + set: descriptor.set ? descriptor.set.bind(attributes) : undefined + }); + } else { + Object.defineProperty(attributes, key, { + ...descriptor, + value: + typeof descriptor.value === 'function' + ? descriptor.value.bind(attributes) + : descriptor.value + }); + } + return true; + }, + getOwnPropertyDescriptor(attributes: NamedNodeMap, key: string): PropertyDescriptor { + if (NamedNodeMap.prototype.hasOwnProperty(key)) { + return; + } + + if (attributes[PropertySymbol.namedItems].has(key)) { + return { + value: attributes[PropertySymbol.namedItems].get(key), + writable: false, + enumerable: false, + configurable: false + }; + } + + const index = parseInt(key, 10); + if (!isNaN(index) && index < attributes.length) { + return { + value: attributes[index], + writable: false, + enumerable: false, + configurable: false + }; + } + } + }); + } +} diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 5d79a1640..22661272d 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -11,7 +11,6 @@ import Node from '../node/Node.js'; import HTMLCollection from '../element/HTMLCollection.js'; import DatasetFactory from '../element/DatasetFactory.js'; import IDataset from '../element/IDataset.js'; -import Attr from '../attr/Attr.js'; import NamedNodeMap from '../element/NamedNodeMap.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; @@ -70,21 +69,6 @@ export default class HTMLElement extends Element { #dataset: IDataset = null; #customElementDefineCallback: () => void = null; - /** - * Constructor. - */ - constructor() { - super(); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); - } - /** * Returns access key. * @@ -522,12 +506,10 @@ export default class HTMLElement extends Element { } /** - * Connects this element to another element. - * * @override * @see https://html.spec.whatwg.org/multipage/dom.html#htmlelement */ - public [PropertySymbol.connectedToDocument](): void { + public override [PropertySymbol.connectedToNode](): void { const localName = this[PropertySymbol.localName]; // This element can potentially be a custom element that has not been defined yet @@ -559,6 +541,7 @@ export default class HTMLElement extends Element { newElement[PropertySymbol.rootNode] = this[PropertySymbol.rootNode]; newElement[PropertySymbol.formNode] = this[PropertySymbol.formNode]; + newElement[PropertySymbol.parentNode] = this[PropertySymbol.parentNode]; newElement[PropertySymbol.selectNode] = this[PropertySymbol.selectNode]; newElement[PropertySymbol.textAreaNode] = this[PropertySymbol.textAreaNode]; newElement[PropertySymbol.mutationListeners] = this[PropertySymbol.mutationListeners]; @@ -573,9 +556,7 @@ export default class HTMLElement extends Element { (>this[PropertySymbol.childNodes]) = new NodeList(); (>this[PropertySymbol.children]) = new HTMLCollection(); - - this[PropertySymbol.childNodes][PropertySymbol.htmlCollection] = - this[PropertySymbol.children]; + this[PropertySymbol.children][PropertySymbol.observe](this); this[PropertySymbol.rootNode] = null; this[PropertySymbol.formNode] = null; @@ -588,7 +569,7 @@ export default class HTMLElement extends Element { const parentChildNodes = (this[PropertySymbol.parentNode])[ PropertySymbol.childNodes ]; - parentChildNodes[PropertySymbol.insertItem](newElement, this.nextElementSibling); + parentChildNodes[PropertySymbol.insertItem](newElement, this.nextSibling); parentChildNodes[PropertySymbol.removeItem](this); if (newElement[PropertySymbol.isConnected] && newElement.connectedCallback) { @@ -620,14 +601,13 @@ export default class HTMLElement extends Element { } } - super[PropertySymbol.connectedToDocument](); + super[PropertySymbol.connectedToNode](); } /** - * Called when disconnected from document. - * @param e + * @override */ - public [PropertySymbol.disconnectedFromDocument](): void { + public override [PropertySymbol.disconnectedFromNode](): void { const localName = this[PropertySymbol.localName]; // This element can potentially be a custom element that has not been defined yet @@ -656,28 +636,6 @@ export default class HTMLElement extends Element { } } - super[PropertySymbol.disconnectedFromDocument](); - } - - /** - * Triggered when an attribute is set. - * - * @param item Item. - */ - #onSetAttribute(item: Attr): void { - if (item[PropertySymbol.name] === 'style' && this[PropertySymbol.style]) { - this[PropertySymbol.style].cssText = item[PropertySymbol.value]; - } - } - - /** - * Triggered when an attribute is removed. - * - * @param removedItem Removed item. - */ - #onRemoveAttribute(removedItem: Attr): void { - if (removedItem && removedItem[PropertySymbol.name] === 'style' && this[PropertySymbol.style]) { - this[PropertySymbol.style].cssText = ''; - } + super[PropertySymbol.disconnectedFromNode](); } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index 726382e81..dd41b88b1 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -198,12 +198,9 @@ export default class HTMLFormControlsCollection extends HTMLCollection< } /** - * Appends item. - * - * @param item Item. - * @returns True if added. + * @override */ - public [PropertySymbol.addItem](item: THTMLFormControlElement): boolean { + public override [PropertySymbol.addItem](item: THTMLFormControlElement): boolean { if (!super[PropertySymbol.addItem](item)) { return false; } @@ -216,13 +213,9 @@ export default class HTMLFormControlsCollection extends HTMLCollection< } /** - * Inserts item before another item. - * - * @param newItem New item. - * @param [referenceItem] Reference item. - * @returns True if inserted. + * @override */ - public [PropertySymbol.insertItem]( + public override [PropertySymbol.insertItem]( newItem: THTMLFormControlElement, referenceItem: THTMLFormControlElement | null ): boolean { @@ -242,12 +235,9 @@ export default class HTMLFormControlsCollection extends HTMLCollection< } /** - * Removes item. - * - * @param item Item. - * @returns True if removed. + * @override */ - public [PropertySymbol.removeItem](item: THTMLFormControlElement): boolean { + public override [PropertySymbol.removeItem](item: THTMLFormControlElement): boolean { const index = this[PropertySymbol.indexOf](item); if (!super[PropertySymbol.removeItem](item)) { @@ -266,21 +256,15 @@ export default class HTMLFormControlsCollection extends HTMLCollection< } /** - * Triggered when an attribute changes. - * - * @param item Item. - * @param name Name. - * @param oldValue Old value. - * @param value Value. + * @override */ - protected [PropertySymbol.onObservedItemAttributeChange]( + protected override [PropertySymbol.onSetAttribute]( item: THTMLFormControlElement, - name: string, - oldValue: string | null, - value: string | null + attribute: Attr, + replacedAttribute: Attr | null ): void { - if (name !== 'form') { - super[PropertySymbol.onObservedItemAttributeChange](item, name, oldValue, value); + if (attribute.name !== 'form') { + super[PropertySymbol.onSetAttribute](item, attribute, replacedAttribute); return; } @@ -294,21 +278,46 @@ export default class HTMLFormControlsCollection extends HTMLCollection< return; } - if (oldValue === id) { + if (replacedAttribute?.value === id) { this.#formElement[PropertySymbol.removeItem](item); } - if (value === id) { + if (attribute.value === id) { this.#formElement[PropertySymbol.addItem](item); } } /** - * Sets named item property. - * - * @param name Name. + * @override + */ + protected override [PropertySymbol.onRemoveAttribute]( + item: THTMLFormControlElement, + removedAttribute: Attr + ): void { + if (removedAttribute.name !== 'form') { + super[PropertySymbol.onRemoveAttribute](item, removedAttribute); + return; + } + + if (!this.#formElement[PropertySymbol.isConnected]) { + return; + } + + const id = this.#formElement[PropertySymbol.attributes]['id']?.value; + + if (!id) { + return; + } + + if (removedAttribute.value === id) { + this.#formElement[PropertySymbol.removeItem](item); + } + } + + /** + * @override */ - protected [PropertySymbol.updateNamedItemProperty](name: string): void { + protected override [PropertySymbol.updateNamedItemProperty](name: string): void { if (!this[PropertySymbol.isValidPropertyName](name)) { return; } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts index 2b694af7f..23f81b070 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts @@ -20,7 +20,6 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis)[PropertySymbol.shadowRoot]) { + // eslint-disable-next-line + (this)[PropertySymbol.shadowRoot][PropertySymbol.connectedToNode](); + } + } + + /** + * Called when disconnected from a node. + */ + public [PropertySymbol.disconnectedFromNode](): void { + if (this[PropertySymbol.isConnected]) { + this[PropertySymbol.disconnectedFromDocument](); + } + + const childNodes = this[PropertySymbol.childNodes]; + for (let i = 0, max = childNodes.length; i < max; i++) { + childNodes[i][PropertySymbol.disconnectedFromNode](); + } + + // eslint-disable-next-line + if ((this)[PropertySymbol.shadowRoot]) { + // eslint-disable-next-line + (this)[PropertySymbol.shadowRoot][PropertySymbol.disconnectedFromNode](); + } + } + /** * Called when connected to document. */ @@ -831,16 +864,6 @@ export default class Node extends EventTarget { .catch(() => asyncTaskManager.endTask(taskID)); } } - - for (const child of this[PropertySymbol.childNodes]) { - child[PropertySymbol.connectedToDocument](); - } - - // eslint-disable-next-line - if ((this)[PropertySymbol.shadowRoot]) { - // eslint-disable-next-line - (this)[PropertySymbol.shadowRoot][PropertySymbol.connectedToDocument](); - } } /** @@ -858,16 +881,6 @@ export default class Node extends EventTarget { if (this.disconnectedCallback) { this.disconnectedCallback(); } - - for (const child of this[PropertySymbol.childNodes]) { - child[PropertySymbol.disconnectedFromDocument](); - } - - // eslint-disable-next-line - if ((this)[PropertySymbol.shadowRoot]) { - // eslint-disable-next-line - (this)[PropertySymbol.shadowRoot][PropertySymbol.disconnectedFromDocument](); - } } /** diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts index 469c2a1e9..e7abb8a7b 100644 --- a/packages/happy-dom/src/nodes/node/NodeList.ts +++ b/packages/happy-dom/src/nodes/node/NodeList.ts @@ -4,6 +4,12 @@ import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import Element from '../element/Element.js'; import IHTMLCollection from '../element/IHTMLCollection.js'; import INodeList from './INodeList.js'; +import NodeTypeEnum from './NodeTypeEnum.js'; + +interface IHTMLCollectionAndFilter { + htmlCollection: IHTMLCollection; + filter: (item: Element) => boolean | null; +} /** * NodeList. @@ -11,7 +17,21 @@ import INodeList from './INodeList.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList */ class NodeList extends Array implements INodeList { - public [PropertySymbol.htmlCollection]: IHTMLCollection | null = null; + public [PropertySymbol.htmlCollections]: IHTMLCollectionAndFilter[] = []; + + /** + * Constructor. + * + * @param items Items. + */ + constructor(items?: T[]) { + super(); + if (items && items instanceof Array) { + for (const item of items) { + this[PropertySymbol.addItem](item); + } + } + } /** * Returns `Symbol.toStringTag`. @@ -62,8 +82,14 @@ class NodeList extends Array implements INodeList { super.push(item); - if (this[PropertySymbol.htmlCollection]) { - this[PropertySymbol.htmlCollection][PropertySymbol.addItem](item); + const htmlCollections = this[PropertySymbol.htmlCollections]; + for (const { htmlCollection, filter } of htmlCollections) { + if ( + item[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(item)) + ) { + htmlCollection[PropertySymbol.addItem](item); + } } return true; @@ -96,12 +122,23 @@ class NodeList extends Array implements INodeList { super.splice(index, 0, newItem); - if (this[PropertySymbol.htmlCollection]) { - const htmlCollectionReferenceItem = this[PropertySymbol.htmlCollection][index] || null; - this[PropertySymbol.htmlCollection][PropertySymbol.insertItem]( - newItem, - htmlCollectionReferenceItem - ); + const htmlCollections = this[PropertySymbol.htmlCollections]; + for (const { htmlCollection, filter } of htmlCollections) { + let isInserted = false; + for (let i = index + 1; i < this.length; i++) { + const referenceItem = this[i]; + if ( + referenceItem[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(referenceItem)) + ) { + isInserted = true; + htmlCollection[PropertySymbol.insertItem](newItem, referenceItem); + break; + } + } + if (!isInserted) { + htmlCollection[PropertySymbol.addItem](newItem); + } } return true; @@ -125,8 +162,14 @@ class NodeList extends Array implements INodeList { super.splice(index, 1); - if (this[PropertySymbol.htmlCollection]) { - this[PropertySymbol.htmlCollection][PropertySymbol.removeItem](item); + const htmlCollections = this[PropertySymbol.htmlCollections]; + for (const { htmlCollection, filter } of htmlCollections) { + if ( + item[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + (!filter || filter(item)) + ) { + htmlCollection[PropertySymbol.removeItem](item); + } } return true; diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts index ee3f9aaf7..dd544cb91 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts @@ -6,7 +6,6 @@ import Event from '../../event/Event.js'; import HTMLElementUtility from '../html-element/HTMLElementUtility.js'; import DatasetFactory from '../element/DatasetFactory.js'; import IDataset from '../element/IDataset.js'; -import Attr from '../attr/Attr.js'; /** * SVG Element. @@ -29,21 +28,6 @@ export default class SVGElement extends Element { // Private properties #dataset: IDataset = null; - /** - * Constructor. - */ - constructor() { - super(); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); - } - /** * Returns viewport. * @@ -127,26 +111,4 @@ export default class SVGElement extends Element { public focus(): void { HTMLElementUtility.focus(this); } - - /** - * Triggered when an attribute is set. - * - * @param item Item - */ - #onSetAttribute(item: Attr): void { - if (item[PropertySymbol.name] === 'style' && this[PropertySymbol.style]) { - this[PropertySymbol.style].cssText = item[PropertySymbol.value]; - } - } - - /** - * Triggered when an attribute is removed. - * - * @param removedItem Removed item. - */ - #onRemoveAttribute(removedItem: Attr): void { - if (removedItem && removedItem[PropertySymbol.name] === 'style' && this[PropertySymbol.style]) { - this[PropertySymbol.style].cssText = ''; - } - } } diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index 806bab04c..b0157688a 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -107,7 +107,8 @@ export default class QuerySelector { const groups = SelectorParser.getSelectorGroups(selector); const nodeList: INodeList = new NodeList(); - const matchesMap: { [position: string]: Element } = {}; + const matchesMap: Map = new Map(); + const matchedPositions: string[] = []; const cachedItem = { result: new WeakRef(nodeList) }; @@ -119,13 +120,16 @@ export default class QuerySelector { ? this.findAll(node, [node], items, cachedItem) : this.findAll(null, (node)[PropertySymbol.children], items, cachedItem); for (const match of matches) { - matchesMap[match.documentPosition] = match.element; + if (!matchesMap.has(match.documentPosition)) { + matchesMap.set(match.documentPosition, match.element); + matchedPositions.push(match.documentPosition); + } } } - const keys = Object.keys(matchesMap).sort(); + const keys = matchedPositions.sort(); for (let i = 0, max = keys.length; i < max; i++) { - nodeList[PropertySymbol.addItem](matchesMap[keys[i]]); + nodeList[PropertySymbol.addItem](matchesMap.get(keys[i])); } return nodeList; diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index de7eb6ee8..9d92369ff 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -245,62 +245,58 @@ export default class SelectorItem { case 'nth-child': let nthChildIndex = -1; for (let i = 0, max = parentChildren.length; i < max; i++) { - if ( - (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) && - parentChildren[i] === element - ) { - nthChildIndex = i; - break; + if (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) { + nthChildIndex++; + } + if (parentChildren[i] === element) { + return nthChildIndex !== -1 && pseudo.nthFunction(nthChildIndex + 1) + ? { priorityWeight: 10 } + : null; } } - return nthChildIndex !== -1 && pseudo.nthFunction(nthChildIndex + 1) - ? { priorityWeight: 10 } - : null; + return null; case 'nth-of-type': if (!element[PropertySymbol.parentNode]) { return null; } let nthOfTypeIndex = -1; for (let i = 0, max = parentChildren.length; i < max; i++) { - if ( - parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName] && - parentChildren[i] === element - ) { - nthOfTypeIndex = i; - break; + if (parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName]) { + nthOfTypeIndex++; + } + if (parentChildren[i] === element) { + return nthOfTypeIndex !== -1 && pseudo.nthFunction(nthOfTypeIndex + 1) + ? { priorityWeight: 10 } + : null; } } - return nthOfTypeIndex !== -1 && pseudo.nthFunction(nthOfTypeIndex + 1) - ? { priorityWeight: 10 } - : null; + return null; case 'nth-last-child': let nthLastChildIndex = -1; for (let i = parentChildren.length - 1; i >= 0; i--) { - if ( - (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) && - parentChildren[i] === element - ) { - nthLastChildIndex = i; - break; + if (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) { + nthLastChildIndex++; + } + if (parentChildren[i] === element) { + return nthLastChildIndex !== -1 && pseudo.nthFunction(nthLastChildIndex + 1) + ? { priorityWeight: 10 } + : null; } } - return nthLastChildIndex !== -1 && pseudo.nthFunction(nthLastChildIndex + 1) - ? { priorityWeight: 10 } - : null; + return null; case 'nth-last-of-type': let nthLastOfTypeIndex = -1; for (let i = parentChildren.length - 1; i >= 0; i--) { - if ( - parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName] && - parentChildren[i] === element - ) { - nthLastOfTypeIndex = i; - break; + if (parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName]) { + nthLastOfTypeIndex++; + } + if (parentChildren[i] === element) { + return nthLastOfTypeIndex !== -1 && pseudo.nthFunction(nthLastOfTypeIndex + 1) + ? { priorityWeight: 10 } + : null; } } - return nthLastOfTypeIndex !== -1 && pseudo.nthFunction(nthLastOfTypeIndex + 1) - ? { priorityWeight: 10 } - : null; + return null; case 'target': const hash = element[PropertySymbol.ownerDocument].location.hash; if (!hash) { diff --git a/packages/happy-dom/test/AdoptedStyleSheetCustomElement.ts b/packages/happy-dom/test/AdoptedStyleSheetCustomElement.ts index 0dc8460c9..3a431b0a2 100644 --- a/packages/happy-dom/test/AdoptedStyleSheetCustomElement.ts +++ b/packages/happy-dom/test/AdoptedStyleSheetCustomElement.ts @@ -67,10 +67,10 @@ export default class AdoptedStyleSheetCustomElement extends HTMLElement {
key1 is "${this.getAttribute('key1')}" and key2 is "${this.getAttribute( - 'key2' - )}". + 'key2' + )}". - ${this.childNodes + ${Array.from(this.childNodes) .map( (child) => '#' + child['nodeType'] + (child['tagName'] || '') + child.textContent diff --git a/packages/happy-dom/test/nodes/child-node/ChildNodeUtility.test.ts b/packages/happy-dom/test/nodes/child-node/ChildNodeUtility.test.ts index ebf2147ef..b3c2eec9c 100644 --- a/packages/happy-dom/test/nodes/child-node/ChildNodeUtility.test.ts +++ b/packages/happy-dom/test/nodes/child-node/ChildNodeUtility.test.ts @@ -35,7 +35,11 @@ describe('ChildNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -57,7 +61,11 @@ describe('ChildNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -75,7 +83,11 @@ describe('ChildNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -97,7 +109,11 @@ describe('ChildNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -115,7 +131,11 @@ describe('ChildNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -131,7 +151,11 @@ describe('ChildNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); @@ -153,7 +177,11 @@ describe('ChildNodeUtility', () => { expect(parent.innerHTML).toBe( '' ); - expect(parent.children.map((element) => element.outerHTML).join('')).toBe( + expect( + Array.from(parent.children) + .map((element) => element.outerHTML) + .join('') + ).toBe( '' ); }); diff --git a/packages/happy-dom/test/nodes/document-fragment/DocumentFragment.test.ts b/packages/happy-dom/test/nodes/document-fragment/DocumentFragment.test.ts index 380bc2cd7..8299a6f26 100644 --- a/packages/happy-dom/test/nodes/document-fragment/DocumentFragment.test.ts +++ b/packages/happy-dom/test/nodes/document-fragment/DocumentFragment.test.ts @@ -1,7 +1,6 @@ import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; import DocumentFragment from '../../../src/nodes/document-fragment/DocumentFragment.js'; -import DocumentFragment from '../../../src/nodes/document-fragment/DocumentFragment.js'; import Node from '../../../src/nodes/node/Node.js'; import ParentNodeUtility from '../../../src/nodes/parent-node/ParentNodeUtility.js'; import QuerySelector from '../../../src/query-selector/QuerySelector.js'; @@ -184,7 +183,7 @@ describe('DocumentFragment', () => { vi.spyOn(QuerySelector, 'querySelectorAll').mockImplementation((parentNode, selector) => { expect(parentNode).toBe(documentFragment); expect(selector).toBe(expectedSelector); - return >[element]; + return new NodeList([element]); }); expect(Array.from(documentFragment.querySelectorAll(expectedSelector))).toEqual([element]); @@ -231,9 +230,11 @@ describe('DocumentFragment', () => { expect(Array.from(clone.childNodes)).toEqual([]); expect(Array.from(clone.children)).toEqual([]); - expect(documentFragment.children.map((child) => child.outerHTML).join('')).toBe( - '
Div
Span' - ); + expect( + Array.from(documentFragment.children) + .map((child) => child.outerHTML) + .join('') + ).toBe('
Div
Span'); }); }); @@ -284,9 +285,11 @@ describe('DocumentFragment', () => { documentFragment.insertBefore(clone, child2); expect(documentFragment.children.length).toBe(4); - expect(documentFragment.children.map((child) => child.outerHTML).join('')).toEqual( - '
Template DIV 1
Template SPAN 1' - ); + expect( + Array.from(documentFragment.children) + .map((child) => child.outerHTML) + .join('') + ).toEqual('
Template DIV 1
Template SPAN 1'); }); }); @@ -323,7 +326,7 @@ describe('DocumentFragment', () => { expect((clone)[PropertySymbol.rootNode]).toBe(clone); expect(clone.childNodes.length).toBe(3); expect(Array.from(clone.children)).toEqual( - Array.from(clone.childNodes.filter((node) => node.nodeType === Node.ELEMENT_NODE)) + Array.from(clone.childNodes).filter((node) => node.nodeType === Node.ELEMENT_NODE) ); }); }); diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index 0a726b051..16f9b7cc5 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -171,7 +171,7 @@ describe('Document', () => { const text1 = document.createTextNode('text1'); const text2 = document.createTextNode('text2'); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { (node.parentNode).removeChild(node); } @@ -192,7 +192,7 @@ describe('Document', () => { const text1 = document.createTextNode('text1'); const text2 = document.createTextNode('text2'); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { (node.parentNode).removeChild(node); } @@ -707,7 +707,7 @@ describe('Document', () => { const div = document.createElement('div'); const span = document.createElement('span'); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { (node.parentNode).removeChild(node); } @@ -729,7 +729,7 @@ describe('Document', () => { const clone = template.content.cloneNode(true); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { (node.parentNode).removeChild(node); } @@ -737,9 +737,11 @@ describe('Document', () => { expect(clone.childNodes.length).toBe(0); expect(clone.children.length).toBe(0); - expect(document.children.map((child) => child.outerHTML).join('')).toBe( - '
Div
Span' - ); + expect( + Array.from(document.children) + .map((child) => child.outerHTML) + .join('') + ).toBe('
Div
Span'); }); }); @@ -748,7 +750,7 @@ describe('Document', () => { const div = document.createElement('div'); const span = document.createElement('span'); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { (node.parentNode).removeChild(node); } @@ -770,7 +772,7 @@ describe('Document', () => { const div2 = document.createElement('div'); const span = document.createElement('span'); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { (node.parentNode).removeChild(node); } @@ -796,7 +798,7 @@ describe('Document', () => { const clone = template.content.cloneNode(true); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { (node.parentNode).removeChild(node); } @@ -806,9 +808,11 @@ describe('Document', () => { document.insertBefore(clone, child2); expect(document.children.length).toBe(4); - expect(document.children.map((child) => child.outerHTML).join('')).toBe( - '
Template DIV 1
Template SPAN 1' - ); + expect( + Array.from(document.children) + .map((child) => child.outerHTML) + .join('') + ).toBe('
Template DIV 1
Template SPAN 1'); }); }); @@ -1201,7 +1205,7 @@ describe('Document', () => { const child = document.createElement('div'); child.className = 'className'; - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { (node.parentNode).removeChild(node); } diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts index 302f93cb8..44e1a7d05 100644 --- a/packages/happy-dom/test/nodes/element/Element.test.ts +++ b/packages/happy-dom/test/nodes/element/Element.test.ts @@ -345,7 +345,7 @@ describe('Element', () => { const text1 = document.createTextNode('text1'); const text2 = document.createTextNode('text2'); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { node.parentNode?.removeChild(node); } @@ -366,7 +366,7 @@ describe('Element', () => { const text1 = document.createTextNode('text1'); const text2 = document.createTextNode('text2'); - for (const node of document.childNodes.slice()) { + for (const node of Array.from(document.childNodes)) { node.parentNode?.removeChild(node); } @@ -868,7 +868,7 @@ describe('Element', () => { (parentNode, requestedClassName) => { expect(parentNode).toBe(element); expect(requestedClassName).toEqual(className); - return >[child]; + return new HTMLCollection([child]); } ); @@ -887,7 +887,7 @@ describe('Element', () => { (parentNode, requestedTagName) => { expect(parentNode).toBe(element); expect(requestedTagName).toEqual(tagName); - return >[child]; + return new HTMLCollection([child]); } ); @@ -908,7 +908,7 @@ describe('Element', () => { expect(parentNode).toBe(element); expect(requestedNamespaceURI).toEqual(namespaceURI); expect(requestedTagName).toEqual(tagName); - return >[child]; + return new HTMLCollection([child]); } ); @@ -1586,6 +1586,7 @@ describe('Element', () => { for (const method of ['setAttributeNode', 'setAttributeNodeNS']) { describe(`${method}()`, () => { it('Sets an Attr node on a
element.', () => { + const element = document.createElement('div'); const attribute1 = document.createAttributeNS(NamespaceURI.svg, 'KEY1'); const attribute2 = document.createAttribute('KEY2'); @@ -1597,33 +1598,47 @@ describe('Element', () => { expect(element.attributes.length).toBe(2); - expect((element.attributes[0]).name).toBe('key1'); - expect((element.attributes[0]).namespaceURI).toBe(NamespaceURI.svg); - expect((element.attributes[0]).value).toBe('value1'); - expect((element.attributes[0]).specified).toBe(true); - expect((element.attributes[0]).ownerElement).toBe(element); - expect((element.attributes[0]).ownerDocument).toBe(document); - - expect((element.attributes[1]).name).toBe('key2'); - expect((element.attributes[1]).namespaceURI).toBe(null); - expect((element.attributes[1]).value).toBe('value2'); - expect((element.attributes[1]).specified).toBe(true); - expect((element.attributes[1]).ownerElement).toBe(element); - expect((element.attributes[1]).ownerDocument).toBe(document); - - expect((element.attributes['key1']).name).toBe('key1'); - expect((element.attributes['key1']).namespaceURI).toBe(NamespaceURI.svg); - expect((element.attributes['key1']).value).toBe('value1'); - expect((element.attributes['key1']).specified).toBe(true); - expect((element.attributes['key1']).ownerElement).toBe(element); - expect((element.attributes['key1']).ownerDocument).toBe(document); - - expect((element.attributes['key2']).name).toBe('key2'); - expect((element.attributes['key2']).namespaceURI).toBe(null); - expect((element.attributes['key2']).value).toBe('value2'); - expect((element.attributes['key2']).specified).toBe(true); - expect((element.attributes['key2']).ownerElement).toBe(element); - expect((element.attributes['key2']).ownerDocument).toBe(document); + expect(element.attributes[0].name).toBe('KEY1'); + expect(element.attributes[0].namespaceURI).toBe(NamespaceURI.svg); + expect(element.attributes[0].value).toBe('value1'); + expect(element.attributes[0].specified).toBe(true); + expect(element.attributes[0].ownerElement === element).toBe(true); + expect(element.attributes[0].ownerDocument === document).toBe(true); + + expect(element.attributes[1].name).toBe('key2'); + expect(element.attributes[1].namespaceURI).toBe(null); + expect(element.attributes[1].value).toBe('value2'); + expect(element.attributes[1].specified).toBe(true); + expect(element.attributes[1].ownerElement === element).toBe(true); + expect(element.attributes[1].ownerDocument === document).toBe(true); + + // "undefined" as the SVG namespace should not lowercase the key + expect(element.attributes['key1']).toBe(undefined); + expect(element.attributes['kEy1']).toBe(undefined); + + // Matching key is fine in the SVG namespace + expect(element.attributes['KEY1'].name).toBe('KEY1'); + expect(element.attributes['KEY1'].namespaceURI).toBe(NamespaceURI.svg); + expect(element.attributes['KEY1'].value).toBe('value1'); + expect(element.attributes['KEY1'].specified).toBe(true); + expect(element.attributes['KEY1'].ownerElement === element).toBe(true); + expect(element.attributes['KEY1'].ownerDocument === document).toBe(true); + + // Is converted to lower case through the Proxy in the HTML namespace + expect(element.attributes['key2'].name).toBe('key2'); + expect(element.attributes['key2'].namespaceURI).toBe(null); + expect(element.attributes['key2'].value).toBe('value2'); + expect(element.attributes['key2'].specified).toBe(true); + expect(element.attributes['key2'].ownerElement === element).toBe(true); + expect(element.attributes['key2'].ownerDocument === document).toBe(true); + + // Is converted to lower case through the Proxy in the HTML namespace + expect(element.attributes['KeY2'].name).toBe('key2'); + expect(element.attributes['KeY2'].namespaceURI).toBe(null); + expect(element.attributes['KeY2'].value).toBe('value2'); + expect(element.attributes['KeY2'].specified).toBe(true); + expect(element.attributes['KeY2'].ownerElement === element).toBe(true); + expect(element.attributes['KeY2'].ownerDocument === document).toBe(true); }); it('Sets an Attr node on an element.', () => { @@ -1639,33 +1654,42 @@ describe('Element', () => { expect(svg.attributes.length).toBe(2); - expect((svg.attributes[0]).name).toBe('KEY1'); - expect((svg.attributes[0]).namespaceURI).toBe(NamespaceURI.svg); - expect((svg.attributes[0]).value).toBe('value1'); - expect((svg.attributes[0]).specified).toBe(true); - expect((svg.attributes[0]).ownerElement).toBe(svg); - expect((svg.attributes[0]).ownerDocument).toBe(document); - - expect((svg.attributes[1]).name).toBe('key2'); - expect((svg.attributes[1]).namespaceURI).toBe(null); - expect((svg.attributes[1]).value).toBe('value2'); - expect((svg.attributes[1]).specified).toBe(true); - expect((svg.attributes[1]).ownerElement).toBe(svg); - expect((svg.attributes[1]).ownerDocument).toBe(document); - - expect((svg.attributes['KEY1']).name).toBe('KEY1'); - expect((svg.attributes['KEY1']).namespaceURI).toBe(NamespaceURI.svg); - expect((svg.attributes['KEY1']).value).toBe('value1'); - expect((svg.attributes['KEY1']).specified).toBe(true); - expect((svg.attributes['KEY1']).ownerElement).toBe(svg); - expect((svg.attributes['KEY1']).ownerDocument).toBe(document); - - expect((svg.attributes['key2']).name).toBe('key2'); - expect((svg.attributes['key2']).namespaceURI).toBe(null); - expect((svg.attributes['key2']).value).toBe('value2'); - expect((svg.attributes['key2']).specified).toBe(true); - expect((svg.attributes['key2']).ownerElement).toBe(svg); - expect((svg.attributes['key2']).ownerDocument).toBe(document); + expect(svg.attributes[0].name).toBe('KEY1'); + expect(svg.attributes[0].namespaceURI).toBe(NamespaceURI.svg); + expect(svg.attributes[0].value).toBe('value1'); + expect(svg.attributes[0].specified).toBe(true); + expect(svg.attributes[0].ownerElement === svg).toBe(true); + expect(svg.attributes[0].ownerDocument).toBe(document); + + expect(svg.attributes[1].name).toBe('key2'); + expect(svg.attributes[1].namespaceURI).toBe(null); + expect(svg.attributes[1].value).toBe('value2'); + expect(svg.attributes[1].specified).toBe(true); + expect(svg.attributes[1].ownerElement === svg).toBe(true); + expect(svg.attributes[1].ownerDocument).toBe(document); + + // "undefined" as the SVG namespace should not lowercase the key + expect(svg.attributes['key1']).toBe(undefined); + expect(svg.attributes['kEy1']).toBe(undefined); + + // Matching key is fine in the SVG namespace + expect(svg.attributes['KEY1'].name).toBe('KEY1'); + expect(svg.attributes['KEY1'].namespaceURI).toBe(NamespaceURI.svg); + expect(svg.attributes['KEY1'].value).toBe('value1'); + expect(svg.attributes['KEY1'].specified).toBe(true); + expect(svg.attributes['KEY1'].ownerElement === svg).toBe(true); + expect(svg.attributes['KEY1'].ownerDocument).toBe(document); + + // "undefined" as the SVG namespace should not lowercase the key + expect(svg.attributes['KeY2']).toBe(undefined); + + // Works when matching in the SVG namespace + expect(svg.attributes['key2'].name).toBe('key2'); + expect(svg.attributes['key2'].namespaceURI).toBe(null); + expect(svg.attributes['key2'].value).toBe('value2'); + expect(svg.attributes['key2'].specified).toBe(true); + expect(svg.attributes['key2'].ownerElement === svg).toBe(true); + expect(svg.attributes['key2'].ownerDocument).toBe(document); }); }); } @@ -1681,9 +1705,9 @@ describe('Element', () => { element.setAttributeNode(attribute1); element.setAttributeNode(attribute2); - expect(element.getAttributeNode('key1') === attribute1).toBe(true); + expect(element.getAttributeNode('key1') === null).toBe(true); expect(element.getAttributeNode('key2') === attribute2).toBe(true); - expect(element.getAttributeNode('KEY1') === attribute1).toBe(true); + expect(element.getAttributeNode('KEY1') === null).toBe(true); expect(element.getAttributeNode('KEY2') === attribute2).toBe(true); }); @@ -1713,7 +1737,7 @@ describe('Element', () => { element.setAttributeNode(attribute1); - expect(element.getAttributeNodeNS(NamespaceURI.svg, 'key1') === attribute1).toBe(true); + expect(element.getAttributeNodeNS(NamespaceURI.svg, 'key1') === null).toBe(true); expect(element.getAttributeNodeNS(NamespaceURI.svg, 'KEY1') === attribute1).toBe(true); }); @@ -1731,19 +1755,17 @@ describe('Element', () => { }); }); - for (const method of ['removeAttributeNode', 'removeAttributeNodeNS']) { - describe(`${method}()`, () => { - it('Removes an Attr node.', () => { - const attribute = document.createAttribute('KEY1'); + describe(`removeAttributeNode()`, () => { + it('Removes an Attr node.', () => { + const attribute = document.createAttribute('KEY1'); - attribute.value = 'value1'; - element.setAttributeNode(attribute); - element[method](attribute); + attribute.value = 'value1'; + element.setAttributeNode(attribute); + element.removeAttributeNode(attribute); - expect(element.attributes.length).toBe(0); - }); + expect(element.attributes.length).toBe(0); }); - } + }); describe('replaceWith()', () => { it('Replaces a node with another node.', () => { diff --git a/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts b/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts index 0d76d2a43..1e61574c6 100644 --- a/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts +++ b/packages/happy-dom/test/nodes/element/HTMLCollection.test.ts @@ -118,7 +118,7 @@ describe('HTMLCollection', () => { it('Supports attributes that has the same name as properties and methods of the HTMLCollection class.', () => { const div = document.createElement('div'); - div.innerHTML = `
`; + div.innerHTML = `
`; const container1 = div.querySelector('.container1'); const container2 = div.querySelector('.container2'); const container3 = div.querySelector('.container3'); @@ -129,10 +129,11 @@ describe('HTMLCollection', () => { expect(div.children[2] === container3).toBe(true); expect(div.children.namedItem('length') === container1).toBe(true); expect(div.children.namedItem('namedItem') === container2).toBe(true); - expect(div.children.namedItem('push') === container3).toBe(true); + expect(div.children.namedItem('item') === container3).toBe(true); + expect(typeof div.children['length']).toBe('number'); expect(typeof div.children['namedItem']).toBe('function'); - expect(typeof div.children['push']).toBe('function'); + expect(typeof div.children['item']).toBe('function'); container2.remove(); @@ -140,7 +141,7 @@ describe('HTMLCollection', () => { expect(div.children[0] === container1).toBe(true); expect(div.children[1] === container3).toBe(true); expect(div.children.namedItem('length') === container1).toBe(true); - expect(div.children.namedItem('push') === container3).toBe(true); + expect(div.children.namedItem('item') === container3).toBe(true); div.insertBefore(container2, container3); @@ -150,10 +151,11 @@ describe('HTMLCollection', () => { expect(div.children[2] === container3).toBe(true); expect(div.children.namedItem('length') === container1).toBe(true); expect(div.children.namedItem('namedItem') === container2).toBe(true); - expect(div.children.namedItem('push') === container3).toBe(true); + expect(div.children.namedItem('item') === container3).toBe(true); + expect(typeof div.children['length']).toBe('number'); expect(typeof div.children['namedItem']).toBe('function'); - expect(typeof div.children['push']).toBe('function'); + expect(typeof div.children['item']).toBe('function'); }); }); }); diff --git a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts index 7b41dd879..0d14a2e4b 100644 --- a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts +++ b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts @@ -489,11 +489,11 @@ describe('HTMLElement', () => { expect(parent.children.length).toBe(1); expect(parent.children[0] instanceof CustomElement).toBe(true); - expect(parent.children[0].shadowRoot.children.length).toBe(0); + expect(parent.children[0].shadowRoot?.children.length).toBe(0); document.body.appendChild(parent); - expect(parent.children[0].shadowRoot.children.length).toBe(2); + expect(parent.children[0].shadowRoot?.children.length).toBe(2); }); it('Copies all properties from the unknown element to the new instance.', () => { diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index d7a5747fb..26805ca91 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -795,8 +795,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-child(n+8)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['span.n8', 'div.n9', 'i.n10']); }); @@ -807,8 +807,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-child(2n)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['span.n2', 'b.n4', 'div.n6', 'span.n8', 'i.n10']); }); @@ -819,8 +819,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-child(-n + 3)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['div.', 'b.n1', 'span.n2', 'div.n3']); }); @@ -831,8 +831,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll('div :nth-child(2n+1)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['div.', 'b.n1', 'div.n3', 'span.n5', 'b.n7', 'div.n9']); }); @@ -843,8 +843,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll('div :nth-child(3n+1)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['div.', 'b.n1', 'b.n4', 'b.n7', 'i.n10']); }); @@ -855,8 +855,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-child(3n+1 of b)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['b.n1']); }); @@ -867,8 +867,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-child(n+1 of span)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['span.n2', 'span.n5', 'span.n8']); }); @@ -876,11 +876,12 @@ describe('QuerySelector', () => { it('Returns all elements matching ":nth-last-child(n+1 of span)".', () => { const container = document.createElement('div'); container.innerHTML = QuerySelectorNthChildHTML; + debugger; const elements = container.querySelectorAll(':nth-last-child(n+1 of span)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['span.n2', 'span.n5', 'span.n8']); }); @@ -891,8 +892,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll('div :nth-child(3n+3)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['div.n3', 'div.n6', 'div.n9']); }); @@ -912,8 +913,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-child(odd)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['div.', 'b.n1', 'div.n3', 'span.n5', 'b.n7', 'div.n9']); }); @@ -924,8 +925,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-child(even)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['span.n2', 'b.n4', 'div.n6', 'span.n8', 'i.n10']); }); @@ -936,8 +937,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-of-type(2n)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['b.n4', 'span.n5', 'div.n6']); }); @@ -948,8 +949,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-of-type(odd)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['div.', 'b.n1', 'span.n2', 'div.n3', 'b.n7', 'span.n8', 'div.n9', 'i.n10']); }); @@ -960,8 +961,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-last-child(2n)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['b.n1', 'div.n3', 'span.n5', 'b.n7', 'div.n9']); }); @@ -972,8 +973,8 @@ describe('QuerySelector', () => { const elements = container.querySelectorAll(':nth-last-of-type(2n)'); expect( - Array.from( - elements.map((element) => `${element.tagName.toLowerCase()}.${element.className}`) + Array.from(elements).map( + (element) => `${element.tagName.toLowerCase()}.${element.className}` ) ).toEqual(['b.n4', 'span.n5', 'div.n6']); }); From 071e01d2e38185baf864ab3da7c9b71c473f5be4 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 9 Jul 2024 14:30:14 +0200 Subject: [PATCH 19/51] chore: [#1332] Continues on implementation --- .../AbstractCSSStyleDeclaration.ts | 23 +------ packages/happy-dom/src/location/Location.ts | 1 + .../HTMLFormControlsCollection.ts | 5 ++ .../html-input-element/HTMLInputElement.ts | 1 + .../src/query-selector/QuerySelector.ts | 14 ++++ .../src/query-selector/SelectorItem.ts | 9 ++- .../HTMLTextAreaElement.test.ts | 6 +- .../test/query-selector/QuerySelector.test.ts | 67 +++++++++++++++---- 8 files changed, 86 insertions(+), 40 deletions(-) diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 31bcf7652..e5e2cf105 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -1,12 +1,9 @@ import Element from '../../nodes/element/Element.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import Attr from '../../nodes/attr/Attr.js'; import CSSRule from '../CSSRule.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import DOMException from '../../exception/DOMException.js'; import CSSStyleDeclarationElementStyle from './element-style/CSSStyleDeclarationElementStyle.js'; import CSSStyleDeclarationPropertyManager from './property-manager/CSSStyleDeclarationPropertyManager.js'; -import NamedNodeMap from '../../nodes/element/NamedNodeMap.js'; /** * CSS Style Declaration. @@ -124,21 +121,10 @@ export default abstract class AbstractCSSStyleDeclaration { if (!stringValue) { this.removeProperty(name); } else if (this.#ownerElement) { - let styleAttribute = this.#ownerElement[PropertySymbol.attributes]['style']; - - if (!styleAttribute) { - styleAttribute = this.#ownerElement[PropertySymbol.ownerDocument].createAttribute('style'); - - (this.#ownerElement[PropertySymbol.attributes])[PropertySymbol.setNamedItem]( - styleAttribute, - true - ); - } - const style = this.#elementStyle.getElementStyle(); style.set(name, stringValue, !!priority); - styleAttribute[PropertySymbol.value] = style.toString(); + this.#ownerElement.setAttribute('style', style.toString()); } else { this.#style.set(name, stringValue, !!priority); } @@ -165,12 +151,9 @@ export default abstract class AbstractCSSStyleDeclaration { const newCSSText = style.toString(); if (newCSSText) { - (this.#ownerElement[PropertySymbol.attributes]['style'])[PropertySymbol.value] = - newCSSText; + this.#ownerElement.setAttribute('style', newCSSText); } else { - (this.#ownerElement[PropertySymbol.attributes])[ - PropertySymbol.removeNamedItem - ]('style', true); + this.#ownerElement.removeAttribute('style'); } } else { this.#style.remove(name); diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index 9bd0fdce5..893932e83 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -47,6 +47,7 @@ export default class Location { this.#browserFrame.window?.dispatchEvent( new HashChangeEvent('hashchange', { oldURL, newURL }) ); + this.#browserFrame.window?.document?.[PropertySymbol.clearCache](); } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index dd41b88b1..b27604ca1 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -94,6 +94,7 @@ export default class HTMLFormControlsCollection extends HTMLCollection< return; } super[PropertySymbol.unobserve](this.#observedFormElement); + this.#observedFormElement = null; } /** @@ -190,11 +191,15 @@ export default class HTMLFormControlsCollection extends HTMLCollection< this.#observedDocumentAttributeListeners.remove ); + this.#observedDocumentAttributeListeners.set = null; + this.#observedDocumentAttributeListeners.remove = null; + if (!this.#observedDocument) { return; } super[PropertySymbol.unobserve](this.#observedDocument); + this.#observedDocument = null; } /** diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 30a8a487a..4dc24b1aa 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -1402,6 +1402,7 @@ export default class HTMLInputElement extends HTMLElement { */ #setChecked(checked: boolean): void { this[PropertySymbol.checked] = checked; + this[PropertySymbol.clearCache](); if (checked && this.type === 'radio' && this.name) { const root = ( diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index b0157688a..5ff08b9da 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -114,6 +114,13 @@ export default class QuerySelector { }; node[PropertySymbol.querySelectorAllCache].items.set(selector, cachedItem); + if (node[PropertySymbol.isConnected]) { + // Document is affected for the ":target" selector + (node[PropertySymbol.ownerDocument] || node)[ + PropertySymbol.querySelectorAllCache + ].affectedItems.push(cachedItem); + } + for (const items of groups) { const matches = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode @@ -215,6 +222,13 @@ export default class QuerySelector { node[PropertySymbol.querySelectorCache].items.set(selector, cachedItem); + if (node[PropertySymbol.isConnected]) { + // Document is affected for the ":target" selector + (node[PropertySymbol.ownerDocument] || node)[ + PropertySymbol.querySelectorCache + ].affectedItems.push(cachedItem); + } + for (const items of SelectorParser.getSelectorGroups(selector)) { const match = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index 9d92369ff..83a6f6680 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -244,6 +244,9 @@ export default class SelectorItem { return !pseudo.selectorItems[0].match(element) ? { priorityWeight: 10 } : null; case 'nth-child': let nthChildIndex = -1; + if (pseudo.selectorItems[0] && !pseudo.selectorItems[0].match(element)) { + return null; + } for (let i = 0, max = parentChildren.length; i < max; i++) { if (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) { nthChildIndex++; @@ -256,9 +259,6 @@ export default class SelectorItem { } return null; case 'nth-of-type': - if (!element[PropertySymbol.parentNode]) { - return null; - } let nthOfTypeIndex = -1; for (let i = 0, max = parentChildren.length; i < max; i++) { if (parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName]) { @@ -273,6 +273,9 @@ export default class SelectorItem { return null; case 'nth-last-child': let nthLastChildIndex = -1; + if (pseudo.selectorItems[0] && !pseudo.selectorItems[0].match(element)) { + return null; + } for (let i = parentChildren.length - 1; i >= 0; i--) { if (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) { nthLastChildIndex++; diff --git a/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts b/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts index 41293ac11..a10034918 100644 --- a/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts +++ b/packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts @@ -119,7 +119,7 @@ describe('HTMLTextAreaElement', () => { const div = document.createElement('div'); div.appendChild(element); form.appendChild(div); - expect(element.form).toBe(form); + expect(element.form === form).toBe(true); }); it('Returns form element by id if the form attribute is set when connecting node to DOM.', () => { @@ -129,7 +129,7 @@ describe('HTMLTextAreaElement', () => { element.setAttribute('form', 'form'); expect(element.form).toBe(null); document.body.appendChild(element); - expect(element.form).toBe(form); + expect(element.form === form).toBe(true); expect(Array.from(form.elements).includes(element)).toBe(true); }); @@ -139,7 +139,7 @@ describe('HTMLTextAreaElement', () => { document.body.appendChild(form); document.body.appendChild(element); element.setAttribute('form', 'form'); - expect(element.form).toBe(form); + expect(element.form === form).toBe(true); expect(Array.from(form.elements).includes(element)).toBe(true); }); }); diff --git a/packages/happy-dom/test/query-selector/QuerySelector.test.ts b/packages/happy-dom/test/query-selector/QuerySelector.test.ts index 26805ca91..0e7aecf76 100644 --- a/packages/happy-dom/test/query-selector/QuerySelector.test.ts +++ b/packages/happy-dom/test/query-selector/QuerySelector.test.ts @@ -675,6 +675,23 @@ describe('QuerySelector', () => { expect(elements[2] === container.children[0].children[1].children[2]).toBe(true); expect(elements[3] === container.children[1]).toBe(true); expect(elements[4] === container.children[1].children[0]).toBe(true); + + // Check that cache works + + container.innerHTML = ''; + + const elements2 = container.querySelectorAll(':last-of-type'); + expect(elements2.length).toBe(0); + + container.innerHTML = QuerySelectorHTML; + + const elements3 = container.querySelectorAll(':last-of-type'); + expect(elements3.length).toBe(5); + expect(elements3[0] === container.children[0].children[0]).toBe(true); + expect(elements3[1] === container.children[0].children[1]).toBe(true); + expect(elements3[2] === container.children[0].children[1].children[2]).toBe(true); + expect(elements3[3] === container.children[1]).toBe(true); + expect(elements3[4] === container.children[1].children[0]).toBe(true); }); it('Returns all input elements matching "input[name="op"]:checked".', () => { @@ -691,13 +708,16 @@ describe('QuerySelector', () => { expect(elements.length).toBe(1); expect(elements[0] === container.children[0].children[0]).toBe(true); - const input = elements[0]; + const radio1 = elements[0]; + + expect(radio1.value).toBe('one'); + + const radio2 = container.querySelector("input[value='two']"); - expect(input.value).toBe('one'); + radio2.checked = true; - const twoEl = container.querySelector("input[value='two']"); + // Here we also tests that cache works - twoEl.checked = true; elements = container.querySelectorAll('input[name="op"]:checked'); expect(elements.length).toBe(1); @@ -713,6 +733,25 @@ describe('QuerySelector', () => { expect(elements.length).toBe(2); expect(elements[0] === container.children[0].children[1].children[1]).toBe(true); expect(elements[1] === container.children[0].children[1].children[2]).toBe(true); + + // Check that cache works + + (elements[0]).setAttribute('type', 'hidden'); + (elements[1]).setAttribute('type', 'hidden'); + + const elements2 = container.querySelectorAll('span:not([type=hidden])'); + + expect(elements2.length).toBe(0); + + (elements[0]).setAttribute('type', 'text'); + (elements[1]).setAttribute('type', 'text'); + + const elements3 = container.querySelectorAll('span:not([type=hidden])'); + + expect(elements3.length).toBe(2); + + expect(elements3[0] === container.children[0].children[1].children[1]).toBe(true); + expect(elements3[1] === container.children[0].children[1].children[2]).toBe(true); }); it('Returns all elements matching "input:not([type]):not([list])" to verify that "screen.getByRole(\'checkbox\')" works in Testing Library.', () => { @@ -876,7 +915,6 @@ describe('QuerySelector', () => { it('Returns all elements matching ":nth-last-child(n+1 of span)".', () => { const container = document.createElement('div'); container.innerHTML = QuerySelectorNthChildHTML; - debugger; const elements = container.querySelectorAll(':nth-last-child(n+1 of span)'); expect( @@ -1134,21 +1172,22 @@ describe('QuerySelector', () => { document.appendChild(section); window.location.hash = '#id'; - expect(section.querySelector(':target')).toBe(headline); - expect(section.querySelector('h2:target')).toBe(headline); - expect(section.querySelector('h3:target')).toBeNull(); + expect(section.querySelector(':target') === headline).toBe(true); + expect(section.querySelector('h2:target') === headline).toBe(true); + expect(section.querySelector('h3:target') === null).toBe(true); + // Here we also test that cache works window.location.hash = '#something-else'; - expect(section.querySelector(':target')).toBeNull(); - expect(section.querySelector('h2:target')).toBeNull(); - expect(section.querySelector('h3:target')).toBeNull(); + expect(section.querySelector(':target') === null).toBe(true); + expect(section.querySelector('h2:target') === null).toBe(true); + expect(section.querySelector('h3:target') === null).toBe(true); // Detached Elements should not match window.location.hash = '#id'; section.remove(); - expect(section.querySelector(':target')).toBeNull(); - expect(section.querySelector('h2:target')).toBeNull(); - expect(section.querySelector('h3:target')).toBeNull(); + expect(section.querySelector(':target') === null).toBe(true); + expect(section.querySelector('h2:target') === null).toBe(true); + expect(section.querySelector('h3:target') === null).toBe(true); }); it('Returns an element by id matching "#:id:".', () => { From fb4eaec62c6a4a130c9b639ce80762fa80493cf6 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Mon, 22 Jul 2024 21:13:25 +0200 Subject: [PATCH 20/51] chore: [#1332] Continues on implementation --- packages/happy-dom/src/PropertySymbol.ts | 35 +- .../CSSStyleDeclarationElementStyle.ts | 18 +- .../happy-dom/src/dom-parser/DOMParser.ts | 19 +- packages/happy-dom/src/event/EventTarget.ts | 8 +- packages/happy-dom/src/form-data/FormData.ts | 45 +- .../src/nodes/child-node/ChildNodeUtility.ts | 6 +- .../document-fragment/DocumentFragment.ts | 38 +- .../happy-dom/src/nodes/document/Document.ts | 127 ++- .../src/nodes/element/DOMStringMap.ts | 91 ++ ...tasetUtility.ts => DOMStringMapUtility.ts} | 4 +- .../src/nodes/element/DatasetFactory.ts | 90 -- .../happy-dom/src/nodes/element/Element.ts | 97 +-- .../src/nodes/element/HTMLCollection.ts | 775 +++--------------- .../src/nodes/element/IHTMLCollection.ts | 87 -- .../element/IHTMLCollectionObservedNode.ts | 11 - .../src/nodes/element/NamedNodeMap.ts | 69 +- .../html-anchor-element/HTMLAnchorElement.ts | 35 +- .../html-area-element/HTMLAreaElement.ts | 35 +- .../html-button-element/HTMLButtonElement.ts | 11 +- .../HTMLDataListElement.ts | 16 +- .../HTMLDetailsElement.ts | 33 +- .../src/nodes/html-element/HTMLElement.ts | 65 +- .../HTMLFieldSetElement.ts | 33 +- .../HTMLFormControlsCollection.ts | 364 +------- .../html-form-element/HTMLFormElement.ts | 224 ++++- .../IHTMLFormControlsCollection.ts | 6 - .../nodes/html-form-element/IRadioNodeList.ts | 11 - .../nodes/html-form-element/RadioNodeList.ts | 3 +- .../html-iframe-element/HTMLIFrameElement.ts | 28 +- .../html-input-element/HTMLInputElement.ts | 11 +- .../HTMLLabelElementUtility.ts | 16 +- .../html-link-element/HTMLLinkElement.ts | 50 +- .../html-option-element/HTMLOptionElement.ts | 52 +- .../html-script-element/HTMLScriptElement.ts | 21 +- .../HTMLOptionsCollection.ts | 322 +------- .../html-select-element/HTMLSelectElement.ts | 344 +++++++- .../html-slot-element/HTMLSlotElement.ts | 43 +- .../html-style-element/HTMLStyleElement.ts | 54 +- .../HTMLTemplateElement.ts | 4 +- .../HTMLTextAreaElement.ts | 68 +- .../nodes/node/ICachedComputedStyleResult.ts | 6 + .../nodes/node/ICachedElementByIdResult.ts | 6 + .../node/ICachedElementByTagNameResult.ts | 6 + .../node/ICachedElementsByTagNameResult.ts | 6 + ...MatchesItem.ts => ICachedMatchesResult.ts} | 3 +- .../nodes/node/ICachedQuerySelectorAllItem.ts | 6 - .../node/ICachedQuerySelectorAllResult.ts | 7 + .../nodes/node/ICachedQuerySelectorItem.ts | 5 - .../nodes/node/ICachedQuerySelectorResult.ts | 6 + .../happy-dom/src/nodes/node/ICachedResult.ts | 3 + .../src/nodes/node/ICachedStyleResult.ts | 6 + .../happy-dom/src/nodes/node/INodeList.ts | 115 --- packages/happy-dom/src/nodes/node/Node.ts | 263 ++++-- packages/happy-dom/src/nodes/node/NodeList.ts | 246 ++---- .../happy-dom/src/nodes/node/NodeUtility.ts | 18 +- .../src/nodes/parent-node/IParentNode.ts | 28 +- .../nodes/parent-node/ParentNodeUtility.ts | 225 +++-- .../src/nodes/shadow-root/ShadowRoot.ts | 4 +- .../src/nodes/svg-element/SVGElement.ts | 9 +- packages/happy-dom/src/nodes/text/Text.ts | 30 + .../src/query-selector/QuerySelector.ts | 60 +- .../src/query-selector/SelectorItem.ts | 9 +- packages/happy-dom/src/range/Range.ts | 48 +- packages/happy-dom/src/range/RangeUtility.ts | 5 +- packages/happy-dom/src/storage/Storage.ts | 73 ++ .../happy-dom/src/storage/StorageFactory.ts | 95 --- .../happy-dom/src/tree-walker/TreeWalker.ts | 4 +- .../happy-dom/src/window/BrowserWindow.ts | 7 +- .../src/xml-serializer/XMLSerializer.ts | 8 +- .../happy-dom/test/storage/Storage.test.ts | 5 +- 70 files changed, 1841 insertions(+), 2840 deletions(-) create mode 100644 packages/happy-dom/src/nodes/element/DOMStringMap.ts rename packages/happy-dom/src/nodes/element/{DatasetUtility.ts => DOMStringMapUtility.ts} (92%) delete mode 100644 packages/happy-dom/src/nodes/element/DatasetFactory.ts delete mode 100644 packages/happy-dom/src/nodes/element/IHTMLCollection.ts delete mode 100644 packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts delete mode 100644 packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts delete mode 100644 packages/happy-dom/src/nodes/html-form-element/IRadioNodeList.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedComputedStyleResult.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedElementByIdResult.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedElementByTagNameResult.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedElementsByTagNameResult.ts rename packages/happy-dom/src/nodes/node/{ICachedMatchesItem.ts => ICachedMatchesResult.ts} (50%) delete mode 100644 packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllItem.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllResult.ts delete mode 100644 packages/happy-dom/src/nodes/node/ICachedQuerySelectorItem.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedQuerySelectorResult.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedResult.ts create mode 100644 packages/happy-dom/src/nodes/node/ICachedStyleResult.ts delete mode 100644 packages/happy-dom/src/nodes/node/INodeList.ts delete mode 100644 packages/happy-dom/src/storage/StorageFactory.ts diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 79fb5adfd..2ff376e29 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -175,17 +175,16 @@ export const removeRemoveListener = Symbol('removeRemoveListener'); export const appendFormControlItemByName = Symbol('appendFormControlItemByName'); export const removeFormControlItemByName = Symbol('removeFormControlItemByName'); export const clone = Symbol('clone'); -export const addItem = Symbol('addItem'); export const addNamedItem = Symbol('addNamedItem'); -export const removeItem = Symbol('removeItem'); export const removeNamedItemNS = Symbol('removeNamedItemNS'); export const removeNamedItem = Symbol('removeNamedItem'); export const items = Symbol('items'); export const removeItemIndex = Symbol('removeItemIndex'); export const indexOf = Symbol('indexOf'); +export const push = Symbol('push'); +export const splice = Symbol('splice'); export const updateNamedItem = Symbol('updateNamedItem'); export const includes = Symbol('includes'); -export const insertItem = Symbol('insertItem'); export const addEventListener = Symbol('addEventListener'); export const removeEventListener = Symbol('removeEventListener'); export const namedItemListeners = Symbol('namedItemListeners'); @@ -198,24 +197,28 @@ export const updateSheet = Symbol('updateSheet'); export const slice = Symbol('slice'); export const replaceItems = Symbol('replaceItems'); export const clearItems = Symbol('clearItems'); -export const addItems = Symbol('addItems'); -export const querySelectorCache = Symbol('querySelectorCache'); -export const querySelectorAllCache = Symbol('querySelectorAllCache'); -export const matchesCache = Symbol('matchesCache'); -export const styleCache = Symbol('styleCache'); -export const computedStyleCache = Symbol('computedStyleCache'); -export const computedStyleCacheReferences = Symbol('computedStyleCacheReferences'); -export const clearComputedStyleCache = Symbol('clearComputedStyleCache'); export const clearCache = Symbol('clearCache'); -export const insertObservedItem = Symbol('insertObservedItem'); -export const removeObservedItem = Symbol('removeObservedItem'); export const observe = Symbol('observe'); export const unobserve = Symbol('unobserve'); -export const loadObservedNodes = Symbol('loadObservedNodes'); -export const unloadObservedNodes = Symbol('unloadObservedNodes'); export const onSetAttribute = Symbol('onSetAttribute'); export const onRemoveAttribute = Symbol('onRemoveAttribute'); export const observeDocument = Symbol('observeDocument'); export const unobserveDocument = Symbol('unobserveDocument'); -export const htmlCollections = Symbol('htmlCollections'); export const removeNamedItemIndex = Symbol('removeNamedItemIndex'); +export const nodeArray = Symbol('nodeArray'); +export const elementArray = Symbol('elementArray'); +export const cache = Symbol('cache'); +export const affectsCache = Symbol('affectsCache'); +export const forms = Symbol('forms'); +export const affectsComputedStyleCache = Symbol('affectsComputedStyleCache'); +export const query = Symbol('query'); +export const computedStyle = Symbol('computedStyle'); +export const getFormControlItems = Symbol('getFormControlItems'); +export const getFormControlNamedItem = Symbol('getFormControlNamedItem'); +export const getWindow = Symbol('getWindow'); +export const onNodeListChange = Symbol('onNodeListChange'); +export const dataset = Symbol('dataset'); +export const customElementDefineCallback = Symbol('customElementDefineCallback'); +export const submit = Symbol('submit'); +export const submitWithBrowserFrame = Symbol('submitWithBrowserFrame'); +export const getDisplaySize = Symbol('getDisplaySize'); diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 1cbae19e4..b0f313cd1 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -55,10 +55,10 @@ export default class CSSStyleDeclarationElementStyle { return this.getComputedElementStyle(); } - const cache = this.element[PropertySymbol.styleCache]; + const cachedResult = this.element[PropertySymbol.cache].style; - if (cache?.result) { - const result = cache.result.deref(); + if (cachedResult?.result) { + const result = cachedResult.result.deref(); if (result) { return result; } @@ -68,7 +68,7 @@ export default class CSSStyleDeclarationElementStyle { if (cssText) { const propertyManager = new CSSStyleDeclarationPropertyManager({ cssText }); - this.element[PropertySymbol.styleCache] = { + this.element[PropertySymbol.cache].style = { result: new WeakRef(propertyManager) }; return propertyManager; @@ -96,10 +96,10 @@ export default class CSSStyleDeclarationElementStyle { return new CSSStyleDeclarationPropertyManager(); } - const cache = this.element[PropertySymbol.computedStyleCache]; + const cacheResult = this.element[PropertySymbol.cache].computedStyle; - if (cache?.result) { - const result = cache.result.deref(); + if (cacheResult?.result) { + const result = cacheResult.result.deref(); if (result) { return result; } @@ -303,8 +303,8 @@ export default class CSSStyleDeclarationElementStyle { result: new WeakRef(propertyManager) }; - this.element[PropertySymbol.computedStyleCache] = cachedResult; - this.element[PropertySymbol.ownerDocument][PropertySymbol.computedStyleCacheReferences].push( + this.element[PropertySymbol.cache].computedStyle = cachedResult; + this.element[PropertySymbol.ownerDocument][PropertySymbol.affectsComputedStyleCache].push( cachedResult ); diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index c3036d3d4..871da462d 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -37,16 +37,17 @@ export default class DOMParser { } const newDocument = this.#createDocument(mimeType); + const documentChildNodes = newDocument[PropertySymbol.nodeArray]; - while (newDocument[PropertySymbol.childNodes].length) { - newDocument.removeChild(newDocument[PropertySymbol.childNodes][0]); + while (documentChildNodes.length) { + newDocument.removeChild(documentChildNodes[0]); } const root = XMLParser.parse(newDocument, string, { evaluateScripts: true }); let documentElement = null; let documentTypeNode = null; - for (const node of root[PropertySymbol.childNodes]) { + for (const node of root[PropertySymbol.nodeArray]) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { @@ -65,16 +66,16 @@ export default class DOMParser { newDocument.appendChild(documentElement); const body = newDocument.body; if (body) { - while (root[PropertySymbol.childNodes].length) { - body.appendChild(root[PropertySymbol.childNodes][0]); + while (root[PropertySymbol.nodeArray].length) { + body.appendChild(root[PropertySymbol.nodeArray][0]); } } } else { switch (mimeType) { case 'image/svg+xml': { - while (root[PropertySymbol.childNodes].length) { - newDocument.appendChild(root[PropertySymbol.childNodes][0]); + while (root[PropertySymbol.nodeArray].length) { + newDocument.appendChild(root[PropertySymbol.nodeArray][0]); } } break; @@ -89,8 +90,8 @@ export default class DOMParser { documentElement.appendChild(bodyElement); newDocument.appendChild(documentElement); - while (root[PropertySymbol.childNodes].length) { - bodyElement.appendChild(root[PropertySymbol.childNodes][0]); + while (root[PropertySymbol.nodeArray].length) { + bodyElement.appendChild(root[PropertySymbol.nodeArray][0]); } } break; diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index ea9a43a6d..4de9c5b99 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -53,7 +53,7 @@ export default abstract class EventTarget { // Tracks the amount of capture event listeners to improve performance when they are not used. if (listenerOptions && listenerOptions.capture) { - const window = this.#getWindow(); + const window = this[PropertySymbol.getWindow](); if (window) { window[PropertySymbol.captureEventListenerCount][type] = window[PropertySymbol.captureEventListenerCount][type] ?? 0; @@ -80,7 +80,7 @@ export default abstract class EventTarget { this[PropertySymbol.listenerOptions][type][index] && this[PropertySymbol.listenerOptions][type][index].capture ) { - const window = this.#getWindow(); + const window = this[PropertySymbol.getWindow](); if (window && window[PropertySymbol.captureEventListenerCount][type]) { window[PropertySymbol.captureEventListenerCount][type]--; } @@ -101,7 +101,7 @@ export default abstract class EventTarget { * @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(). */ public dispatchEvent(event: Event): boolean { - const window = this.#getWindow(); + const window = this[PropertySymbol.getWindow](); if (event.eventPhase === EventPhaseEnum.none) { event[PropertySymbol.target] = this; @@ -277,7 +277,7 @@ export default abstract class EventTarget { * * @returns Window. */ - #getWindow(): BrowserWindow | null { + private [PropertySymbol.getWindow](): BrowserWindow | null { if (((this))[PropertySymbol.ownerDocument]) { return ((this))[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; } diff --git a/packages/happy-dom/src/form-data/FormData.ts b/packages/happy-dom/src/form-data/FormData.ts index 50414e6da..94cbe1537 100644 --- a/packages/happy-dom/src/form-data/FormData.ts +++ b/packages/happy-dom/src/form-data/FormData.ts @@ -3,9 +3,6 @@ import * as PropertySymbol from '../PropertySymbol.js'; import File from '../file/File.js'; import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; -import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; -import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; -import IRadioNodeList from '../nodes/html-form-element/IRadioNodeList.js'; type FormDataEntry = { name: string; @@ -32,36 +29,26 @@ export default class FormData implements Iterable<[string, string | File]> { return; } - for (let radioNodeList of (form[PropertySymbol.elements])[ - PropertySymbol.namedItems - ].values()) { + const items = form[PropertySymbol.getFormControlItems](); + + for (const item of items) { if ( - radioNodeList[0][PropertySymbol.tagName] === 'INPUT' && - (radioNodeList[0].type === 'checkbox' || radioNodeList[0].type === 'radio') + item.name && + SUBMITTABLE_ELEMENTS.includes(item[PropertySymbol.tagName]) && + (item[PropertySymbol.tagName] !== 'INPUT' || + (item.type !== 'checkbox' && item.type !== 'radio') || + (item).checked) ) { - const newRadioNodeList: IRadioNodeList = new RadioNodeList(); - for (const node of radioNodeList) { - if ((node).checked) { - newRadioNodeList[PropertySymbol.addItem](node); - break; - } - } - radioNodeList = newRadioNodeList; - } - - for (const node of radioNodeList) { - if (node.name && SUBMITTABLE_ELEMENTS.includes(node[PropertySymbol.tagName])) { - if (node[PropertySymbol.tagName] === 'INPUT' && node.type === 'file') { - if ((node)[PropertySymbol.files].length === 0) { - this.append(node.name, new File([], '', { type: 'application/octet-stream' })); - } else { - for (const file of (node)[PropertySymbol.files]) { - this.append(node.name, file); - } + if (item[PropertySymbol.tagName] === 'INPUT' && item.type === 'file') { + if ((item)[PropertySymbol.files].length === 0) { + this.append(item.name, new File([], '', { type: 'application/octet-stream' })); + } else { + for (const file of (item)[PropertySymbol.files]) { + this.append(item.name, file); } - } else if ((node).value) { - this.append(node.name, (node).value); } + } else if ((item).value) { + this.append(item.name, (item).value); } } } diff --git a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts index 81efd2989..a47bffbb3 100644 --- a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts +++ b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts @@ -39,7 +39,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes]; + ))[PropertySymbol.nodeArray]; while (newChildNodes.length) { parent.insertBefore(newChildNodes[0], childNode); } @@ -68,7 +68,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes]; + ))[PropertySymbol.nodeArray]; while (newChildNodes.length) { parent.insertBefore(newChildNodes[0], childNode); } @@ -97,7 +97,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode[PropertySymbol.ownerDocument], node) - ))[PropertySymbol.childNodes]; + ))[PropertySymbol.nodeArray]; while (newChildNodes.length) { if (!nextSibling) { parent.appendChild(newChildNodes[0]); diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index f149f5b09..17a22d8da 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -4,34 +4,28 @@ import Element from '../element/Element.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; import HTMLCollection from '../element/HTMLCollection.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; import IHTMLElementTagNameMap from '../../config/IHTMLElementTagNameMap.js'; import ISVGElementTagNameMap from '../../config/ISVGElementTagNameMap.js'; -import INodeList from '../node/INodeList.js'; +import NodeList from '../node/NodeList.js'; /** * DocumentFragment. */ export default class DocumentFragment extends Node { - public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); + public [PropertySymbol.children]: HTMLCollection | null = null; public [PropertySymbol.rootNode]: Node = this; public [PropertySymbol.nodeType] = NodeTypeEnum.documentFragmentNode; public declare cloneNode: (deep?: boolean) => DocumentFragment; - /** - * Constructor. - */ - constructor() { - super(); - - this[PropertySymbol.children][PropertySymbol.observe](this); - } - /** * Returns the document fragment children. */ - public get children(): IHTMLCollection { + public get children(): HTMLCollection { + if (!this[PropertySymbol.children]) { + const elements = this[PropertySymbol.elementArray]; + this[PropertySymbol.children] = new HTMLCollection(() => elements); + } return this[PropertySymbol.children]; } @@ -41,7 +35,7 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get childElementCount(): number { - return this[PropertySymbol.children].length; + return this[PropertySymbol.elementArray].length; } /** @@ -50,7 +44,7 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get firstElementChild(): Element { - return this[PropertySymbol.children][0] ?? null; + return this[PropertySymbol.elementArray][0] ?? null; } /** @@ -59,7 +53,7 @@ export default class DocumentFragment extends Node { * @returns Element. */ public get lastElementChild(): Element { - const children = this[PropertySymbol.children]; + const children = this[PropertySymbol.elementArray]; return children[children.length - 1] ?? null; } @@ -70,7 +64,7 @@ export default class DocumentFragment extends Node { */ public get textContent(): string { let result = ''; - for (const childNode of this[PropertySymbol.childNodes]) { + for (const childNode of this[PropertySymbol.nodeArray]) { if ( childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode || childNode[PropertySymbol.nodeType] === NodeTypeEnum.textNode @@ -87,7 +81,7 @@ export default class DocumentFragment extends Node { * @param textContent Text content. */ public set textContent(textContent: string) { - const childNodes = this[PropertySymbol.childNodes]; + const childNodes = this[PropertySymbol.nodeArray]; while (childNodes.length) { this.removeChild(childNodes[0]); } @@ -131,7 +125,7 @@ export default class DocumentFragment extends Node { */ public querySelectorAll( selector: K - ): INodeList; + ): NodeList; /** * Query CSS selector to find matching elments. @@ -141,7 +135,7 @@ export default class DocumentFragment extends Node { */ public querySelectorAll( selector: K - ): INodeList; + ): NodeList; /** * Query CSS selector to find matching elments. @@ -149,7 +143,7 @@ export default class DocumentFragment extends Node { * @param selector CSS selector. * @returns Matching elements. */ - public querySelectorAll(selector: string): INodeList; + public querySelectorAll(selector: string): NodeList; /** * Query CSS selector to find matching elments. @@ -157,7 +151,7 @@ export default class DocumentFragment extends Node { * @param selector CSS selector. * @returns Matching elements. */ - public querySelectorAll(selector: string): INodeList { + public querySelectorAll(selector: string): NodeList { return QuerySelector.querySelectorAll(this, selector); } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 488a114d0..88e559eb3 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -20,9 +20,7 @@ import HTMLElement from '../html-element/HTMLElement.js'; import Comment from '../comment/Comment.js'; import Text from '../text/Text.js'; import NodeList from '../node/NodeList.js'; -import INodeList from '../node/INodeList.js'; import HTMLCollection from '../element/HTMLCollection.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; import HTMLLinkElement from '../html-link-element/HTMLLinkElement.js'; import HTMLStyleElement from '../html-style-element/HTMLStyleElement.js'; import DocumentReadyStateEnum from './DocumentReadyStateEnum.js'; @@ -44,10 +42,11 @@ import SVGElement from '../svg-element/SVGElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLAnchorElement from '../html-anchor-element/HTMLAnchorElement.js'; import HTMLElementConfig from '../../config/HTMLElementConfig.js'; -import CSSStyleDeclarationPropertyManager from '../../css/declaration/property-manager/CSSStyleDeclarationPropertyManager.js'; import HTMLHtmlElement from '../html-html-element/HTMLHtmlElement.js'; import HTMLBodyElement from '../html-body-element/HTMLBodyElement.js'; import HTMLHeadElement from '../html-head-element/HTMLHeadElement.js'; +import HTMLBaseElement from '../html-base-element/HTMLBaseElement.js'; +import ICachedResult from '../node/ICachedResult.js'; const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; @@ -56,7 +55,7 @@ const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; */ export default class Document extends Node { // Internal properties - public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); + public [PropertySymbol.children]: HTMLCollection | null = null; public [PropertySymbol.activeElement]: HTMLElement | SVGElement = null; public [PropertySymbol.nextActiveElement]: HTMLElement | SVGElement = null; public [PropertySymbol.currentScript]: HTMLScriptElement = null; @@ -71,9 +70,8 @@ export default class Document extends Node { public [PropertySymbol.referrer] = ''; public [PropertySymbol.defaultView]: BrowserWindow | null = null; public [PropertySymbol.ownerWindow]: BrowserWindow; - public [PropertySymbol.computedStyleCacheReferences]: Array<{ - result: WeakRef; - }> = []; + public [PropertySymbol.forms]: HTMLCollection | null = null; + public [PropertySymbol.affectsComputedStyleCache]: ICachedResult[] = []; public declare cloneNode: (deep?: boolean) => Document; // Private properties @@ -202,8 +200,6 @@ export default class Document extends Node { super(); this.#browserFrame = injected.browserFrame; this[PropertySymbol.ownerWindow] = injected.window; - - this[PropertySymbol.children][PropertySymbol.observe](this); } /** @@ -263,7 +259,11 @@ export default class Document extends Node { /** * Returns document children. */ - public get children(): IHTMLCollection { + public get children(): HTMLCollection { + if (!this[PropertySymbol.children]) { + const elements = this[PropertySymbol.elementArray]; + this[PropertySymbol.children] = new HTMLCollection(() => elements); + } return this[PropertySymbol.children]; } @@ -283,7 +283,10 @@ export default class Document extends Node { * @returns Character set. */ public get characterSet(): string { - const charset = this.querySelector('meta[charset]')?.getAttributeNS(null, 'charset'); + const charset = QuerySelector.querySelector(this, 'meta[charset]')?.getAttributeNS( + null, + 'charset' + ); return charset ? charset : 'UTF-8'; } @@ -293,7 +296,7 @@ export default class Document extends Node { * @returns Title. */ public get title(): string { - const element = QuerySelector.querySelector(this, 'title'); + const element = ParentNodeUtility.getElementById(this, 'title'); if (element) { return element.textContent; } @@ -305,7 +308,7 @@ export default class Document extends Node { * */ public set title(title: string) { - const element = QuerySelector.querySelector(this, 'title'); + const element = ParentNodeUtility.getElementById(this, 'title'); if (element) { element.textContent = title; } else { @@ -318,15 +321,20 @@ export default class Document extends Node { /** * Returns a collection of all area elements and a elements in a document with a value for the href attribute. */ - public get links(): INodeList { - return >this.querySelectorAll('a[href],area[href]'); + public get links(): NodeList { + return >QuerySelector.querySelectorAll(this, 'a[href],area[href]'); } /** * Returns a collection of all form elements in a document. */ public get forms(): HTMLCollection { - return this.getElementsByTagName('form'); + if (!this[PropertySymbol.forms]) { + this[PropertySymbol.forms] = >( + ParentNodeUtility.getElementsByTagName(this, 'form') + ); + } + return this[PropertySymbol.forms]; } /** @@ -335,7 +343,7 @@ export default class Document extends Node { * @returns Element. */ public get childElementCount(): number { - return this[PropertySymbol.children].length; + return this[PropertySymbol.elementArray].length; } /** @@ -344,7 +352,7 @@ export default class Document extends Node { * @returns Element. */ public get firstElementChild(): Element { - return this[PropertySymbol.children][0] ?? null; + return this[PropertySymbol.elementArray][0] ?? null; } /** @@ -353,7 +361,7 @@ export default class Document extends Node { * @returns Element. */ public get lastElementChild(): Element { - const children = this[PropertySymbol.children]; + const children = this[PropertySymbol.elementArray]; return children[children.length - 1] ?? null; } @@ -400,7 +408,7 @@ export default class Document extends Node { * @returns Element. */ public get documentElement(): HTMLHtmlElement { - return QuerySelector.querySelector(this, 'html'); + return ParentNodeUtility.getElementByTagName(this, 'html'); } /** @@ -409,7 +417,7 @@ export default class Document extends Node { * @returns Document type. */ public get doctype(): DocumentType { - for (const node of this[PropertySymbol.childNodes]) { + for (const node of this[PropertySymbol.nodeArray]) { if (node instanceof DocumentType) { return node; } @@ -423,7 +431,7 @@ export default class Document extends Node { * @returns Element. */ public get body(): HTMLBodyElement { - return QuerySelector.querySelector(this, 'body'); + return ParentNodeUtility.getElementByTagName(this, 'body'); } /** @@ -432,7 +440,7 @@ export default class Document extends Node { * @returns Element. */ public get head(): HTMLHeadElement { - return QuerySelector.querySelector(this, 'head'); + return ParentNodeUtility.getElementByTagName(this, 'head'); } /** @@ -442,7 +450,7 @@ export default class Document extends Node { */ public get styleSheets(): CSSStyleSheet[] { const styles = >( - this.querySelectorAll('link[rel="stylesheet"][href],style') + QuerySelector.querySelectorAll(this, 'link[rel="stylesheet"][href],style') ); const styleSheets = []; for (const style of styles) { @@ -518,7 +526,7 @@ export default class Document extends Node { * @returns Base URI. */ public get baseURI(): string { - const element = QuerySelector.querySelector(this, 'base'); + const element = ParentNodeUtility.getElementByTagName(this, 'base'); if (element) { return element.href; } @@ -701,7 +709,7 @@ export default class Document extends Node { * @param className Tag name. * @returns Matching element. */ - public getElementsByClassName(className: string): IHTMLCollection { + public getElementsByClassName(className: string): HTMLCollection { return ParentNodeUtility.getElementsByClassName(this, className); } @@ -731,7 +739,7 @@ export default class Document extends Node { * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagName(tagName: string): IHTMLCollection; + public getElementsByTagName(tagName: string): HTMLCollection; /** * Returns an elements by tag name. @@ -739,7 +747,7 @@ export default class Document extends Node { * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagName(tagName: string): IHTMLCollection { + public getElementsByTagName(tagName: string): HTMLCollection { return ParentNodeUtility.getElementsByTagName(this, tagName); } @@ -753,7 +761,7 @@ export default class Document extends Node { public getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/1999/xhtml', tagName: K - ): IHTMLCollection; + ): HTMLCollection; /** * Returns an elements by tag name and namespace. @@ -765,7 +773,7 @@ export default class Document extends Node { public getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/2000/svg', tagName: K - ): IHTMLCollection; + ): HTMLCollection; /** * Returns an elements by tag name and namespace. @@ -774,7 +782,7 @@ export default class Document extends Node { * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection; + public getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection; /** * Returns an elements by tag name and namespace. @@ -783,7 +791,7 @@ export default class Document extends Node { * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection { + public getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection { return ParentNodeUtility.getElementsByTagNameNS(this, namespaceURI, tagName); } @@ -804,22 +812,7 @@ export default class Document extends Node { * @param name */ public getElementsByName(name: string): NodeList { - const getElementsByName = ( - parentNode: Element | DocumentFragment | Document, - name: string - ): NodeList => { - const matches = new NodeList(); - for (const child of (parentNode)[PropertySymbol.children]) { - if (child.getAttributeNS(null, 'name') === name) { - matches[PropertySymbol.addItem](child); - } - for (const match of getElementsByName(child, name)) { - matches[PropertySymbol.addItem](match); - } - } - return matches; - }; - return getElementsByName(this, name); + return QuerySelector.querySelectorAll(this, `[name="${name}"]`); } /** @@ -843,7 +836,7 @@ export default class Document extends Node { let documentElement = null; let documentTypeNode = null; - for (const node of root[PropertySymbol.childNodes]) { + for (const node of root[PropertySymbol.nodeArray]) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { @@ -863,8 +856,8 @@ export default class Document extends Node { this.appendChild(documentElement); - const head = QuerySelector.querySelector(this, 'head'); - let body = QuerySelector.querySelector(this, 'body'); + const head = ParentNodeUtility.getElementByTagName(this, 'head'); + let body = ParentNodeUtility.getElementByTagName(this, 'body'); if (!body) { body = this.createElement('body'); @@ -875,10 +868,10 @@ export default class Document extends Node { documentElement.insertBefore(this.createElement('head'), body); } } else { - const rootBody = QuerySelector.querySelector(root, 'body'); - const body = QuerySelector.querySelector(this, 'body'); + const rootBody = ParentNodeUtility.getElementByTagName(root, 'body'); + const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (rootBody && body) { - const childNodes = rootBody[PropertySymbol.childNodes]; + const childNodes = rootBody[PropertySymbol.nodeArray]; while (childNodes.length) { body.appendChild(childNodes[0]); } @@ -886,9 +879,9 @@ export default class Document extends Node { } // Remaining nodes outside the element are added to the element. - const body = QuerySelector.querySelector(this, 'body'); + const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (body) { - const childNodes = root[PropertySymbol.childNodes]; + const childNodes = root[PropertySymbol.nodeArray]; while (childNodes.length) { const child = childNodes[0]; if ( @@ -903,7 +896,7 @@ export default class Document extends Node { const documentElement = this.createElement('html'); const bodyElement = this.createElement('body'); const headElement = this.createElement('head'); - const childNodes = root[PropertySymbol.childNodes]; + const childNodes = root[PropertySymbol.nodeArray]; while (childNodes.length) { bodyElement.appendChild(childNodes[0]); @@ -915,9 +908,9 @@ export default class Document extends Node { this.appendChild(documentElement); } } else { - const bodyNode = QuerySelector.querySelector(root, 'body'); - const body = QuerySelector.querySelector(this, 'body'); - const childNodes = ((bodyNode || root))[PropertySymbol.childNodes]; + const bodyNode = ParentNodeUtility.getElementByTagName(root, 'body'); + const body = ParentNodeUtility.getElementByTagName(this, 'body'); + const childNodes = ((bodyNode || root))[PropertySymbol.nodeArray]; while (childNodes.length) { body.appendChild(childNodes[0]); } @@ -941,7 +934,7 @@ export default class Document extends Node { } } - const childNodes = this[PropertySymbol.childNodes]; + const childNodes = this[PropertySymbol.nodeArray]; while (childNodes.length) { this.removeChild(childNodes[0]); } @@ -1336,16 +1329,6 @@ export default class Document extends Node { return null; } - /** - * Clears computed style cache. - */ - public [PropertySymbol.clearComputedStyleCache](): void { - for (const item of this[PropertySymbol.computedStyleCacheReferences]) { - item.result = null; - } - this[PropertySymbol.computedStyleCacheReferences] = []; - } - /** * Imports a node. * @@ -1354,7 +1337,7 @@ export default class Document extends Node { #importNode(node: Node): void { node[PropertySymbol.ownerDocument] = this; - for (const child of node[PropertySymbol.childNodes]) { + for (const child of node[PropertySymbol.nodeArray]) { this.#importNode(child); } } diff --git a/packages/happy-dom/src/nodes/element/DOMStringMap.ts b/packages/happy-dom/src/nodes/element/DOMStringMap.ts new file mode 100644 index 000000000..26918db8b --- /dev/null +++ b/packages/happy-dom/src/nodes/element/DOMStringMap.ts @@ -0,0 +1,91 @@ +import Element from './Element.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; +import DOMStringMapUtility from './DOMStringMapUtility.js'; + +/** + * Dataset factory. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset + */ +export default class DOMStringMap { + [key: string]: string; + + /** + * Constructor. + * + * @param element Element. + */ + constructor(element: Element) { + // Documentation for Proxy: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy + return new Proxy(this, { + get(_target, property: string): string { + const attribute = element[PropertySymbol.attributes].getNamedItem( + 'data-' + DOMStringMapUtility.camelCaseToKebab(property) + ); + if (attribute) { + return attribute[PropertySymbol.value]; + } + }, + set(_target, property: string, value: string): boolean { + element.setAttribute('data-' + DOMStringMapUtility.camelCaseToKebab(property), value); + return true; + }, + deleteProperty(_target, property: string): boolean { + const attributes = element[PropertySymbol.attributes]; + const dataKey = 'data-' + DOMStringMapUtility.camelCaseToKebab(property); + const item = attributes.getNamedItem(dataKey); + if (!item) { + return true; + } + attributes[PropertySymbol.removeNamedItem](item); + return true; + }, + ownKeys(_target): string[] { + // According to Mozilla we have to update the dataset object (target) to contain the same keys as what we return: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys + // "The result List must contain the keys of all non-configurable own properties of the target object." + const keys = []; + const attributes = element[PropertySymbol.attributes]; + for (let i = 0, max = attributes.length; i < max; i++) { + const name = attributes[i][PropertySymbol.name]; + if (name.startsWith('data-')) { + keys.push(DOMStringMapUtility.kebabToCamelCase(name.replace('data-', ''))); + } + } + return keys; + }, + has(_target, property: string): boolean { + return !!element[PropertySymbol.attributes].getNamedItem( + 'data-' + DOMStringMapUtility.camelCaseToKebab(property) + ); + }, + defineProperty(_target, property: string, descriptor): boolean { + if (descriptor.value === undefined) { + return false; + } + + element.setAttribute( + 'data-' + DOMStringMapUtility.camelCaseToKebab(property), + descriptor.value + ); + + return true; + }, + getOwnPropertyDescriptor(_target, property: string): PropertyDescriptor { + const attribute = element[PropertySymbol.attributes].getNamedItem( + 'data-' + DOMStringMapUtility.camelCaseToKebab(property) + ); + if (attribute) { + return { + value: attribute[PropertySymbol.value], + writable: true, + enumerable: true, + configurable: true + }; + } + } + }); + } +} diff --git a/packages/happy-dom/src/nodes/element/DatasetUtility.ts b/packages/happy-dom/src/nodes/element/DOMStringMapUtility.ts similarity index 92% rename from packages/happy-dom/src/nodes/element/DatasetUtility.ts rename to packages/happy-dom/src/nodes/element/DOMStringMapUtility.ts index 6025046d7..55d1f1290 100644 --- a/packages/happy-dom/src/nodes/element/DatasetUtility.ts +++ b/packages/happy-dom/src/nodes/element/DOMStringMapUtility.ts @@ -1,10 +1,10 @@ /** - * Dataset utility. + * DOMStringMap utility. * * Reference: * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset */ -export default class DatasetUtility { +export default class DOMStringMapUtility { /** * Transforms a kebab cased string to camel case. * diff --git a/packages/happy-dom/src/nodes/element/DatasetFactory.ts b/packages/happy-dom/src/nodes/element/DatasetFactory.ts deleted file mode 100644 index 0998fee94..000000000 --- a/packages/happy-dom/src/nodes/element/DatasetFactory.ts +++ /dev/null @@ -1,90 +0,0 @@ -import Element from './Element.js'; -import * as PropertySymbol from '../../PropertySymbol.js'; -import DatasetUtility from './DatasetUtility.js'; -import IDataset from './IDataset.js'; - -/** - * Dataset factory. - * - * Reference: - * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset - */ -export default class DatasetFactory { - /** - * @param element The parent element. - */ - public static createDataset(element: Element): IDataset { - // Build the initial dataset record from all data attributes. - const dataset: IDataset = {}; - - for (let i = 0, max = element[PropertySymbol.attributes].length; i < max; i++) { - const attribute = element[PropertySymbol.attributes][i]; - if (attribute[PropertySymbol.name].startsWith('data-')) { - const key = DatasetUtility.kebabToCamelCase( - attribute[PropertySymbol.name].replace('data-', '') - ); - dataset[key] = attribute[PropertySymbol.value]; - } - } - - // Documentation for Proxy: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy - return new Proxy(dataset, { - get(dataset: IDataset, key: string): string { - const attribute = element[PropertySymbol.attributes].getNamedItem( - 'data-' + DatasetUtility.camelCaseToKebab(key) - ); - if (attribute) { - return (dataset[key] = attribute[PropertySymbol.value]); - } - delete dataset[key]; - return undefined; - }, - set(dataset: IDataset, key: string, value: string): boolean { - element.setAttribute('data-' + DatasetUtility.camelCaseToKebab(key), value); - dataset[key] = value; - return true; - }, - deleteProperty(dataset: IDataset, key: string): boolean { - const attributes = element[PropertySymbol.attributes]; - const dataKey = 'data-' + DatasetUtility.camelCaseToKebab(key); - const item = attributes.getNamedItem(dataKey); - delete dataset[key]; - if (!item) { - return true; - } - attributes[PropertySymbol.removeNamedItem](item); - return true; - }, - ownKeys(dataset: IDataset): string[] { - // According to Mozilla we have to update the dataset object (target) to contain the same keys as what we return: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys - // "The result List must contain the keys of all non-configurable own properties of the target object." - const keys = []; - const deleteKeys = []; - for (let i = 0, max = element[PropertySymbol.attributes].length; i < max; i++) { - const attribute = element[PropertySymbol.attributes][i]; - if (attribute[PropertySymbol.name].startsWith('data-')) { - const key = DatasetUtility.kebabToCamelCase( - attribute[PropertySymbol.name].replace('data-', '') - ); - keys.push(key); - dataset[key] = attribute[PropertySymbol.value]; - if (!dataset[key]) { - deleteKeys.push(key); - } - } - } - for (const key of deleteKeys) { - delete dataset[key]; - } - return keys; - }, - has(_dataset: IDataset, key: string): boolean { - return !!element[PropertySymbol.attributes].getNamedItem( - 'data-' + DatasetUtility.camelCaseToKebab(key) - ); - } - }); - } -} diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index b619ff1e5..c4f033a86 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -11,7 +11,6 @@ import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; import NonDocumentChildNodeUtility from '../child-node/NonDocumentChildNodeUtility.js'; import DOMException from '../../exception/DOMException.js'; import HTMLCollection from './HTMLCollection.js'; -import IHTMLCollection from './IHTMLCollection.js'; import Text from '../text/Text.js'; import DOMRectList from './DOMRectList.js'; import Attr from '../attr/Attr.js'; @@ -31,10 +30,10 @@ import INonDocumentTypeChildNode from '../child-node/INonDocumentTypeChildNode.j import IParentNode from '../parent-node/IParentNode.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; -import INodeList from '../node/INodeList.js'; -import CSSStyleDeclarationPropertyManager from '../../css/declaration/property-manager/CSSStyleDeclarationPropertyManager.js'; import NamedNodeMapProxyFactory from './NamedNodeMapProxyFactory.js'; import NamespaceURI from '../../config/NamespaceURI.js'; +import NodeList from '../node/NodeList.js'; +import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; type InsertAdjacentPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'; @@ -107,24 +106,8 @@ export default class Element public [PropertySymbol.attributesProxy]: NamedNodeMap | null = null; public [PropertySymbol.namespaceURI]: string | null = this.constructor[PropertySymbol.namespaceURI] || null; - public [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); - public [PropertySymbol.styleCache]: { - result: WeakRef | null; - } | null = null; - public [PropertySymbol.computedStyleCache]: { - result: WeakRef | null; - } | null = null; - - /** - * Constructor. - */ - constructor() { - super(); - const attributes = this[PropertySymbol.attributes]; - attributes[PropertySymbol.addEventListener]('set', this.#onSetAttribute.bind(this)); - attributes[PropertySymbol.addEventListener]('remove', this.#onRemoveAttribute.bind(this)); - this[PropertySymbol.children][PropertySymbol.observe](this); - } + public [PropertySymbol.children]: HTMLCollection | null = null; + public [PropertySymbol.computedStyle]: CSSStyleDeclaration | null = null; /** * Returns tag name. @@ -234,7 +217,11 @@ export default class Element /** * Returns element children. */ - public get children(): IHTMLCollection { + public get children(): HTMLCollection { + if (!this[PropertySymbol.children]) { + const elements = this[PropertySymbol.elementArray]; + this[PropertySymbol.children] = new HTMLCollection(() => elements); + } return this[PropertySymbol.children]; } @@ -347,7 +334,7 @@ export default class Element */ public get textContent(): string { let result = ''; - for (const childNode of this[PropertySymbol.childNodes]) { + for (const childNode of this[PropertySymbol.nodeArray]) { if ( childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode || childNode[PropertySymbol.nodeType] === NodeTypeEnum.textNode @@ -364,7 +351,7 @@ export default class Element * @param textContent Text content. */ public set textContent(textContent: string) { - const childNodes = this[PropertySymbol.childNodes]; + const childNodes = this[PropertySymbol.nodeArray]; while (childNodes.length) { this.removeChild(childNodes[0]); } @@ -388,7 +375,7 @@ export default class Element * @param html HTML. */ public set innerHTML(html: string) { - const childNodes = this[PropertySymbol.childNodes]; + const childNodes = this[PropertySymbol.nodeArray]; while (childNodes.length) { this.removeChild(childNodes[0]); @@ -421,7 +408,7 @@ export default class Element * @returns Element. */ public get childElementCount(): number { - return this[PropertySymbol.children].length; + return this[PropertySymbol.elementArray].length; } /** @@ -430,7 +417,7 @@ export default class Element * @returns Element. */ public get firstElementChild(): Element { - return this[PropertySymbol.children][0] ?? null; + return this[PropertySymbol.elementArray][0] ?? null; } /** @@ -439,7 +426,7 @@ export default class Element * @returns Element. */ public get lastElementChild(): Element { - const children = this[PropertySymbol.children]; + const children = this[PropertySymbol.elementArray]; return children[children.length - 1] ?? null; } @@ -487,7 +474,7 @@ export default class Element escapeEntities: false }); let xml = ''; - for (const node of this[PropertySymbol.childNodes]) { + for (const node of this[PropertySymbol.nodeArray]) { xml += xmlSerializer.serializeToString(node); } return xml; @@ -618,7 +605,7 @@ export default class Element public insertAdjacentHTML(position: InsertAdjacentPosition, text: string): void { const childNodes = (( XMLParser.parse(this[PropertySymbol.ownerDocument], text) - ))[PropertySymbol.childNodes]; + ))[PropertySymbol.nodeArray]; while (childNodes.length) { this.insertAdjacentElement(position, childNodes[0]); } @@ -881,7 +868,7 @@ export default class Element */ public querySelectorAll( selector: K - ): INodeList; + ): NodeList; /** * Query CSS selector to find matching elments. @@ -891,7 +878,7 @@ export default class Element */ public querySelectorAll( selector: K - ): INodeList; + ): NodeList; /** * Query CSS selector to find matching elments. @@ -899,7 +886,7 @@ export default class Element * @param selector CSS selector. * @returns Matching elements. */ - public querySelectorAll(selector: string): INodeList; + public querySelectorAll(selector: string): NodeList; /** * Query CSS selector to find matching elments. @@ -907,7 +894,7 @@ export default class Element * @param selector CSS selector. * @returns Matching elements. */ - public querySelectorAll(selector: string): INodeList { + public querySelectorAll(selector: string): NodeList { return QuerySelector.querySelectorAll(this, selector); } @@ -955,7 +942,7 @@ export default class Element * @param className Tag name. * @returns Matching element. */ - public getElementsByClassName(className: string): IHTMLCollection { + public getElementsByClassName(className: string): HTMLCollection { return ParentNodeUtility.getElementsByClassName(this, className); } @@ -967,7 +954,7 @@ export default class Element */ public getElementsByTagName( tagName: K - ): IHTMLCollection; + ): HTMLCollection; /** * Returns an elements by tag name. @@ -977,7 +964,7 @@ export default class Element */ public getElementsByTagName( tagName: K - ): IHTMLCollection; + ): HTMLCollection; /** * Returns an elements by tag name. @@ -985,7 +972,7 @@ export default class Element * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagName(tagName: string): IHTMLCollection; + public getElementsByTagName(tagName: string): HTMLCollection; /** * Returns an elements by tag name. @@ -993,7 +980,7 @@ export default class Element * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagName(tagName: string): IHTMLCollection { + public getElementsByTagName(tagName: string): HTMLCollection { return ParentNodeUtility.getElementsByTagName(this, tagName); } @@ -1007,7 +994,7 @@ export default class Element public getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/1999/xhtml', tagName: K - ): IHTMLCollection; + ): HTMLCollection; /** * Returns an elements by tag name and namespace. @@ -1019,7 +1006,7 @@ export default class Element public getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/2000/svg', tagName: K - ): IHTMLCollection; + ): HTMLCollection; /** * Returns an elements by tag name and namespace. @@ -1028,7 +1015,7 @@ export default class Element * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection; + public getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection; /** * Returns an elements by tag name and namespace. @@ -1037,7 +1024,7 @@ export default class Element * @param tagName Tag name. * @returns Matching element. */ - public getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection { + public getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection { return ParentNodeUtility.getElementsByTagNameNS(this, namespaceURI, tagName); } @@ -1204,7 +1191,7 @@ export default class Element */ public [PropertySymbol.appendChild](node: Node): Node { const returnValue = super[PropertySymbol.appendChild](node); - this.#onNodeListChange(node); + this[PropertySymbol.onNodeListChange](node); return returnValue; } @@ -1216,7 +1203,7 @@ export default class Element */ public [PropertySymbol.removeChild](node: Node): Node { const returnValue = super[PropertySymbol.removeChild](node); - this.#onNodeListChange(node); + this[PropertySymbol.onNodeListChange](node); return returnValue; } @@ -1229,7 +1216,7 @@ export default class Element */ public [PropertySymbol.insertBefore](newNode: Node, referenceNode: Node | null): Node { const returnValue = super[PropertySymbol.insertBefore](newNode, referenceNode); - this.#onNodeListChange(newNode); + this[PropertySymbol.onNodeListChange](newNode); return returnValue; } @@ -1239,7 +1226,7 @@ export default class Element * @param attribute Attribute. * @param replacedAttribute Replaced attribute. */ - #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + public [PropertySymbol.onSetAttribute](attribute: Attr, replacedAttribute: Attr | null): void { if (!attribute[PropertySymbol.name]) { return null; } @@ -1272,9 +1259,9 @@ export default class Element } } - if (this[PropertySymbol.styleCache]) { - this[PropertySymbol.styleCache].result = null; - this[PropertySymbol.styleCache] = null; + if (this[PropertySymbol.cache].style) { + this[PropertySymbol.cache].style.result = null; + this[PropertySymbol.cache].style = null; } if ( @@ -1306,7 +1293,7 @@ export default class Element * * @param removedAttribute Attribute. */ - #onRemoveAttribute(removedAttribute: Attr): void { + public [PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void { if (removedAttribute[PropertySymbol.name] === 'class' && this[PropertySymbol.classList]) { this[PropertySymbol.classList][PropertySymbol.updateIndices](); } @@ -1323,9 +1310,9 @@ export default class Element } } - if (this[PropertySymbol.styleCache]) { - this[PropertySymbol.styleCache].result = null; - this[PropertySymbol.styleCache] = null; + if (this[PropertySymbol.cache].style) { + this[PropertySymbol.cache].style.result = null; + this[PropertySymbol.cache].style = null; } if ( @@ -1357,7 +1344,7 @@ export default class Element * * @param node Changed node. */ - #onNodeListChange(node: Node): void { + private [PropertySymbol.onNodeListChange](node: Node): void { if (this[PropertySymbol.shadowRoot]) { if (node['slot']) { const slot = this[PropertySymbol.shadowRoot].querySelector(`slot[name="${node['slot']}"]`); diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index 36dd173e7..6d572a32d 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -1,20 +1,5 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import MutationRecord from '../../mutation-observer/MutationRecord.js'; -import NodeFilter from '../../tree-walker/NodeFilter.js'; -import TreeWalker from '../../tree-walker/TreeWalker.js'; -import Attr from '../attr/Attr.js'; -import DocumentFragment from '../document-fragment/DocumentFragment.js'; -import Document from '../document/Document.js'; -import INodeList from '../node/INodeList.js'; -import Node from '../node/Node.js'; -import NodeList from '../node/NodeList.js'; -import NodeTypeEnum from '../node/NodeTypeEnum.js'; import Element from './Element.js'; -import IHTMLCollection from './IHTMLCollection.js'; -import IHTMLCollectionObservedNode from './IHTMLCollectionObservedNode.js'; -import TNamedNodeMapListener from './TNamedNodeMapListener.js'; - -const NAMED_ITEM_ATTRIBUTES = ['id', 'name']; /** * HTMLCollection. @@ -24,697 +9,195 @@ const NAMED_ITEM_ATTRIBUTES = ['id', 'name']; * * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection */ -export default class HTMLCollection - extends Array - implements IHTMLCollection -{ - public [PropertySymbol.namedItems] = new Map>(); - #observedNodes: IHTMLCollectionObservedNode[] = []; - #attributeListeners = new Map(); +export default class HTMLCollection { + [index: number]: T; + protected [PropertySymbol.query]: () => T[]; /** * Constructor. * - * @param items Items. - */ - constructor(items?: T[]) { - super(); - if (items && items instanceof Array) { - for (const item of items) { - this[PropertySymbol.addItem](item); - } - } - } - - /** - * Returns `Symbol.toStringTag`. - * - * @returns `Symbol.toStringTag`. + * @param query Query function. */ - public get [Symbol.toStringTag](): string { - return this.constructor.name; - } - - /** - * Returns `[object HTMLCollection]`. - * - * @returns `[object HTMLCollection]`. - */ - public toLocaleString(): string { - return `[object ${this.constructor.name}]`; - } - - /** - * Returns `[object HTMLCollection]`. - * - * @returns `[object HTMLCollection]`. - */ - public toString(): string { - return `[object ${this.constructor.name}]`; - } - - /** - * Returns item by index. - * - * @param index Index. - */ - public item(index: number): T { - return index >= 0 && this[index] ? this[index] : null; - } - - /** - * Returns named item. - * - * @param name Name. - * @returns Node. - */ - public namedItem(name: string): NamedItem | null { - return (this[PropertySymbol.namedItems].get(name)?.[0]) ?? null; - } - - /** - * Appends item. - * - * @param item Item. - * @returns True if the item was added. - */ - public [PropertySymbol.addItem](item: T): boolean { - if (item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || super.includes(item)) { - return false; - } - - super.push(item); - - this.#addNamedItem(item); - - return true; - } - - /** - * Inserts item before another item. - * - * @param newItem New item. - * @param [referenceItem] Reference item. - * @returns True if the item was added. - */ - public [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean { - if (!referenceItem) { - return this[PropertySymbol.addItem](newItem); - } - - if (newItem[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || super.includes(newItem)) { - return false; - } - - const referenceItemIndex = this[PropertySymbol.indexOf](referenceItem); - - if (referenceItemIndex === -1) { - throw new Error( - 'Failed to execute "insertItem" on "HTMLCollection": The node before which the new node is to be inserted is not an item of this collection.' - ); - } - - super.splice(referenceItemIndex, 0, newItem); - - this.#addNamedItem(newItem); + constructor(query: () => T[]) { + this[PropertySymbol.query] = query; - return true; - } - - /** - * Removes item. - * - * @param item Item. - * @returns True if removed. - */ - public [PropertySymbol.removeItem](item: T): boolean { - const index = super.indexOf(item); - - if (index === -1) { - return false; - } - - super.splice(index, 1); - - this.#removeNamedItem(item); - - return true; - } - - /** - * Destroys the collection. - */ - public [PropertySymbol.destroy](): void { - const observedNodes = this.#observedNodes; - - while (observedNodes.length) { - this[PropertySymbol.unobserve](observedNodes[observedNodes.length - 1]); - } - - while (this.length) { - this[PropertySymbol.removeItem](this[this.length - 1]); - } - } - - /** - * Observes node. - * - * @param node Root node. - * @param [options] Options. - * @param [options.subtree] Subtree. - * @param [options.filter] Filter. - * @returns Observed node. - */ - public [PropertySymbol.observe]( - node: Element | DocumentFragment | Document, - options?: { subtree: boolean; filter?: (item: T) => boolean } - ): IHTMLCollectionObservedNode { - const observedNode: IHTMLCollectionObservedNode = { - node, - filter: options?.filter ?? null, - subtree: options?.subtree ?? false, - mutationListener: null - }; - - this.#observedNodes.push(observedNode); - - if (observedNode.subtree) { - observedNode.mutationListener = { - options: { - childList: true, - subtree: true - }, - callback: new WeakRef((record: MutationRecord) => { - if (record.addedNodes.length) { - this[PropertySymbol.insertObservedItem](observedNode, record.addedNodes[0]); - } else { - this[PropertySymbol.removeObservedItem](observedNode, record.removedNodes[0]); + return new Proxy(this, { + get: (target, property, reciever) => { + if (property in target || typeof property === 'symbol') { + return Reflect.get(target, property, reciever); + } + const index = Number(property); + if (!isNaN(index)) { + return query()[index]; + } + return target.namedItem(property) || undefined; + }, + set(): boolean { + return true; + }, + deleteProperty(): boolean { + return true; + }, + ownKeys(): string[] { + const keys: string[] = []; + const items = query(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const name = + item.attributes['id']?.[PropertySymbol.value] || + item.attributes['name']?.[PropertySymbol.value]; + keys.push(String(i)); + + if (name) { + keys.push(name); } - }) - }; - - node[PropertySymbol.observeMutations](observedNode.mutationListener); - } else { - node[PropertySymbol.childNodes][PropertySymbol.htmlCollections].push({ - htmlCollection: this, - filter: observedNode.filter - }); - } - - this[PropertySymbol.loadObservedNodes](observedNode, node); - - return observedNode; - } - - /** - * Unobserves node. - * - * @param observedNode Observed node. - */ - public [PropertySymbol.unobserve](observedNode: IHTMLCollectionObservedNode): void { - const index = this.#observedNodes.indexOf(observedNode); - - if (index === -1) { - return; - } - - this.#observedNodes.splice(index, 1); + } + return keys; + }, + has(target, property): boolean { + if (property in target) { + return true; + } - this[PropertySymbol.unloadObservedNodes](observedNode, observedNode.node); + const items = query(); + const index = Number(property); - if (observedNode.subtree) { - observedNode.node[PropertySymbol.unobserveMutations](observedNode.mutationListener); - } else { - const htmlCollections = - observedNode.node[PropertySymbol.childNodes][PropertySymbol.htmlCollections]; - htmlCollections.splice(htmlCollections.indexOf(this), 1); - } - } - - /** - * Index of item. - * - * @param item Item. - * @returns Index. - */ - public [PropertySymbol.indexOf](item: T): number { - return super.indexOf(item); - } + if (!isNaN(index) && index >= 0 && index < items.length) { + return true; + } - /** - * Returns true if the item is in the list. - * - * @param item Item. - * @returns True if the item is in the list. - */ - public [PropertySymbol.includes](item: T): boolean { - return super.includes(item); - } + property = String(property); - /** - * Observes node. - * - * @param observedNode Observed node. - * @param parentNode Parent node. - */ - protected [PropertySymbol.loadObservedNodes]( - observedNode: IHTMLCollectionObservedNode, - parentNode: Element | DocumentFragment | Document - ): void { - const childNodes = parentNode[PropertySymbol.childNodes]; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const name = + item.attributes['id']?.[PropertySymbol.value] || + item.attributes['name']?.[PropertySymbol.value]; - if (observedNode.subtree) { - for (let i = 0, max = childNodes.length; i < max; i++) { - if ( - childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - this.#isObservedItem(observedNode, childNodes[i]) - ) { - if (!observedNode.filter || observedNode.filter(childNodes[i])) { - this[PropertySymbol.addItem](childNodes[i]); + if (name && name === property) { + return true; } - - this[PropertySymbol.loadObservedNodes](observedNode, childNodes[i]); } - } - return; - } - for (let i = 0, max = childNodes.length; i < max; i++) { - if ( - childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!observedNode.filter || observedNode.filter(childNodes[i])) - ) { - this[PropertySymbol.addItem](childNodes[i]); - } - } - } + return false; + }, + defineProperty(target, property, descriptor): boolean { + if (property in target) { + Reflect.defineProperty(target, property, descriptor); + return true; + } - /** - * Unobserves node. - * - * @param observedNode Observed node. - * @param parentNode Parent node. - */ - protected [PropertySymbol.unloadObservedNodes]( - observedNode: IHTMLCollectionObservedNode, - parentNode: Element | DocumentFragment | Document - ): void { - const childNodes = parentNode[PropertySymbol.childNodes]; + return false; + }, + getOwnPropertyDescriptor(target, property): PropertyDescriptor { + if (property in target) { + return; + } - if (observedNode.subtree) { - for (let i = 0, max = childNodes.length; i < max; i++) { - if ( - childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - this.#isObservedItem(observedNode, childNodes[i]) - ) { - if (!observedNode.filter || observedNode.filter(childNodes[i])) { - this[PropertySymbol.removeItem](childNodes[i]); - } + const items = query(); + const index = Number(property); - this[PropertySymbol.unloadObservedNodes](observedNode, childNodes[i]); + if (!isNaN(index) && index >= 0 && index < items.length) { + return { + value: items[index], + writable: false, + enumerable: true, + configurable: false + }; } - } - return; - } - for (let i = 0, max = childNodes.length; i < max; i++) { - if ( - childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!observedNode.filter || observedNode.filter(childNodes[i])) - ) { - this[PropertySymbol.removeItem](childNodes[i]); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const name = + item.attributes['id']?.[PropertySymbol.value] || + item.attributes['name']?.[PropertySymbol.value]; + + if (name && name === property) { + return { + value: item, + writable: false, + enumerable: true, + configurable: false + }; + } + } } - } + }); } /** - * Inserts new observed item. + * Returns length. * - * @param observedNode Observed node. - * @param newItem New item. + * @returns Length. */ - protected [PropertySymbol.insertObservedItem]( - observedNode: IHTMLCollectionObservedNode, - newItem: Element - ): void { - // Is part of a subtree. - if (observedNode.subtree) { - // Check if the item is observed by this listener - if (!this.#isObservedItem(observedNode, newItem)) { - return; - } - - // Find all children that pass the filter inside the new item. - const items = this.#getItemsInElement(observedNode, newItem); - - if (!items.length) { - return; - } - - // As the new item is part of a subtree, we need to walk the tree to find the first item that passes the filter. - // We start with the last item in the collection. - const treeWalker = new TreeWalker(observedNode.node, NodeFilter.SHOW_ELEMENT, (node) => - !observedNode.filter || observedNode.filter(node) - ? NodeFilter.FILTER_ACCEPT - : NodeFilter.FILTER_SKIP - ); - treeWalker.currentNode = items[items.length - 1]; - const referenceItem = treeWalker.nextNode() || null; - - for (const item of items) { - this[PropertySymbol.insertItem](item, referenceItem); - } - - return; - } - - // Check if the new item is an element node and passes the filter. - if ( - newItem[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || - (newItem[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - observedNode.filter && - !observedNode.filter(newItem)) - ) { - return; - } - - // Is not part of a subtree. - // We can therefore find the reference item by iterating over the child nodes to find the first element node that passes the filter. - const childNodes = newItem.parentNode[PropertySymbol.childNodes]; - let referenceItemIndex = -1; - for ( - let i = childNodes[PropertySymbol.indexOf](newItem) + 1, max = childNodes.length; - i < max; - i++ - ) { - if ( - childNodes[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!observedNode.filter || observedNode.filter(childNodes[i])) - ) { - referenceItemIndex = this[PropertySymbol.indexOf](childNodes[i]); - break; - } - } - - if (referenceItemIndex === -1) { - this[PropertySymbol.addItem](newItem); - return; - } - - this[PropertySymbol.insertItem](newItem, this[referenceItemIndex]); + public get length(): number { + return this[PropertySymbol.query]().length; } /** - * Removes observed item. + * Returns `Symbol.toStringTag`. * - * @param observedNode Observed node. - * @param item Item. + * @returns `Symbol.toStringTag`. */ - protected [PropertySymbol.removeObservedItem]( - observedNode: IHTMLCollectionObservedNode, - item: T - ): void { - // Is part of a subtree. - if (observedNode.subtree) { - // Find all children that pass the filter inside the item. - const items = this.#getItemsInElement(observedNode, item); - - for (let i = items.length - 1; i >= 0; i--) { - this[PropertySymbol.removeItem](items[i]); - } - - return; - } - - // Check if the item is an element node and passes the filter. - if ( - item[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode || - (item[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - observedNode.filter && - !observedNode.filter(item)) - ) { - return; - } - - this[PropertySymbol.removeItem](item); + public get [Symbol.toStringTag](): string { + return this.constructor.name; } /** - * Triggered when an attribute is set. + * Returns `[object HTMLCollection]`. * - * @param item Item. - * @param attribute Attribute. - * @param replacedAttribute Replaced attribute. + * @returns `[object HTMLCollection]`. */ - protected [PropertySymbol.onSetAttribute]( - item: T, - attribute: Attr, - replacedAttribute: Attr | null - ): void { - const name = attribute[PropertySymbol.name]; - - if (name !== 'id' && name !== 'name') { - return; - } - - const replacedValue = replacedAttribute?.[PropertySymbol.value]; - - if (replacedValue) { - const namedItems = this[PropertySymbol.namedItems].get(replacedValue); - - if (namedItems) { - namedItems[PropertySymbol.removeItem](item); - } - - this[PropertySymbol.updateNamedItemProperty](replacedValue); - } - - const value = attribute.value; - - if (value) { - const namedItems = - this[PropertySymbol.namedItems].get(value) || - this[PropertySymbol.createNamedItemsNodeList](); - - if (!namedItems[PropertySymbol.includes](item)) { - namedItems[PropertySymbol.addItem](item); - this[PropertySymbol.namedItems].set(value, namedItems); - this[PropertySymbol.updateNamedItemProperty](value); - } - } + public toLocaleString(): string { + return `[object ${this.constructor.name}]`; } /** - * Triggered when an attribute is set. + * Returns `[object HTMLCollection]`. * - * @param item Item. - * @param removedAttribute Attribute. + * @returns `[object HTMLCollection]`. */ - protected [PropertySymbol.onRemoveAttribute](item: T, removedAttribute: Attr): void { - if (removedAttribute.name !== 'id' && removedAttribute.name !== 'name') { - return; - } - - if (removedAttribute.value) { - const namedItems = this[PropertySymbol.namedItems].get(removedAttribute.value); - - if (namedItems) { - namedItems[PropertySymbol.removeItem](item); - } - - this[PropertySymbol.updateNamedItemProperty](removedAttribute.value); - } + public toString(): string { + return `[object ${this.constructor.name}]`; } /** - * Updates named item property. + * Returns item by index. * - * @param name Name. + * @param index Index. */ - protected [PropertySymbol.updateNamedItemProperty](name: string): void { - if (!this[PropertySymbol.isValidPropertyName](name)) { - return; - } - - const namedItems = this[PropertySymbol.namedItems].get(name); - - if (namedItems?.length) { - if (Object.getOwnPropertyDescriptor(this, name)?.value !== namedItems[0]) { - Object.defineProperty(this, name, { - value: namedItems[0], - writable: false, - enumerable: true, - configurable: true - }); - } - } else { - delete this[name]; - } + public item(index: number): T { + const items = this[PropertySymbol.query](); + return index >= 0 && items[index] ? items[index] : null; } /** - * Creates a new NodeList to be used as a named item. + * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. * - * @returns NodeList. + * @returns Iterator. */ - protected [PropertySymbol.createNamedItemsNodeList](): INodeList { - return new NodeList(); + public [Symbol.iterator](): IterableIterator { + const items = this[PropertySymbol.query](); + return items[Symbol.iterator](); } /** - * Returns "true" if the property name is valid. + * Returns named item. * * @param name Name. - * @returns True if the property name is valid. - */ - protected [PropertySymbol.isValidPropertyName](name: string): boolean { - return ( - !!name && - !this.constructor.prototype.hasOwnProperty(name) && - (isNaN(Number(name)) || name.includes('.')) - ); - } - - /** - * Returns true if the item is observed by the observed node and no other observers in the collection also observe it. - * - * @param observedNode Observed node. - * @param node Node. - * @returns True if the item is observed. + * @returns Node. */ - #isObservedItem(observedNode: IHTMLCollectionObservedNode, node: Node): boolean { - // This method should not be executed when not in a subtree - if (!observedNode.subtree) { - return true; - } - - if (!node[PropertySymbol.mutationListeners].includes(observedNode.mutationListener)) { - return false; - } - - for (const observedNodeItem of this.#observedNodes) { + public namedItem(name: string): NamedItem | null { + const items = this[PropertySymbol.query](); + name = String(name); + for (const item of items) { if ( - observedNodeItem !== observedNode && - node[PropertySymbol.mutationListeners].includes(observedNodeItem.mutationListener) + item.attributes['id']?.[PropertySymbol.value] === name || + item.attributes['name']?.[PropertySymbol.value] === name ) { - return false; - } - } - - return true; - } - - /** - * Returns items in element. - * - * @param observedNode Observed node. - * @param element Element. - * @param [items] Items. - * @returns Items. - */ - #getItemsInElement( - observedNode: IHTMLCollectionObservedNode, - element: Node, - items: T[] = [] - ): T[] { - if ( - element[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!observedNode.filter || observedNode.filter(element)) - ) { - items.push(element); - } - - for (let i = 0, max = element[PropertySymbol.childNodes].length; i < max; i++) { - if (element[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - this.#getItemsInElement(observedNode, element[PropertySymbol.childNodes][i], items); - } - } - - return items; - } - - /** - * Adds named item to collection. - * - * @param item Item. - */ - #addNamedItem(item: T): void { - const listeners = { - set: (attribute: Attr, replacedAttribute: Attr) => - this[PropertySymbol.onSetAttribute](item, attribute, replacedAttribute), - remove: (attribute: Attr) => this[PropertySymbol.onRemoveAttribute](item, attribute) - }; - - item[PropertySymbol.attributes][PropertySymbol.addEventListener]('set', listeners.set); - item[PropertySymbol.attributes][PropertySymbol.addEventListener]('remove', listeners.remove); - - this.#attributeListeners.set(item, listeners); - - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { - const name = (item)[PropertySymbol.attributes][attributeName]?.value; - if (name) { - const namedItems = - this[PropertySymbol.namedItems].get(name) || - this[PropertySymbol.createNamedItemsNodeList](); - - if (namedItems[PropertySymbol.includes](item)) { - return; - } - - namedItems[PropertySymbol.addItem](item); - - this[PropertySymbol.namedItems].set(name, namedItems); - - this[PropertySymbol.updateNamedItemProperty](name); - } - } - } - - /** - * Removes named item from collection. - * - * @param item Item. - */ - #removeNamedItem(item: T): void { - const listeners = this.#attributeListeners.get(item); - - if (listeners) { - item[PropertySymbol.attributes][PropertySymbol.removeEventListener]('set', listeners.set); - item[PropertySymbol.attributes][PropertySymbol.removeEventListener]( - 'remove', - listeners.remove - ); - } - - for (const attributeName of NAMED_ITEM_ATTRIBUTES) { - const name = (item)[PropertySymbol.attributes][attributeName]?.value; - if (name) { - const namedItems = this[PropertySymbol.namedItems].get(name); - - if (!namedItems) { - return; - } - - namedItems[PropertySymbol.removeItem](item); - - this[PropertySymbol.updateNamedItemProperty](name); + return (item); } } - } -} - -// Removes Array methods from HTMLCollection. -const descriptors = Object.getOwnPropertyDescriptors(Array.prototype); -for (const key of Object.keys(descriptors)) { - if (key !== 'item' && key !== 'constructor') { - const descriptor = descriptors[key]; - if (key === 'length') { - Object.defineProperty(HTMLCollection.prototype, key, { - set: () => {}, - get: descriptor.get - }); - } else if (typeof descriptor.value === 'function') { - Object.defineProperty(HTMLCollection.prototype, key, {}); - } + return null; } } diff --git a/packages/happy-dom/src/nodes/element/IHTMLCollection.ts b/packages/happy-dom/src/nodes/element/IHTMLCollection.ts deleted file mode 100644 index 4efed878f..000000000 --- a/packages/happy-dom/src/nodes/element/IHTMLCollection.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable filenames/match-exported */ - -import * as PropertySymbol from '../../PropertySymbol.js'; -import Element from './Element.js'; - -/** - * HTMLCollection. - * - * This interface is used to hide Array methods from the outside. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection - */ -export default interface IHTMLCollection { - [index: number]: T; - - /** - * Returns the number of items in the collection. - * - * @returns Number of items. - */ - readonly length: number; - - /** - * Returns item by index. - * - * @param index Index. - * @returns Item. - */ - item(index: number): T | null; - - /** - * Returns item by name. - * - * @param name Name. - * @returns Item. - */ - namedItem(name: string): NamedItem | null; - - /** - * Appends item. - * - * @param item Item. - * @returns True if added. - */ - [PropertySymbol.addItem](item: T): boolean; - - /** - * Inserts item before another item. - * - * @param newItem New item. - * @param [referenceItem] Reference item. - * @returns True if inserted. - */ - [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean; - - /** - * Removes item. - * - * @param item Item. - * @returns True if removed. - */ - [PropertySymbol.removeItem](item: T): boolean; - - /** - * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. - * - * @returns Iterator. - */ - [Symbol.iterator](): IterableIterator; - - /** - * Index of item. - * - * @param item Item. - * @returns Index. - */ - [PropertySymbol.indexOf](item?: T): number; - - /** - * Returns true if the item is in the list. - * - * @param item Item. - * @returns True if the item is in the list. - */ - [PropertySymbol.includes](item: T): boolean; -} diff --git a/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts b/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts deleted file mode 100644 index e8ed01508..000000000 --- a/packages/happy-dom/src/nodes/element/IHTMLCollectionObservedNode.ts +++ /dev/null @@ -1,11 +0,0 @@ -import IMutationListener from '../../mutation-observer/IMutationListener.js'; -import DocumentFragment from '../document-fragment/DocumentFragment.js'; -import Document from '../document/Document.js'; -import Element from './Element.js'; - -export default interface IHTMLCollectionObservedNode { - node: Element | DocumentFragment | Document; - filter: (item: Element) => boolean | null; - subtree: boolean; - mutationListener: IMutationListener; -} diff --git a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts index 24b9c46d1..64c33a343 100644 --- a/packages/happy-dom/src/nodes/element/NamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/NamedNodeMap.ts @@ -18,14 +18,6 @@ export default class NamedNodeMap { public [PropertySymbol.namedItems]: Map = new Map(); public [PropertySymbol.ownerElement]: Element; - #eventListeners: { - set: WeakRef[]; - remove: WeakRef[]; - } = { - set: [], - remove: [] - }; - /** * Constructor. * @@ -157,63 +149,6 @@ export default class NamedNodeMap { return item; } - /** - * Adds event listener. - * - * @param type Type. - * @param listener Listener. - */ - public [PropertySymbol.addEventListener]( - type: 'set' | 'remove', - listener: TNamedNodeMapListener - ): void { - this.#eventListeners[type].push(new WeakRef(listener)); - } - - /** - * Removes event listener. - * - * @param type Type. - * @param listener Listener. - */ - public [PropertySymbol.removeEventListener]( - type: 'set' | 'remove', - listener: TNamedNodeMapListener - ): void { - const listeners = this.#eventListeners[type]; - for (let i = 0, max = listeners.length; i < max; i++) { - if (listeners[i].deref() === listener) { - listeners.splice(i, 1); - return; - } - } - } - - /** - * Dispatches event. - * - * @param type Type. - * @param attribute Attribute. - * @param replacedAttribute Replaced attribute. - */ - public [PropertySymbol.dispatchEvent]( - type: 'set' | 'remove', - attribute: Attr, - replacedAttribute?: Attr | null - ): void { - const listeners = this.#eventListeners[type]; - for (let i = 0, max = listeners.length; i < max; i++) { - const listener = listeners[i].deref(); - if (listener) { - listener(attribute, replacedAttribute); - } else { - listeners.splice(i, 1); - i--; - max--; - } - } - } - /** * Sets named item. * @@ -248,7 +183,7 @@ export default class NamedNodeMap { } if (!ignoreListeners) { - this[PropertySymbol.dispatchEvent]('set', item, replacedItem); + this[PropertySymbol.ownerElement][PropertySymbol.onSetAttribute](item, replacedItem); } return replacedItem; @@ -272,7 +207,7 @@ export default class NamedNodeMap { this[PropertySymbol.namedItems].delete(name); if (!ignoreListeners) { - this[PropertySymbol.dispatchEvent]('remove', item); + this[PropertySymbol.ownerElement][PropertySymbol.onRemoveAttribute](item); } } diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index 1fcdf03d9..736a4bae7 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -18,21 +18,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper public [PropertySymbol.relList]: DOMTokenList = null; #htmlHyperlinkElementUtility = new HTMLHyperlinkElementUtility(this); - /** - * Constructor. - */ - constructor() { - super(); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); - } - /** * Returns download. * @@ -414,22 +399,24 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLHyper } /** - * Triggered when an attribute is set. - * @param item + * @override */ - #onSetAttribute(item: Attr): void { - if (item[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + public override [PropertySymbol.onSetAttribute]( + attribute: Attr, + replacedAttribute: Attr | null + ): void { + super[PropertySymbol.onSetAttribute](attribute, replacedAttribute); + if (attribute[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { this[PropertySymbol.relList][PropertySymbol.updateIndices](); } } /** - * Triggered when an attribute is removed. - * @param name - * @param removedItem + * @override */ - #onRemoveAttribute(removedItem: Attr): void { - if (removedItem[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + public override [PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void { + super[PropertySymbol.onRemoveAttribute](removedAttribute); + if (removedAttribute[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { this[PropertySymbol.relList][PropertySymbol.updateIndices](); } } diff --git a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts index d5b01bc0d..50e74db4a 100644 --- a/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-area-element/HTMLAreaElement.ts @@ -17,21 +17,6 @@ export default class HTMLAreaElement extends HTMLElement implements IHTMLHyperli public [PropertySymbol.relList]: DOMTokenList = null; #htmlHyperlinkElementUtility = new HTMLHyperlinkElementUtility(this); - /** - * Constructor. - */ - constructor() { - super(); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); - } - /** * Returns alt. * @@ -413,22 +398,24 @@ export default class HTMLAreaElement extends HTMLElement implements IHTMLHyperli } /** - * Triggered when an attribute is set. - * @param item + * @override */ - #onSetAttribute(item: Attr): void { - if (item[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + public override [PropertySymbol.onSetAttribute]( + attribute: Attr, + replacedAttribute: Attr | null + ): void { + super[PropertySymbol.onSetAttribute](attribute, replacedAttribute); + if (attribute[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { this[PropertySymbol.relList][PropertySymbol.updateIndices](); } } /** - * Triggered when an attribute is removed. - * @param name - * @param removedItem + * @override */ - #onRemoveAttribute(removedItem: Attr): void { - if (removedItem[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + public override [PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void { + super[PropertySymbol.onRemoveAttribute](removedAttribute); + if (removedAttribute[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { this[PropertySymbol.relList][PropertySymbol.updateIndices](); } } diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index 6bbf984c6..6cbffdcd6 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -8,7 +8,7 @@ import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtili import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import { URL } from 'url'; import MouseEvent from '../../event/events/MouseEvent.js'; -import NodeList from '../node/INodeList.js'; +import NodeList from '../node/NodeList.js'; const BUTTON_TYPES = ['submit', 'reset', 'button', 'menu']; @@ -228,7 +228,14 @@ export default class HTMLButtonElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - return this[PropertySymbol.formNode]; + if (this[PropertySymbol.formNode]) { + return this[PropertySymbol.formNode]; + } + const id = this.attributes['form']?.[PropertySymbol.value]; + if (!id || !this[PropertySymbol.isConnected]) { + return null; + } + return this[PropertySymbol.ownerDocument].getElementById(id); } /** diff --git a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts index aa525ae26..899d2189a 100644 --- a/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts +++ b/packages/happy-dom/src/nodes/html-data-list-element/HTMLDataListElement.ts @@ -1,8 +1,8 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; -import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; import HTMLCollection from '../element/HTMLCollection.js'; +import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; +import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; /** * HTMLDataListElement @@ -10,20 +10,18 @@ import HTMLCollection from '../element/HTMLCollection.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataListElement */ export default class HTMLDataListElement extends HTMLElement { - public [PropertySymbol.options]: IHTMLCollection | null = null; + public [PropertySymbol.options]: HTMLCollection | null = null; /** * Returns options. * * @returns Options. */ - public get options(): IHTMLCollection { + public get options(): HTMLCollection { if (!this[PropertySymbol.options]) { - this[PropertySymbol.options] = new HTMLCollection(); - this[PropertySymbol.options][PropertySymbol.observe](this, { - subtree: true, - filter: (item) => item[PropertySymbol.tagName] === 'OPTION' - }); + this[PropertySymbol.options] = >( + ParentNodeUtility.getElementsByTagName(this, 'OPTION') + ); } return this[PropertySymbol.options]; } diff --git a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts index df9dd6ad7..c5b9cc561 100644 --- a/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts +++ b/packages/happy-dom/src/nodes/html-details-element/HTMLDetailsElement.ts @@ -12,21 +12,6 @@ export default class HTMLDetailsElement extends HTMLElement { // Events public ontoggle: (event: Event) => void | null = null; - /** - * Constructor. - */ - constructor() { - super(); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); - } - /** * Returns the open attribute. */ @@ -48,12 +33,13 @@ export default class HTMLDetailsElement extends HTMLElement { } /** - * Triggered when an attribute is set. - * - * @param attribute Attribute - * @param replacedAttribute Replaced item + * @override */ - #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + public override [PropertySymbol.onSetAttribute]( + attribute: Attr, + replacedAttribute: Attr | null + ): void { + super[PropertySymbol.onSetAttribute](attribute, replacedAttribute); if (attribute[PropertySymbol.name] === 'open') { if (attribute[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value]) { this.dispatchEvent(new Event('toggle')); @@ -62,11 +48,10 @@ export default class HTMLDetailsElement extends HTMLElement { } /** - * Triggered when an attribute is removed. - * - * @param removedAttribute Removed attribute. + * @override */ - #onRemoveAttribute(removedAttribute: Attr): void { + public override [PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void { + super[PropertySymbol.onRemoveAttribute](removedAttribute); if (removedAttribute && removedAttribute[PropertySymbol.name] === 'open') { this.dispatchEvent(new Event('toggle')); } diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 22661272d..c2164f90e 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -8,11 +8,8 @@ import Event from '../../event/Event.js'; import HTMLElementUtility from './HTMLElementUtility.js'; import NodeList from '../node/NodeList.js'; import Node from '../node/Node.js'; -import HTMLCollection from '../element/HTMLCollection.js'; -import DatasetFactory from '../element/DatasetFactory.js'; -import IDataset from '../element/IDataset.js'; import NamedNodeMap from '../element/NamedNodeMap.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; +import DOMStringMap from '../element/DOMStringMap.js'; /** * HTML Element. @@ -64,10 +61,8 @@ export default class HTMLElement extends Element { public [PropertySymbol.clientLeft] = 0; public [PropertySymbol.clientTop] = 0; public [PropertySymbol.style]: CSSStyleDeclaration = null; - - // Private properties - #dataset: IDataset = null; - #customElementDefineCallback: () => void = null; + private [PropertySymbol.dataset]: DOMStringMap | null = null; + private [PropertySymbol.customElementDefineCallback]: () => void = null; /** * Returns access key. @@ -222,7 +217,7 @@ export default class HTMLElement extends Element { let result = ''; - for (const childNode of this[PropertySymbol.childNodes]) { + for (const childNode of this[PropertySymbol.nodeArray]) { if (childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { const childElement = childNode; const computedStyle = @@ -274,7 +269,7 @@ export default class HTMLElement extends Element { * @param innerText Inner text. */ public set innerText(text: string) { - const childNodes = this[PropertySymbol.childNodes]; + const childNodes = this[PropertySymbol.nodeArray]; while (childNodes.length) { this.removeChild(childNodes[0]); @@ -359,8 +354,8 @@ export default class HTMLElement extends Element { * * @returns Data set. */ - public get dataset(): IDataset { - return (this.#dataset ??= DatasetFactory.createDataset(this)); + public get dataset(): DOMStringMap { + return (this[PropertySymbol.dataset] ??= new DOMStringMap(this)); } /** @@ -526,18 +521,20 @@ export default class HTMLElement extends Element { PropertySymbol.callbacks ]; - if (!this.#customElementDefineCallback) { + if (!this[PropertySymbol.customElementDefineCallback]) { const callback = (): void => { if (this[PropertySymbol.parentNode]) { const newElement = ( this[PropertySymbol.ownerDocument].createElement(localName) ); - (>newElement[PropertySymbol.childNodes]) = >( - this[PropertySymbol.childNodes] + const newCache = newElement[PropertySymbol.cache]; + newElement[PropertySymbol.nodeArray] = this[PropertySymbol.nodeArray]; + newElement[PropertySymbol.elementArray] = this[PropertySymbol.elementArray]; + newElement[PropertySymbol.childNodes] = new NodeList( + newElement[PropertySymbol.nodeArray] ); - (>newElement[PropertySymbol.children]) = - this[PropertySymbol.children]; - (newElement[PropertySymbol.isConnected]) = this[PropertySymbol.isConnected]; + newElement[PropertySymbol.children] = null; + newElement[PropertySymbol.isConnected] = this[PropertySymbol.isConnected]; newElement[PropertySymbol.rootNode] = this[PropertySymbol.rootNode]; newElement[PropertySymbol.formNode] = this[PropertySymbol.formNode]; @@ -546,6 +543,8 @@ export default class HTMLElement extends Element { newElement[PropertySymbol.textAreaNode] = this[PropertySymbol.textAreaNode]; newElement[PropertySymbol.mutationListeners] = this[PropertySymbol.mutationListeners]; newElement[PropertySymbol.isValue] = this[PropertySymbol.isValue]; + newElement[PropertySymbol.cache] = this[PropertySymbol.cache]; + newElement[PropertySymbol.affectsCache] = this[PropertySymbol.affectsCache]; for (let i = 0, max = this[PropertySymbol.attributes].length; i < max; i++) { newElement[PropertySymbol.attributes].setNamedItem( @@ -553,10 +552,10 @@ export default class HTMLElement extends Element { ); } - (>this[PropertySymbol.childNodes]) = new NodeList(); - (>this[PropertySymbol.children]) = - new HTMLCollection(); - this[PropertySymbol.children][PropertySymbol.observe](this); + this[PropertySymbol.nodeArray] = []; + this[PropertySymbol.elementArray] = []; + this[PropertySymbol.childNodes] = new NodeList(this[PropertySymbol.nodeArray]); + this[PropertySymbol.children] = null; this[PropertySymbol.rootNode] = null; this[PropertySymbol.formNode] = null; @@ -564,13 +563,15 @@ export default class HTMLElement extends Element { this[PropertySymbol.textAreaNode] = null; this[PropertySymbol.mutationListeners] = []; this[PropertySymbol.isValue] = null; + this[PropertySymbol.cache] = newCache; + this[PropertySymbol.affectsCache] = []; (this[PropertySymbol.attributes]) = new NamedNodeMap(this); - const parentChildNodes = (this[PropertySymbol.parentNode])[ - PropertySymbol.childNodes - ]; - parentChildNodes[PropertySymbol.insertItem](newElement, this.nextSibling); - parentChildNodes[PropertySymbol.removeItem](this); + const parentChildNodes = this[PropertySymbol.parentNode][PropertySymbol.nodeArray]; + const parentChildElements = + this[PropertySymbol.parentNode][PropertySymbol.elementArray]; + parentChildNodes[parentChildNodes.indexOf(this)] = newElement; + parentChildElements[parentChildElements.indexOf(this)] = newElement; if (newElement[PropertySymbol.isConnected] && newElement.connectedCallback) { const result = >newElement.connectedCallback(); @@ -597,7 +598,7 @@ export default class HTMLElement extends Element { }; callbacks[localName] = callbacks[localName] || []; callbacks[localName].push(callback); - this.#customElementDefineCallback = callback; + this[PropertySymbol.customElementDefineCallback] = callback; } } @@ -624,15 +625,17 @@ export default class HTMLElement extends Element { PropertySymbol.callbacks ]; - if (callbacks[localName] && this.#customElementDefineCallback) { - const index = callbacks[localName].indexOf(this.#customElementDefineCallback); + if (callbacks[localName] && this[PropertySymbol.customElementDefineCallback]) { + const index = callbacks[localName].indexOf( + this[PropertySymbol.customElementDefineCallback] + ); if (index !== -1) { callbacks[localName].splice(index, 1); } if (!callbacks[localName].length) { delete callbacks[localName]; } - this.#customElementDefineCallback = null; + this[PropertySymbol.customElementDefineCallback] = null; } } diff --git a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts index ab4167c13..343827fc4 100644 --- a/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts +++ b/packages/happy-dom/src/nodes/html-field-set-element/HTMLFieldSetElement.ts @@ -1,13 +1,12 @@ import HTMLElement from '../html-element/HTMLElement.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; import HTMLCollection from '../element/HTMLCollection.js'; import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; -import Element from '../element/Element.js'; +import QuerySelector from '../../query-selector/QuerySelector.js'; type THTMLFieldSetElement = | HTMLInputElement @@ -25,7 +24,7 @@ export default class HTMLFieldSetElement extends HTMLElement { public declare cloneNode: (deep?: boolean) => HTMLFieldSetElement; // Internal properties - public [PropertySymbol.elements]: IHTMLCollection | null = null; + public [PropertySymbol.elements]: HTMLCollection | null = null; public [PropertySymbol.formNode]: HTMLFormElement | null = null; /** @@ -33,17 +32,16 @@ export default class HTMLFieldSetElement extends HTMLElement { * * @returns Elements. */ - public get elements(): IHTMLCollection { + public get elements(): HTMLCollection { if (!this[PropertySymbol.elements]) { - this[PropertySymbol.elements] = new HTMLCollection(); - this[PropertySymbol.elements][PropertySymbol.observe](this, { - subtree: true, - filter: (item: Element) => - item.tagName === 'INPUT' || - item.tagName === 'BUTTON' || - item.tagName === 'TEXTAREA' || - item.tagName === 'SELECT' - }); + this[PropertySymbol.elements] = new HTMLCollection( + () => + ( + QuerySelector.querySelectorAll(this, 'input,button,textarea,select')[ + PropertySymbol.elements + ] + ) + ); } return this[PropertySymbol.elements]; } @@ -54,7 +52,14 @@ export default class HTMLFieldSetElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - return this[PropertySymbol.formNode]; + if (this[PropertySymbol.formNode]) { + return this[PropertySymbol.formNode]; + } + const id = this.attributes['form']?.[PropertySymbol.value]; + if (!id || !this[PropertySymbol.isConnected]) { + return null; + } + return this[PropertySymbol.ownerDocument].getElementById(id); } /** diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index b27604ca1..13f6f74fd 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -1,14 +1,6 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import EventTarget from '../../event/EventTarget.js'; -import Attr from '../attr/Attr.js'; -import Element from '../element/Element.js'; import HTMLCollection from '../element/HTMLCollection.js'; -import IHTMLCollectionObservedNode from '../element/IHTMLCollectionObservedNode.js'; -import TNamedNodeMapListener from '../element/TNamedNodeMapListener.js'; -import HTMLElement from '../html-element/HTMLElement.js'; -import Node from '../node/Node.js'; import HTMLFormElement from './HTMLFormElement.js'; -import IRadioNodeList from './IRadioNodeList.js'; import RadioNodeList from './RadioNodeList.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; @@ -19,363 +11,23 @@ import THTMLFormControlElement from './THTMLFormControlElement.js'; */ export default class HTMLFormControlsCollection extends HTMLCollection< THTMLFormControlElement, - THTMLFormControlElement | IRadioNodeList + THTMLFormControlElement | RadioNodeList > { - public [PropertySymbol.namedItems] = new Map(); - #observedFormElement: IHTMLCollectionObservedNode | null = null; - #observedDocument: IHTMLCollectionObservedNode | null = null; - #observedDocumentAttributeListeners: { - set: TNamedNodeMapListener | null; - remove: TNamedNodeMapListener | null; - } = { - set: null, - remove: null - }; - #formElement: HTMLFormElement; - + private declare [PropertySymbol.ownerElement]: HTMLFormElement; /** * Constructor. * - * @param formElement Form element. - */ - constructor(formElement: HTMLFormElement) { - super(); - this.#formElement = formElement; - } - - /** - * @override - */ - public namedItem(name: string): THTMLFormControlElement | IRadioNodeList | null { - const namedItems = this[PropertySymbol.namedItems].get(name); - - if (!namedItems?.length) { - return null; - } - - if (namedItems.length === 1) { - return namedItems[0]; - } - - return namedItems; - } - - /** - * Observes node. - * - * @returns Observed node. - */ - public [PropertySymbol.observe](): IHTMLCollectionObservedNode { - if (this.#observedFormElement) { - return; - } - const observedNode = super[PropertySymbol.observe](this.#formElement, { - subtree: true, - filter: (item: THTMLFormControlElement) => - item[PropertySymbol.tagName] === 'INPUT' || - item[PropertySymbol.tagName] === 'SELECT' || - item[PropertySymbol.tagName] === 'TEXTAREA' || - item[PropertySymbol.tagName] === 'BUTTON' || - item[PropertySymbol.tagName] === 'FIELDSET' - }); - - this.#observedFormElement = observedNode; - - return observedNode; - } - - /** - * Unobserves node. - * - * @param observedNode Observed node. - */ - public [PropertySymbol.unobserve](): void { - if (!this.#observedFormElement) { - return; - } - super[PropertySymbol.unobserve](this.#observedFormElement); - this.#observedFormElement = null; - } - - /** - * Observes node. - * - * @returns Observed node. - */ - public [PropertySymbol.observeDocument](): IHTMLCollectionObservedNode { - if (this.#observedDocumentAttributeListeners.set) { - return; - } - - const formElement = this.#formElement; - - if (!formElement[PropertySymbol.isConnected]) { - return; - } - - this.#observedDocumentAttributeListeners.set = (attribute: Attr, replacedAttribute?: Attr) => { - if (attribute.name === 'id') { - if (replacedAttribute[PropertySymbol.value]) { - super[PropertySymbol.unobserve](this.#observedDocument); - this.#observedDocument = null; - } - if (attribute[PropertySymbol.value]) { - this.#observedDocument = super[PropertySymbol.observe]( - formElement[PropertySymbol.ownerDocument] - ); - } - } - }; - this.#observedDocumentAttributeListeners.remove = (attribute: Attr) => { - if (attribute.name === 'id') { - super[PropertySymbol.unobserve](this.#observedDocument); - this.#observedDocument = null; - } - }; - - formElement[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#observedDocumentAttributeListeners.set - ); - formElement[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#observedDocumentAttributeListeners.remove - ); - - const id = formElement[PropertySymbol.attributes]['id']?.value; - - if (!id) { - return; - } - - const observedNode = super[PropertySymbol.observe](formElement[PropertySymbol.ownerDocument], { - subtree: true, - filter: (item: THTMLFormControlElement) => { - if (!id) { - return false; - } - return ( - (item[PropertySymbol.tagName] === 'INPUT' || - item[PropertySymbol.tagName] === 'SELECT' || - item[PropertySymbol.tagName] === 'TEXTAREA' || - item[PropertySymbol.tagName] === 'BUTTON' || - item[PropertySymbol.tagName] === 'FIELDSET') && - item[PropertySymbol.attributes]['form']?.value === id - ); - } - }); - - this.#observedDocument = observedNode; - - return observedNode; - } - - /** - * Unobserves node. - * - * @param observedNode Observed node. - */ - public [PropertySymbol.unobserveDocument](): void { - if (!this.#observedDocumentAttributeListeners.set) { - return; - } - - const formElement = this.#formElement; - - formElement[PropertySymbol.attributes][PropertySymbol.removeEventListener]( - 'set', - this.#observedDocumentAttributeListeners.set - ); - formElement[PropertySymbol.attributes][PropertySymbol.removeEventListener]( - 'remove', - this.#observedDocumentAttributeListeners.remove - ); - - this.#observedDocumentAttributeListeners.set = null; - this.#observedDocumentAttributeListeners.remove = null; - - if (!this.#observedDocument) { - return; - } - - super[PropertySymbol.unobserve](this.#observedDocument); - this.#observedDocument = null; - } - - /** - * @override - */ - public override [PropertySymbol.addItem](item: THTMLFormControlElement): boolean { - if (!super[PropertySymbol.addItem](item)) { - return false; - } - - item[PropertySymbol.formNode] = this.#formElement; - - this.#formElement[this.length - 1] = item; - - return true; - } - - /** - * @override - */ - public override [PropertySymbol.insertItem]( - newItem: THTMLFormControlElement, - referenceItem: THTMLFormControlElement | null - ): boolean { - if (!super[PropertySymbol.insertItem](newItem, referenceItem)) { - return false; - } - - newItem[PropertySymbol.formNode] = this.#formElement; - - const index = this[PropertySymbol.indexOf](newItem); - - for (let i = index, max = this.length; i < max; i++) { - this.#formElement[i] = this[i]; - } - - return true; - } - - /** - * @override - */ - public override [PropertySymbol.removeItem](item: THTMLFormControlElement): boolean { - const index = this[PropertySymbol.indexOf](item); - - if (!super[PropertySymbol.removeItem](item)) { - return false; - } - - item[PropertySymbol.formNode] = null; - - for (let i = index, max = this.length; i < max; i++) { - this.#formElement[i] = this[i]; - } - - delete this.#formElement[this.length]; - - return true; - } - - /** - * @override - */ - protected override [PropertySymbol.onSetAttribute]( - item: THTMLFormControlElement, - attribute: Attr, - replacedAttribute: Attr | null - ): void { - if (attribute.name !== 'form') { - super[PropertySymbol.onSetAttribute](item, attribute, replacedAttribute); - return; - } - - if (!this.#formElement[PropertySymbol.isConnected]) { - return; - } - - const id = this.#formElement[PropertySymbol.attributes]['id']?.value; - - if (!id) { - return; - } - - if (replacedAttribute?.value === id) { - this.#formElement[PropertySymbol.removeItem](item); - } - - if (attribute.value === id) { - this.#formElement[PropertySymbol.addItem](item); - } - } - - /** - * @override + * @param ownerElement Form element. */ - protected override [PropertySymbol.onRemoveAttribute]( - item: THTMLFormControlElement, - removedAttribute: Attr - ): void { - if (removedAttribute.name !== 'form') { - super[PropertySymbol.onRemoveAttribute](item, removedAttribute); - return; - } - - if (!this.#formElement[PropertySymbol.isConnected]) { - return; - } - - const id = this.#formElement[PropertySymbol.attributes]['id']?.value; - - if (!id) { - return; - } - - if (removedAttribute.value === id) { - this.#formElement[PropertySymbol.removeItem](item); - } + constructor(ownerElement: HTMLFormElement) { + super(() => ownerElement[PropertySymbol.getFormControlItems]()); + this[PropertySymbol.ownerElement] = ownerElement; } /** * @override */ - protected override [PropertySymbol.updateNamedItemProperty](name: string): void { - if (!this[PropertySymbol.isValidPropertyName](name)) { - return; - } - - const namedItems = this[PropertySymbol.namedItems].get(name); - - if (namedItems?.length) { - const newValue = namedItems.length === 1 ? namedItems[0] : namedItems; - if (Object.getOwnPropertyDescriptor(this, name)?.value !== newValue) { - Object.defineProperty(this, name, { - value: newValue, - writable: false, - enumerable: true, - configurable: true - }); - - Object.defineProperty(this.#formElement, name, { - value: newValue, - writable: false, - enumerable: true, - configurable: true - }); - } - } else { - delete this[name]; - delete this.#formElement[name]; - } - } - - /** - * Returns "true" if the property name is valid. - * - * @param name Name. - * @returns True if the property name is valid. - */ - protected [PropertySymbol.isValidPropertyName](name: string): boolean { - return ( - !HTMLCollection.prototype.hasOwnProperty(name) && - !this.#formElement.constructor.prototype.hasOwnProperty(name) && - !HTMLElement.constructor.prototype.hasOwnProperty(name) && - !Element.constructor.prototype.hasOwnProperty(name) && - !Node.constructor.hasOwnProperty(name) && - !EventTarget.constructor.hasOwnProperty(name) && - super[PropertySymbol.isValidPropertyName](name) - ); - } - - /** - * Creates a new NodeList to be used as a named item. - * - * @returns NodeList. - */ - protected [PropertySymbol.createNamedItemsNodeList](): IRadioNodeList { - return new RadioNodeList(); + public namedItem(name: string): THTMLFormControlElement | RadioNodeList | null { + return this[PropertySymbol.ownerElement][PropertySymbol.getFormControlNamedItem](name); } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index 159837fff..a96d05826 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -3,7 +3,6 @@ import * as PropertySymbol from '../../PropertySymbol.js'; import Event from '../../event/Event.js'; import SubmitEvent from '../../event/events/SubmitEvent.js'; import HTMLFormControlsCollection from './HTMLFormControlsCollection.js'; -import Node from '../node/Node.js'; import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; @@ -11,7 +10,9 @@ import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import BrowserFrameNavigator from '../../browser/utilities/BrowserFrameNavigator.js'; import FormData from '../../form-data/FormData.js'; import BrowserWindow from '../../window/BrowserWindow.js'; -import IHTMLFormControlsCollection from './IHTMLFormControlsCollection.js'; +import THTMLFormControlElement from './THTMLFormControlElement.js'; +import QuerySelector from '../../query-selector/QuerySelector.js'; +import RadioNodeList from './RadioNodeList.js'; /** * HTML Form Element. @@ -24,18 +25,16 @@ export default class HTMLFormElement extends HTMLElement { public declare cloneNode: (deep?: boolean) => HTMLFormElement; // Internal properties. - public [PropertySymbol.elements]: HTMLFormControlsCollection = new HTMLFormControlsCollection( - this - ); - public [PropertySymbol.formNode]: Node = this; + public [PropertySymbol.elements]: HTMLFormControlsCollection | null = null; // Events public onformdata: (event: Event) => void | null = null; public onreset: (event: Event) => void | null = null; public onsubmit: (event: Event) => void | null = null; - // Private properties - #browserFrame: IBrowserFrame; + private declare [PropertySymbol.submit]: ( + submitter?: HTMLInputElement | HTMLButtonElement + ) => void; /** * Constructor. @@ -45,9 +44,118 @@ export default class HTMLFormElement extends HTMLElement { constructor(browserFrame: IBrowserFrame) { super(); - this.#browserFrame = browserFrame; + this[PropertySymbol.submit] = this[PropertySymbol.submitWithBrowserFrame].bind( + this, + browserFrame + ); - this[PropertySymbol.elements][PropertySymbol.observe](); + const proxy = new Proxy(this, { + get: (target, property, reciever) => { + if (property in target || typeof property === 'symbol') { + return Reflect.get(target, property, reciever); + } + const index = Number(property); + if (!isNaN(index)) { + return target[PropertySymbol.getFormControlItems]()[index]; + } + return target[PropertySymbol.getFormControlNamedItem](property) || undefined; + }, + set(target, property, newValue, reciever): boolean { + if (property in target || typeof property === 'symbol') { + Reflect.set(target, property, newValue, reciever); + } + return true; + }, + deleteProperty(): boolean { + return true; + }, + ownKeys(target): string[] { + const keys: string[] = []; + const items = target[PropertySymbol.getFormControlItems](); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const name = + item.attributes['id']?.[PropertySymbol.value] || + item.attributes['name']?.[PropertySymbol.value]; + keys.push(String(i)); + + if (name) { + keys.push(name); + } + } + return keys; + }, + has(target, property): boolean { + if (property in target) { + return true; + } + + const items = target[PropertySymbol.getFormControlItems](); + const index = Number(property); + + if (!isNaN(index) && index >= 0 && index < items.length) { + return true; + } + + property = String(property); + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const name = + item.attributes['id']?.[PropertySymbol.value] || + item.attributes['name']?.[PropertySymbol.value]; + + if (name && name === property) { + return true; + } + } + + return false; + }, + defineProperty(target, property, descriptor): boolean { + if (property in target) { + Reflect.defineProperty(target, property, descriptor); + return true; + } + + return false; + }, + getOwnPropertyDescriptor(target, property): PropertyDescriptor { + if (property in target) { + return Reflect.getOwnPropertyDescriptor(target, property); + } + + const items = target[PropertySymbol.getFormControlItems](); + const index = Number(property); + + if (!isNaN(index) && index >= 0 && index < items.length) { + return { + value: items[index], + writable: false, + enumerable: true, + configurable: false + }; + } + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const name = + item.attributes['id']?.[PropertySymbol.value] || + item.attributes['name']?.[PropertySymbol.value]; + + if (name && name === property) { + return { + value: item, + writable: false, + enumerable: true, + configurable: false + }; + } + } + } + }); + this[PropertySymbol.formNode] = proxy; + return proxy; } /** @@ -55,7 +163,10 @@ export default class HTMLFormElement extends HTMLElement { * * @returns Elements. */ - public get elements(): IHTMLFormControlsCollection { + public get elements(): HTMLFormControlsCollection { + if (!this[PropertySymbol.elements]) { + this[PropertySymbol.elements] = new HTMLFormControlsCollection(this); + } return this[PropertySymbol.elements]; } @@ -65,7 +176,7 @@ export default class HTMLFormElement extends HTMLElement { * @returns Length. */ public get length(): number { - return this[PropertySymbol.elements].length; + return this[PropertySymbol.getFormControlItems]().length; } /** @@ -247,7 +358,7 @@ export default class HTMLFormElement extends HTMLElement { * Submits form. No submit event is raised. In particular, the form's "submit" event handler is not run. */ public submit(): void { - this.#submit(); + this[PropertySymbol.submit](); } /** @@ -261,7 +372,7 @@ export default class HTMLFormElement extends HTMLElement { this.dispatchEvent( new SubmitEvent('submit', { bubbles: true, cancelable: true, submitter: submitter || this }) ); - this.#submit(submitter); + this[PropertySymbol.submit](submitter); } } @@ -269,7 +380,7 @@ export default class HTMLFormElement extends HTMLElement { * Resets form. */ public reset(): void { - for (const element of this[PropertySymbol.elements]) { + for (const element of this[PropertySymbol.getFormControlItems]()) { if ( element[PropertySymbol.tagName] === 'INPUT' || element[PropertySymbol.tagName] === 'TEXTAREA' @@ -305,7 +416,7 @@ export default class HTMLFormElement extends HTMLElement { const radioValidationState: { [k: string]: boolean } = {}; let isFormValid = true; - for (const element of this[PropertySymbol.elements]) { + for (const element of this[PropertySymbol.getFormControlItems]()) { if (element[PropertySymbol.tagName] === 'INPUT' && element.type === 'radio' && element.name) { if (!radioValidationState[element.name]) { radioValidationState[element.name] = true; @@ -338,29 +449,78 @@ export default class HTMLFormElement extends HTMLElement { } /** - * @override + * Returns form control items. + * + * @returns Form control items. */ - public override [PropertySymbol.connectedToDocument](): void { - super[PropertySymbol.connectedToDocument](); + public [PropertySymbol.getFormControlItems](): THTMLFormControlElement[] { + const elements = ( + QuerySelector.querySelectorAll(this, 'input,select,textarea,button,fieldset')[ + PropertySymbol.items + ] + ); + + if (this[PropertySymbol.isConnected]) { + const id = this[PropertySymbol.attributes]['id']?.value; + for (const element of ( + QuerySelector.querySelectorAll( + this[PropertySymbol.ownerDocument], + `input[form="${id}"],select[form="${id}"],textarea[form="${id}"],button[form="${id}"],fieldset[form="${id}"]` + )[PropertySymbol.items] + )) { + if (!elements.includes(element)) { + elements.push(element); + } + } + } - this[PropertySymbol.elements][PropertySymbol.observeDocument](); + return elements; } /** - * @override + * Returns form control named item. + * + * @param name + * @returns Form control named item. */ - public override [PropertySymbol.disconnectedFromDocument](): void { - super[PropertySymbol.disconnectedFromDocument](); + public [PropertySymbol.getFormControlNamedItem]( + name: string + ): THTMLFormControlElement | RadioNodeList | null { + const items = this[PropertySymbol.getFormControlItems](); + const namedItems = []; + + name = String(name); + + for (const item of items) { + if ( + item.attributes['id']?.[PropertySymbol.value] === name || + item.attributes['name']?.[PropertySymbol.value] === name + ) { + namedItems.push(item); + } + } + + if (!namedItems.length) { + return null; + } + + if (namedItems.length === 1) { + namedItems[0]; + } - this[PropertySymbol.elements][PropertySymbol.unobserveDocument](); + return new RadioNodeList(namedItems); } /** * Submits form. * + * @param browserFrame Browser frame. Injected by the constructor. * @param [submitter] Submitter. */ - #submit(submitter?: HTMLInputElement | HTMLButtonElement): void { + private [PropertySymbol.submitWithBrowserFrame]( + browserFrame: IBrowserFrame, + submitter?: HTMLInputElement | HTMLButtonElement + ): void { const action = submitter?.hasAttribute('formaction') ? submitter?.formAction || this.action : this.action; @@ -379,18 +539,18 @@ export default class HTMLFormElement extends HTMLElement { switch (submitter?.formTarget || this.target) { default: case '_self': - targetFrame = this.#browserFrame; + targetFrame = browserFrame; break; case '_top': - targetFrame = this.#browserFrame.page.mainFrame; + targetFrame = browserFrame.page.mainFrame; break; case '_parent': - targetFrame = this.#browserFrame.parentFrame ?? this.#browserFrame; + targetFrame = browserFrame.parentFrame ?? browserFrame; break; case '_blank': - const newPage = this.#browserFrame.page.context.newPage(); + const newPage = browserFrame.page.context.newPage(); targetFrame = newPage.mainFrame; - targetFrame[PropertySymbol.openerFrame] = this.#browserFrame; + targetFrame[PropertySymbol.openerFrame] = browserFrame; break; } @@ -410,7 +570,7 @@ export default class HTMLFormElement extends HTMLElement { frame: targetFrame, url: url.href, goToOptions: { - referrer: this.#browserFrame.page.mainFrame.window.location.origin + referrer: browserFrame.page.mainFrame.window.location.origin } }); @@ -426,7 +586,7 @@ export default class HTMLFormElement extends HTMLElement { url: action, formData, goToOptions: { - referrer: this.#browserFrame.page.mainFrame.window.location.origin + referrer: browserFrame.page.mainFrame.window.location.origin } }); } diff --git a/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts deleted file mode 100644 index 36354571c..000000000 --- a/packages/happy-dom/src/nodes/html-form-element/IHTMLFormControlsCollection.ts +++ /dev/null @@ -1,6 +0,0 @@ -import IHTMLCollection from '../element/IHTMLCollection.js'; -import IRadioNodeList from './IRadioNodeList.js'; -import THTMLFormControlElement from './THTMLFormControlElement.js'; - -export default interface IHTMLFormControlsCollection - extends IHTMLCollection {} diff --git a/packages/happy-dom/src/nodes/html-form-element/IRadioNodeList.ts b/packages/happy-dom/src/nodes/html-form-element/IRadioNodeList.ts deleted file mode 100644 index 9c6b9cf40..000000000 --- a/packages/happy-dom/src/nodes/html-form-element/IRadioNodeList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import INodeList from '../node/INodeList.js'; -import THTMLFormControlElement from './THTMLFormControlElement.js'; - -export default interface IRadioNodeList extends INodeList { - /** - * Returns value. - * - * @returns Value. - */ - readonly value: string; -} diff --git a/packages/happy-dom/src/nodes/html-form-element/RadioNodeList.ts b/packages/happy-dom/src/nodes/html-form-element/RadioNodeList.ts index 5b9d4152e..e82467b67 100644 --- a/packages/happy-dom/src/nodes/html-form-element/RadioNodeList.ts +++ b/packages/happy-dom/src/nodes/html-form-element/RadioNodeList.ts @@ -1,6 +1,7 @@ import HTMLInputElement from '../html-input-element/HTMLInputElement.js'; import NodeList from '../node/NodeList.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; /** * RadioNodeList @@ -14,7 +15,7 @@ export default class RadioNodeList extends NodeList { * @returns Value. */ public get value(): string { - for (const node of this) { + for (const node of this[PropertySymbol.items]) { if ((node).checked) { return (node).value; } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 553fe9da1..06f367001 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -63,15 +63,6 @@ export default class HTMLIFrameElement extends HTMLElement { constructor(browserFrame: IBrowserFrame) { super(); this.#browserFrame = browserFrame; - - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); } /** @@ -269,12 +260,13 @@ export default class HTMLIFrameElement extends HTMLElement { } /** - * Triggered when an attribute is set. - * - * @param attribute Attribute. - * @param replacedAttribute Replaced attribute. + * @override */ - #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + public override [PropertySymbol.onSetAttribute]( + attribute: Attr, + replacedAttribute: Attr | null + ): void { + super[PropertySymbol.onSetAttribute](attribute, replacedAttribute); if (attribute[PropertySymbol.name] === 'srcdoc') { this.#loadPage(); } @@ -300,11 +292,11 @@ export default class HTMLIFrameElement extends HTMLElement { } /** - * Triggered when an attribute is removed. - * - * @param removedAttribute Removed attribute. + * @override */ - #onRemoveAttribute(removedAttribute: Attr): void { + public override [PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void { + super[PropertySymbol.onRemoveAttribute](removedAttribute); + if ( removedAttribute[PropertySymbol.name] === 'srcdoc' || removedAttribute[PropertySymbol.name] === 'src' diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 4dc24b1aa..d04b5efd4 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -19,7 +19,7 @@ import Document from '../document/Document.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; import { URL } from 'url'; import MouseEvent from '../../event/events/MouseEvent.js'; -import NodeList from '../node/INodeList.js'; +import NodeList from '../node/NodeList.js'; /** * HTML Input Element. @@ -202,7 +202,14 @@ export default class HTMLInputElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - return this[PropertySymbol.formNode]; + if (this[PropertySymbol.formNode]) { + return this[PropertySymbol.formNode]; + } + const id = this.attributes['form']?.[PropertySymbol.value]; + if (!id || !this[PropertySymbol.isConnected]) { + return null; + } + return this[PropertySymbol.ownerDocument].getElementById(id); } /** diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts index a92231b1c..9671c0d8b 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElementUtility.ts @@ -4,7 +4,6 @@ import HTMLLabelElement from './HTMLLabelElement.js'; import NodeList from '../node/NodeList.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import INodeList from '../node/INodeList.js'; /** * Utility for finding labels associated with a form element. @@ -16,27 +15,28 @@ export default class HTMLLabelElementUtility { * @param element Element to get labels for. * @returns Label elements. */ - public static getAssociatedLabelElements(element: HTMLElement): INodeList { + public static getAssociatedLabelElements(element: HTMLElement): NodeList { const id = element.id; - let labels: INodeList; + let labels: HTMLLabelElement[]; + if (id && element[PropertySymbol.isConnected]) { const rootNode = element[PropertySymbol.rootNode] || element[PropertySymbol.ownerDocument]; - labels = >rootNode.querySelectorAll(`label[for="${id}"]`); - } else { - labels = new NodeList(); + labels = ( + rootNode.querySelectorAll(`label[for="${id}"]`)[PropertySymbol.items] + ); } let parent = element[PropertySymbol.parentNode]; while (parent) { if (parent['tagName'] === 'LABEL') { - labels[PropertySymbol.addItem](parent); + labels.push(parent); break; } parent = parent[PropertySymbol.parentNode]; } - return labels; + return new NodeList(labels); } } diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index a1ee8d1d0..1c65102fb 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -39,14 +39,6 @@ export default class HTMLLinkElement extends HTMLElement { super(); this.#browserFrame = browserFrame; - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); } /** @@ -232,29 +224,32 @@ export default class HTMLLinkElement extends HTMLElement { } /** - * Triggered when an attribute is set. - * - * @param item Item + * @override */ - #onSetAttribute(item: Attr): void { - if (item[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + public override [PropertySymbol.onSetAttribute]( + attribute: Attr, + replacedAttribute: Attr | null + ): void { + super[PropertySymbol.onSetAttribute](attribute, replacedAttribute); + + if (attribute[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { this[PropertySymbol.relList][PropertySymbol.updateIndices](); } - if (item[PropertySymbol.name] === 'rel') { - this.#loadStyleSheet(this.getAttribute('href'), item[PropertySymbol.value]); - } else if (item[PropertySymbol.name] === 'href') { - this.#loadStyleSheet(item[PropertySymbol.value], this.getAttribute('rel')); + if (attribute[PropertySymbol.name] === 'rel') { + this.#loadStyleSheet(this.getAttribute('href'), attribute[PropertySymbol.value]); + } else if (attribute[PropertySymbol.name] === 'href') { + this.#loadStyleSheet(attribute[PropertySymbol.value], this.getAttribute('rel')); } } /** - * Triggered when an attribute is removed. - * - * @param removedItem Removed item. + * @override */ - #onRemoveAttribute(removedItem: Attr): void { - if (removedItem[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { + public override [PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void { + super[PropertySymbol.onRemoveAttribute](removedAttribute); + + if (removedAttribute[PropertySymbol.name] === 'rel' && this[PropertySymbol.relList]) { this[PropertySymbol.relList][PropertySymbol.updateIndices](); } } @@ -328,7 +323,16 @@ export default class HTMLLinkElement extends HTMLElement { const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(code); this[PropertySymbol.sheet] = styleSheet; - this[PropertySymbol.ownerDocument][PropertySymbol.clearComputedStyleCache](); + + // Computed style cache is affected by all mutations. + const document = this[PropertySymbol.ownerDocument]; + if (document) { + for (const item of document[PropertySymbol.affectsComputedStyleCache]) { + item.result = null; + } + document[PropertySymbol.affectsComputedStyleCache] = []; + } + this.dispatchEvent(new Event('load')); } } diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index d8f3ae9ad..f09876ab2 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -1,4 +1,5 @@ import * as PropertySymbol from '../../PropertySymbol.js'; +import QuerySelector from '../../query-selector/QuerySelector.js'; import Attr from '../attr/Attr.js'; import HTMLElement from '../html-element/HTMLElement.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; @@ -15,21 +16,6 @@ export default class HTMLOptionElement extends HTMLElement { public [PropertySymbol.dirtyness] = false; public [PropertySymbol.selectNode]: HTMLSelectElement | null = null; - /** - * Constructor. - */ - constructor() { - super(); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); - } - /** * Returns inner text, which is the rendered appearance of text. * @@ -54,9 +40,13 @@ export default class HTMLOptionElement extends HTMLElement { * @returns Index. */ public get index(): number { - return this[PropertySymbol.selectNode] - ? (this[PropertySymbol.selectNode]).options.indexOf(this) - : 0; + if (!this[PropertySymbol.selectNode]) { + return 0; + } + const options = QuerySelector.querySelectorAll(this[PropertySymbol.selectNode], 'option')[ + PropertySymbol.elements + ]; + return options.indexOf(this); } /** @@ -65,7 +55,7 @@ export default class HTMLOptionElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - return (this[PropertySymbol.selectNode])?.form; + return (this[PropertySymbol.selectNode])?.form || null; } /** @@ -89,7 +79,7 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = Boolean(selected); if (selectNode) { - selectNode[PropertySymbol.options][PropertySymbol.updateSelectedness]( + selectNode[PropertySymbol.updateSelectedness]( this[PropertySymbol.selectedness] ? this : null ); } @@ -136,12 +126,13 @@ export default class HTMLOptionElement extends HTMLElement { } /** - * Triggered when an attribute is set. - * - * @param attribute Attribute. - * @param replacedAttribute Replaced attribute. + * @override */ - #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + public override [PropertySymbol.onSetAttribute]( + attribute: Attr, + replacedAttribute: Attr | null + ): void { + super[PropertySymbol.onSetAttribute](attribute, replacedAttribute); if ( !this[PropertySymbol.dirtyness] && attribute[PropertySymbol.name] === 'selected' && @@ -152,17 +143,16 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = true; if (selectNode) { - selectNode[PropertySymbol.options][PropertySymbol.updateSelectedness](this); + selectNode[PropertySymbol.updateSelectedness](this); } } } /** - * Triggered when an attribute is removed. - * - * @param removedAttribute Removed attribute. + * @override */ - #onRemoveAttribute(removedAttribute: Attr): void { + public override [PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void { + super[PropertySymbol.onRemoveAttribute](removedAttribute); if ( removedAttribute && !this[PropertySymbol.dirtyness] && @@ -173,7 +163,7 @@ export default class HTMLOptionElement extends HTMLElement { this[PropertySymbol.selectedness] = false; if (selectNode) { - selectNode[PropertySymbol.options][PropertySymbol.updateSelectedness](); + selectNode[PropertySymbol.updateSelectedness](); } } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index e7a37ec41..299c2e372 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -42,10 +42,6 @@ export default class HTMLScriptElement extends HTMLElement { super(); this.#browserFrame = browserFrame; - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); } /** @@ -249,17 +245,20 @@ export default class HTMLScriptElement extends HTMLElement { } /** - * Triggered when an attribute is set. - * - * @param item Item + * @override */ - #onSetAttribute(item: Attr): void { + public override [PropertySymbol.onSetAttribute]( + attribute: Attr, + replacedAttribute: Attr | null + ): void { + super[PropertySymbol.onSetAttribute](attribute, replacedAttribute); + if ( - item[PropertySymbol.name] === 'src' && - item[PropertySymbol.value] !== null && + attribute[PropertySymbol.name] === 'src' && + attribute[PropertySymbol.value] !== null && this[PropertySymbol.isConnected] ) { - this.#loadScript(item[PropertySymbol.value]); + this.#loadScript(attribute[PropertySymbol.value]); } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts index 23f81b070..6e2ae48e2 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts @@ -1,14 +1,8 @@ -import DOMException from '../../exception/DOMException.js'; import HTMLCollection from '../element/HTMLCollection.js'; import HTMLSelectElement from './HTMLSelectElement.js'; import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js'; -import Element from '../element/Element.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import HTMLElement from '../html-element/HTMLElement.js'; -import NodeTypeEnum from '../node/NodeTypeEnum.js'; -import Node from '../node/Node.js'; -import EventTarget from '../../event/EventTarget.js'; -import IHTMLCollectionObservedNode from '../element/IHTMLCollectionObservedNode.js'; +import QuerySelector from '../../query-selector/QuerySelector.js'; /** * HTML Options Collection. @@ -17,19 +11,22 @@ import IHTMLCollectionObservedNode from '../element/IHTMLCollectionObservedNode. * https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection. */ export default class HTMLOptionsCollection extends HTMLCollection { - #selectedIndex: number = -1; - #selectElement: HTMLSelectElement; - #observedSelectElement: IHTMLCollectionObservedNode | null = null; + private declare [PropertySymbol.ownerElement]: HTMLSelectElement; /** * Constructor. * - * @param selectElement Select element. + * @param ownerElement Select element. */ - constructor(selectElement: HTMLSelectElement) { - super(); + constructor(ownerElement: HTMLSelectElement) { + super( + () => + ( + QuerySelector.querySelectorAll(ownerElement, 'option')[PropertySymbol.items] + ) + ); - this.#selectElement = selectElement; + this[PropertySymbol.ownerElement] = ownerElement; } /** @@ -38,7 +35,7 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[i]; - option[PropertySymbol.selectedness] = false; - selectedOptions?.[PropertySymbol.removeItem](option); - } - this.#selectedIndex = -1; - return; - } - - const selectedOption = this[selectedIndex]; - - selectedOption[PropertySymbol.selectedness] = true; - selectedOption[PropertySymbol.dirtyness] = true; - - this[PropertySymbol.updateSelectedness](selectedOption); - } - - /** - * Returns item by index. - * - * @param index Index. - */ - public item(index: number): HTMLOptionElement { - return this[index]; + this[PropertySymbol.ownerElement].selectedIndex = selectedIndex; } /** @@ -85,37 +53,7 @@ export default class HTMLOptionsCollection extends HTMLCollectionbefore < 0) { - return; - } - - const optionsElement = this[before]; - - if (!optionsElement) { - throw new DOMException( - "Failed to execute 'add' on 'DOMException': The node before which the new node is to be inserted is not a child of this node." - ); - } - - this.#selectElement.insertBefore(element, optionsElement); - return; - } - - const index = this[PropertySymbol.indexOf](before); - - if (index === -1) { - throw new DOMException( - "Failed to execute 'add' on 'DOMException': The node before which the new node is to be inserted is not a child of this node." - ); - } - - this.#selectElement.insertBefore(element, this[index]); + this[PropertySymbol.ownerElement].add(element, before); } /** @@ -124,236 +62,6 @@ export default class HTMLOptionsCollection extends HTMLCollectionthis[index]); - } - } - - /** - * @override - */ - public [PropertySymbol.addItem](item: HTMLOptionElement): boolean { - if (!super[PropertySymbol.addItem](item)) { - return false; - } - - this.#selectElement[this.length - 1] = item; - - item[PropertySymbol.selectNode] = this.#selectElement; - this[PropertySymbol.updateSelectedness](item[PropertySymbol.selectedness] ? item : null); - - return true; - } - - /** - * @override - */ - public [PropertySymbol.insertItem]( - newItem: HTMLOptionElement, - referenceItem: HTMLOptionElement | null - ): boolean { - if (!super[PropertySymbol.insertItem](newItem, referenceItem)) { - return false; - } - - newItem[PropertySymbol.selectNode] = this.#selectElement; - - const index = this[PropertySymbol.indexOf](newItem); - - for (let i = index, max = this.length; i < max; i++) { - this.#selectElement[i] = this[i]; - } - - this[PropertySymbol.updateSelectedness](newItem[PropertySymbol.selectedness] ? newItem : null); - - return true; - } - - /** - * @override - */ - public [PropertySymbol.removeItem](item: HTMLOptionElement): boolean { - const index = this[PropertySymbol.indexOf](item); - - if (!super[PropertySymbol.removeItem](item)) { - return false; - } - - item[PropertySymbol.selectNode] = null; - - for (let i = index, max = this.length; i < max; i++) { - this.#selectElement[i] = this[i]; - } - - delete this.#selectElement[this.length]; - - this[PropertySymbol.updateSelectedness](); - - return true; - } - - /** - * Observes node. - * - * @returns Observed node. - */ - public [PropertySymbol.observe](): IHTMLCollectionObservedNode { - if (this.#observedSelectElement) { - return; - } - const observedNode = super[PropertySymbol.observe](this.#selectElement, { - subtree: true, - filter: (item) => item[PropertySymbol.tagName] === 'OPTION' - }); - - this.#observedSelectElement = observedNode; - - return observedNode; - } - - /** - * Unobserves node. - * - * @param observedNode Observed node. - */ - public [PropertySymbol.unobserve](): void { - if (!this.#observedSelectElement) { - return; - } - super[PropertySymbol.unobserve](this.#observedSelectElement); - } - - /** - * Sets named item property. - * - * @param name Name. - */ - protected [PropertySymbol.updateNamedItemProperty](name: string): void { - super[PropertySymbol.updateNamedItemProperty](name); - - if (this[name]) { - Object.defineProperty(this.#selectElement, name, { - value: this[name], - writable: false, - enumerable: true, - configurable: true - }); - } else { - delete this.#selectElement[name]; - } - } - - /** - * Returns "true" if the property name is valid. - * - * @param name Name. - * @returns True if the property name is valid. - */ - protected [PropertySymbol.isValidPropertyName](name: string): boolean { - return ( - !HTMLCollection.prototype.hasOwnProperty(name) && - !this.#selectElement.constructor.prototype.hasOwnProperty(name) && - !HTMLElement.constructor.prototype.hasOwnProperty(name) && - !Element.constructor.prototype.hasOwnProperty(name) && - !Node.constructor.hasOwnProperty(name) && - !EventTarget.constructor.hasOwnProperty(name) && - super[PropertySymbol.isValidPropertyName](name) - ); - } - - /** - * Updates option item. - * - * Based on: - * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLSelectElement-impl.js - * - * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm - * @param [selectedOption] Selected option. - */ - public [PropertySymbol.updateSelectedness](selectedOption?: HTMLOptionElement): void { - const isMultiple = this.#selectElement.hasAttribute('multiple'); - const selectedOptions = this.#selectElement[PropertySymbol.selectedOptions]; - const selected: HTMLOptionElement[] = []; - - if (selectedOptions) { - while (selectedOptions.length) { - selectedOptions[PropertySymbol.removeItem](selectedOptions[selectedOptions.length - 1]); - } - } - - this.#selectedIndex = -1; - - if (isMultiple) { - if (selectedOptions) { - for (let i = 0, max = this.length; i < max; i++) { - const option = this[i]; - if (option[PropertySymbol.selectedness]) { - selectedOptions[PropertySymbol.addItem](option); - } - } - } - } else { - for (let i = 0, max = this.length; i < max; i++) { - const option = this[i]; - if (selectedOption) { - option[PropertySymbol.selectedness] = option === selectedOption; - } - - if (option[PropertySymbol.selectedness]) { - selected.push(option); - - if (this.#selectedIndex === -1) { - this.#selectedIndex = i; - } - - selectedOptions?.[PropertySymbol.addItem](option); - } - } - } - - const size = this.#getDisplaySize(); - - if (size === 1 && !selected.length) { - for (let i = 0, max = this.length; i < max; i++) { - const option = this[i]; - const parentNode = option[PropertySymbol.parentNode]; - let disabled = option.hasAttributeNS(null, 'disabled'); - - if ( - parentNode && - parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - parentNode[PropertySymbol.tagName] === 'OPTGROUP' && - parentNode.hasAttributeNS(null, 'disabled') - ) { - disabled = true; - } - - if (!disabled) { - this.#selectedIndex = i; - option[PropertySymbol.selectedness] = true; - break; - } - } - } else if (selected.length >= 2) { - for (let i = 0, max = this.length; i < max; i++) { - (this[i])[PropertySymbol.selectedness] = i === selected.length - 1; - } - } - } - - /** - * Returns display size. - * - * @returns Display size. - */ - #getDisplaySize(): number { - const selectElement = this.#selectElement; - if (selectElement.hasAttributeNS(null, 'size')) { - const size = parseInt(selectElement.getAttribute('size')); - if (!isNaN(size) && size >= 0) { - return size; - } - } - return selectElement.hasAttributeNS(null, 'multiple') ? 4 : 1; + this[PropertySymbol.ownerElement].remove(index); } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index 7fd587a72..1eb6f9b2e 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -8,8 +8,10 @@ import HTMLOptionsCollection from './HTMLOptionsCollection.js'; import Event from '../../event/Event.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import HTMLCollection from '../element/HTMLCollection.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; -import NodeList from '../node/INodeList.js'; +import NodeTypeEnum from '../node/NodeTypeEnum.js'; +import QuerySelector from '../../query-selector/QuerySelector.js'; +import NodeList from '../node/NodeList.js'; +import DOMException from '../../exception/DOMException.js'; /** * HTML Select Element. @@ -22,8 +24,7 @@ export default class HTMLSelectElement extends HTMLElement { public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.options]: HTMLOptionsCollection = new HTMLOptionsCollection(this); - public [PropertySymbol.formNode]: HTMLFormElement | null = null; - public [PropertySymbol.selectedOptions]: IHTMLCollection | null = null; + public [PropertySymbol.selectedOptions]: HTMLCollection | null = null; // Events public onchange: (event: Event) => void | null = null; @@ -34,7 +35,126 @@ export default class HTMLSelectElement extends HTMLElement { */ constructor() { super(); - this[PropertySymbol.options][PropertySymbol.observe](); + this[PropertySymbol.options] = new HTMLOptionsCollection(this); + + const proxy = new Proxy(this, { + get: (target, property, reciever) => { + if (property in target || typeof property === 'symbol') { + return Reflect.get(target, property, reciever); + } + const index = Number(property); + if (!isNaN(index)) { + return QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items][index]; + } + }, + set(target, property, newValue, reciever): boolean { + if (property in target || typeof property === 'symbol') { + Reflect.set(target, property, newValue, reciever); + return true; + } + + const index = Number(property); + + if (isNaN(index) || !newValue || !(newValue instanceof HTMLOptionElement)) { + return false; + } + + const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items]; + + if (!options[index]) { + return false; + } + + const childNodes = target[PropertySymbol.nodeArray]; + + while (childNodes.length) { + target[PropertySymbol.removeChild](childNodes[0]); + } + + for (let i = 0, max = options.length; i < max; i++) { + target[PropertySymbol.appendChild](i === index ? newValue : options[i]); + } + + return true; + }, + deleteProperty(): boolean { + return false; + }, + ownKeys(target): string[] { + return Object.keys(QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items]); + }, + has(target, property): boolean { + if (property in target) { + return true; + } + + const index = Number(property); + + if (!isNaN(index)) { + return !!QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items][index]; + } + + return false; + }, + defineProperty(target, property, descriptor): boolean { + if (property in target) { + Reflect.defineProperty(target, property, descriptor); + return true; + } + + const index = Number(property); + + if (isNaN(index) || !descriptor.value || !(descriptor.value instanceof HTMLOptionElement)) { + return false; + } + + const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items]; + + if (!options[index]) { + return false; + } + + const childNodes = target[PropertySymbol.nodeArray]; + + while (childNodes.length) { + target[PropertySymbol.removeChild](childNodes[0]); + } + + for (let i = 0, max = options.length; i < max; i++) { + target[PropertySymbol.appendChild](i === index ? descriptor.value : options[i]); + } + + return true; + }, + getOwnPropertyDescriptor(target, property): PropertyDescriptor { + if (property in target) { + return Reflect.getOwnPropertyDescriptor(target, property); + } + + const index = Number(property); + + if (isNaN(index)) { + return; + } + + const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items]; + + if (!options[index]) { + return; + } + + return { + value: options[index], + writable: true, + enumerable: true, + configurable: true + }; + } + }); + + this[PropertySymbol.selectNode] = proxy; + + return proxy; } /** @@ -43,7 +163,7 @@ export default class HTMLSelectElement extends HTMLElement { * @returns Length. */ public get length(): number { - return this[PropertySymbol.options].length; + return QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items].length; } /** @@ -194,8 +314,10 @@ export default class HTMLSelectElement extends HTMLElement { * @returns Value. */ public get value(): string { - for (let i = 0, max = this[PropertySymbol.options].length; i < max; i++) { - const option = this[PropertySymbol.options][i]; + const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items]; + + for (let i = 0, max = options.length; i < max; i++) { + const option = options[i]; if (option[PropertySymbol.selectedness]) { return option.value; } @@ -210,8 +332,10 @@ export default class HTMLSelectElement extends HTMLElement { * @param value Value. */ public set value(value: string) { - for (let i = 0, max = this[PropertySymbol.options].length; i < max; i++) { - const option = this[PropertySymbol.options][i]; + const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items]; + + for (let i = 0, max = options.length; i < max; i++) { + const option = options[i]; if (option.value === value) { option[PropertySymbol.selectedness] = true; option[PropertySymbol.dirtyness] = true; @@ -227,7 +351,15 @@ export default class HTMLSelectElement extends HTMLElement { * @returns Value. */ public get selectedIndex(): number { - return this[PropertySymbol.options].selectedIndex; + const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items]; + + for (let i = 0, max = options.length; i < max; i++) { + if ((options[i])[PropertySymbol.selectedness]) { + return i; + } + } + + return -1; } /** @@ -236,7 +368,19 @@ export default class HTMLSelectElement extends HTMLElement { * @param selectedIndex Selected index. */ public set selectedIndex(selectedIndex: number) { - this[PropertySymbol.options].selectedIndex = selectedIndex; + const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items]; + + if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) { + for (let i = 0, max = options.length; i < max; i++) { + (options[i])[PropertySymbol.selectedness] = false; + } + + const selectedOption = options[selectedIndex]; + if (selectedOption) { + selectedOption[PropertySymbol.selectedness] = true; + selectedOption[PropertySymbol.dirtyness] = true; + } + } } /** @@ -244,15 +388,31 @@ export default class HTMLSelectElement extends HTMLElement { * * @returns HTMLCollection. */ - public get selectedOptions(): IHTMLCollection { + public get selectedOptions(): HTMLCollection { if (!this[PropertySymbol.selectedOptions]) { - this[PropertySymbol.selectedOptions] = new HTMLCollection(); - for (const option of this[PropertySymbol.options]) { - if (option[PropertySymbol.selectedness]) { - this[PropertySymbol.selectedOptions][PropertySymbol.addItem](option); + this[PropertySymbol.selectedOptions] = new HTMLCollection(() => { + const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items]; + + // If we hit the cache, we should recieve the same Array instance as before, which will then contain the selected options. + if (options[PropertySymbol.selectedOptions]) { + return options[PropertySymbol.selectedOptions]; } - } + + const selectedOptions = []; + + for (let i = 0, max = options.length; i < max; i++) { + const option = options[i]; + if (option[PropertySymbol.selectedness]) { + selectedOptions.push(option); + } + } + + options[PropertySymbol.selectedOptions] = selectedOptions; + + return selectedOptions; + }); } + return this[PropertySymbol.selectedOptions]; } @@ -271,7 +431,14 @@ export default class HTMLSelectElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - return this[PropertySymbol.formNode]; + if (this[PropertySymbol.formNode]) { + return this[PropertySymbol.formNode]; + } + const id = this.attributes['form']?.[PropertySymbol.value]; + if (!id || !this[PropertySymbol.isConnected]) { + return null; + } + return this[PropertySymbol.ownerDocument].getElementById(id); } /** @@ -305,7 +472,54 @@ export default class HTMLSelectElement extends HTMLElement { * @param before HTMLOptionElement or index number. */ public add(element: HTMLOptionElement, before?: number | HTMLOptionElement): void { - this[PropertySymbol.options].add(element, before); + const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items]; + + if (!before && before !== 0) { + const childNodes = this[PropertySymbol.nodeArray]; + + while (childNodes.length) { + this[PropertySymbol.removeChild](childNodes[0]); + } + + for (const option of options) { + this[PropertySymbol.appendChild](option); + } + + this[PropertySymbol.appendChild](element); + + return; + } + + if (typeof before !== 'number') { + if (!(before instanceof HTMLOptionElement)) { + throw new DOMException( + "Failed to execute 'add' on 'HTMLFormElement': The node before which the new node is to be inserted before is not an 'HTMLOptionElement'." + ); + } + + before = options.indexOf(before); + } + + const optionsElement = options[before]; + + if (!optionsElement) { + throw new DOMException( + "Failed to execute 'add' on 'HTMLFormElement': The node before which the new node is to be inserted before is not a child of this node." + ); + } + + const childNodes = this[PropertySymbol.nodeArray]; + + while (childNodes.length) { + this[PropertySymbol.removeChild](childNodes[0]); + } + + for (let i = 0, max = options.length; i < max; i++) { + if (i === before) { + this[PropertySymbol.appendChild](element); + } + this[PropertySymbol.appendChild](options[i]); + } } /** @@ -315,7 +529,23 @@ export default class HTMLSelectElement extends HTMLElement { */ public override remove(index?: number): void { if (typeof index === 'number') { - this[PropertySymbol.options].remove(index); + const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items]; + + if (!options[index]) { + return; + } + + const childNodes = this[PropertySymbol.nodeArray]; + + while (childNodes.length) { + this[PropertySymbol.removeChild](childNodes[0]); + } + + for (let i = 0, max = options.length; i < max; i++) { + if (i !== index) { + this[PropertySymbol.appendChild](options[i]); + } + } } else { super.remove(); } @@ -351,4 +581,76 @@ export default class HTMLSelectElement extends HTMLElement { public reportValidity(): boolean { return this.checkValidity(); } + + /** + * Updates option item. + * + * Based on: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLSelectElement-impl.js + * + * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm + * @param [selectedOption] Selected option. + */ + public [PropertySymbol.updateSelectedness](selectedOption?: HTMLOptionElement): void { + const isMultiple = this.hasAttribute('multiple'); + const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items]; + const selected: HTMLOptionElement[] = []; + + if (!isMultiple) { + for (let i = 0, max = options.length; i < max; i++) { + const option = options[i]; + + if (selectedOption) { + option[PropertySymbol.selectedness] = option === selectedOption; + } + + if (option[PropertySymbol.selectedness]) { + selected.push(option); + } + } + } + + const size = this[PropertySymbol.getDisplaySize](); + + if (size === 1 && !selected.length) { + for (let i = 0, max = options.length; i < max; i++) { + const option = options[i]; + const parentNode = option[PropertySymbol.parentNode]; + let disabled = option.hasAttributeNS(null, 'disabled'); + + if ( + parentNode && + parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && + parentNode[PropertySymbol.tagName] === 'OPTGROUP' && + parentNode.hasAttributeNS(null, 'disabled') + ) { + disabled = true; + } + + if (!disabled) { + option[PropertySymbol.selectedness] = true; + break; + } + } + } else if (selected.length >= 2) { + for (let i = 0, max = options.length; i < max; i++) { + (options[i])[PropertySymbol.selectedness] = i === selected.length - 1; + } + } + } + + /** + * Returns display size. + * + * @returns Display size. + */ + private [PropertySymbol.getDisplaySize](): number { + if (this.hasAttributeNS(null, 'size')) { + const size = parseInt(this.getAttribute('size')); + if (!isNaN(size) && size >= 0) { + return size; + } + } + return this.hasAttributeNS(null, 'multiple') ? 4 : 1; + } } diff --git a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts index 186bbc86b..4fefccbed 100644 --- a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts +++ b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts @@ -20,23 +20,6 @@ export default class HTMLSlotElement extends HTMLElement { // Events public onslotchange: (event: Event) => void | null = null; - /** - * - */ - constructor() { - super(); - - // Attribute listeners - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'set', - this.#onSetAttribute.bind(this) - ); - this[PropertySymbol.attributes][PropertySymbol.addEventListener]( - 'remove', - this.#onRemoveAttribute.bind(this) - ); - } - /** * Returns name. * @@ -94,12 +77,13 @@ export default class HTMLSlotElement extends HTMLElement { } /** - * Triggered when an attribute is set. - * - * @param attribute Attribute. - * @param replacedAttribute Replaced attribute. + * @override */ - #onSetAttribute(attribute: Attr, replacedAttribute: Attr | null): void { + public override [PropertySymbol.onSetAttribute]( + attribute: Attr, + replacedAttribute: Attr | null + ): void { + super[PropertySymbol.onSetAttribute](attribute, replacedAttribute); if ( attribute[PropertySymbol.name] === 'name' && attribute[PropertySymbol.value] !== replacedAttribute?.[PropertySymbol.value] @@ -121,11 +105,10 @@ export default class HTMLSlotElement extends HTMLElement { } /** - * Triggered when an attribute is set. - * - * @param removedAttribute Attribute. + * @override */ - #onRemoveAttribute(removedAttribute: Attr): void { + public override [PropertySymbol.onRemoveAttribute](removedAttribute: Attr): void { + super[PropertySymbol.onRemoveAttribute](removedAttribute); if ( removedAttribute[PropertySymbol.name] === 'name' && removedAttribute[PropertySymbol.value] && @@ -154,9 +137,9 @@ export default class HTMLSlotElement extends HTMLElement { const assignedElements = []; - for (const slotNode of (host)[PropertySymbol.childNodes]) { + for (const slotNode of (host)[PropertySymbol.nodeArray]) { if (name && slotNode['slot'] && slotNode['slot'] === name) { - for (const child of slotNode[PropertySymbol.childNodes]) { + for (const child of slotNode[PropertySymbol.nodeArray]) { assignedElements.push(child); } } else if (!name && !slotNode['slot']) { @@ -186,9 +169,9 @@ export default class HTMLSlotElement extends HTMLElement { const assignedElements = []; - for (const slotElement of (host)[PropertySymbol.children]) { + for (const slotElement of (host)[PropertySymbol.elementArray]) { if (name && slotElement.slot === name) { - for (const child of slotElement[PropertySymbol.children]) { + for (const child of slotElement[PropertySymbol.elementArray]) { assignedElements.push(child); } } else if (!name && !slotElement.slot) { diff --git a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts index b96db9479..e142d2765 100644 --- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts +++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts @@ -1,10 +1,6 @@ import CSSStyleSheet from '../../css/CSSStyleSheet.js'; -import MutationRecord from '../../mutation-observer/MutationRecord.js'; import * as PropertySymbol from '../../PropertySymbol.js'; -import Element from '../element/Element.js'; import HTMLElement from '../html-element/HTMLElement.js'; -import NodeTypeEnum from '../node/NodeTypeEnum.js'; -import Text from '../text/Text.js'; /** * HTML Style Element. @@ -14,35 +10,7 @@ import Text from '../text/Text.js'; */ export default class HTMLStyleElement extends HTMLElement { private [PropertySymbol.sheet]: CSSStyleSheet | null = null; - - /** - * Constructor. - */ - constructor() { - super(); - - this[PropertySymbol.observeMutations]({ - options: { - childList: true, - subtree: true - }, - callback: new WeakRef((record: MutationRecord) => { - const node = record.addedNodes[0] || record.removedNodes[0]; - if (node instanceof Text) { - node[PropertySymbol.styleNode] = record.addedNodes[0] ? this : null; - this[PropertySymbol.updateSheet](); - } else { - const textNodes = this.#findTextNodes(node); - if (textNodes.length) { - for (const textNode of textNodes) { - textNode[PropertySymbol.styleNode] = record.addedNodes[0] ? this : null; - } - this[PropertySymbol.updateSheet](); - } - } - }) - }); - } + public [PropertySymbol.styleNode] = this; /** * Returns CSS style sheet. @@ -136,24 +104,4 @@ export default class HTMLStyleElement extends HTMLElement { this[PropertySymbol.sheet].replaceSync(this.textContent); } } - - /** - * Finds all text nodes in the element. - * - * @param parentElement Parent element. - * @returns Text nodes. - */ - #findTextNodes(parentElement: Element): Text[] { - const textNodes: Text[] = []; - for (const childNode of parentElement[PropertySymbol.childNodes]) { - if (childNode instanceof Text) { - textNodes.push(childNode); - } else if (childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - for (const textNode of this.#findTextNodes(childNode)) { - textNodes.push(textNode); - } - } - } - return textNodes; - } } diff --git a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts index 1231f28fd..b10532cf4 100644 --- a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts +++ b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts @@ -40,7 +40,7 @@ export default class HTMLTemplateElement extends HTMLElement { */ public set innerHTML(html: string) { const content = this[PropertySymbol.content]; - const childNodes = content[PropertySymbol.childNodes]; + const childNodes = content[PropertySymbol.nodeArray]; while (childNodes.length) { content.removeChild(childNodes[0]); @@ -75,7 +75,7 @@ export default class HTMLTemplateElement extends HTMLElement { }); const content = this[PropertySymbol.content]; let xml = ''; - for (const node of content[PropertySymbol.childNodes]) { + for (const node of content[PropertySymbol.nodeArray]) { xml += xmlSerializer.serializeToString(node); } return xml; diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index fa474ead8..854c76201 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -9,11 +9,7 @@ import HTMLInputElementSelectionModeEnum from '../html-input-element/HTMLInputEl import ValidityState from '../../validity-state/ValidityState.js'; import HTMLLabelElement from '../html-label-element/HTMLLabelElement.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; -import Text from '../text/Text.js'; -import INodeList from '../node/INodeList.js'; -import MutationRecord from '../../mutation-observer/MutationRecord.js'; -import Element from '../element/Element.js'; -import NodeTypeEnum from '../node/NodeTypeEnum.js'; +import NodeList from '../node/NodeList.js'; /** * HTML Text Area Element. @@ -34,7 +30,7 @@ export default class HTMLTextAreaElement extends HTMLElement { public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.value] = null; - public [PropertySymbol.textAreaNode]: HTMLTextAreaElement = this; + public [PropertySymbol.textAreaNode] = this; public [PropertySymbol.formNode]: HTMLFormElement | null = null; // Private properties @@ -42,35 +38,6 @@ export default class HTMLTextAreaElement extends HTMLElement { #selectionEnd = null; #selectionDirection = HTMLInputElementSelectionDirectionEnum.none; - /** - * Constructor. - */ - constructor() { - super(); - - this[PropertySymbol.observeMutations]({ - options: { - childList: true, - subtree: true - }, - callback: new WeakRef((record: MutationRecord) => { - const node = record.addedNodes[0] || record.removedNodes[0]; - if (node instanceof Text) { - node[PropertySymbol.textAreaNode] = record.addedNodes[0] ? this : null; - this[PropertySymbol.resetSelection](); - } else if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - const textNodes = this.#findTextNodes(node); - if (textNodes.length) { - for (const textNode of textNodes) { - textNode[PropertySymbol.textAreaNode] = record.addedNodes[0] ? this : null; - } - this[PropertySymbol.resetSelection](); - } - } - }) - }); - } - /** * Returns validation message. * @@ -444,7 +411,14 @@ export default class HTMLTextAreaElement extends HTMLElement { * @returns Form. */ public get form(): HTMLFormElement { - return this[PropertySymbol.formNode]; + if (this[PropertySymbol.formNode]) { + return this[PropertySymbol.formNode]; + } + const id = this.attributes['form']?.[PropertySymbol.value]; + if (!id || !this[PropertySymbol.isConnected]) { + return null; + } + return this[PropertySymbol.ownerDocument].getElementById(id); } /** @@ -461,7 +435,7 @@ export default class HTMLTextAreaElement extends HTMLElement { * * @returns Label elements. */ - public get labels(): INodeList { + public get labels(): NodeList { return HTMLLabelElementUtility.getAssociatedLabelElements(this); } @@ -619,24 +593,4 @@ export default class HTMLTextAreaElement extends HTMLElement { this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; } } - - /** - * Finds all text nodes in the element. - * - * @param parentElement Parent element. - * @returns Text nodes. - */ - #findTextNodes(parentElement: Element): Text[] { - const textNodes: Text[] = []; - for (const childNode of parentElement[PropertySymbol.childNodes]) { - if (childNode instanceof Text) { - textNodes.push(childNode); - } else if (childNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { - for (const textNode of this.#findTextNodes(childNode)) { - textNodes.push(textNode); - } - } - } - return textNodes; - } } diff --git a/packages/happy-dom/src/nodes/node/ICachedComputedStyleResult.ts b/packages/happy-dom/src/nodes/node/ICachedComputedStyleResult.ts new file mode 100644 index 000000000..edf9b0a98 --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedComputedStyleResult.ts @@ -0,0 +1,6 @@ +import CSSStyleDeclarationPropertyManager from '../../css/declaration/property-manager/CSSStyleDeclarationPropertyManager.js'; +import ICachedResult from './ICachedResult.js'; + +export default interface ICachedComputedStyleResult extends ICachedResult { + result: WeakRef | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedElementByIdResult.ts b/packages/happy-dom/src/nodes/node/ICachedElementByIdResult.ts new file mode 100644 index 000000000..577f8cb7a --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedElementByIdResult.ts @@ -0,0 +1,6 @@ +import Element from '../element/Element.js'; +import ICachedResult from './ICachedResult.js'; + +export default interface ICachedElementByIdResult extends ICachedResult { + result: WeakRef | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedElementByTagNameResult.ts b/packages/happy-dom/src/nodes/node/ICachedElementByTagNameResult.ts new file mode 100644 index 000000000..50237368b --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedElementByTagNameResult.ts @@ -0,0 +1,6 @@ +import Element from '../element/Element.js'; +import ICachedResult from './ICachedResult.js'; + +export default interface ICachedElementByTagNameResult extends ICachedResult { + result: WeakRef | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedElementsByTagNameResult.ts b/packages/happy-dom/src/nodes/node/ICachedElementsByTagNameResult.ts new file mode 100644 index 000000000..9c4e0de91 --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedElementsByTagNameResult.ts @@ -0,0 +1,6 @@ +import Element from '../element/Element.js'; +import ICachedResult from './ICachedResult.js'; + +export default interface ICachedElementsByTagNameResult extends ICachedResult { + result: WeakRef | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedMatchesItem.ts b/packages/happy-dom/src/nodes/node/ICachedMatchesResult.ts similarity index 50% rename from packages/happy-dom/src/nodes/node/ICachedMatchesItem.ts rename to packages/happy-dom/src/nodes/node/ICachedMatchesResult.ts index efbfe439e..f46b8d25e 100644 --- a/packages/happy-dom/src/nodes/node/ICachedMatchesItem.ts +++ b/packages/happy-dom/src/nodes/node/ICachedMatchesResult.ts @@ -1,5 +1,6 @@ import ISelectorMatch from '../../query-selector/ISelectorMatch.js'; +import ICachedResult from './ICachedResult.js'; -export default interface ICachedMatchesItem { +export default interface ICachedMatchesResult extends ICachedResult { result: { match: ISelectorMatch | null } | null; } diff --git a/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllItem.ts b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllItem.ts deleted file mode 100644 index 76f1f20ec..000000000 --- a/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllItem.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Element from '../element/Element.js'; -import INodeList from './INodeList.js'; - -export default interface ICachedQuerySelectorAllItem { - result: WeakRef> | null; -} diff --git a/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllResult.ts b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllResult.ts new file mode 100644 index 000000000..3f1b49654 --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorAllResult.ts @@ -0,0 +1,7 @@ +import Element from '../element/Element.js'; +import ICachedResult from './ICachedResult.js'; +import NodeList from './NodeList.js'; + +export default interface ICachedQuerySelectorAllResult extends ICachedResult { + result: WeakRef> | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedQuerySelectorItem.ts b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorItem.ts deleted file mode 100644 index 5d064cf4d..000000000 --- a/packages/happy-dom/src/nodes/node/ICachedQuerySelectorItem.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Element from '../element/Element.js'; - -export default interface ICachedQuerySelectorItem { - result: WeakRef | null; -} diff --git a/packages/happy-dom/src/nodes/node/ICachedQuerySelectorResult.ts b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorResult.ts new file mode 100644 index 000000000..28bc64df6 --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedQuerySelectorResult.ts @@ -0,0 +1,6 @@ +import Element from '../element/Element.js'; +import ICachedResult from './ICachedResult.js'; + +export default interface ICachedQuerySelectorResult extends ICachedResult { + result: WeakRef | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedResult.ts b/packages/happy-dom/src/nodes/node/ICachedResult.ts new file mode 100644 index 000000000..7fab994a2 --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedResult.ts @@ -0,0 +1,3 @@ +export default interface ICachedResult { + result: any | null; +} diff --git a/packages/happy-dom/src/nodes/node/ICachedStyleResult.ts b/packages/happy-dom/src/nodes/node/ICachedStyleResult.ts new file mode 100644 index 000000000..b66d70cda --- /dev/null +++ b/packages/happy-dom/src/nodes/node/ICachedStyleResult.ts @@ -0,0 +1,6 @@ +import CSSStyleDeclarationPropertyManager from '../../css/declaration/property-manager/CSSStyleDeclarationPropertyManager.js'; +import ICachedResult from './ICachedResult.js'; + +export default interface ICachedStyleResult extends ICachedResult { + result: WeakRef | null; +} diff --git a/packages/happy-dom/src/nodes/node/INodeList.ts b/packages/happy-dom/src/nodes/node/INodeList.ts deleted file mode 100644 index b6c18daa3..000000000 --- a/packages/happy-dom/src/nodes/node/INodeList.ts +++ /dev/null @@ -1,115 +0,0 @@ -import * as PropertySymbol from '../../PropertySymbol.js'; - -/** - * NodeList. - * - * This interface is used to hide Array methods from the outside. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList - */ -export default interface INodeList { - readonly [index: number]: T; - - /** - * The number of items in the NodeList. - */ - readonly length: number; - - /** - * Returns `Symbol.toStringTag`. - * - * @returns `Symbol.toStringTag`. - */ - readonly [Symbol.toStringTag]: string; - - /** - * Returns `[object NodeList]`. - * - * @returns `[object NodeList]`. - */ - toLocaleString(): string; - - /** - * Returns `[object NodeList]`. - * - * @returns `[object NodeList]`. - */ - toString(): string; - - /** - * Returns item by index. - * - * @param index Index. - */ - item(index: number): T; - - /** - * Appends item. - * - * @param item Item. - * @returns True if added. - */ - [PropertySymbol.addItem](item: T): boolean; - - /** - * Inserts item before another item. - * - * @param newItem New item. - * @param [referenceItem] Reference item. - * @returns True if inserted. - */ - [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean; - - /** - * Removes item. - * - * @param item Item. - * @returns True if removed. - */ - [PropertySymbol.removeItem](item: T): boolean; - - /** - * Index of item. - * - * @param item Item. - * @returns Index. - */ - [PropertySymbol.indexOf](item: T): number; - - /** - * Returns true if the item is in the list. - * - * @param item Item. - * @returns True if the item is in the list. - */ - [PropertySymbol.includes](item: T): boolean; - - /** - * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. - * - * @returns Iterator. - */ - [Symbol.iterator](): IterableIterator; - - /** - * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. - * - * @returns Iterator. - */ - values(): IterableIterator; - - /** - * Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object. - * - * @returns Iterator. - * - */ - keys(): IterableIterator; - - /** - * Returns an iterator, allowing you to go through all key/value pairs contained in this object. - * - * @returns Iterator. - */ - entries(): IterableIterator<[number, T]>; -} diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 25ea000b9..4c420fa4a 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -12,11 +12,20 @@ import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import INodeList from './INodeList.js'; import IMutationListener from '../../mutation-observer/IMutationListener.js'; -import ICachedQuerySelectorAllItem from './ICachedQuerySelectorAllItem.js'; -import ICachedQuerySelectorItem from './ICachedQuerySelectorItem.js'; -import ICachedMatchesItem from './ICachedMatchesItem.js'; +import ICachedQuerySelectorAllResult from './ICachedQuerySelectorAllResult.js'; +import ICachedQuerySelectorResult from './ICachedQuerySelectorResult.js'; +import ICachedMatchesResult from './ICachedMatchesResult.js'; +import ICachedElementsByTagNameResult from './ICachedElementsByTagNameResult.js'; +import ICachedElementByTagNameResult from './ICachedElementByTagNameResult.js'; +import ICachedStyleResult from './ICachedStyleResult.js'; +import ICachedComputedStyleResult from './ICachedComputedStyleResult.js'; +import ICachedResult from './ICachedResult.js'; +import ICachedElementByIdResult from './ICachedElementByIdResult.js'; +import HTMLStyleElement from '../html-style-element/HTMLStyleElement.js'; +import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; +import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; +import HTMLTextAreaElement from '../html-text-area-element/HTMLTextAreaElement.js'; /** * Node. @@ -66,29 +75,36 @@ export default class Node extends EventTarget { public [PropertySymbol.parentNode]: Node | null = null; public [PropertySymbol.nodeType]: NodeTypeEnum; public [PropertySymbol.rootNode]: Node = null; + public [PropertySymbol.styleNode]: HTMLStyleElement | null = null; + public [PropertySymbol.textAreaNode]: HTMLTextAreaElement | null = null; + public [PropertySymbol.formNode]: HTMLFormElement | null = null; + public [PropertySymbol.selectNode]: HTMLSelectElement | null = null; public [PropertySymbol.mutationListeners]: IMutationListener[] = []; - public [PropertySymbol.childNodes]: INodeList = new NodeList(); - public [PropertySymbol.querySelectorCache]: { - items: Map; - affectedItems: ICachedQuerySelectorItem[]; + public [PropertySymbol.nodeArray]: Node[] = []; + public [PropertySymbol.elementArray]: Element[] = []; + public [PropertySymbol.childNodes] = new NodeList(this[PropertySymbol.nodeArray]); + public [PropertySymbol.cache]: { + querySelector: Map; + querySelectorAll: Map; + matches: Map; + elementsByTagName: Map; + elementsByTagNameNS: Map; + elementByTagName: Map; + elementById: Map; + style: ICachedStyleResult | null; + computedStyle: ICachedComputedStyleResult | null; } = { - items: new Map(), - affectedItems: [] - }; - public [PropertySymbol.querySelectorAllCache]: { - items: Map; - affectedItems: ICachedQuerySelectorAllItem[]; - } = { - items: new Map(), - affectedItems: [] - }; - public [PropertySymbol.matchesCache]: { - items: Map; - affectedItems: ICachedMatchesItem[]; - } = { - items: new Map(), - affectedItems: [] + querySelector: new Map(), + querySelectorAll: new Map(), + matches: new Map(), + elementsByTagName: new Map(), + elementsByTagNameNS: new Map(), + elementByTagName: new Map(), + elementById: new Map(), + style: null, + computedStyle: null }; + public [PropertySymbol.affectsCache]: ICachedResult[] = []; /** * Constructor. @@ -160,7 +176,7 @@ export default class Node extends EventTarget { * * @returns Child nodes list. */ - public get childNodes(): INodeList { + public get childNodes(): NodeList { return this[PropertySymbol.childNodes]; } @@ -216,11 +232,10 @@ export default class Node extends EventTarget { */ public get previousSibling(): Node { if (this[PropertySymbol.parentNode]) { - const index = (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][ - PropertySymbol.indexOf - ](this); + const nodeArray = this[PropertySymbol.parentNode][PropertySymbol.nodeArray]; + const index = nodeArray.indexOf(this); if (index > 0) { - return (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][index - 1]; + return nodeArray[index - 1]; } } return null; @@ -233,14 +248,10 @@ export default class Node extends EventTarget { */ public get nextSibling(): Node { if (this[PropertySymbol.parentNode]) { - const index = (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][ - PropertySymbol.indexOf - ](this); - if ( - index > -1 && - index + 1 < (this[PropertySymbol.parentNode])[PropertySymbol.childNodes].length - ) { - return (this[PropertySymbol.parentNode])[PropertySymbol.childNodes][index + 1]; + const nodeArray = this[PropertySymbol.parentNode][PropertySymbol.nodeArray]; + const index = nodeArray.indexOf(this); + if (index > -1 && index + 1 < nodeArray.length) { + return nodeArray[index + 1]; } } return null; @@ -252,8 +263,9 @@ export default class Node extends EventTarget { * @returns Node. */ public get firstChild(): Node { - if (this[PropertySymbol.childNodes].length > 0) { - return this[PropertySymbol.childNodes][0]; + const nodeArray = this[PropertySymbol.nodeArray]; + if (nodeArray.length > 0) { + return nodeArray[0]; } return null; } @@ -264,8 +276,9 @@ export default class Node extends EventTarget { * @returns Node. */ public get lastChild(): Node { - if (this[PropertySymbol.childNodes].length > 0) { - return this[PropertySymbol.childNodes][this[PropertySymbol.childNodes].length - 1]; + const nodeArray = this[PropertySymbol.nodeArray]; + if (nodeArray.length > 0) { + return nodeArray[nodeArray.length - 1]; } return null; } @@ -312,7 +325,7 @@ export default class Node extends EventTarget { * @returns "true" if the node has child nodes. */ public hasChildNodes(): boolean { - return this[PropertySymbol.childNodes].length > 0; + return this[PropertySymbol.nodeArray].length > 0; } /** @@ -432,18 +445,22 @@ export default class Node extends EventTarget { ); // Document has childNodes directly when it is created - if (clone[PropertySymbol.childNodes].length) { - const childNodes = clone[PropertySymbol.childNodes]; + if (clone[PropertySymbol.nodeArray].length) { + const childNodes = clone[PropertySymbol.nodeArray]; while (childNodes.length) { clone.removeChild(childNodes[0]); } } if (deep) { - for (const childNode of this[PropertySymbol.childNodes]) { + for (const childNode of this[PropertySymbol.nodeArray]) { const childClone = childNode.cloneNode(true); childClone[PropertySymbol.parentNode] = clone; - clone[PropertySymbol.childNodes][PropertySymbol.addItem](childClone); + clone[PropertySymbol.nodeArray].push(childClone); + + if (childClone[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + clone[PropertySymbol.elementArray].push(childClone); + } } } @@ -473,7 +490,7 @@ export default class Node extends EventTarget { // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode) { - const childNodes = node[PropertySymbol.childNodes]; + const childNodes = node[PropertySymbol.nodeArray]; while (childNodes.length) { this.appendChild(childNodes[0]); } @@ -489,7 +506,11 @@ export default class Node extends EventTarget { node[PropertySymbol.clearCache](); - this[PropertySymbol.childNodes][PropertySymbol.addItem](node); + this[PropertySymbol.nodeArray].push(node); + + if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + this[PropertySymbol.elementArray].push(node); + } node[PropertySymbol.connectedToNode](); @@ -522,7 +543,22 @@ export default class Node extends EventTarget { node[PropertySymbol.clearCache](); - this[PropertySymbol.childNodes][PropertySymbol.removeItem](node); + const index = this[PropertySymbol.nodeArray].indexOf(node); + + if (index === -1) { + throw new DOMException( + `Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.` + ); + } + + this[PropertySymbol.nodeArray].splice(index, 1); + + if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + const index = this[PropertySymbol.elementArray].indexOf(node); + if (index !== -1) { + this[PropertySymbol.elementArray].splice(index, 1); + } + } node[PropertySymbol.disconnectedFromNode](); @@ -561,7 +597,7 @@ export default class Node extends EventTarget { // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment if (newNode[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode) { - const childNodes = (newNode)[PropertySymbol.childNodes]; + const childNodes = (newNode)[PropertySymbol.nodeArray]; while (childNodes.length > 0) { this.insertBefore(childNodes[0], referenceNode); } @@ -575,7 +611,9 @@ export default class Node extends EventTarget { return newNode; } - if (!this[PropertySymbol.childNodes][PropertySymbol.includes](referenceNode)) { + const index = this[PropertySymbol.nodeArray].indexOf(referenceNode); + + if (index === -1) { throw new DOMException( "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node." ); @@ -589,7 +627,14 @@ export default class Node extends EventTarget { newNode[PropertySymbol.clearCache](); - this[PropertySymbol.childNodes][PropertySymbol.insertItem](newNode, referenceNode); + this[PropertySymbol.nodeArray].splice(index, 0, newNode); + + if (newNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { + const index = this[PropertySymbol.elementArray].indexOf(referenceNode); + if (index !== -1) { + this[PropertySymbol.elementArray].splice(index, 0, newNode); + } + } newNode[PropertySymbol.connectedToNode](); @@ -655,7 +700,7 @@ export default class Node extends EventTarget { public [PropertySymbol.observeMutations](listener: IMutationListener): void { this[PropertySymbol.mutationListeners].push(listener); if (listener.options.subtree) { - for (const node of this[PropertySymbol.childNodes]) { + for (const node of this[PropertySymbol.nodeArray]) { (node)[PropertySymbol.observeMutations](listener); } } @@ -674,7 +719,7 @@ export default class Node extends EventTarget { this[PropertySymbol.mutationListeners].splice(index, 1); } if (listener.options.subtree) { - for (const node of this[PropertySymbol.childNodes]) { + for (const node of this[PropertySymbol.nodeArray]) { node[PropertySymbol.unobserveMutations](listener); } } @@ -733,68 +778,112 @@ export default class Node extends EventTarget { * Clears query selector cache. */ public [PropertySymbol.clearCache](): void { - if (this[PropertySymbol.querySelectorCache].items.size) { - for (const item of this[PropertySymbol.querySelectorCache].items.values()) { + const cache = this[PropertySymbol.cache]; + + if (cache.querySelector.size) { + for (const item of cache.querySelector.values()) { + if (item.result) { + item.result = null; + } + } + cache.querySelector = new Map(); + } + + if (cache.querySelectorAll.size) { + for (const item of cache.querySelectorAll.values()) { if (item.result) { item.result = null; } } - this[PropertySymbol.querySelectorCache].items = new Map(); + cache.querySelectorAll = new Map(); } - if (this[PropertySymbol.querySelectorCache].affectedItems.length) { - for (const item of this[PropertySymbol.querySelectorCache].affectedItems) { + if (cache.matches.size) { + for (const item of cache.matches.values()) { if (item.result) { item.result = null; } } - this[PropertySymbol.querySelectorCache].affectedItems = []; + cache.matches = new Map(); } - if (this[PropertySymbol.querySelectorAllCache].items.size) { - for (const item of this[PropertySymbol.querySelectorAllCache].items.values()) { + if (cache.elementsByTagName.size) { + for (const item of cache.elementsByTagName.values()) { if (item.result) { item.result = null; } } - this[PropertySymbol.querySelectorAllCache].items = new Map(); + cache.elementsByTagName = new Map(); } - if (this[PropertySymbol.querySelectorAllCache].affectedItems.length) { - for (const item of this[PropertySymbol.querySelectorAllCache].affectedItems) { + if (cache.elementsByTagNameNS.size) { + for (const item of cache.elementsByTagNameNS.values()) { if (item.result) { item.result = null; } } - this[PropertySymbol.querySelectorAllCache].affectedItems = []; + cache.elementsByTagNameNS = new Map(); } - if (this[PropertySymbol.matchesCache].items.size) { - for (const item of this[PropertySymbol.matchesCache].items.values()) { + if (cache.elementByTagName.size) { + for (const item of cache.elementByTagName.values()) { if (item.result) { item.result = null; } } - this[PropertySymbol.matchesCache].items = new Map(); + cache.elementByTagName = new Map(); } - if (this[PropertySymbol.matchesCache].affectedItems.length) { - for (const item of this[PropertySymbol.matchesCache].affectedItems) { + if (cache.elementById.size) { + for (const item of cache.elementById.values()) { if (item.result) { item.result = null; } } - this[PropertySymbol.matchesCache].affectedItems = []; + cache.elementById = new Map(); + } + + for (const item of this[PropertySymbol.affectsCache]) { + item.result = null; } - this[PropertySymbol.ownerDocument]?.[PropertySymbol.clearComputedStyleCache](); + this[PropertySymbol.affectsCache] = []; + + // Computed style cache is affected by all mutations. + const document = this[PropertySymbol.ownerDocument]; + + if (document) { + for (const item of document[PropertySymbol.affectsComputedStyleCache]) { + item.result = null; + } + document[PropertySymbol.affectsComputedStyleCache] = []; + } } /** * Called when connected to a node. */ public [PropertySymbol.connectedToNode](): void { - const parent = this[PropertySymbol.parentNode] || this[PropertySymbol.host]; + const parentNode = this[PropertySymbol.parentNode]; + const parent = parentNode || this[PropertySymbol.host]; + + if (parentNode) { + if (parentNode[PropertySymbol.styleNode] && this[PropertySymbol.tagName] !== 'STYLE') { + this[PropertySymbol.styleNode] = parentNode[PropertySymbol.styleNode]; + } + + if (parentNode[PropertySymbol.textAreaNode] && this[PropertySymbol.tagName] !== 'TEXTAREA') { + this[PropertySymbol.textAreaNode] = parentNode[PropertySymbol.textAreaNode]; + } + + if (parentNode[PropertySymbol.formNode] && this[PropertySymbol.tagName] !== 'FORM') { + this[PropertySymbol.formNode] = parentNode[PropertySymbol.formNode]; + } + + if (parentNode[PropertySymbol.selectNode] && this[PropertySymbol.tagName] !== 'SELECT') { + this[PropertySymbol.selectNode] = parentNode[PropertySymbol.selectNode]; + } + } if (!this[PropertySymbol.isConnected] && parent[PropertySymbol.isConnected]) { this[PropertySymbol.connectedToDocument](); @@ -802,7 +891,7 @@ export default class Node extends EventTarget { this[PropertySymbol.disconnectedFromDocument](); } - const childNodes = this[PropertySymbol.childNodes]; + const childNodes = this[PropertySymbol.nodeArray]; for (let i = 0, max = childNodes.length; i < max; i++) { childNodes[i][PropertySymbol.connectedToNode](); } @@ -822,7 +911,23 @@ export default class Node extends EventTarget { this[PropertySymbol.disconnectedFromDocument](); } - const childNodes = this[PropertySymbol.childNodes]; + if (this[PropertySymbol.tagName] !== 'STYLE') { + this[PropertySymbol.styleNode] = null; + } + + if (this[PropertySymbol.tagName] !== 'TEXTAREA') { + this[PropertySymbol.textAreaNode] = null; + } + + if (this[PropertySymbol.tagName] !== 'FORM') { + this[PropertySymbol.formNode] = null; + } + + if (this[PropertySymbol.tagName] !== 'SELECT') { + this[PropertySymbol.selectNode] = null; + } + + const childNodes = this[PropertySymbol.nodeArray]; for (let i = 0, max = childNodes.length; i < max; i++) { childNodes[i][PropertySymbol.disconnectedFromNode](); } @@ -1027,9 +1132,9 @@ export default class Node extends EventTarget { const node2Node = reverseArrayIndex(node2Ancestors, commonAncestorIndex + 1); const node1Node = reverseArrayIndex(node1Ancestors, commonAncestorIndex + 1); - const computeNodeIndexes = (nodes: INodeList): void => { + const computeNodeIndexes = (nodes: Node[]): void => { for (const childNode of nodes) { - computeNodeIndexes(childNode[PropertySymbol.childNodes]); + computeNodeIndexes(childNode[PropertySymbol.nodeArray]); if (childNode === node2Node) { node2Index = indexes; @@ -1045,7 +1150,7 @@ export default class Node extends EventTarget { } }; - computeNodeIndexes(commonAncestor[PropertySymbol.childNodes]); + computeNodeIndexes(commonAncestor[PropertySymbol.nodeArray]); /** * 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING. diff --git a/packages/happy-dom/src/nodes/node/NodeList.ts b/packages/happy-dom/src/nodes/node/NodeList.ts index e7abb8a7b..45de3290a 100644 --- a/packages/happy-dom/src/nodes/node/NodeList.ts +++ b/packages/happy-dom/src/nodes/node/NodeList.ts @@ -1,36 +1,84 @@ import * as PropertySymbol from '../../PropertySymbol.js'; -import DOMException from '../../exception/DOMException.js'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import Element from '../element/Element.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; -import INodeList from './INodeList.js'; -import NodeTypeEnum from './NodeTypeEnum.js'; - -interface IHTMLCollectionAndFilter { - htmlCollection: IHTMLCollection; - filter: (item: Element) => boolean | null; -} +import Node from './Node.js'; /** * NodeList. * * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeList */ -class NodeList extends Array implements INodeList { - public [PropertySymbol.htmlCollections]: IHTMLCollectionAndFilter[] = []; +class NodeList { + [index: number]: T; + public [PropertySymbol.items]: T[]; /** * Constructor. * * @param items Items. */ - constructor(items?: T[]) { - super(); - if (items && items instanceof Array) { - for (const item of items) { - this[PropertySymbol.addItem](item); + constructor(items: T[]) { + this[PropertySymbol.items] = items; + + return new Proxy(this, { + get: (target, property, reciever) => { + if (property in target || typeof property === 'symbol') { + return Reflect.get(target, property, reciever); + } + const index = Number(property); + if (!isNaN(index)) { + return items[index]; + } + }, + set(): boolean { + return true; + }, + deleteProperty(): boolean { + return true; + }, + ownKeys(): string[] { + return Object.keys(items); + }, + has(target, property): boolean { + if (property in target) { + return true; + } + + const index = Number(property); + return !isNaN(index) && index >= 0 && index < items.length; + }, + defineProperty(target, property, descriptor): boolean { + if (property in target) { + Reflect.defineProperty(target, property, descriptor); + return true; + } + + return false; + }, + getOwnPropertyDescriptor(target, property): PropertyDescriptor { + if (property in target) { + return; + } + + const index = Number(property); + + if (!isNaN(index) && items[index]) { + return { + value: items[index], + writable: false, + enumerable: true, + configurable: false + }; + } } - } + }); + } + + /** + * Returns length. + * + * @returns Length. + */ + public get length(): number { + return this[PropertySymbol.items].length; } /** @@ -66,167 +114,55 @@ class NodeList extends Array implements INodeList { * @param index Index. */ public item(index: number): T { - return index >= 0 && this[index] ? this[index] : null; + const nodes = this[PropertySymbol.items]; + return index >= 0 && nodes[index] ? nodes[index] : null; } /** - * Appends item. + * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. * - * @param item Item. - * @returns True if added. + * @returns Iterator. */ - public [PropertySymbol.addItem](item: T): boolean { - if (super.includes(item)) { - return false; - } - - super.push(item); - - const htmlCollections = this[PropertySymbol.htmlCollections]; - for (const { htmlCollection, filter } of htmlCollections) { - if ( - item[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(item)) - ) { - htmlCollection[PropertySymbol.addItem](item); - } - } - - return true; - } - - /** - * Inserts item before another item. - * - * @param newItem New item. - * @param [referenceItem] Reference item. - * @returns True if inserted. - */ - public [PropertySymbol.insertItem](newItem: T, referenceItem: T | null): boolean { - if (!referenceItem) { - return this[PropertySymbol.addItem](newItem); - } - - if (super.includes(newItem)) { - return false; - } - - const index = super.indexOf(referenceItem); - - if (index === -1) { - throw new DOMException( - "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.", - DOMExceptionNameEnum.notFoundError - ); - } - - super.splice(index, 0, newItem); - - const htmlCollections = this[PropertySymbol.htmlCollections]; - for (const { htmlCollection, filter } of htmlCollections) { - let isInserted = false; - for (let i = index + 1; i < this.length; i++) { - const referenceItem = this[i]; - if ( - referenceItem[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(referenceItem)) - ) { - isInserted = true; - htmlCollection[PropertySymbol.insertItem](newItem, referenceItem); - break; - } - } - if (!isInserted) { - htmlCollection[PropertySymbol.addItem](newItem); - } - } - - return true; + public [Symbol.iterator](): IterableIterator { + const items = this[PropertySymbol.items]; + return items[Symbol.iterator](); } /** - * Removes item. + * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. * - * @param item Item. - * @returns True if removed. + * @returns Iterator. */ - public [PropertySymbol.removeItem](item: T): boolean { - const index = super.indexOf(item); - - if (index === -1) { - throw new DOMException( - "Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.", - DOMExceptionNameEnum.notFoundError - ); - } - - super.splice(index, 1); - - const htmlCollections = this[PropertySymbol.htmlCollections]; - for (const { htmlCollection, filter } of htmlCollections) { - if ( - item[PropertySymbol.nodeType] === NodeTypeEnum.elementNode && - (!filter || filter(item)) - ) { - htmlCollection[PropertySymbol.removeItem](item); - } - } - - return true; + public values(): IterableIterator { + return (this[PropertySymbol.items]).values(); } /** - * Index of item. + * Returns an iterator, allowing you to go through all key/value pairs contained in this object. * - * @param item Item. - * @returns Index. + * @returns Iterator. */ - public [PropertySymbol.indexOf](item: T): number { - return super.indexOf(item); + public entries(): IterableIterator<[number, T]> { + return (this[PropertySymbol.items]).entries(); } /** - * Returns true if the item is in the list. + * Executes a provided callback function once for each DOMTokenList element. * - * @param item Item. - * @returns True if the item is in the list. + * @param callback Function. + * @param thisArg thisArg. */ - public [PropertySymbol.includes](item: T): boolean { - return super.includes(item); + public forEach(callback: (currentValue, currentIndex, listObj) => void, thisArg?: this): void { + return (this[PropertySymbol.items]).forEach(callback, thisArg); } /** - * Returns a shallow copy of a portion of an array into a new array object selected from start to end. + * Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object. * - * @param [start] Start. - * @param [end] End. - * @returns A new array containing the extracted elements. + * @returns Iterator. */ - public [PropertySymbol.slice](start?: number, end?: number): T[] { - return super.slice(start, end); - } -} - -// Removes Array methods from NodeList. -const descriptors = Object.getOwnPropertyDescriptors(Array.prototype); -for (const key of Object.keys(descriptors)) { - if ( - typeof key !== 'symbol' && - key !== 'item' && - key !== 'entries' && - key !== 'values' && - key !== 'keys' && - key !== 'constructor' - ) { - const descriptor = descriptors[key]; - if (key === 'length') { - Object.defineProperty(NodeList.prototype, key, { - set: () => {}, - get: descriptor.get - }); - } else if (typeof descriptor.value === 'function') { - Object.defineProperty(NodeList.prototype, key, {}); - } + public keys(): IterableIterator { + return (this[PropertySymbol.items]).keys(); } } diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index 2bb749248..39940c506 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -48,7 +48,7 @@ export default class NodeUtility { return true; } - if (!(ancestorNode)[PropertySymbol.childNodes].length) { + if (!(ancestorNode)[PropertySymbol.nodeArray].length) { return false; } @@ -77,8 +77,8 @@ export default class NodeUtility { parent = parent[PropertySymbol.parentNode] ? parent[PropertySymbol.parentNode] : includeShadowRoots && (parent).host - ? (parent).host - : null; + ? (parent).host + : null; } return false; @@ -134,7 +134,7 @@ export default class NodeUtility { return (node).data.length; default: - return (node)[PropertySymbol.childNodes].length; + return (node)[PropertySymbol.nodeArray].length; } } @@ -293,15 +293,15 @@ export default class NodeUtility { } if ( - (nodeA)[PropertySymbol.childNodes].length !== - (nodeB)[PropertySymbol.childNodes].length + (nodeA)[PropertySymbol.nodeArray].length !== + (nodeB)[PropertySymbol.nodeArray].length ) { return false; } - for (let i = 0; i < (nodeA)[PropertySymbol.childNodes].length; i++) { - const childNodeA = (nodeA)[PropertySymbol.childNodes][i]; - const childNodeB = (nodeB)[PropertySymbol.childNodes][i]; + for (let i = 0; i < (nodeA)[PropertySymbol.nodeArray].length; i++) { + const childNodeA = (nodeA)[PropertySymbol.nodeArray][i]; + const childNodeB = (nodeB)[PropertySymbol.nodeArray][i]; if (!NodeUtility.isEqualNode(childNodeA, childNodeB)) { return false; diff --git a/packages/happy-dom/src/nodes/parent-node/IParentNode.ts b/packages/happy-dom/src/nodes/parent-node/IParentNode.ts index 655ac4168..1aa2af346 100644 --- a/packages/happy-dom/src/nodes/parent-node/IParentNode.ts +++ b/packages/happy-dom/src/nodes/parent-node/IParentNode.ts @@ -1,7 +1,7 @@ -import IHTMLCollection from '../element/IHTMLCollection.js'; +import HTMLCollection from '../element/HTMLCollection.js'; import Element from '../element/Element.js'; import Node from '../node/Node.js'; -import INodeList from '../node/INodeList.js'; +import NodeList from '../node/NodeList.js'; import IHTMLElementTagNameMap from '../../config/IHTMLElementTagNameMap.js'; import ISVGElementTagNameMap from '../../config/ISVGElementTagNameMap.js'; @@ -9,7 +9,7 @@ export default interface IParentNode extends Node { readonly childElementCount: number; readonly firstElementChild: Element; readonly lastElementChild: Element; - readonly children: IHTMLCollection; + readonly children: HTMLCollection; /** * Inserts a set of Node objects or DOMString objects after the last child of the ParentNode. DOMString objects are inserted as equivalent Text nodes. @@ -47,11 +47,11 @@ export default interface IParentNode extends Node { */ querySelectorAll( selector: K - ): INodeList; + ): NodeList; querySelectorAll( selector: K - ): INodeList; - querySelectorAll(selector: string): INodeList; + ): NodeList; + querySelectorAll(selector: string): NodeList; /** * Query CSS selector to find matching nodes. @@ -59,7 +59,7 @@ export default interface IParentNode extends Node { * @param selector CSS selector. * @returns Matching elements. */ - querySelectorAll(selector: string): INodeList; + querySelectorAll(selector: string): NodeList; /** * Returns an elements by class name. @@ -67,7 +67,7 @@ export default interface IParentNode extends Node { * @param className Tag name. * @returns Matching element. */ - getElementsByClassName(className: string): IHTMLCollection; + getElementsByClassName(className: string): HTMLCollection; /** * Returns an elements by tag name. @@ -77,11 +77,11 @@ export default interface IParentNode extends Node { */ getElementsByTagName( tagName: K - ): IHTMLCollection; + ): HTMLCollection; getElementsByTagName( tagName: K - ): IHTMLCollection; - getElementsByTagName(tagName: string): IHTMLCollection; + ): HTMLCollection; + getElementsByTagName(tagName: string): HTMLCollection; /** * Returns an elements by tag name and namespace. @@ -93,12 +93,12 @@ export default interface IParentNode extends Node { getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/1999/xhtml', tagName: K - ): IHTMLCollection; + ): HTMLCollection; getElementsByTagNameNS( namespaceURI: 'http://www.w3.org/2000/svg', tagName: K - ): IHTMLCollection; - getElementsByTagNameNS(namespaceURI: string, tagName: string): IHTMLCollection; + ): HTMLCollection; + getElementsByTagNameNS(namespaceURI: string, tagName: string): HTMLCollection; /** * Replaces the existing children of a node with a specified new set of children. diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index 59b11d899..cfb5f4d9b 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -5,8 +5,9 @@ import Document from '../document/Document.js'; import Element from '../element/Element.js'; import Node from '../node/Node.js'; import NamespaceURI from '../../config/NamespaceURI.js'; -import IHTMLCollection from '../element/IHTMLCollection.js'; import HTMLCollection from '../element/HTMLCollection.js'; +import QuerySelector from '../../query-selector/QuerySelector.js'; +import ICachedResult from '../node/ICachedResult.js'; /** * Parent node utility. @@ -49,7 +50,7 @@ export default class ParentNodeUtility { const childNodes = XMLParser.parse( parentNode[PropertySymbol.ownerDocument], node - )[PropertySymbol.childNodes]; + )[PropertySymbol.nodeArray]; while (childNodes.length) { parentNode.insertBefore(childNodes[0], firstChild); @@ -70,7 +71,7 @@ export default class ParentNodeUtility { parentNode: Element | Document | DocumentFragment, ...nodes: (string | Node)[] ): void { - const childNodes = (parentNode)[PropertySymbol.childNodes]; + const childNodes = (parentNode)[PropertySymbol.nodeArray]; while (childNodes.length) { parentNode.removeChild(childNodes[0]); @@ -89,15 +90,10 @@ export default class ParentNodeUtility { public static getElementsByClassName( parentNode: Element | DocumentFragment | Document, className: string - ): IHTMLCollection { - const htmlCollection = new HTMLCollection(); - - htmlCollection[PropertySymbol.observe](parentNode, { - subtree: true, - filter: (item: Element) => (item).className.split(' ').includes(className) - }); - - return htmlCollection; + ): HTMLCollection { + return new HTMLCollection( + () => QuerySelector.querySelectorAll(parentNode, `.${className}`)[PropertySymbol.items] + ); } /** @@ -110,17 +106,52 @@ export default class ParentNodeUtility { public static getElementsByTagName( parentNode: Element | DocumentFragment | Document, tagName: string - ): IHTMLCollection { + ): HTMLCollection { const upperTagName = tagName.toUpperCase(); const includeAll = tagName === '*'; - const htmlCollection = new HTMLCollection(); - htmlCollection[PropertySymbol.observe](parentNode, { - subtree: true, - filter: (item: Element) => includeAll || item[PropertySymbol.tagName] === upperTagName - }); + const find = ( + parent: Element | DocumentFragment | Document, + cachedResult: ICachedResult + ): Element[] => { + const elements: Element[] = []; + + for (const element of (parent)[PropertySymbol.elementArray]) { + if (includeAll || element[PropertySymbol.tagName] === upperTagName) { + elements.push(element); + } + + element[PropertySymbol.affectsCache].push(cachedResult); + + for (const foundElement of find(element, cachedResult)) { + elements.push(foundElement); + } + } + + return elements; + }; + + const query = (): Element[] => { + const cache = parentNode[PropertySymbol.cache].elementsByTagName; + const cachedItems = cache.get(tagName); + + if (cachedItems?.result) { + const items = cachedItems.result.deref(); + if (items) { + return items; + } + } + + const cachedResult = { result: null }; + const items = find(parentNode, cachedResult); - return htmlCollection; + cachedResult.result = new WeakRef(items); + cache.set(tagName, cachedResult); + + return items; + }; + + return new HTMLCollection(query); } /** @@ -135,48 +166,108 @@ export default class ParentNodeUtility { parentNode: Element | DocumentFragment | Document, namespaceURI: string, tagName: string - ): IHTMLCollection { + ): HTMLCollection { // When the namespace is HTML, the tag name is case-insensitive. const formattedTagName = namespaceURI === NamespaceURI.html ? tagName.toUpperCase() : tagName; const includeAll = tagName === '*'; - const htmlCollection = new HTMLCollection(); - htmlCollection[PropertySymbol.observe](parentNode, { - subtree: true, - filter: (item: Element) => - (includeAll || item[PropertySymbol.tagName] === formattedTagName) && - item[PropertySymbol.namespaceURI] === namespaceURI - }); + const find = ( + parent: Element | DocumentFragment | Document, + cachedResult: ICachedResult + ): Element[] => { + const elements: Element[] = []; + + for (const element of (parent)[PropertySymbol.elementArray]) { + if ( + (includeAll || element[PropertySymbol.tagName] === formattedTagName) && + element[PropertySymbol.namespaceURI] === namespaceURI + ) { + elements.push(element); + } + + element[PropertySymbol.affectsCache].push(cachedResult); + + for (const foundElement of find(element, cachedResult)) { + elements.push(foundElement); + } + } + + return elements; + }; + + const query = (): Element[] => { + const cache = parentNode[PropertySymbol.cache].elementsByTagNameNS; + const cachedItems = cache.get(tagName); + + if (cachedItems?.result) { + const items = cachedItems.result.deref(); + if (items) { + return items; + } + } + + const cachedResult = { result: null }; + const items = find(parentNode, cachedResult); - return htmlCollection; + cachedResult.result = new WeakRef(items); + cache.set(tagName, cachedResult); + + return items; + }; + + return new HTMLCollection(query); } /** * Returns the first element matching a tag name. - * This is not part of the browser standard and is only used internally in the document. + * This is not part of the browser standard and is only used internally (used in Document). * * @param parentNode Parent node. * @param tagName Tag name. * @returns Matching element. */ - // public static getElementByTagName( - // parentNode: Element | DocumentFragment | Document, - // tagName: string - // ): Element { - // const upperTagName = tagName.toUpperCase(); - - // for (const child of (parentNode)[PropertySymbol.children]) { - // if (child[PropertySymbol.tagName] === upperTagName) { - // return child; - // } - // const match = this.getElementByTagName(child, tagName); - // if (match) { - // return match; - // } - // } - - // return null; - // } + public static getElementByTagName( + parentNode: Element | DocumentFragment | Document, + tagName: string + ): Element { + const upperTagName = tagName.toUpperCase(); + + const find = ( + parent: Element | DocumentFragment | Document, + cachedResult: ICachedResult + ): Element | null => { + for (const element of (parent)[PropertySymbol.elementArray]) { + element[PropertySymbol.affectsCache].push(cachedResult); + + if (element[PropertySymbol.tagName] === upperTagName) { + return element; + } + + const foundElement = find(element, cachedResult); + if (foundElement) { + return foundElement; + } + } + }; + + const cache = parentNode[PropertySymbol.cache].elementByTagName; + const cachedItem = cache.get(tagName); + + if (cachedItem?.result) { + const item = cachedItem.result.deref(); + if (item) { + return item; + } + } + + const cachedResult = { result: null }; + const item = find(parentNode, cachedResult); + + cachedResult.result = new WeakRef(item); + cache.set(tagName, cachedResult); + + return item; + } /** * Returns an element by ID. @@ -190,18 +281,44 @@ export default class ParentNodeUtility { id: string ): Element | null { id = String(id); - for (const child of (parentNode)[PropertySymbol.children]) { - if (child.id === id) { - return child; + + const find = ( + parent: Element | DocumentFragment | Document, + cachedResult: ICachedResult + ): Element | null => { + for (const element of (parent)[PropertySymbol.elementArray]) { + element[PropertySymbol.affectsCache].push(cachedResult); + + if (element.id === id) { + return element; + } + + const foundElement = find(element, cachedResult); + + if (foundElement) { + return foundElement; + } } - const match = this.getElementById(child, id); + return null; + }; - if (match) { - return match; + const cache = parentNode[PropertySymbol.cache].elementById; + const cachedItem = cache.get(id); + + if (cachedItem?.result) { + const item = cachedItem.result.deref(); + if (item) { + return item; } } - return null; + const cachedResult = { result: null }; + const item = find(parentNode, cachedResult); + + cachedResult.result = new WeakRef(item); + cache.set(id, cachedResult); + + return item; } } diff --git a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts index 57b53c6db..502f098a4 100644 --- a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts +++ b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts @@ -51,7 +51,7 @@ export default class ShadowRoot extends DocumentFragment { escapeEntities: false }); let xml = ''; - for (const node of this[PropertySymbol.childNodes]) { + for (const node of this[PropertySymbol.nodeArray]) { xml += xmlSerializer.serializeToString(node); } return xml; @@ -63,7 +63,7 @@ export default class ShadowRoot extends DocumentFragment { * @param html HTML. */ public set innerHTML(html: string) { - const childNodes = this[PropertySymbol.childNodes]; + const childNodes = this[PropertySymbol.nodeArray]; while (childNodes.length) { this.removeChild(childNodes[0]); diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts index dd544cb91..6c276df54 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts @@ -4,8 +4,7 @@ import Element from '../element/Element.js'; import SVGSVGElement from './SVGSVGElement.js'; import Event from '../../event/Event.js'; import HTMLElementUtility from '../html-element/HTMLElementUtility.js'; -import DatasetFactory from '../element/DatasetFactory.js'; -import IDataset from '../element/IDataset.js'; +import DOMStringMap from '../element/DOMStringMap.js'; /** * SVG Element. @@ -26,7 +25,7 @@ export default class SVGElement extends Element { public [PropertySymbol.style]: CSSStyleDeclaration | null = null; // Private properties - #dataset: IDataset = null; + #dataset: DOMStringMap | null = null; /** * Returns viewport. @@ -59,8 +58,8 @@ export default class SVGElement extends Element { * * @returns Data set. */ - public get dataset(): IDataset { - return (this.#dataset ??= DatasetFactory.createDataset(this)); + public get dataset(): DOMStringMap { + return (this.#dataset ??= new DOMStringMap(this)); } /** diff --git a/packages/happy-dom/src/nodes/text/Text.ts b/packages/happy-dom/src/nodes/text/Text.ts index 826693e72..0170bd264 100644 --- a/packages/happy-dom/src/nodes/text/Text.ts +++ b/packages/happy-dom/src/nodes/text/Text.ts @@ -91,4 +91,34 @@ export default class Text extends CharacterData { public override [PropertySymbol.cloneNode](deep = false): Text { return super[PropertySymbol.cloneNode](deep); } + + /** + * @override + */ + public override [PropertySymbol.connectedToNode](): void { + super[PropertySymbol.connectedToNode](); + + if (this[PropertySymbol.textAreaNode]) { + (this[PropertySymbol.textAreaNode])[PropertySymbol.resetSelection](); + } + + if (this[PropertySymbol.styleNode] && this[PropertySymbol.data]) { + this[PropertySymbol.styleNode][PropertySymbol.updateSheet](); + } + } + + /** + * @override + */ + public override [PropertySymbol.disconnectedFromNode](): void { + if (this[PropertySymbol.textAreaNode]) { + (this[PropertySymbol.textAreaNode])[PropertySymbol.resetSelection](); + } + + if (this[PropertySymbol.styleNode] && this[PropertySymbol.data]) { + this[PropertySymbol.styleNode][PropertySymbol.updateSheet](); + } + + super[PropertySymbol.disconnectedFromNode](); + } } diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index 5ff08b9da..d77437233 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -10,11 +10,9 @@ import SelectorParser from './SelectorParser.js'; import ISelectorMatch from './ISelectorMatch.js'; import IHTMLElementTagNameMap from '../config/IHTMLElementTagNameMap.js'; import ISVGElementTagNameMap from '../config/ISVGElementTagNameMap.js'; -import IHTMLCollection from '../nodes/element/IHTMLCollection.js'; -import INodeList from '../nodes/node/INodeList.js'; -import ICachedQuerySelectorAllItem from '../nodes/node/ICachedQuerySelectorAllItem.js'; -import ICachedQuerySelectorItem from '../nodes/node/ICachedQuerySelectorItem.js'; -import ICachedMatchesItem from '../nodes/node/ICachedMatchesItem.js'; +import ICachedQuerySelectorAllItem from '../nodes/node/ICachedQuerySelectorAllResult.js'; +import ICachedQuerySelectorItem from '../nodes/node/ICachedQuerySelectorResult.js'; +import ICachedMatchesItem from '../nodes/node/ICachedMatchesResult.js'; type DocumentPositionAndElement = { documentPosition: string; @@ -78,7 +76,7 @@ export default class QuerySelector { public static querySelectorAll( node: Element | Document | DocumentFragment, selector: string - ): INodeList { + ): NodeList { if (selector === '') { throw new Error( `Failed to execute 'querySelectorAll' on '${node.constructor.name}': The provided selector is empty.` @@ -86,11 +84,11 @@ export default class QuerySelector { } if (selector === null || selector === undefined) { - return new NodeList(); + return new NodeList([]); } - const cache = node[PropertySymbol.querySelectorAllCache]; - const cachedResult = cache.items.get(selector); + const cache = node[PropertySymbol.cache].querySelectorAll; + const cachedResult = cache.get(selector); if (cachedResult?.result) { const result = cachedResult.result.deref(); @@ -106,26 +104,25 @@ export default class QuerySelector { } const groups = SelectorParser.getSelectorGroups(selector); - const nodeList: INodeList = new NodeList(); + const items: Element[] = []; + const nodeList = new NodeList(items); const matchesMap: Map = new Map(); const matchedPositions: string[] = []; const cachedItem = { result: new WeakRef(nodeList) }; - node[PropertySymbol.querySelectorAllCache].items.set(selector, cachedItem); + node[PropertySymbol.cache].querySelectorAll.set(selector, cachedItem); if (node[PropertySymbol.isConnected]) { // Document is affected for the ":target" selector - (node[PropertySymbol.ownerDocument] || node)[ - PropertySymbol.querySelectorAllCache - ].affectedItems.push(cachedItem); + (node[PropertySymbol.ownerDocument] || node)[PropertySymbol.affectsCache].push(cachedItem); } for (const items of groups) { const matches = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode ? this.findAll(node, [node], items, cachedItem) - : this.findAll(null, (node)[PropertySymbol.children], items, cachedItem); + : this.findAll(null, (node)[PropertySymbol.elementArray], items, cachedItem); for (const match of matches) { if (!matchesMap.has(match.documentPosition)) { matchesMap.set(match.documentPosition, match.element); @@ -135,8 +132,9 @@ export default class QuerySelector { } const keys = matchedPositions.sort(); + for (let i = 0, max = keys.length; i < max; i++) { - nodeList[PropertySymbol.addItem](matchesMap.get(keys[i])); + items.push(matchesMap.get(keys[i])); } return nodeList; @@ -199,7 +197,7 @@ export default class QuerySelector { return null; } - const cachedResult = node[PropertySymbol.querySelectorCache].items.get(selector); + const cachedResult = node[PropertySymbol.cache].querySelector.get(selector); if (cachedResult?.result) { const result = cachedResult.result.deref(); @@ -220,20 +218,18 @@ export default class QuerySelector { } }; - node[PropertySymbol.querySelectorCache].items.set(selector, cachedItem); + node[PropertySymbol.cache].querySelector.set(selector, cachedItem); if (node[PropertySymbol.isConnected]) { // Document is affected for the ":target" selector - (node[PropertySymbol.ownerDocument] || node)[ - PropertySymbol.querySelectorCache - ].affectedItems.push(cachedItem); + (node[PropertySymbol.ownerDocument] || node)[PropertySymbol.affectsCache].push(cachedItem); } for (const items of SelectorParser.getSelectorGroups(selector)) { const match = node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode ? this.findFirst(node, [node], items, cachedItem) - : this.findFirst(null, (node)[PropertySymbol.children], items, cachedItem); + : this.findFirst(null, (node)[PropertySymbol.elementArray], items, cachedItem); if (match) { cachedItem.result = new WeakRef(match); @@ -269,7 +265,7 @@ export default class QuerySelector { } const ignoreErrors = options?.ignoreErrors; - const cachedResult = element[PropertySymbol.matchesCache].items.get(selector); + const cachedResult = element[PropertySymbol.cache].matches.get(selector); if (cachedResult?.result) { return cachedResult.result.match; @@ -288,7 +284,7 @@ export default class QuerySelector { result: { match: null } }; - element[PropertySymbol.matchesCache].items.set(selector, cachedItem); + element[PropertySymbol.cache].matches.set(selector, cachedItem); for (const items of SelectorParser.getSelectorGroups(selector, options)) { const result = this.matchSelector(element, element, items.reverse(), cachedItem); @@ -333,7 +329,7 @@ export default class QuerySelector { case SelectorCombinatorEnum.adjacentSibling: const previousElementSibling = currentElement.previousElementSibling; if (previousElementSibling) { - previousElementSibling[PropertySymbol.matchesCache].affectedItems.push(cachedItem); + previousElementSibling[PropertySymbol.affectsCache].push(cachedItem); const match = this.matchSelector( targetElement, previousElementSibling, @@ -351,7 +347,7 @@ export default class QuerySelector { case SelectorCombinatorEnum.descendant: const parentElement = currentElement.parentElement; if (parentElement) { - parentElement[PropertySymbol.matchesCache].affectedItems.push(cachedItem); + parentElement[PropertySymbol.affectsCache].push(cachedItem); const match = this.matchSelector( targetElement, parentElement, @@ -397,7 +393,7 @@ export default class QuerySelector { */ private static findAll( rootElement: Element, - children: Element[] | IHTMLCollection, + children: Element[], selectorItems: SelectorItem[], cachedItem: ICachedQuerySelectorAllItem, documentPosition?: string @@ -408,10 +404,10 @@ export default class QuerySelector { for (let i = 0, max = children.length; i < max; i++) { const child = children[i]; - const childrenOfChild = (child)[PropertySymbol.children]; + const childrenOfChild = (child)[PropertySymbol.elementArray]; const position = (documentPosition ? documentPosition + '>' : '') + String.fromCharCode(i); - child[PropertySymbol.querySelectorAllCache].affectedItems.push(cachedItem); + child[PropertySymbol.affectsCache].push(cachedItem); if (selectorItem.match(child)) { if (!nextSelectorItem) { @@ -473,7 +469,7 @@ export default class QuerySelector { */ private static findFirst( rootElement: Element, - children: Element[] | IHTMLCollection, + children: Element[], selectorItems: SelectorItem[], cachedItem: ICachedQuerySelectorItem ): Element { @@ -481,9 +477,9 @@ export default class QuerySelector { const nextSelectorItem = selectorItems[1]; for (const child of children) { - const childrenOfChild = (child)[PropertySymbol.children]; + const childrenOfChild = (child)[PropertySymbol.elementArray]; - child[PropertySymbol.querySelectorCache].affectedItems.push(cachedItem); + child[PropertySymbol.affectsCache].push(cachedItem); if (selectorItem.match(child)) { if (!nextSelectorItem) { diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index 83a6f6680..3c20dd77c 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -6,7 +6,6 @@ import SelectorCombinatorEnum from './SelectorCombinatorEnum.js'; import ISelectorAttribute from './ISelectorAttribute.js'; import ISelectorMatch from './ISelectorMatch.js'; import ISelectorPseudo from './ISelectorPseudo.js'; -import IHTMLCollection from '../nodes/element/IHTMLCollection.js'; /** * Selector item. @@ -122,7 +121,7 @@ export default class SelectorItem { private matchPseudo(element: Element): ISelectorMatch | null { const parent = element[PropertySymbol.parentNode]; const parentChildren = element[PropertySymbol.parentNode] - ? (element[PropertySymbol.parentNode])[PropertySymbol.children] + ? (element[PropertySymbol.parentNode])[PropertySymbol.elementArray] : []; if (!this.pseudos) { @@ -192,7 +191,7 @@ export default class SelectorItem { */ private matchPseudoItem( element: Element, - parentChildren: Element[] | IHTMLCollection, + parentChildren: Element[], pseudo: ISelectorPseudo ): ISelectorMatch | null { switch (pseudo.name) { @@ -237,7 +236,9 @@ export default class SelectorItem { ? { priorityWeight: 10 } : null; case 'empty': - return !(element)[PropertySymbol.children].length ? { priorityWeight: 10 } : null; + return !(element)[PropertySymbol.elementArray].length + ? { priorityWeight: 10 } + : null; case 'root': return element[PropertySymbol.tagName] === 'HTML' ? { priorityWeight: 10 } : null; case 'not': diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 0611a86ba..f484925bd 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -326,7 +326,7 @@ export default class Range { const containedChildren = []; - for (const node of (commonAncestor)[PropertySymbol.childNodes]) { + for (const node of (commonAncestor)[PropertySymbol.nodeArray]) { if (RangeUtility.isContained(node, this)) { if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { throw new DOMException( @@ -495,9 +495,9 @@ export default class Range { newNode = referenceNode[PropertySymbol.parentNode]; newOffset = - (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes][ - PropertySymbol.indexOf - ](referenceNode) + 1; + (referenceNode[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf( + referenceNode + ) + 1; } if ( @@ -619,7 +619,7 @@ export default class Range { const containedChildren = []; - for (const node of (commonAncestor)[PropertySymbol.childNodes]) { + for (const node of (commonAncestor)[PropertySymbol.nodeArray]) { if (RangeUtility.isContained(node, this)) { if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentTypeNode) { throw new DOMException( @@ -656,9 +656,9 @@ export default class Range { newNode = referenceNode[PropertySymbol.parentNode]; newOffset = - (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes][ - PropertySymbol.indexOf - ](referenceNode) + 1; + (referenceNode[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf( + referenceNode + ) + 1; } if ( @@ -808,8 +808,8 @@ export default class Range { let referenceNode = this[PropertySymbol.start].node[PropertySymbol.nodeType] === NodeTypeEnum.textNode ? this[PropertySymbol.start].node - : (this[PropertySymbol.start].node)[PropertySymbol.childNodes][this.startOffset] || - null; + : (this[PropertySymbol.start].node)[PropertySymbol.nodeArray][this.startOffset] || + null; const parent = !referenceNode ? this[PropertySymbol.start].node : referenceNode[PropertySymbol.parentNode]; @@ -829,9 +829,9 @@ export default class Range { let newOffset = !referenceNode ? NodeUtility.getNodeLength(parent) - : (referenceNode[PropertySymbol.parentNode])[PropertySymbol.childNodes][ - PropertySymbol.indexOf - ](referenceNode); + : (referenceNode[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf( + referenceNode + ); newOffset += newNode[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode ? NodeUtility.getNodeLength(newNode) @@ -863,7 +863,7 @@ export default class Range { return true; } - const offset = (parent)[PropertySymbol.childNodes][PropertySymbol.indexOf](node); + const offset = (parent)[PropertySymbol.nodeArray].indexOf(node); return ( RangeUtility.compareBoundaryPointsPosition( @@ -891,9 +891,7 @@ export default class Range { ); } - const index = (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][ - PropertySymbol.indexOf - ](node); + const index = (node[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf(node); this[PropertySymbol.start].node = node[PropertySymbol.parentNode]; this[PropertySymbol.start].offset = index; @@ -990,9 +988,7 @@ export default class Range { } this.setEnd( node[PropertySymbol.parentNode], - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( - node - ) + 1 + (node[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf(node) + 1 ); } @@ -1011,9 +1007,7 @@ export default class Range { } this.setEnd( node[PropertySymbol.parentNode], - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( - node - ) + (node[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf(node) ); } @@ -1032,9 +1026,7 @@ export default class Range { } this.setStart( node[PropertySymbol.parentNode], - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( - node - ) + 1 + (node[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf(node) + 1 ); } @@ -1053,9 +1045,7 @@ export default class Range { } this.setStart( node[PropertySymbol.parentNode], - (node[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( - node - ) + (node[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf(node) ); } diff --git a/packages/happy-dom/src/range/RangeUtility.ts b/packages/happy-dom/src/range/RangeUtility.ts index 7b79acab9..1b70d60c0 100644 --- a/packages/happy-dom/src/range/RangeUtility.ts +++ b/packages/happy-dom/src/range/RangeUtility.ts @@ -51,9 +51,8 @@ export default class RangeUtility { } if ( - (child[PropertySymbol.parentNode])[PropertySymbol.childNodes][PropertySymbol.indexOf]( - child - ) < pointA.offset + (child[PropertySymbol.parentNode])[PropertySymbol.nodeArray].indexOf(child) < + pointA.offset ) { return 1; } diff --git a/packages/happy-dom/src/storage/Storage.ts b/packages/happy-dom/src/storage/Storage.ts index e77bca8a9..a35d1e5c0 100644 --- a/packages/happy-dom/src/storage/Storage.ts +++ b/packages/happy-dom/src/storage/Storage.ts @@ -8,6 +8,79 @@ import * as PropertySymbol from '../PropertySymbol.js'; export default class Storage { public [PropertySymbol.data]: { [key: string]: string } = {}; + /** + * Constructor. + */ + constructor() { + return new Proxy(this, { + get: (target, property) => { + if (property in target || typeof property === 'symbol') { + const returnValue = target[property]; + if (typeof returnValue === 'function') { + return returnValue.bind(target); + } + return returnValue; + } + const value = target[PropertySymbol.data][String(property)]; + if (value !== undefined) { + return value; + } + }, + set(target, property, newValue): boolean { + if (property in target) { + return false; + } + target[PropertySymbol.data][String(property)] = String(newValue); + }, + deleteProperty(target, property): boolean { + if (property in target) { + return false; + } + + delete target[PropertySymbol.data][String(property)]; + }, + ownKeys(target): string[] { + return Object.keys(target[PropertySymbol.data]); + }, + has(target, property): boolean { + if (property in target || property in target[PropertySymbol.data]) { + return true; + } + + return false; + }, + defineProperty(target, property, descriptor): boolean { + if (property in target) { + Object.defineProperty(target, property, descriptor); + return true; + } + + if (descriptor.value !== undefined) { + target[PropertySymbol.data][String(property)] = String(descriptor.value); + return true; + } + + return false; + }, + getOwnPropertyDescriptor(target, property): PropertyDescriptor { + if (property in target) { + return; + } + + const value = target[PropertySymbol.data][String(property)]; + + if (value !== undefined) { + return { + value: value, + writable: true, + enumerable: true, + configurable: true + }; + } + } + }); + } + /** * Returns length. * diff --git a/packages/happy-dom/src/storage/StorageFactory.ts b/packages/happy-dom/src/storage/StorageFactory.ts deleted file mode 100644 index b04d3db67..000000000 --- a/packages/happy-dom/src/storage/StorageFactory.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as PropertySymbol from '../PropertySymbol.js'; -import Storage from './Storage.js'; - -/** - * Dataset factory. - * - * Reference: - * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/storage - */ -export default class StorageFactory { - /** - * Creates a new storage. - */ - public static createStorage(): Storage { - // Documentation for Proxy: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy - return new Proxy(new Storage(), { - get(storage: Storage, key: string): string | number | boolean | Function { - if (Storage.prototype.hasOwnProperty(key)) { - const descriptor = Object.getOwnPropertyDescriptor(Storage.prototype, key); - if (descriptor.value !== undefined) { - if (typeof descriptor.value === 'function') { - return storage[key].bind(storage); - } - return descriptor.value; - } - if (descriptor.get) { - return descriptor.get.call(storage); - } - return storage[key]; - } - return storage[PropertySymbol.data][key]; - }, - set(storage: Storage, key: string, value: string): boolean { - if (Storage.prototype.hasOwnProperty(key)) { - return true; - } - storage[PropertySymbol.data][key] = String(value); - return true; - }, - deleteProperty(storage: Storage, key: string): boolean { - if (Storage.prototype.hasOwnProperty(key)) { - return true; - } - return delete storage[PropertySymbol.data][key]; - }, - ownKeys(storage: Storage): string[] { - return Object.keys(storage[PropertySymbol.data]); - }, - has(storage: Storage, key: string): boolean { - return storage[PropertySymbol.data][key] !== undefined; - }, - defineProperty(storage: Storage, key: string, descriptor: PropertyDescriptor): boolean { - if (Storage.prototype.hasOwnProperty(key)) { - if (descriptor.get || descriptor.set) { - Object.defineProperty(storage, key, { - ...descriptor, - get: descriptor.get ? descriptor.get.bind(storage) : undefined, - set: descriptor.set ? descriptor.set.bind(storage) : undefined - }); - } else { - Object.defineProperty(storage, key, { - ...descriptor, - value: - typeof descriptor.value === 'function' - ? descriptor.value.bind(storage) - : descriptor.value - }); - } - return true; - } - if (descriptor.value === undefined) { - return false; - } - storage[PropertySymbol.data][key] = String(descriptor.value); - return true; - }, - getOwnPropertyDescriptor(storage: Storage, key: string): PropertyDescriptor { - if ( - Storage.prototype.hasOwnProperty(key) || - storage[PropertySymbol.data][key] === undefined - ) { - return; - } - - return { - value: storage[PropertySymbol.data][key], - writable: true, - enumerable: true, - configurable: true - }; - } - }); - } -} diff --git a/packages/happy-dom/src/tree-walker/TreeWalker.ts b/packages/happy-dom/src/tree-walker/TreeWalker.ts index 7cc678e12..301e58f93 100644 --- a/packages/happy-dom/src/tree-walker/TreeWalker.ts +++ b/packages/happy-dom/src/tree-walker/TreeWalker.ts @@ -87,7 +87,7 @@ export default class TreeWalker { * @returns Current node. */ public firstChild(): Node { - const childNodes = this.currentNode ? (this.currentNode)[PropertySymbol.childNodes] : []; + const childNodes = this.currentNode ? (this.currentNode)[PropertySymbol.nodeArray] : []; if (childNodes.length > 0) { this.currentNode = childNodes[0]; @@ -108,7 +108,7 @@ export default class TreeWalker { * @returns Current node. */ public lastChild(): Node { - const childNodes = this.currentNode ? (this.currentNode)[PropertySymbol.childNodes] : []; + const childNodes = this.currentNode ? (this.currentNode)[PropertySymbol.nodeArray] : []; if (childNodes.length > 0) { this.currentNode = childNodes[childNodes.length - 1]; diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 7913e56cb..a93e3a700 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -178,7 +178,6 @@ import ResizeObserver from '../resize-observer/ResizeObserver.js'; import Screen from '../screen/Screen.js'; import Selection from '../selection/Selection.js'; import Storage from '../storage/Storage.js'; -import StorageFactory from '../storage/StorageFactory.js'; import NodeFilter from '../tree-walker/NodeFilter.js'; import NodeIterator from '../tree-walker/NodeIterator.js'; import TreeWalker from '../tree-walker/TreeWalker.js'; @@ -577,8 +576,8 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this[PropertySymbol.navigator] = new Navigator(this); this[PropertySymbol.history] = new History(); this[PropertySymbol.screen] = new Screen(); - this[PropertySymbol.sessionStorage] = StorageFactory.createStorage(); - this[PropertySymbol.localStorage] = StorageFactory.createStorage(); + this[PropertySymbol.sessionStorage] = new Storage(); + this[PropertySymbol.localStorage] = new Storage(); this[PropertySymbol.location] = new Location(this.#browserFrame, options?.url ?? 'about:blank'); this[PropertySymbol.asyncTaskManager] = asyncTaskManager; @@ -1435,7 +1434,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this[PropertySymbol.mutationObservers] = []; // Disconnects nodes from the document, so that they can be garbage collected. - const childNodes = this.document[PropertySymbol.childNodes]; + const childNodes = this.document[PropertySymbol.nodeArray]; while (childNodes.length > 0) { // Makes sure that something won't be triggered by the disconnect. diff --git a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts index cec652d05..953095787 100644 --- a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts +++ b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts @@ -58,8 +58,8 @@ export default class XMLSerializer { const childNodes = localName === 'template' - ? ((root).content)[PropertySymbol.childNodes] - : (root)[PropertySymbol.childNodes]; + ? ((root).content)[PropertySymbol.nodeArray] + : (root)[PropertySymbol.nodeArray]; let innerHTML = ''; for (const node of childNodes) { @@ -71,7 +71,7 @@ export default class XMLSerializer { if (this.options.includeShadowRoots && element.shadowRoot) { innerHTML += `