Skip to content

Commit

Permalink
#728@patch: Improves support for NamedNodeMap, which is used as the E…
Browse files Browse the repository at this point in the history
…lement.attributes property. It will now reflect any changes done to it on the Element itself.
  • Loading branch information
capricorn86 committed Aug 2, 2023
1 parent e9038c7 commit f9c9ee9
Show file tree
Hide file tree
Showing 38 changed files with 1,053 additions and 722 deletions.
1 change: 0 additions & 1 deletion packages/happy-dom/bin/change-file-extension.cjs
Expand Up @@ -64,7 +64,6 @@ async function renameFiles(files, args) {
for (const file of newFiles) {
writePromises.push(
FS.promises.readFile(file.oldPath).then((content) => {
debugger;
return FS.promises
.writeFile(
file.newPath,
Expand Down
@@ -1,10 +1,11 @@
import IElement from '../../nodes/element/IElement.js';
import Attr from '../../nodes/attr/Attr.js';
import IAttr from '../../nodes/attr/IAttr.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 '../../named-node-map/NamedNodeMap.js';

/**
* CSS Style Declaration.
Expand Down Expand Up @@ -77,17 +78,21 @@ export default abstract class AbstractCSSStyleDeclaration {

if (this._ownerElement) {
const style = new CSSStyleDeclarationPropertyManager({ cssText });
if (!this._ownerElement['_attributes']['style']) {
Attr._ownerDocument = this._ownerElement.ownerDocument;
this._ownerElement['_attributes']['style'] = new Attr();
this._ownerElement['_attributes']['style'].name = 'style';
let styleAttribute = <IAttr>this._ownerElement.attributes['style'];

if (!styleAttribute) {
styleAttribute = this._ownerElement.ownerDocument.createAttribute('style');
// We use "_setNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute.
(<NamedNodeMap>this._ownerElement.attributes)._setNamedItemWithoutConsequences(
styleAttribute
);
}

if (this._ownerElement.isConnected) {
this._ownerElement.ownerDocument['_cacheID']++;
}

this._ownerElement['_attributes']['style'].value = style.toString();
styleAttribute.value = style.toString();
} else {
this._style = new CSSStyleDeclarationPropertyManager({ cssText });
}
Expand Down Expand Up @@ -130,20 +135,25 @@ export default abstract class AbstractCSSStyleDeclaration {
if (!stringValue) {
this.removeProperty(name);
} else if (this._ownerElement) {
if (!this._ownerElement['_attributes']['style']) {
Attr._ownerDocument = this._ownerElement.ownerDocument;
this._ownerElement['_attributes']['style'] = new Attr();
this._ownerElement['_attributes']['style'].name = 'style';
}
let styleAttribute = <IAttr>this._ownerElement.attributes['style'];

const style = this._elementStyle.getElementStyle();
style.set(name, stringValue, !!priority);
if (!styleAttribute) {
styleAttribute = this._ownerElement.ownerDocument.createAttribute('style');

// We use "_setNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute.
(<NamedNodeMap>this._ownerElement.attributes)._setNamedItemWithoutConsequences(
styleAttribute
);
}

if (this._ownerElement.isConnected) {
this._ownerElement.ownerDocument['_cacheID']++;
}

this._ownerElement['_attributes']['style'].value = style.toString();
const style = this._elementStyle.getElementStyle();
style.set(name, stringValue, !!priority);

styleAttribute.value = style.toString();
} else {
this._style.set(name, stringValue, !!priority);
}
Expand All @@ -168,14 +178,16 @@ export default abstract class AbstractCSSStyleDeclaration {
const style = this._elementStyle.getElementStyle();
style.remove(name);
const newCSSText = style.toString();
if (newCSSText) {
if (this._ownerElement.isConnected) {
this._ownerElement.ownerDocument['_cacheID']++;
}

this._ownerElement['_attributes']['style'].value = newCSSText;
if (this._ownerElement.isConnected) {
this._ownerElement.ownerDocument['_cacheID']++;
}

if (newCSSText) {
(<IAttr>this._ownerElement.attributes['style']).value = newCSSText;
} else {
delete this._ownerElement['_attributes']['style'];
// We use "_removeNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute.
(<NamedNodeMap>this._ownerElement.attributes)._removeNamedItemWithoutConsequences('style');
}
} else {
this._style.remove(name);
Expand Down
Expand Up @@ -63,7 +63,7 @@ export default class CSSStyleDeclarationElementStyle {
return this.getComputedElementStyle();
}

const cssText = this.element['_attributes']['style']?.value;
const cssText = this.element.attributes['style']?.value;

if (cssText) {
if (this.cache.propertyManager && this.cache.cssText === cssText) {
Expand Down Expand Up @@ -182,8 +182,9 @@ export default class CSSStyleDeclarationElementStyle {
elementCSSText += cssText.cssText;
}

if (parentElement.element['_attributes']['style']?.value) {
elementCSSText += parentElement.element['_attributes']['style'].value;
const elementStyleAttribute = (<IElement>parentElement.element).attributes['style'];
if (elementStyleAttribute) {
elementCSSText += elementStyleAttribute.value;
}

CSSStyleDeclarationCSSParser.parse(elementCSSText, (name, value, important) => {
Expand Down
52 changes: 26 additions & 26 deletions packages/happy-dom/src/named-node-map/INamedNodeMap.ts
Expand Up @@ -6,65 +6,65 @@ import IAttr from '../nodes/attr/IAttr.js';
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap.
*/
export default interface INamedNodeMap extends Iterable<IAttr> {
export default interface INamedNodeMap {
[index: number]: IAttr;
[Symbol.toStringTag]: string;
readonly length: number;

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

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

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

/**
* Adds a new attribute node.
* Sets named item.
*
* @param attr Attribute.
* @returns Replaced attribute.
* @param item Item.
* @returns Replaced item.
*/
setNamedItem: (attr: IAttr) => IAttr;
setNamedItem(item: IAttr): IAttr | null;

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

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

/**
* Removes a namespaced attribute.
* Removes a namespaced item.
*
* @param namespace Namespace.
* @param localName Local name of the attribute.
* @returns Removed attribute.
* @param localName Local name of the item.
* @returns Removed item.
*/
removeNamedItemNS: (namespace: string, localName: string) => IAttr;
removeNamedItemNS(namespace: string, localName: string): IAttr | null;
}

0 comments on commit f9c9ee9

Please sign in to comment.