attributes.js 19 KB

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