Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#308@minor: Implement NamedNodeMap and use it for Element.attributes #626

Merged
merged 6 commits into from Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
70 changes: 70 additions & 0 deletions packages/happy-dom/src/named-node-map/INamedNodeMap.ts
@@ -0,0 +1,70 @@
import IAttr from '../nodes/attr/IAttr';

/**
* NamedNodeMap.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap.
*/
export default interface INamedNodeMap extends Iterable<IAttr> {
[index: number]: IAttr;
[Symbol.toStringTag]: string;
readonly length: number;

/**
* Returns attribute by index.
*
* @param index Index.
*/
item: (index: number) => IAttr;

/**
* Returns attribute by name.
*
* @param qualifiedName Name.
* @returns Attribute.
*/
getNamedItem: (qualifiedName: string) => IAttr;

/**
* Returns attribute by name and namespace.
*
* @param namespace Namespace.
* @param localName Local name of the attribute.
* @returns Attribute.
*/
getNamedItemNS: (namespace: string, localName: string) => IAttr;

/**
* Adds a new attribute node.
*
* @param attr Attribute.
* @returns Replaced attribute.
*/
setNamedItem: (attr: IAttr) => IAttr;

/**
* Adds a new namespaced attribute node.
*
* @param attr Attribute.
* @returns Replaced attribute.
*/
setNamedItemNS: (attr: IAttr) => IAttr;

/**
* Removes an attribute.
*
* @param qualifiedName Name of the attribute.
* @returns Removed attribute.
*/
removeNamedItem: (qualifiedName: string) => IAttr;

/**
* Removes a namespaced attribute.
*
* @param namespace Namespace.
* @param localName Local name of the attribute.
* @returns Removed attribute.
*/
removeNamedItemNS: (namespace: string, localName: string) => IAttr;
}
145 changes: 145 additions & 0 deletions packages/happy-dom/src/named-node-map/NamedNodeMap.ts
@@ -0,0 +1,145 @@
import type Element from '../nodes/element/Element';
import IAttr from '../nodes/attr/IAttr';
import INamedNodeMap from './INamedNodeMap';

