From 1417badd37dee09e27e6783ac9929c25d45179c2 Mon Sep 17 00:00:00 2001 From: karpiuMG Date: Fri, 7 Mar 2025 00:01:02 +0100 Subject: [PATCH] fix: [#1463] Element.contentEditable should be synced with the "contenteditable" attribute (#1757) --- packages/happy-dom/src/PropertySymbol.ts | 2 - .../src/nodes/html-element/HTMLElement.ts | 42 +++++++-- .../nodes/html-element/HTMLElement.test.ts | 88 +++++++++++++++++-- 3 files changed, 118 insertions(+), 14 deletions(-) diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 654fa588..cfec6138 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -86,8 +86,6 @@ export const attributesProxy = Symbol('attributesProxy'); export const namespaceURI = Symbol('namespaceURI'); export const accessKey = Symbol('accessKey'); export const accessKeyLabel = Symbol('accessKeyLabel'); -export const contentEditable = Symbol('contentEditable'); -export const isContentEditable = Symbol('isContentEditable'); export const offsetHeight = Symbol('offsetHeight'); export const offsetWidth = Symbol('offsetWidth'); export const offsetLeft = Symbol('offsetLeft'); diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index c6b03313..ac3e2e09 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -22,8 +22,6 @@ export default class HTMLElement extends Element { // Internal properties public [PropertySymbol.accessKey] = ''; - public [PropertySymbol.contentEditable] = 'inherit'; - public [PropertySymbol.isContentEditable] = false; public [PropertySymbol.offsetHeight] = 0; public [PropertySymbol.offsetWidth] = 0; public [PropertySymbol.offsetLeft] = 0; @@ -500,7 +498,16 @@ export default class HTMLElement extends Element { * @returns Content editable. */ public get contentEditable(): string { - return this[PropertySymbol.contentEditable]; + const contentEditable = String(this.getAttribute('contentEditable')).toLowerCase(); + + switch (contentEditable) { + case 'false': + case 'true': + case 'plaintext-only': + return contentEditable; + default: + return 'inherit'; + } } /** @@ -509,7 +516,20 @@ export default class HTMLElement extends Element { * @param contentEditable Content editable. */ public set contentEditable(contentEditable: string) { - this[PropertySymbol.contentEditable] = contentEditable; + contentEditable = String(contentEditable).toLowerCase(); + + switch (contentEditable) { + case 'false': + case 'true': + case 'plaintext-only': + case 'inherit': + this.setAttribute('contentEditable', contentEditable); + break; + default: + throw new this[PropertySymbol.window].SyntaxError( + `Failed to set the 'contentEditable' property on 'HTMLElement': The value provided ('${contentEditable}') is not one of 'true', 'false', 'plaintext-only', or 'inherit'.` + ); + } } /** @@ -518,7 +538,17 @@ export default class HTMLElement extends Element { * @returns Is content editable. */ public get isContentEditable(): boolean { - return this[PropertySymbol.isContentEditable]; + const contentEditable = this.contentEditable; + + if (contentEditable === 'true' || contentEditable === 'plaintext-only') { + return true; + } + + if (contentEditable === 'inherit') { + return (this[PropertySymbol.parentNode])?.isContentEditable ?? false; + } + + return false; } /** @@ -944,8 +974,6 @@ export default class HTMLElement extends Element { const clone = super[PropertySymbol.cloneNode](deep); clone[PropertySymbol.accessKey] = this[PropertySymbol.accessKey]; - clone[PropertySymbol.contentEditable] = this[PropertySymbol.contentEditable]; - clone[PropertySymbol.isContentEditable] = this[PropertySymbol.isContentEditable]; return clone; } 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 aa2d5921..375f5dc1 100644 --- a/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts +++ b/packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts @@ -134,18 +134,96 @@ describe('HTMLElement', () => { }); } - describe('contentEditable', () => { - it('Returns "inherit".', () => { - const div = document.createElement('div'); + describe('get contentEditable()', () => { + it('Returns "inherit" by default.', () => { + const div = document.createElement('div'); + expect(div.contentEditable).toBe('inherit'); + }); + + it('Returns the "contenteditable" attribute value.', () => { + for (const value of [ + 'true', + 'false', + 'plaintext-only', + 'inherit', + 'TRUE', + 'FALSE', + 'PLAINTEXT-ONLY', + 'INHERIT' + ]) { + const div = document.createElement('div'); + div.setAttribute('contenteditable', value); + expect(div.contentEditable).toBe(value.toLowerCase()); + } + }); + + it('Returns "inherit" when the "contenteditable" attribute is set to an invalid value.', () => { + const div = document.createElement('div'); + div.setAttribute('contenteditable', 'invalid'); expect(div.contentEditable).toBe('inherit'); }); }); - describe('isContentEditable', () => { - it('Returns "false".', () => { + describe('set contentEditable()', () => { + it('Sets the "contenteditable" attribute.', () => { + const div = document.createElement('div'); + div.contentEditable = 'true'; + expect(div.getAttribute('contenteditable')).toBe('true'); + div.contentEditable = 'false'; + expect(div.getAttribute('contenteditable')).toBe('false'); + div.contentEditable = 'plaintext-only'; + expect(div.getAttribute('contenteditable')).toBe('plaintext-only'); + div.contentEditable = 'inherit'; + expect(div.getAttribute('contenteditable')).toBe('inherit'); + div.contentEditable = (true); + expect(div.getAttribute('contenteditable')).toBe('true'); + div.contentEditable = (false); + expect(div.getAttribute('contenteditable')).toBe('false'); + div.contentEditable = 'TRUE'; + expect(div.getAttribute('contenteditable')).toBe('true'); + div.contentEditable = 'FALSE'; + expect(div.getAttribute('contenteditable')).toBe('false'); + div.contentEditable = 'PLAINTEXT-ONLY'; + expect(div.getAttribute('contenteditable')).toBe('plaintext-only'); + div.contentEditable = 'INHERIT'; + expect(div.getAttribute('contenteditable')).toBe('inherit'); + }); + + it('Throws an error when an invalid value is provided.', () => { + const div = document.createElement('div'); + expect(() => { + div.contentEditable = 'invalid'; + }).toThrowError( + new SyntaxError( + `Failed to set the 'contentEditable' property on 'HTMLElement': The value provided ('invalid') is not one of 'true', 'false', 'plaintext-only', or 'inherit'.` + ) + ); + }); + }); + + describe('get isContentEditable()', () => { + it('Returns "false" by default.', () => { + const div = document.createElement('div'); + expect(div.isContentEditable).toBe(false); + }); + + it('Returns "true" when the "contenteditable" attribute is set to "true" or "plaintext-only".', () => { const div = document.createElement('div'); + div.setAttribute('contenteditable', 'true'); + expect(div.isContentEditable).toBe(true); + div.setAttribute('contenteditable', 'plaintext-only'); + expect(div.isContentEditable).toBe(true); + div.setAttribute('contenteditable', 'false'); expect(div.isContentEditable).toBe(false); }); + + it('Returns "true" when parent element is content editable and value is "inherit".', () => { + const parent = document.createElement('div'); + const div = document.createElement('div'); + parent.setAttribute('contenteditable', 'true'); + parent.appendChild(div); + expect(div.isContentEditable).toBe(true); + }); }); describe('get tabIndex()', () => {