Skip to content

Commit d76300b

Browse files
authored
fix(attributes): Allow empty string in data, and simplify (#2818)
1 parent e2f0d1f commit d76300b

File tree

2 files changed

+68
-60
lines changed

2 files changed

+68
-60
lines changed

src/api/attributes.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,12 @@ describe('$(...)', () => {
530530
expect($el.data('custom')).toBe('{{templatevar}}');
531531
});
532532

533+
it('("") : should accept the empty string as a name', () => {
534+
const $el = cheerio('<div data-="a">');
535+
536+
expect($el.data('')).toBe('a');
537+
});
538+
533539
it('(hyphen key) : data addribute with hyphen should be camelized ;-)', () => {
534540
const data = $('.frey').data();
535541
expect(data).toStrictEqual({

src/api/attributes.ts

Lines changed: 62 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,7 @@ import { innerText, textContent } from 'domutils';
1212
const hasOwn = Object.prototype.hasOwnProperty;
1313
const rspace = /\s+/;
1414
const dataAttrPrefix = 'data-';
15-
/*
16-
* Lookup table for coercing string data-* attributes to their corresponding
17-
* JavaScript primitives
18-
*/
19-
const primitives: Record<string, unknown> = {
20-
null: null,
21-
true: true,
22-
false: false,
23-
};
15+
2416
// Attributes that are booleans
2517
const rboolean =
2618
/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i;
@@ -475,17 +467,15 @@ interface DataElement extends Element {
475467
* Sets the value of a data attribute.
476468
*
477469
* @private
478-
* @param el - The element to set the data attribute on.
470+
* @param elem - The element to set the data attribute on.
479471
* @param name - The data attribute's name.
480472
* @param value - The data attribute's value.
481473
*/
482474
function setData(
483-
el: Element,
475+
elem: DataElement,
484476
name: string | Record<string, unknown>,
485477
value?: unknown
486478
) {
487-
const elem: DataElement = el;
488-
489479
elem.data ??= {};
490480

491481
if (typeof name === 'object') Object.assign(elem.data, name);
@@ -494,62 +484,77 @@ function setData(
494484
}
495485
}
496486

487+
/**
488+
* Read _all_ HTML5 `data-*` attributes from the equivalent HTML5 `data-*`
489+
* attribute, and cache the value in the node's internal data store.
490+
*
491+
* @private
492+
* @category Attributes
493+
* @param el - Element to get the data attribute of.
494+
* @returns A map with all of the data attributes.
495+
*/
496+
function readAllData(el: DataElement): unknown {
497+
for (const domName of Object.keys(el.attribs)) {
498+
if (!domName.startsWith(dataAttrPrefix)) {
499+
continue;
500+
}
501+
502+
const jsName = camelCase(domName.slice(dataAttrPrefix.length));
503+
504+
if (!hasOwn.call(el.data, jsName)) {
505+
el.data![jsName] = parseDataValue(el.attribs[domName]);
506+
}
507+
}
508+
509+
return el.data;
510+
}
511+
497512
/**
498513
* Read the specified attribute from the equivalent HTML5 `data-*` attribute,
499-
* and (if present) cache the value in the node's internal data store. If no
500-
* attribute name is specified, read _all_ HTML5 `data-*` attributes in this
501-
* manner.
514+
* and (if present) cache the value in the node's internal data store.
502515
*
503516
* @private
504517
* @category Attributes
505518
* @param el - Element to get the data attribute of.
506519
* @param name - Name of the data attribute.
507-
* @returns The data attribute's value, or a map with all of the data
508-
* attributes.
520+
* @returns The data attribute's value.
509521
*/
510-
function readData(el: DataElement, name?: string): unknown {
511-
let domNames;
512-
let jsNames;
513-
let value;
522+
function readData(el: DataElement, name: string): unknown {
523+
const domName = dataAttrPrefix + cssCase(name);
524+
const data = el.data!;
514525

515-
if (name == null) {
516-
domNames = Object.keys(el.attribs).filter((attrName) =>
517-
attrName.startsWith(dataAttrPrefix)
518-
);
519-
jsNames = domNames.map((domName) =>
520-
camelCase(domName.slice(dataAttrPrefix.length))
521-
);
522-
} else {
523-
domNames = [dataAttrPrefix + cssCase(name)];
524-
jsNames = [name];
526+
if (hasOwn.call(data, name)) {
527+
return data[name];
525528
}
526529

527-
for (let idx = 0; idx < domNames.length; ++idx) {
528-
const domName = domNames[idx];
529-
const jsName = jsNames[idx];
530-
if (
531-
hasOwn.call(el.attribs, domName) &&
532-
!hasOwn.call((el as DataElement).data, jsName)
533-
) {
534-
value = el.attribs[domName];
535-
536-
if (hasOwn.call(primitives, value)) {
537-
value = primitives[value];
538-
} else if (value === String(Number(value))) {
539-
value = Number(value);
540-
} else if (rbrace.test(value)) {
541-
try {
542-
value = JSON.parse(value);
543-
} catch (e) {
544-
/* Ignore */
545-
}
546-
}
530+
if (hasOwn.call(el.attribs, domName)) {
531+
return (data[name] = parseDataValue(el.attribs[domName]));
532+
}
533+
534+
return undefined;
535+
}
547536

548-
(el.data as Record<string, unknown>)[jsName] = value;
537+
/**
538+
* Coerce string data-* attributes to their corresponding JavaScript primitives.
539+
*
540+
* @private
541+
* @category Attributes
542+
* @returns The parsed value.
543+
*/
544+
function parseDataValue(value: string): unknown {
545+
if (value === 'null') return null;
546+
if (value === 'true') return true;
547+
if (value === 'false') return false;
548+
const num = Number(value);
549+
if (value === String(num)) return num;
550+
if (rbrace.test(value)) {
551+
try {
552+
return JSON.parse(value);
553+
} catch (e) {
554+
/* Ignore */
549555
}
550556
}
551-
552-
return name == null ? el.data : value;
557+
return value;
553558
}
554559

555560
/**
@@ -650,8 +655,8 @@ export function data<T extends AnyNode>(
650655
dataEl.data ??= {};
651656

652657
// Return the entire data object if no data specified
653-
if (!name) {
654-
return readData(dataEl);
658+
if (name == null) {
659+
return readAllData(dataEl);
655660
}
656661

657662
// Set the value (with attr map support)
@@ -664,9 +669,6 @@ export function data<T extends AnyNode>(
664669
});
665670
return this;
666671
}
667-
if (hasOwn.call(dataEl.data, name)) {
668-
return dataEl.data[name];
669-
}
670672

671673
return readData(dataEl, name);
672674
}

0 commit comments

Comments
 (0)