/**
* NamedNodeMap.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap.
*/
export default class NamedNodeMap implements INamedNodeMap {
[index: number]: IAttr;

/**
* Reference to the element.
*/
#ownerElement: Element;

/**
* Constructor.
*
* @param element Associated element.
*/
constructor(element: Element) {
this.#ownerElement = element;
}

/**
* Returns string.
*
* @returns string.
*/
public get [Symbol.toStringTag](): string {
return this.constructor.name;
}

/**
* Length.
*
* @returns Length.
*/
public get length(): number {
return Object.keys(this.#ownerElement._attributes).length;
}

/**
* Returns attribute by index.
*
* @param index Index.
*/
public item(index: number): IAttr | null {
if (index < 0) {
return null;
}
const attr = Object.values(this.#ownerElement._attributes)[index];
return attr ? attr : null;
}

/**
* Returns attribute by name.
*
* @param qualifiedName Name.
* @returns Attribute.
*/
public getNamedItem(qualifiedName: string): IAttr | null {
return this.#ownerElement.getAttributeNode(qualifiedName);
}

/**
* Returns attribute by name and namespace.
*
* @param namespace Namespace.
* @param localName Local name of the attribute.
* @returns Attribute.
*/
public getNamedItemNS(namespace: string, localName: string): IAttr | null {
return this.#ownerElement.getAttributeNodeNS(namespace, localName);
}

/**
* Adds a new attribute node.
*
* @param attr Attribute.
* @returns Replaced attribute.
*/
public setNamedItem(attr: IAttr): IAttr {
return this.#ownerElement.setAttributeNode(attr);
}

/**
* Adds a new namespaced attribute node.
*
* @param attr Attribute.
* @returns Replaced attribute.
*/
public setNamedItemNS(attr: IAttr): IAttr {
return this.#ownerElement.setAttributeNodeNS(attr);
}

/**
* Removes an attribute.
*
* @param qualifiedName Name of the attribute.
* @returns Removed attribute.
*/
public removeNamedItem(qualifiedName: string): IAttr | null {
const attr = this.getNamedItem(qualifiedName);

if (attr) {
this.#ownerElement.removeAttributeNode(attr);
}
return attr;
}

/**
* Removes a namespaced attribute.
*
* @param namespace Namespace.
* @param localName Local name of the attribute.
* @returns Removed attribute.
*/
public removeNamedItemNS(namespace: string, localName: string): IAttr | null {
const attr = this.getNamedItemNS(namespace, localName);

if (attr) {
this.#ownerElement.removeAttributeNode(attr);
}
return attr;
}

/**
* Iterator.
*
* @returns Iterator.
*/
public [Symbol.iterator](): Iterator<IAttr> {
let index = -1;
return {
next: () => {
index++;
return { value: this.item(index), done: index >= this.length };
}
};
}
}
10 changes: 5 additions & 5 deletions packages/happy-dom/src/nodes/element/Element.ts
@@ -1,6 +1,7 @@
import Node from '../node/Node';
import ShadowRoot from '../shadow-root/ShadowRoot';
import Attr from '../attr/Attr';
import NamedNodeMap from '../../named-node-map/NamedNodeMap';
import DOMRect from './DOMRect';
import DOMTokenList from '../../dom-token-list/DOMTokenList';
import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
Expand All @@ -26,6 +27,8 @@ import IText from '../text/IText';
import IDOMRectList from './IDOMRectList';
import DOMRectListFactory from './DOMRectListFactory';
import IAttr from '../attr/IAttr';
import INamedNodeMap from '../../named-node-map/INamedNodeMap';

import Event from '../../event/Event';

/**
Expand Down Expand Up @@ -249,11 +252,8 @@ export default class Element extends Node implements IElement {
*
* @returns Attributes.
*/
public get attributes(): { [k: string | number]: IAttr } & { length: number } {
const attributes = Object.values(this._attributes);
return Object.assign({}, this._attributes, attributes, {
length: attributes.length
});
public get attributes(): INamedNodeMap {
return Object.assign(new NamedNodeMap(this), Object.values(this._attributes), this._attributes);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/happy-dom/src/nodes/element/IElement.ts
@@ -1,5 +1,6 @@
import IShadowRoot from '../shadow-root/IShadowRoot';
import IAttr from '../attr/IAttr';
import INamedNodeMap from '../../named-node-map/INamedNodeMap';
import DOMRect from './DOMRect';
import IDOMTokenList from '../../dom-token-list/IDOMTokenList';
import INode from './../node/INode';
Expand Down Expand Up @@ -29,7 +30,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
slot: string;
readonly nodeName: string;
readonly localName: string;
readonly attributes: { [k: string | number]: IAttr } & { length: number };
readonly attributes: INamedNodeMap;

// Events
oncancel: (event: Event) => void | null;
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/nodes/node/NodeUtility.ts
Expand Up @@ -148,8 +148,8 @@ export default class NodeUtility {
* @param elementB
*/
public static attributeListsEqual(elementA: IElement, elementB: IElement): boolean {
const listA = Object.values(elementA.attributes);
const listB = Object.values(elementB.attributes);
const listA = <Array<IAttr>>Object.values(elementA['_attributes']);
const listB = <Array<IAttr>>Object.values(elementB['_attributes']);

const lengthA = listA.length;
const lengthB = listB.length;
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-dom/src/window/IWindow.ts
Expand Up @@ -95,6 +95,7 @@ import MediaQueryList from '../match-media/MediaQueryList';
import DOMRect from '../nodes/element/DOMRect';
import Window from './Window';
import Attr from '../nodes/attr/Attr';
import NamedNodeMap from '../named-node-map/NamedNodeMap';
import { URLSearchParams } from 'url';
import { Performance } from 'perf_hooks';
import IElement from '../nodes/element/IElement';
Expand Down Expand Up @@ -134,6 +135,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global {
readonly HTMLBaseElement: typeof HTMLBaseElement;
readonly HTMLDialogElement: typeof HTMLDialogElement;
readonly Attr: typeof Attr;
readonly NamedNodeMap: typeof NamedNodeMap;
readonly SVGSVGElement: typeof SVGSVGElement;
readonly SVGElement: typeof SVGElement;
readonly Image: typeof Image;
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-dom/src/window/Window.ts
Expand Up @@ -108,6 +108,7 @@ import { Buffer } from 'buffer';
import Base64 from '../base64/Base64';
import IDocument from '../nodes/document/IDocument';
import Attr from '../nodes/attr/Attr';
import NamedNodeMap from '../named-node-map/NamedNodeMap';
import IElement from '../nodes/element/IElement';
import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction';

Expand Down Expand Up @@ -167,6 +168,7 @@ export default class Window extends EventTarget implements IWindow {
public readonly HTMLBaseElement = HTMLBaseElement;
public readonly HTMLDialogElement = HTMLDialogElement;
public readonly Attr = Attr;
public readonly NamedNodeMap = NamedNodeMap;
public readonly SVGSVGElement = SVGSVGElement;
public readonly SVGElement = SVGElement;
public readonly Text = Text;
Expand Down