Skip to content

Commit

Permalink
capricorn86#308@minor: Implement NamedNodeMap.
Browse files Browse the repository at this point in the history
  • Loading branch information
jledentu committed Oct 18, 2022
1 parent a6407b7 commit 167cabf
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 15 deletions.
23 changes: 23 additions & 0 deletions packages/happy-dom/src/nodes/attr/INamedNodeMap.ts
@@ -0,0 +1,23 @@
import IAttr from './IAttr';

export type INamedNodeMapProps = {
readonly length: number;
item: (index: number) => IAttr;
getNamedItem: (qualifiedName: string) => IAttr;
getNamedItemNS: (namespace: string, localName: string) => IAttr;
setNamedItem: (attr: IAttr) => IAttr;
setNamedItemNS: (attr: IAttr) => IAttr;
removeNamedItem: (qualifiedName: string) => IAttr;
removeNamedItemNS: (namespace: string, localName: string) => IAttr;
[Symbol.toStringTag]: string;
} & Iterable<IAttr>;
type INamedNodeMapProperties = keyof INamedNodeMapProps;

type INamedNodeMap = INamedNodeMapProps & {
[k in Exclude<string | number, INamedNodeMapProperties>]: IAttr;
};

/**
* NamedNodeMap interface.
*/
export default INamedNodeMap;
133 changes: 133 additions & 0 deletions packages/happy-dom/src/nodes/attr/NamedNodeMap.ts
@@ -0,0 +1,133 @@
import type Element from '../element/Element';
import IAttr from './IAttr';
import type { INamedNodeMapProps } from './INamedNodeMap';

/**
* NamedNodeMap.
*
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap.
*/
export default class NamedNodeMap implements INamedNodeMapProps {
private _element: Element;

/**
* Constructor.
*
* @param element Associated element.
*/
constructor(element: Element) {
Object.defineProperty(this, '_element', { enumerable: false, writable: true, value: element });
}

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

/**
* Length.
*/
public get length(): number {
return Object.keys(this._element._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._element._attributes)[index];
return attr ? attr : null;
}

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

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

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

/**
* Adds a new namespaced attribute node.
*
* @param attr Attribute.
* @returns Replaced attribute.
*/
public setNamedItemNS(attr: IAttr): IAttr {
return this._element.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._element.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._element.removeAttributeNode(attr);
}
return attr;
}

/**
* 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 '../attr/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 '../attr/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 '../attr/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.from(elementA.attributes);
const listB = Array.from(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 '../nodes/attr/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 '../nodes/attr/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
10 changes: 3 additions & 7 deletions packages/happy-dom/test/nodes/element/Element.test.ts
Expand Up @@ -1198,19 +1198,15 @@ describe('Element', () => {
it('Removes an attribute.', () => {
element.setAttribute('key1', 'value1');
element.removeAttribute('key1');
expect(element.attributes).toEqual({
length: 0
});
expect(element.attributes.length).toBe(0);
});
});

describe('removeAttributeNS()', () => {
it('Removes a namespace attribute.', () => {
element.setAttributeNS(NAMESPACE_URI, 'global:local', 'value');
element.removeAttributeNS(NAMESPACE_URI, 'local');
expect(element.attributes).toEqual({
length: 0
});
expect(element.attributes.length).toBe(0);
});
});

Expand Down Expand Up @@ -1478,7 +1474,7 @@ describe('Element', () => {
element.setAttributeNode(attribute);
element[method](attribute);

expect(element.attributes).toEqual({ length: 0 });
expect(element.attributes.length).toBe(0);
});
});
}
Expand Down

0 comments on commit 167cabf

Please sign in to comment.