Skip to content

Commit

Permalink
Merge pull request #850 from danielrentz/task/841-assigned-keys-to-sv…
Browse files Browse the repository at this point in the history
…gelement-dataset-are-ignored

#841@minor: Add dataset proxy for SVGElement.
  • Loading branch information
capricorn86 committed Apr 11, 2023
2 parents c70d674 + fa4e309 commit ed686e3
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 105 deletions.
105 changes: 105 additions & 0 deletions packages/happy-dom/src/nodes/element/Dataset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Element from '../element/Element';

/**
* Storage type for a dataset proxy.
*/
type DatasetRecord = Record<string, string>;

/**
* Dataset helper proxy.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
*/
export default class Dataset {
public readonly proxy: DatasetRecord;

/**
* @param element The parent element.
*/
constructor(element: Element) {
// Build the initial dataset record from all data attributes.
const dataset: DatasetRecord = {};
const attributes = element._attributes;
for (const name of Object.keys(attributes)) {
if (name.startsWith('data-')) {
const key = Dataset.kebabToCamelCase(name.replace('data-', ''));
dataset[key] = attributes[name].value;
}
}

// Documentation for Proxy:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
this.proxy = new Proxy(dataset, {
get(dataset: DatasetRecord, key: string): string {
const name = 'data-' + Dataset.camelCaseToKebab(key);
if (name in attributes) {
return (dataset[key] = attributes[name].value);
}
delete dataset[key];
return undefined;
},
set(dataset: DatasetRecord, key: string, value: string): boolean {
element.setAttribute('data-' + Dataset.camelCaseToKebab(key), value);
dataset[key] = value;
return true;
},
deleteProperty(dataset: DatasetRecord, key: string): boolean {
const name = 'data-' + Dataset.camelCaseToKebab(key);
const result1 = delete attributes[name];
const result2 = delete dataset[key];
return result1 && result2;
},
ownKeys(dataset: DatasetRecord): string[] {
// According to Mozilla we have to update the dataset object (target) to contain the same keys as what we return:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys
// "The result List must contain the keys of all non-configurable own properties of the target object."
const keys = [];
const deleteKeys = [];
for (const name of Object.keys(attributes)) {
if (name.startsWith('data-')) {
const key = Dataset.kebabToCamelCase(name.replace('data-', ''));
keys.push(key);
dataset[key] = attributes[name].value;
if (!dataset[key]) {
deleteKeys.push(key);
}
}
}
for (const key of deleteKeys) {
delete dataset[key];
}
return keys;
},
has(_dataset: DatasetRecord, key: string): boolean {
return !!attributes['data-' + Dataset.camelCaseToKebab(key)];
}
});
}

/**
* Transforms a kebab cased string to camel case.
*
* @param text Text string.
* @returns Camel cased string.
*/
public static kebabToCamelCase(text: string): string {
const parts = text.split('-');
for (let i = 0, max = parts.length; i < max; i++) {
parts[i] = i > 0 ? parts[i].charAt(0).toUpperCase() + parts[i].slice(1) : parts[i];
}
return parts.join('');
}

/**
* Transforms a camel cased string to kebab case.
*
* @param text Text string.
* @returns Kebab cased string.
*/
public static camelCaseToKebab(text: string): string {
return text
.toString()
.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase());
}
}
30 changes: 0 additions & 30 deletions packages/happy-dom/src/nodes/html-element/DatasetUtility.ts

This file was deleted.

