diff --git a/src/api/attributes.spec.ts b/src/api/attributes.spec.ts index d144525056..c026e3511e 100644 --- a/src/api/attributes.spec.ts +++ b/src/api/attributes.spec.ts @@ -530,6 +530,12 @@ describe('$(...)', () => { expect($el.data('custom')).toBe('{{templatevar}}'); }); + it('("") : should accept the empty string as a name', () => { + const $el = cheerio('
'); + + expect($el.data('')).toBe('a'); + }); + it('(hyphen key) : data addribute with hyphen should be camelized ;-)', () => { const data = $('.frey').data(); expect(data).toStrictEqual({ diff --git a/src/api/attributes.ts b/src/api/attributes.ts index de945b2a83..20305fe04f 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -12,15 +12,7 @@ import { innerText, textContent } from 'domutils'; const hasOwn = Object.prototype.hasOwnProperty; const rspace = /\s+/; const dataAttrPrefix = 'data-'; -/* - * Lookup table for coercing string data-* attributes to their corresponding - * JavaScript primitives - */ -const primitives: Record = { - null: null, - true: true, - false: false, -}; + // Attributes that are booleans const rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i; @@ -475,17 +467,15 @@ interface DataElement extends Element { * Sets the value of a data attribute. * * @private - * @param el - The element to set the data attribute on. + * @param elem - The element to set the data attribute on. * @param name - The data attribute's name. * @param value - The data attribute's value. */ function setData( - el: Element, + elem: DataElement, name: string | Record, value?: unknown ) { - const elem: DataElement = el; - elem.data ??= {}; if (typeof name === 'object') Object.assign(elem.data, name); @@ -494,62 +484,77 @@ function setData( } } +/** + * Read _all_ HTML5 `data-*` attributes from the equivalent HTML5 `data-*` + * attribute, and cache the value in the node's internal data store. + * + * @private + * @category Attributes + * @param el - Element to get the data attribute of. + * @returns A map with all of the data attributes. + */ +function readAllData(el: DataElement): unknown { + for (const domName of Object.keys(el.attribs)) { + if (!domName.startsWith(dataAttrPrefix)) { + continue; + } + + const jsName = camelCase(domName.slice(dataAttrPrefix.length)); + + if (!hasOwn.call(el.data, jsName)) { + el.data![jsName] = parseDataValue(el.attribs[domName]); + } + } + + return el.data; +} + /** * Read the specified attribute from the equivalent HTML5 `data-*` attribute, - * and (if present) cache the value in the node's internal data store. If no - * attribute name is specified, read _all_ HTML5 `data-*` attributes in this - * manner. + * and (if present) cache the value in the node's internal data store. * * @private * @category Attributes * @param el - Element to get the data attribute of. * @param name - Name of the data attribute. - * @returns The data attribute's value, or a map with all of the data - * attributes. + * @returns The data attribute's value. */ -function readData(el: DataElement, name?: string): unknown { - let domNames; - let jsNames; - let value; +function readData(el: DataElement, name: string): unknown { + const domName = dataAttrPrefix + cssCase(name); + const data = el.data!; - if (name == null) { - domNames = Object.keys(el.attribs).filter((attrName) => - attrName.startsWith(dataAttrPrefix) - ); - jsNames = domNames.map((domName) => - camelCase(domName.slice(dataAttrPrefix.length)) - ); - } else { - domNames = [dataAttrPrefix + cssCase(name)]; - jsNames = [name]; + if (hasOwn.call(data, name)) { + return data[name]; } - for (let idx = 0; idx < domNames.length; ++idx) { - const domName = domNames[idx]; - const jsName = jsNames[idx]; - if ( - hasOwn.call(el.attribs, domName) && - !hasOwn.call((el as DataElement).data, jsName) - ) { - value = el.attribs[domName]; - - if (hasOwn.call(primitives, value)) { - value = primitives[value]; - } else if (value === String(Number(value))) { - value = Number(value); - } else if (rbrace.test(value)) { - try { - value = JSON.parse(value); - } catch (e) { - /* Ignore */ - } - } + if (hasOwn.call(el.attribs, domName)) { + return (data[name] = parseDataValue(el.attribs[domName])); + } + + return undefined; +} - (el.data as Record)[jsName] = value; +/** + * Coerce string data-* attributes to their corresponding JavaScript primitives. + * + * @private + * @category Attributes + * @returns The parsed value. + */ +function parseDataValue(value: string): unknown { + if (value === 'null') return null; + if (value === 'true') return true; + if (value === 'false') return false; + const num = Number(value); + if (value === String(num)) return num; + if (rbrace.test(value)) { + try { + return JSON.parse(value); + } catch (e) { + /* Ignore */ } } - - return name == null ? el.data : value; + return value; } /** @@ -650,8 +655,8 @@ export function data( dataEl.data ??= {}; // Return the entire data object if no data specified - if (!name) { - return readData(dataEl); + if (name == null) { + return readAllData(dataEl); } // Set the value (with attr map support) @@ -664,9 +669,6 @@ export function data( }); return this; } - if (hasOwn.call(dataEl.data, name)) { - return dataEl.data[name]; - } return readData(dataEl, name); }