/** * Methods for getting and modifying attributes. * * @module cheerio/attributes */ import { text } from '../static.js'; import { isTag, domEach, camelCase, cssCase } from '../utils.js'; 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 = { 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; // Matches strings that look like JSON objects or arrays const rbrace = /^{[^]*}$|^\[[^]*]$/; function getAttr(elem, name, xmlMode) { var _a; if (!elem || !isTag(elem)) return undefined; (_a = elem.attribs) !== null && _a !== void 0 ? _a : (elem.attribs = {}); // Return the entire attribs object if no attribute specified if (!name) { return elem.attribs; } if (hasOwn.call(elem.attribs, name)) { // Get the (decoded) attribute return !xmlMode && rboolean.test(name) ? name : elem.attribs[name]; } // Mimic the DOM and return text content as value for `option's` if (elem.name === 'option' && name === 'value') { return text(elem.children); } // Mimic DOM with default value for radios/checkboxes if (elem.name === 'input' && (elem.attribs['type'] === 'radio' || elem.attribs['type'] === 'checkbox') && name === 'value') { return 'on'; } return undefined; } /** * Sets the value of an attribute. The attribute will be deleted if the value is `null`. * * @private * @param el - The element to set the attribute on. * @param name - The attribute's name. * @param value - The attribute's value. */ function setAttr(el, name, value) { if (value === null) { removeAttribute(el, name); } else { el.attribs[name] = `${value}`; } } export function attr(name, value) { // Set the value (with attr map support) if (typeof name === 'object' || value !== undefined) { if (typeof value === 'function') { if (typeof name !== 'string') { { throw new Error('Bad combination of arguments.'); } } return domEach(this, (el, i) => { if (isTag(el)) setAttr(el, name, value.call(el, i, el.attribs[name])); }); } return domEach(this, (el) => { if (!isTag(el)) return; if (typeof name === 'object') { Object.keys(name).forEach((objName) => { const objValue = name[objName]; setAttr(el, objName, objValue); }); } else { setAttr(el, name, value); } }); } return arguments.length > 1 ? this : getAttr(this[0], name, this.options.xmlMode); } /** * Gets a node's prop. * * @private * @category Attributes * @param el - Element to get the prop of. * @param name - Name of the prop. * @returns The prop's value. */ function getProp(el, name, xmlMode) { return name in el ? // @ts-expect-error TS doesn't like us accessing the value directly here. el[name] : !xmlMode && rboolean.test(name) ? getAttr(el, name, false) !== undefined : getAttr(el, name, xmlMode); } /** * Sets the value of a prop. * * @private * @param el - The element to set the prop on. * @param name - The prop's name. * @param value - The prop's value. */ function setProp(el, name, value, xmlMode) { if (name in el) { // @ts-expect-error Overriding value el[name] = value; } else { setAttr(el, name, !xmlMode && rboolean.test(name) ? (value ? '' : null) : `${value}`); } } export function prop(name, value) { var _a; if (typeof name === 'string' && value === undefined) { const el = this[0]; if (!el || !isTag(el)) return undefined; switch (name) { case 'style': { const property = this.css(); const keys = Object.keys(property); keys.forEach((p, i) => { property[i] = p; }); property.length = keys.length; return property; } case 'tagName': case 'nodeName': { return el.name.toUpperCase(); } case 'href': case 'src': { const prop = (_a = el.attribs) === null || _a === void 0 ? void 0 : _a[name]; /* eslint-disable node/no-unsupported-features/node-builtins */ if (typeof URL !== 'undefined' && ((name === 'href' && (el.tagName === 'a' || el.name === 'link')) || (name === 'src' && (el.tagName === 'img' || el.tagName === 'iframe' || el.tagName === 'audio' || el.tagName === 'video' || el.tagName === 'source'))) && prop !== undefined && this.options.baseURI) { return new URL(prop, this.options.baseURI).href; } /* eslint-enable node/no-unsupported-features/node-builtins */ return prop; } case 'innerText': { return innerText(el); } case 'textContent': { return textContent(el); } case 'outerHTML': return this.clone().wrap('').parent().html(); case 'innerHTML': return this.html(); default: return getProp(el, name, this.options.xmlMode); } } if (typeof name === 'object' || value !== undefined) { if (typeof value === 'function') { if (typeof name === 'object') { throw new Error('Bad combination of arguments.'); } return domEach(this, (el, i) => { if (isTag(el)) { setProp(el, name, value.call(el, i, getProp(el, name, this.options.xmlMode)), this.options.xmlMode); } }); } return domEach(this, (el) => { if (!isTag(el)) return; if (typeof name === 'object') { Object.keys(name).forEach((key) => { const val = name[key]; setProp(el, key, val, this.options.xmlMode); }); } else { setProp(el, name, value, this.options.xmlMode); } }); } return undefined; } /** * Sets the value of a data attribute. * * @private * @param el - 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, name, value) { var _a; const elem = el; (_a = elem.data) !== null && _a !== void 0 ? _a : (elem.data = {}); if (typeof name === 'object') Object.assign(elem.data, name); else if (typeof name === 'string' && value !== undefined) { elem.data[name] = value; } } /** * 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. * * @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. */ function readData(el, name) { let domNames; let jsNames; let value; 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]; } 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.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 */ } } el.data[jsName] = value; } } return name == null ? el.data : value; } export function data(name, value) { var _a; const elem = this[0]; if (!elem || !isTag(elem)) return; const dataEl = elem; (_a = dataEl.data) !== null && _a !== void 0 ? _a : (dataEl.data = {}); // Return the entire data object if no data specified if (!name) { return readData(dataEl); } // Set the value (with attr map support) if (typeof name === 'object' || value !== undefined) { domEach(this, (el) => { if (isTag(el)) { if (typeof name === 'object') setData(el, name); else setData(el, name, value); } }); return this; } if (hasOwn.call(dataEl.data, name)) { return dataEl.data[name]; } return readData(dataEl, name); } export function val(value) { const querying = arguments.length === 0; const element = this[0]; if (!element || !isTag(element)) return querying ? undefined : this; switch (element.name) { case 'textarea': return this.text(value); case 'select': { const option = this.find('option:selected'); if (!querying) { if (this.attr('multiple') == null && typeof value === 'object') { return this; } this.find('option').removeAttr('selected'); const values = typeof value !== 'object' ? [value] : value; for (let i = 0; i < values.length; i++) { this.find(`option[value="${values[i]}"]`).attr('selected', ''); } return this; } return this.attr('multiple') ? option.toArray().map((el) => text(el.children)) : option.attr('value'); } case 'input': case 'option': return querying ? this.attr('value') : this.attr('value', value); } return undefined; } /** * Remove an attribute. * * @private * @param elem - Node to remove attribute from. * @param name - Name of the attribute to remove. */ function removeAttribute(elem, name) { if (!elem.attribs || !hasOwn.call(elem.attribs, name)) return; delete elem.attribs[name]; } /** * Splits a space-separated list of names to individual names. * * @category Attributes * @param names - Names to split. * @returns - Split names. */ function splitNames(names) { return names ? names.trim().split(rspace) : []; } /** * Method for removing attributes by `name`. * * @category Attributes * @example * * ```js * $('.pear').removeAttr('class').html(); * //=>
  • Pear
  • * * $('.apple').attr('id', 'favorite'); * $('.apple').removeAttr('id class').html(); * //=>
  • Apple
  • * ``` * * @param name - Name of the attribute. * @returns The instance itself. * @see {@link https://api.jquery.com/removeAttr/} */ export function removeAttr(name) { const attrNames = splitNames(name); for (let i = 0; i < attrNames.length; i++) { domEach(this, (elem) => { if (isTag(elem)) removeAttribute(elem, attrNames[i]); }); } return this; } /** * Check to see if _any_ of the matched elements have the given `className`. * * @category Attributes * @example * * ```js * $('.pear').hasClass('pear'); * //=> true * * $('apple').hasClass('fruit'); * //=> false * * $('li').hasClass('pear'); * //=> true * ``` * * @param className - Name of the class. * @returns Indicates if an element has the given `className`. * @see {@link https://api.jquery.com/hasClass/} */ export function hasClass(className) { return this.toArray().some((elem) => { const clazz = isTag(elem) && elem.attribs['class']; let idx = -1; if (clazz && className.length) { while ((idx = clazz.indexOf(className, idx + 1)) > -1) { const end = idx + className.length; if ((idx === 0 || rspace.test(clazz[idx - 1])) && (end === clazz.length || rspace.test(clazz[end]))) { return true; } } } return false; }); } /** * Adds class(es) to all of the matched elements. Also accepts a `function`. * * @category Attributes * @example * * ```js * $('.pear').addClass('fruit').html(); * //=>
  • Pear
  • * * $('.apple').addClass('fruit red').html(); * //=>
  • Apple
  • * ``` * * @param value - Name of new class. * @returns The instance itself. * @see {@link https://api.jquery.com/addClass/} */ export function addClass(value) { // Support functions if (typeof value === 'function') { return domEach(this, (el, i) => { if (isTag(el)) { const className = el.attribs['class'] || ''; addClass.call([el], value.call(el, i, className)); } }); } // Return if no value or not a string or function if (!value || typeof value !== 'string') return this; const classNames = value.split(rspace); const numElements = this.length; for (let i = 0; i < numElements; i++) { const el = this[i]; // If selected element isn't a tag, move on if (!isTag(el)) continue; // If we don't already have classes — always set xmlMode to false here, as it doesn't matter for classes const className = getAttr(el, 'class', false); if (!className) { setAttr(el, 'class', classNames.join(' ').trim()); } else { let setClass = ` ${className} `; // Check if class already exists for (let j = 0; j < classNames.length; j++) { const appendClass = `${classNames[j]} `; if (!setClass.includes(` ${appendClass}`)) setClass += appendClass; } setAttr(el, 'class', setClass.trim()); } } return this; } /** * Removes one or more space-separated classes from the selected elements. If no * `className` is defined, all classes will be removed. Also accepts a `function`. * * @category Attributes * @example * * ```js * $('.pear').removeClass('pear').html(); * //=>
  • Pear
  • * * $('.apple').addClass('red').removeClass().html(); * //=>
  • Apple
  • * ``` * * @param name - Name of the class. If not specified, removes all elements. * @returns The instance itself. * @see {@link https://api.jquery.com/removeClass/} */ export function removeClass(name) { // Handle if value is a function if (typeof name === 'function') { return domEach(this, (el, i) => { if (isTag(el)) { removeClass.call([el], name.call(el, i, el.attribs['class'] || '')); } }); } const classes = splitNames(name); const numClasses = classes.length; const removeAll = arguments.length === 0; return domEach(this, (el) => { if (!isTag(el)) return; if (removeAll) { // Short circuit the remove all case as this is the nice one el.attribs['class'] = ''; } else { const elClasses = splitNames(el.attribs['class']); let changed = false; for (let j = 0; j < numClasses; j++) { const index = elClasses.indexOf(classes[j]); if (index >= 0) { elClasses.splice(index, 1); changed = true; /* * We have to do another pass to ensure that there are not duplicate * classes listed */ j--; } } if (changed) { el.attribs['class'] = elClasses.join(' '); } } }); } /** * Add or remove class(es) from the matched elements, depending on either the * class's presence or the value of the switch argument. Also accepts a `function`. * * @category Attributes * @example * * ```js * $('.apple.green').toggleClass('fruit green red').html(); * //=>
  • Apple
  • * * $('.apple.green').toggleClass('fruit green red', true).html(); * //=>
  • Apple
  • * ``` * * @param value - Name of the class. Can also be a function. * @param stateVal - If specified the state of the class. * @returns The instance itself. * @see {@link https://api.jquery.com/toggleClass/} */ export function toggleClass(value, stateVal) { // Support functions if (typeof value === 'function') { return domEach(this, (el, i) => { if (isTag(el)) { toggleClass.call([el], value.call(el, i, el.attribs['class'] || '', stateVal), stateVal); } }); } // Return if no value or not a string or function if (!value || typeof value !== 'string') return this; const classNames = value.split(rspace); const numClasses = classNames.length; const state = typeof stateVal === 'boolean' ? (stateVal ? 1 : -1) : 0; const numElements = this.length; for (let i = 0; i < numElements; i++) { const el = this[i]; // If selected element isn't a tag, move on if (!isTag(el)) continue; const elementClasses = splitNames(el.attribs['class']); // Check if class already exists for (let j = 0; j < numClasses; j++) { // Check if the class name is currently defined const index = elementClasses.indexOf(classNames[j]); // Add if stateValue === true or we are toggling and there is no value if (state >= 0 && index < 0) { elementClasses.push(classNames[j]); } else if (state <= 0 && index >= 0) { // Otherwise remove but only if the item exists elementClasses.splice(index, 1); } } el.attribs['class'] = elementClasses.join(' '); } return this; } //# sourceMappingURL=attributes.js.map