attributes.js 19 KB

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