123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619 |
- /**
- * Methods for getting and modifying attributes.
- *
- * @module cheerio/attributes
- */
- import { text } from '../static.js';
- import { domEach, camelCase, cssCase } from '../utils.js';
- import { isTag } from 'domhandler';
- import { innerText, textContent } from 'domutils';
- const hasOwn = Object.prototype.hasOwnProperty;
- const rspace = /\s+/;
- const dataAttrPrefix = 'data-';
- // 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') {
- for (const objName of Object.keys(name)) {
- 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.
- * @param xmlMode - Disable handling of special HTML attributes.
- * @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.
- * @param xmlMode - Disable handling of special HTML attributes.
- */
- 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);
- for (let i = 0; i < keys.length; i++) {
- property[i] = keys[i];
- }
- 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];
- if (typeof URL !== 'undefined' &&
- ((name === 'href' && (el.tagName === 'a' || el.tagName === '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;
- }
- return prop;
- }
- case 'innerText': {
- return innerText(el);
- }
- case 'textContent': {
- return textContent(el);
- }
- case 'outerHTML': {
- return this.clone().wrap('<container />').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 TypeError('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') {
- for (const key of Object.keys(name)) {
- 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 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(elem, name, value) {
- var _a;
- (_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 _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) {
- 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.
- *
- * @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.
- */
- function readData(el, name) {
- const domName = dataAttrPrefix + cssCase(name);
- const data = el.data;
- if (hasOwn.call(data, name)) {
- return data[name];
- }
- if (hasOwn.call(el.attribs, domName)) {
- return (data[name] = parseDataValue(el.attribs[domName]));
- }
- return undefined;
- }
- /**
- * Coerce string data-* attributes to their corresponding JavaScript primitives.
- *
- * @private
- * @category Attributes
- * @param value - The value to parse.
- * @returns The parsed value.
- */
- function parseDataValue(value) {
- 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 {
- /* Ignore */
- }
- }
- return 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 == null) {
- return readAllData(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;
- }
- 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 (const val of values) {
- this.find(`option[value="${val}"]`).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();
- * //=> <li>Pear</li>
- *
- * $('.apple').attr('id', 'favorite');
- * $('.apple').removeAttr('id class').html();
- * //=> <li>Apple</li>
- * ```
- *
- * @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 (const attrName of attrNames) {
- domEach(this, (elem) => {
- if (isTag(elem))
- removeAttribute(elem, attrName);
- });
- }
- 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 > 0) {
- 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();
- * //=> <li class="pear fruit">Pear</li>
- *
- * $('.apple').addClass('fruit red').html();
- * //=> <li class="apple fruit red">Apple</li>
- * ```
- *
- * @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) {
- let setClass = ` ${className} `;
- // Check if class already exists
- for (const cn of classNames) {
- const appendClass = `${cn} `;
- if (!setClass.includes(` ${appendClass}`))
- setClass += appendClass;
- }
- setAttr(el, 'class', setClass.trim());
- }
- else {
- setAttr(el, 'class', classNames.join(' ').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();
- * //=> <li class="">Pear</li>
- *
- * $('.apple').addClass('red').removeClass().html();
- * //=> <li class="">Apple</li>
- * ```
- *
- * @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();
- * //=> <li class="apple fruit red">Apple</li>
- *
- * $('.apple.green').toggleClass('fruit green red', true).html();
- * //=> <li class="apple green fruit red">Apple</li>
- * ```
- *
- * @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
|