71 changes: 3 additions & 68 deletions packages/happy-dom/src/nodes/html-element/HTMLElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration';
import IAttr from '../attr/IAttr';
import FocusEvent from '../../event/events/FocusEvent';
import PointerEvent from '../../event/events/PointerEvent';
import DatasetUtility from './DatasetUtility';
import Dataset from '../element/Dataset';
import NodeTypeEnum from '../node/NodeTypeEnum';
import DOMException from '../../exception/DOMException';
import Event from '../../event/Event';
Expand All @@ -28,7 +28,7 @@ export default class HTMLElement extends Element implements IHTMLElement {
public readonly clientWidth = 0;

private _style: CSSStyleDeclaration = null;
private _dataset: { [key: string]: string } = null;
private _dataset: Dataset = null;

// Events
public oncopy: (event: Event) => void | null = null;
Expand Down Expand Up @@ -216,72 +216,7 @@ export default class HTMLElement extends Element implements IHTMLElement {
* @returns Data set.
*/
public get dataset(): { [key: string]: string } {
if (this._dataset) {
return this._dataset;
}

const dataset: { [key: string]: string } = {};
const attributes = this._attributes;

for (const name of Object.keys(attributes)) {
if (name.startsWith('data-')) {
const key = DatasetUtility.kebabToCamelCase(name.replace('data-', ''));
dataset[key] = attributes[name].value;
}
}

// Documentation for Proxy:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
this._dataset = new Proxy(dataset, {
get: (dataset: { [key: string]: string }, key: string): string => {
const name = 'data-' + DatasetUtility.camelCaseToKebab(key);
if (this._attributes[name]) {
dataset[key] = this._attributes[name].value;
return this._attributes[name].value;
}
if (dataset[key] !== undefined) {
delete dataset[key];
}
return undefined;
},
set: (dataset: { [key: string]: string }, key: string, value: string): boolean => {
this.setAttribute('data-' + DatasetUtility.camelCaseToKebab(key), value);
dataset[key] = value;
return true;
},
deleteProperty: (dataset: { [key: string]: string }, key: string) => {
const name = 'data-' + DatasetUtility.camelCaseToKebab(key);
const result1 = delete attributes[name];
const result2 = delete dataset[key];
return result1 && result2;
},
ownKeys: (dataset: { [key: string]: string }) => {
// According to Mozilla we have to update the dataset object (target) to contain the same keys as what we return:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys
// "The result List must contain the keys of all non-configurable own properties of the target object."
const keys = [];
const deleteKeys = [];
for (const name of Object.keys(attributes)) {
if (name.startsWith('data-')) {
const key = DatasetUtility.kebabToCamelCase(name.replace('data-', ''));
keys.push(key);
dataset[key] = attributes[name].value;
if (!dataset[key]) {
deleteKeys.push(key);
}
}
}
for (const key of deleteKeys) {
delete dataset[key];
}
return keys;
},
has: (_dataset: { [key: string]: string }, key: string) => {
return !!attributes['data-' + DatasetUtility.camelCaseToKebab(key)];
}
});

return this._dataset;
return (this._dataset ??= new Dataset(this)).proxy;
}

/**
Expand Down
10 changes: 3 additions & 7 deletions packages/happy-dom/src/nodes/svg-element/SVGElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ISVGElement from './ISVGElement';
import ISVGSVGElement from './ISVGSVGElement';
import IAttr from '../attr/IAttr';
import Event from '../../event/Event';
import Dataset from '../element/Dataset';

/**
* SVG Element.
Expand All @@ -21,6 +22,7 @@ export default class SVGElement extends Element implements ISVGElement {
public onunload: (event: Event) => void | null = null;

private _style: CSSStyleDeclaration = null;
private _dataset: Dataset = null;

/**
* Returns viewport.
Expand Down Expand Up @@ -54,13 +56,7 @@ export default class SVGElement extends Element implements ISVGElement {
* @returns Data set.
*/
public get dataset(): { [key: string]: string } {
const dataset = {};
for (const name of Object.keys(this._attributes)) {
if (name.startsWith('data-')) {
dataset[name.replace('data-', '')] = this._attributes[name].value;
}
}
return dataset;
return (this._dataset ??= new Dataset(this)).proxy;
}

/**
Expand Down
33 changes: 33 additions & 0 deletions packages/happy-dom/test/nodes/svg-element/SVGElement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,38 @@ describe('SVGElement', () => {
const ownerSVG = line.ownerSVGElement;
expect(ownerSVG).toBe(null);
});

describe('get dataset()', () => {
it('Returns a Proxy behaving like an object that can add, remove, set and get element attributes prefixed with "data-".', () => {
element.setAttribute('test-alpha', 'value1');
element.setAttribute('data-test-alpha', 'value2');
element.setAttribute('test-beta', 'value3');
element.setAttribute('data-test-beta', 'value4');

const dataset = element.dataset;

expect(dataset).toBe(element.dataset);
expect(Object.keys(dataset)).toEqual(['testAlpha', 'testBeta']);
expect(Object.values(dataset)).toEqual(['value2', 'value4']);

dataset.testGamma = 'value5';

expect(element.getAttribute('data-test-gamma')).toBe('value5');
expect(Object.keys(dataset)).toEqual(['testAlpha', 'testBeta', 'testGamma']);
expect(Object.values(dataset)).toEqual(['value2', 'value4', 'value5']);

element.setAttribute('data-test-delta', 'value6');

expect(dataset.testDelta).toBe('value6');
expect(Object.keys(dataset)).toEqual(['testAlpha', 'testBeta', 'testGamma', 'testDelta']);
expect(Object.values(dataset)).toEqual(['value2', 'value4', 'value5', 'value6']);

delete dataset.testDelta;

expect(element.getAttribute('data-test-delta')).toBe(null);
expect(Object.keys(dataset)).toEqual(['testAlpha', 'testBeta', 'testGamma']);
expect(Object.values(dataset)).toEqual(['value2', 'value4', 'value5']);
});
});
});
});

0 comments on commit ed686e3

Please sign in to comment.