attributes.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. /**
  2. * Methods for getting and modifying attributes.
  3. *
  4. * @module cheerio/attributes
  5. */
  6. import { text } from '../static.js';
  7. import { isTag, domEach, camelCase, cssCase } from '../utils.js';
  8. import { innerText, textContent } from 'domutils';
  9. const hasOwn = Object.prototype.hasOwnProperty;
  10. const rspace = /\s+/;
  11. const dataAttrPrefix = 'data-';
  12. /*
  13. * Lookup table for coercing string data-* attributes to their corresponding
  14. * JavaScript primitives
  15. */
  16. const primitives = {
  17. null: null,
  18. true: true,
  19. false: false,
  20. };
  21. // Attributes that are booleans
  22. const rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i;
  23. // Matches strings that look like JSON objects or arrays
  24. const rbrace = /^{[^]*}$|^\[[^]*]$/;
  25. function getAttr(elem, name, xmlMode) {
  26. var _a;
  27. if (!elem || !isTag(elem))
  28. return undefined;
  29. (_a = elem.attribs) !== null && _a !== void 0 ? _a : (elem.attribs = {});
  30. // Return the entire attribs object if no attribute specified
  31. if (!name) {
  32. return elem.attribs;
  33. }
  34. if (hasOwn.call(elem.attribs, name)) {
  35. // Get the (decoded) attribute
  36. return !xmlMode && rboolean.test(name) ? name : elem.attribs[name];
  37. }
  38. // Mimic the DOM and return text content as value for `option's`
  39. if (elem.name === 'option' && name === 'value') {
  40. return text(elem.children);
  41. }
  42. // Mimic DOM with default value for radios/checkboxes
  43. if (elem.name === 'input' &&
  44. (elem.attribs['type'] === 'radio' || elem.attribs['type'] === 'checkbox') &&
  45. name === 'value') {
  46. return 'on';
  47. }
  48. return undefined;
  49. }
  50. /**
  51. * Sets the value of an attribute. The attribute will be deleted if the value is `null`.
  52. *
  53. * @private
  54. * @param el - The element to set the attribute on.
  55. * @param name - The attribute's name.
  56. * @param value - The attribute's value.
  57. */
  58. function setAttr(el, name, value) {
  59. if (value === null) {
  60. removeAttribute(el, name);
  61. }
  62. else {
  63. el.attribs[name] = `${value}`;
  64. }
  65. }
  66. export function attr(name, value) {
  67. // Set the value (with attr map support)
  68. if (typeof name === 'object' || value !== undefined) {
  69. if (typeof value === 'function') {
  70. if (typeof name !== 'string') {
  71. {
  72. throw new Error('Bad combination of arguments.');
  73. }
  74. }
  75. return domEach(this, (el, i) => {
  76. if (isTag(el))
  77. setAttr(el, name, value.call(el, i, el.attribs[name]));
  78. });
  79. }
  80. return domEach(this, (el) => {
  81. if (!isTag(el))
  82. return;
  83. if (typeof name === 'object') {
  84. Object.keys(name).forEach((objName) => {
  85. const objValue = name[objName];
  86. setAttr(el, objName, objValue);
  87. });
  88. }
  89. else {
  90. setAttr(el, name, value);
  91. }
  92. });
  93. }
  94. return arguments.length > 1
  95. ? this
  96. : getAttr(this[0], name, this.options.xmlMode);
  97. }
  98. /**
  99. * Gets a node's prop.
  100. *
  101. * @private
  102. * @category Attributes
  103. * @param el - Element to get the prop of.
  104. * @param name - Name of the prop.
  105. * @returns The prop's value.
  106. */
  107. function getProp(el, name, xmlMode) {
  108. return name in el
  109. ? // @ts-expect-error TS doesn't like us accessing the value directly here.
  110. el[name]
  111. : !xmlMode && rboolean.test(name)
  112. ? getAttr(el, name, false) !== undefined
  113. : getAttr(el, name, xmlMode);
  114. }
  115. /**
  116. * Sets the value of a prop.
  117. *
  118. * @private
  119. * @param el - The element to set the prop on.
  120. * @param name - The prop's name.
  121. * @param value - The prop's value.
  122. */
  123. function setProp(el, name, value, xmlMode) {
  124. if (name in el) {
  125. // @ts-expect-error Overriding value
  126. el[name] = value;
  127. }
  128. else {
  129. setAttr(el, name, !xmlMode && rboolean.test(name) ? (value ? '' : null) : `${value}`);
  130. }
  131. }
  132. export function prop(name, value) {
  133. var _a;
  134. if (typeof name === 'string' && value === undefined) {
  135. const el = this[0];
  136. if (!el || !isTag(el))
  137. return undefined;
  138. switch (name) {
  139. case 'style': {
  140. const property = this.css();
  141. const keys = Object.keys(property);
  142. keys.forEach((p, i) => {
  143. property[i] = p;
  144. });
  145. property.length = keys.length;
  146. return property;
  147. }
  148. case 'tagName':
  149. case 'nodeName': {
  150. return el.name.toUpperCase();
  151. }
  152. case 'href':
  153. case 'src': {
  154. const prop = (_a = el.attribs) === null || _a === void 0 ? void 0 : _a[name];
  155. /* eslint-disable node/no-unsupported-features/node-builtins */
  156. if (typeof URL !== 'undefined' &&
  157. ((name === 'href' && (el.tagName === 'a' || el.name === 'link')) ||
  158. (name === 'src' &&
  159. (el.tagName === 'img' ||
  160. el.tagName === 'iframe' ||
  161. el.tagName === 'audio' ||
  162. el.tagName === 'video' ||
  163. el.tagName === 'source'))) &&
  164. prop !== undefined &&
  165. this.options.baseURI) {
  166. return new URL(prop, this.options.baseURI).href;
  167. }
  168. /* eslint-enable node/no-unsupported-features/node-builtins */
  169. return prop;
  170. }
  171. case 'innerText': {
  172. return innerText(el);
  173. }
  174. case 'textContent': {
  175. return textContent(el);
  176. }
  177. case 'outerHTML':
  178. return this.clone().wrap('<container />').parent().html();
  179. case 'innerHTML':
  180. return this.html();
  181. default:
  182. return getProp(el, name, this.options.xmlMode);
  183. }
  184. }
  185. if (typeof name === 'object' || value !== undefined) {
  186. if (typeof value === 'function') {
  187. if (typeof name === 'object') {
  188. throw new Error('Bad combination of arguments.');
  189. }
  190. return domEach(this, (el, i) => {
  191. if (isTag(el)) {
  192. setProp(el, name, value.call(el, i, getProp(el, name, this.options.xmlMode)), this.options.xmlMode);
  193. }
  194. });
  195. }
  196. return domEach(this, (el) => {
  197. if (!isTag(el))
  198. return;
  199. if (typeof name === 'object') {
  200. Object.keys(name).forEach((key) => {
  201. const val = name[key];
  202. setProp(el, key, val, this.options.xmlMode);
  203. });
  204. }
  205. else {
  206. setProp(el, name, value, this.options.xmlMode);
  207. }
  208. });
  209. }
  210. return undefined;
  211. }
  212. /**
  213. * Sets the value of a data attribute.
  214. *
  215. * @private
  216. * @param el - The element to set the data attribute on.
  217. * @param name - The data attribute's name.
  218. * @param value - The data attribute's value.
  219. */
  220. function setData(el, name, value) {
  221. var _a;
  222. const elem = el;
  223. (_a = elem.data) !== null && _a !== void 0 ? _a : (elem.data = {});
  224. if (typeof name === 'object')
  225. Object.assign(elem.data, name);
  226. else if (typeof name === 'string' && value !== undefined) {
  227. elem.data[name] = value;
  228. }
  229. }
  230. /**
  231. * Read the specified attribute from the equivalent HTML5 `data-*` attribute,
  232. * and (if present) cache the value in the node's internal data store. If no
  233. * attribute name is specified, read _all_ HTML5 `data-*` attributes in this manner.
  234. *
  235. * @private
  236. * @category Attributes
  237. * @param el - Element to get the data attribute of.
  238. * @param name - Name of the data attribute.
  239. * @returns The data attribute's value, or a map with all of the data attributes.
  240. */
  241. function readData(el, name) {
  242. let domNames;
  243. let jsNames;
  244. let value;
  245. if (name == null) {
  246. domNames = Object.keys(el.attribs).filter((attrName) => attrName.startsWith(dataAttrPrefix));
  247. jsNames = domNames.map((domName) => camelCase(domName.slice(dataAttrPrefix.length)));
  248. }
  249. else {
  250. domNames = [dataAttrPrefix + cssCase(name)];
  251. jsNames = [name];
  252. }
  253. for (let idx = 0; idx < domNames.length; ++idx) {
  254. const domName = domNames[idx];
  255. const jsName = jsNames[idx];
  256. if (hasOwn.call(el.attribs, domName) &&
  257. !hasOwn.call(el.data, jsName)) {
  258. value = el.attribs[domName];
  259. if (hasOwn.call(primitives, value)) {
  260. value = primitives[value];
  261. }
  262. else if (value === String(Number(value))) {
  263. value = Number(value);
  264. }
  265. else if (rbrace.test(value)) {
  266. try {
  267. value = JSON.parse(value);
  268. }
  269. catch (e) {
  270. /* Ignore */
  271. }
  272. }
  273. el.data[jsName] = value;
  274. }
  275. }
  276. return name == null ? el.data : value;
  277. }
  278. export function data(name, value) {
  279. var _a;
  280. const elem = this[0];
  281. if (!elem || !isTag(elem))
  282. return;
  283. const dataEl = elem;
  284. (_a = dataEl.data) !== null && _a !== void 0 ? _a : (dataEl.data = {});
  285. // Return the entire data object if no data specified
  286. if (!name) {
  287. return readData(dataEl);
  288. }
  289. // Set the value (with attr map support)
  290. if (typeof name === 'object' || value !== undefined) {
  291. domEach(this, (el) => {
  292. if (isTag(el)) {
  293. if (typeof name === 'object')
  294. setData(el, name);
  295. else
  296. setData(el, name, value);
  297. }
  298. });
  299. return this;
  300. }
  301. if (hasOwn.call(dataEl.data, name)) {
  302. return dataEl.data[name];
  303. }
  304. return readData(dataEl, name);
  305. }
  306. export function val(value) {
  307. const querying = arguments.length === 0;
  308. const element = this[0];
  309. if (!element || !isTag(element))
  310. return querying ? undefined : this;
  311. switch (element.name) {
  312. case 'textarea':
  313. return this.text(value);
  314. case 'select': {
  315. const option = this.find('option:selected');
  316. if (!querying) {
  317. if (this.attr('multiple') == null && typeof value === 'object') {
  318. return this;
  319. }
  320. this.find('option').removeAttr('selected');
  321. const values = typeof value !== 'object' ? [value] : value;
  322. for (let i = 0; i < values.length; i++) {
  323. this.find(`option[value="${values[i]}"]`).attr('selected', '');
  324. }
  325. return this;
  326. }
  327. return this.attr('multiple')
  328. ? option.toArray().map((el) => text(el.children))
  329. : option.attr('value');
  330. }
  331. case 'input':
  332. case 'option':
  333. return querying
  334. ? this.attr('value')
  335. : this.attr('value', value);
  336. }
  337. return undefined;
  338. }
  339. /**
  340. * Remove an attribute.
  341. *
  342. * @private
  343. * @param elem - Node to remove attribute from.
  344. * @param name - Name of the attribute to remove.
  345. */
  346. function removeAttribute(elem, name) {
  347. if (!elem.attribs || !hasOwn.call(elem.attribs, name))
  348. return;
  349. delete elem.attribs[name];
  350. }
  351. /**
  352. * Splits a space-separated list of names to individual names.
  353. *
  354. * @category Attributes
  355. * @param names - Names to split.
  356. * @returns - Split names.
  357. */
  358. function splitNames(names) {
  359. return names ? names.trim().split(rspace) : [];
  360. }
  361. /**
  362. * Method for removing attributes by `name`.
  363. *
  364. * @category Attributes
  365. * @example
  366. *
  367. * ```js
  368. * $('.pear').removeAttr('class').html();
  369. * //=> <li>Pear</li>
  370. *
  371. * $('.apple').attr('id', 'favorite');
  372. * $('.apple').removeAttr('id class').html();
  373. * //=> <li>Apple</li>
  374. * ```
  375. *
  376. * @param name - Name of the attribute.
  377. * @returns The instance itself.
  378. * @see {@link https://api.jquery.com/removeAttr/}
  379. */
  380. export function removeAttr(name) {
  381. const attrNames = splitNames(name);
  382. for (let i = 0; i < attrNames.length; i++) {
  383. domEach(this, (elem) => {
  384. if (isTag(elem))
  385. removeAttribute(elem, attrNames[i]);
  386. });
  387. }
  388. return this;
  389. }
  390. /**
  391. * Check to see if _any_ of the matched elements have the given `className`.
  392. *
  393. * @category Attributes
  394. * @example
  395. *
  396. * ```js
  397. * $('.pear').hasClass('pear');
  398. * //=> true
  399. *
  400. * $('apple').hasClass('fruit');
  401. * //=> false
  402. *
  403. * $('li').hasClass('pear');
  404. * //=> true
  405. * ```
  406. *
  407. * @param className - Name of the class.
  408. * @returns Indicates if an element has the given `className`.
  409. * @see {@link https://api.jquery.com/hasClass/}
  410. */
  411. export function hasClass(className) {
  412. return this.toArray().some((elem) => {
  413. const clazz = isTag(elem) && elem.attribs['class'];
  414. let idx = -1;
  415. if (clazz && className.length) {
  416. while ((idx = clazz.indexOf(className, idx + 1)) > -1) {
  417. const end = idx + className.length;
  418. if ((idx === 0 || rspace.test(clazz[idx - 1])) &&
  419. (end === clazz.length || rspace.test(clazz[end]))) {
  420. return true;
  421. }
  422. }
  423. }
  424. return false;
  425. });
  426. }
  427. /**
  428. * Adds class(es) to all of the matched elements. Also accepts a `function`.
  429. *
  430. * @category Attributes
  431. * @example
  432. *
  433. * ```js
  434. * $('.pear').addClass('fruit').html();
  435. * //=> <li class="pear fruit">Pear</li>
  436. *
  437. * $('.apple').addClass('fruit red').html();
  438. * //=> <li class="apple fruit red">Apple</li>
  439. * ```
  440. *
  441. * @param value - Name of new class.
  442. * @returns The instance itself.
  443. * @see {@link https://api.jquery.com/addClass/}
  444. */
  445. export function addClass(value) {
  446. // Support functions
  447. if (typeof value === 'function') {
  448. return domEach(this, (el, i) => {
  449. if (isTag(el)) {
  450. const className = el.attribs['class'] || '';
  451. addClass.call([el], value.call(el, i, className));
  452. }
  453. });
  454. }
  455. // Return if no value or not a string or function
  456. if (!value || typeof value !== 'string')
  457. return this;
  458. const classNames = value.split(rspace);
  459. const numElements = this.length;
  460. for (let i = 0; i < numElements; i++) {
  461. const el = this[i];
  462. // If selected element isn't a tag, move on
  463. if (!isTag(el))
  464. continue;
  465. // If we don't already have classes — always set xmlMode to false here, as it doesn't matter for classes
  466. const className = getAttr(el, 'class', false);
  467. if (!className) {
  468. setAttr(el, 'class', classNames.join(' ').trim());
  469. }
  470. else {
  471. let setClass = ` ${className} `;
  472. // Check if class already exists
  473. for (let j = 0; j < classNames.length; j++) {
  474. const appendClass = `${classNames[j]} `;
  475. if (!setClass.includes(` ${appendClass}`))
  476. setClass += appendClass;
  477. }
  478. setAttr(el, 'class', setClass.trim());
  479. }
  480. }
  481. return this;
  482. }
  483. /**
  484. * Removes one or more space-separated classes from the selected elements. If no
  485. * `className` is defined, all classes will be removed. Also accepts a `function`.
  486. *
  487. * @category Attributes
  488. * @example
  489. *
  490. * ```js
  491. * $('.pear').removeClass('pear').html();
  492. * //=> <li class="">Pear</li>
  493. *
  494. * $('.apple').addClass('red').removeClass().html();
  495. * //=> <li class="">Apple</li>
  496. * ```
  497. *
  498. * @param name - Name of the class. If not specified, removes all elements.
  499. * @returns The instance itself.
  500. * @see {@link https://api.jquery.com/removeClass/}
  501. */
  502. export function removeClass(name) {
  503. // Handle if value is a function
  504. if (typeof name === 'function') {
  505. return domEach(this, (el, i) => {
  506. if (isTag(el)) {
  507. removeClass.call([el], name.call(el, i, el.attribs['class'] || ''));
  508. }
  509. });
  510. }
  511. const classes = splitNames(name);
  512. const numClasses = classes.length;
  513. const removeAll = arguments.length === 0;
  514. return domEach(this, (el) => {
  515. if (!isTag(el))
  516. return;
  517. if (removeAll) {
  518. // Short circuit the remove all case as this is the nice one
  519. el.attribs['class'] = '';
  520. }
  521. else {
  522. const elClasses = splitNames(el.attribs['class']);
  523. let changed = false;
  524. for (let j = 0; j < numClasses; j++) {
  525. const index = elClasses.indexOf(classes[j]);
  526. if (index >= 0) {
  527. elClasses.splice(index, 1);
  528. changed = true;
  529. /*
  530. * We have to do another pass to ensure that there are not duplicate
  531. * classes listed
  532. */
  533. j--;
  534. }
  535. }
  536. if (changed) {
  537. el.attribs['class'] = elClasses.join(' ');
  538. }
  539. }
  540. });
  541. }
  542. /**
  543. * Add or remove class(es) from the matched elements, depending on either the
  544. * class's presence or the value of the switch argument. Also accepts a `function`.
  545. *
  546. * @category Attributes
  547. * @example
  548. *
  549. * ```js
  550. * $('.apple.green').toggleClass('fruit green red').html();
  551. * //=> <li class="apple fruit red">Apple</li>
  552. *
  553. * $('.apple.green').toggleClass('fruit green red', true).html();
  554. * //=> <li class="apple green fruit red">Apple</li>
  555. * ```
  556. *
  557. * @param value - Name of the class. Can also be a function.
  558. * @param stateVal - If specified the state of the class.
  559. * @returns The instance itself.
  560. * @see {@link https://api.jquery.com/toggleClass/}
  561. */
  562. export function toggleClass(value, stateVal) {
  563. // Support functions
  564. if (typeof value === 'function') {
  565. return domEach(this, (el, i) => {
  566. if (isTag(el)) {
  567. toggleClass.call([el], value.call(el, i, el.attribs['class'] || '', stateVal), stateVal);
  568. }
  569. });
  570. }
  571. // Return if no value or not a string or function
  572. if (!value || typeof value !== 'string')
  573. return this;
  574. const classNames = value.split(rspace);
  575. const numClasses = classNames.length;
  576. const state = typeof stateVal === 'boolean' ? (stateVal ? 1 : -1) : 0;
  577. const numElements = this.length;
  578. for (let i = 0; i < numElements; i++) {
  579. const el = this[i];
  580. // If selected element isn't a tag, move on
  581. if (!isTag(el))
  582. continue;
  583. const elementClasses = splitNames(el.attribs['class']);
  584. // Check if class already exists
  585. for (let j = 0; j < numClasses; j++) {
  586. // Check if the class name is currently defined
  587. const index = elementClasses.indexOf(classNames[j]);
  588. // Add if stateValue === true or we are toggling and there is no value
  589. if (state >= 0 && index < 0) {
  590. elementClasses.push(classNames[j]);
  591. }
  592. else if (state <= 0 && index >= 0) {
  593. // Otherwise remove but only if the item exists
  594. elementClasses.splice(index, 1);
  595. }
  596. }
  597. el.attribs['class'] = elementClasses.join(' ');
  598. }
  599. return this;
  600. }
  601. //# sourceMappingURL=attributes.js.map