attributes.js 20 KB

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