Skip to content

Commit

Permalink
Merge pull request #626 from jledentu/namednodemap
Browse files Browse the repository at this point in the history
#308@minor: Implement NamedNodeMap and use it for Element.attributes
  • Loading branch information
capricorn86 committed Oct 18, 2022
2 parents 999cd31 + fa88531 commit 42c92a6
Show file tree
Hide file tree
Showing 10 changed files with 484 additions and 114 deletions.
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

0 comments on commit 42c92a6

Please sign in to comment.