attributes.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. var $ = require('../static'),
  2. utils = require('../utils'),
  3. isTag = utils.isTag,
  4. domEach = utils.domEach,
  5. hasOwn = Object.prototype.hasOwnProperty,
  6. camelCase = utils.camelCase,
  7. cssCase = utils.cssCase,
  8. rspace = /\s+/,
  9. dataAttrPrefix = 'data-',
  10. _ = {
  11. forEach: require('lodash.foreach'),
  12. extend: require('lodash.assignin'),
  13. some: require('lodash.some')
  14. },
  15. // Lookup table for coercing string data-* attributes to their corresponding
  16. // JavaScript primitives
  17. primitives = {
  18. null: null,
  19. true: true,
  20. false: false
  21. },
  22. // Attributes that are booleans
  23. rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
  24. // Matches strings that look like JSON objects or arrays
  25. rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/;
  26. var getAttr = function(elem, name) {
  27. if (!elem || !isTag(elem)) return;
  28. if (!elem.attribs) {
  29. elem.attribs = {};
  30. }
  31. // Return the entire attribs object if no attribute specified
  32. if (!name) {
  33. return elem.attribs;
  34. }
  35. if (hasOwn.call(elem.attribs, name)) {
  36. // Get the (decoded) attribute
  37. return rboolean.test(name) ? name : elem.attribs[name];
  38. }
  39. // Mimic the DOM and return text content as value for `option's`
  40. if (elem.name === 'option' && name === 'value') {
  41. return $.text(elem.children);
  42. }
  43. // Mimic DOM with default value for radios/checkboxes
  44. if (elem.name === 'input' &&
  45. (elem.attribs.type === 'radio' || elem.attribs.type === 'checkbox') &&
  46. name === 'value') {
  47. return 'on';
  48. }
  49. };
  50. var setAttr = function(el, name, value) {
  51. if (value === null) {
  52. removeAttribute(el, name);
  53. } else {
  54. el.attribs[name] = value+'';
  55. }
  56. };
  57. exports.attr = function(name, value) {
  58. // Set the value (with attr map support)
  59. if (typeof name === 'object' || value !== undefined) {
  60. if (typeof value === 'function') {
  61. return domEach(this, function(i, el) {
  62. setAttr(el, name, value.call(el, i, el.attribs[name]));
  63. });
  64. }
  65. return domEach(this, function(i, el) {
  66. if (!isTag(el)) return;
  67. if (typeof name === 'object') {
  68. _.forEach(name, function(value, name) {
  69. setAttr(el, name, value);
  70. });
  71. } else {
  72. setAttr(el, name, value);
  73. }
  74. });
  75. }
  76. return getAttr(this[0], name);
  77. };
  78. var getProp = function (el, name) {
  79. if (!el || !isTag(el)) return;
  80. return el.hasOwnProperty(name)
  81. ? el[name]
  82. : rboolean.test(name)
  83. ? getAttr(el, name) !== undefined
  84. : getAttr(el, name);
  85. };
  86. var setProp = function (el, name, value) {
  87. el[name] = rboolean.test(name) ? !!value : value;
  88. };
  89. exports.prop = function (name, value) {
  90. var i = 0,
  91. property;
  92. if (typeof name === 'string' && value === undefined) {
  93. switch (name) {
  94. case 'style':
  95. property = this.css();
  96. _.forEach(property, function (v, p) {
  97. property[i++] = p;
  98. });
  99. property.length = i;
  100. break;
  101. case 'tagName':
  102. case 'nodeName':
  103. property = this[0].name.toUpperCase();
  104. break;
  105. default:
  106. property = getProp(this[0], name);
  107. }
  108. return property;
  109. }
  110. if (typeof name === 'object' || value !== undefined) {
  111. if (typeof value === 'function') {
  112. return domEach(this, function(i, el) {
  113. setProp(el, name, value.call(el, i, getProp(el, name)));
  114. });
  115. }
  116. return domEach(this, function(i, el) {
  117. if (!isTag(el)) return;
  118. if (typeof name === 'object') {
  119. _.forEach(name, function(val, name) {
  120. setProp(el, name, val);
  121. });
  122. } else {
  123. setProp(el, name, value);
  124. }
  125. });
  126. }
  127. };
  128. var setData = function(el, name, value) {
  129. if (!el.data) {
  130. el.data = {};
  131. }
  132. if (typeof name === 'object') return _.extend(el.data, name);
  133. if (typeof name === 'string' && value !== undefined) {
  134. el.data[name] = value;
  135. } else if (typeof name === 'object') {
  136. _.extend(el.data, name);
  137. }
  138. };
  139. // Read the specified attribute from the equivalent HTML5 `data-*` attribute,
  140. // and (if present) cache the value in the node's internal data store. If no
  141. // attribute name is specified, read *all* HTML5 `data-*` attributes in this
  142. // manner.
  143. var readData = function(el, name) {
  144. var readAll = arguments.length === 1;
  145. var domNames, domName, jsNames, jsName, value, idx, length;
  146. if (readAll) {
  147. domNames = Object.keys(el.attribs).filter(function(attrName) {
  148. return attrName.slice(0, dataAttrPrefix.length) === dataAttrPrefix;
  149. });
  150. jsNames = domNames.map(function(domName) {
  151. return camelCase(domName.slice(dataAttrPrefix.length));
  152. });
  153. } else {
  154. domNames = [dataAttrPrefix + cssCase(name)];
  155. jsNames = [name];
  156. }
  157. for (idx = 0, length = domNames.length; idx < length; ++idx) {
  158. domName = domNames[idx];
  159. jsName = jsNames[idx];
  160. if (hasOwn.call(el.attribs, domName)) {
  161. value = el.attribs[domName];
  162. if (hasOwn.call(primitives, value)) {
  163. value = primitives[value];
  164. } else if (value === String(Number(value))) {
  165. value = Number(value);
  166. } else if (rbrace.test(value)) {
  167. try {
  168. value = JSON.parse(value);
  169. } catch(e){ }
  170. }
  171. el.data[jsName] = value;
  172. }
  173. }
  174. return readAll ? el.data : value;
  175. };
  176. exports.data = function(name, value) {
  177. var elem = this[0];
  178. if (!elem || !isTag(elem)) return;
  179. if (!elem.data) {
  180. elem.data = {};
  181. }
  182. // Return the entire data object if no data specified
  183. if (!name) {
  184. return readData(elem);
  185. }
  186. // Set the value (with attr map support)
  187. if (typeof name === 'object' || value !== undefined) {
  188. domEach(this, function(i, el) {
  189. setData(el, name, value);
  190. });
  191. return this;
  192. } else if (hasOwn.call(elem.data, name)) {
  193. return elem.data[name];
  194. }
  195. return readData(elem, name);
  196. };
  197. /**
  198. * Get the value of an element
  199. */
  200. exports.val = function(value) {
  201. var querying = arguments.length === 0,
  202. element = this[0];
  203. if(!element) return;
  204. switch (element.name) {
  205. case 'textarea':
  206. return this.text(value);
  207. case 'input':
  208. switch (this.attr('type')) {
  209. case 'radio':
  210. if (querying) {
  211. return this.attr('value');
  212. } else {
  213. this.attr('value', value);
  214. return this;
  215. }
  216. break;
  217. default:
  218. return this.attr('value', value);
  219. }
  220. return;
  221. case 'select':
  222. var option = this.find('option:selected'),
  223. returnValue;
  224. if (option === undefined) return undefined;
  225. if (!querying) {
  226. if (!this.attr().hasOwnProperty('multiple') && typeof value == 'object') {
  227. return this;
  228. }
  229. if (typeof value != 'object') {
  230. value = [value];
  231. }
  232. this.find('option').removeAttr('selected');
  233. for (var i = 0; i < value.length; i++) {
  234. this.find('option[value="' + value[i] + '"]').attr('selected', '');
  235. }
  236. return this;
  237. }
  238. returnValue = option.attr('value');
  239. if (this.attr().hasOwnProperty('multiple')) {
  240. returnValue = [];
  241. domEach(option, function(i, el) {
  242. returnValue.push(getAttr(el, 'value'));
  243. });
  244. }
  245. return returnValue;
  246. case 'option':
  247. if (!querying) {
  248. this.attr('value', value);
  249. return this;
  250. }
  251. return this.attr('value');
  252. }
  253. };
  254. /**
  255. * Remove an attribute
  256. */
  257. var removeAttribute = function(elem, name) {
  258. if (!elem.attribs || !hasOwn.call(elem.attribs, name))
  259. return;
  260. delete elem.attribs[name];
  261. };
  262. exports.removeAttr = function(name) {
  263. domEach(this, function(i, elem) {
  264. removeAttribute(elem, name);
  265. });
  266. return this;
  267. };
  268. exports.hasClass = function(className) {
  269. return _.some(this, function(elem) {
  270. var attrs = elem.attribs,
  271. clazz = attrs && attrs['class'],
  272. idx = -1,
  273. end;
  274. if (clazz) {
  275. while ((idx = clazz.indexOf(className, idx+1)) > -1) {
  276. end = idx + className.length;
  277. if ((idx === 0 || rspace.test(clazz[idx-1]))
  278. && (end === clazz.length || rspace.test(clazz[end]))) {
  279. return true;
  280. }
  281. }
  282. }
  283. });
  284. };
  285. exports.addClass = function(value) {
  286. // Support functions
  287. if (typeof value === 'function') {
  288. return domEach(this, function(i, el) {
  289. var className = el.attribs['class'] || '';
  290. exports.addClass.call([el], value.call(el, i, className));
  291. });
  292. }
  293. // Return if no value or not a string or function
  294. if (!value || typeof value !== 'string') return this;
  295. var classNames = value.split(rspace),
  296. numElements = this.length;
  297. for (var i = 0; i < numElements; i++) {
  298. // If selected element isn't a tag, move on
  299. if (!isTag(this[i])) continue;
  300. // If we don't already have classes
  301. var className = getAttr(this[i], 'class'),
  302. numClasses,
  303. setClass;
  304. if (!className) {
  305. setAttr(this[i], 'class', classNames.join(' ').trim());
  306. } else {
  307. setClass = ' ' + className + ' ';
  308. numClasses = classNames.length;
  309. // Check if class already exists
  310. for (var j = 0; j < numClasses; j++) {
  311. var appendClass = classNames[j] + ' ';
  312. if (setClass.indexOf(' ' + appendClass) < 0)
  313. setClass += appendClass;
  314. }
  315. setAttr(this[i], 'class', setClass.trim());
  316. }
  317. }
  318. return this;
  319. };
  320. var splitClass = function(className) {
  321. return className ? className.trim().split(rspace) : [];
  322. };
  323. exports.removeClass = function(value) {
  324. var classes,
  325. numClasses,
  326. removeAll;
  327. // Handle if value is a function
  328. if (typeof value === 'function') {
  329. return domEach(this, function(i, el) {
  330. exports.removeClass.call(
  331. [el], value.call(el, i, el.attribs['class'] || '')
  332. );
  333. });
  334. }
  335. classes = splitClass(value);
  336. numClasses = classes.length;
  337. removeAll = arguments.length === 0;
  338. return domEach(this, function(i, el) {
  339. if (!isTag(el)) return;
  340. if (removeAll) {
  341. // Short circuit the remove all case as this is the nice one
  342. el.attribs.class = '';
  343. } else {
  344. var elClasses = splitClass(el.attribs.class),
  345. index,
  346. changed;
  347. for (var j = 0; j < numClasses; j++) {
  348. index = elClasses.indexOf(classes[j]);
  349. if (index >= 0) {
  350. elClasses.splice(index, 1);
  351. changed = true;
  352. // We have to do another pass to ensure that there are not duplicate
  353. // classes listed
  354. j--;
  355. }
  356. }
  357. if (changed) {
  358. el.attribs.class = elClasses.join(' ');
  359. }
  360. }
  361. });
  362. };
  363. exports.toggleClass = function(value, stateVal) {
  364. // Support functions
  365. if (typeof value === 'function') {
  366. return domEach(this, function(i, el) {
  367. exports.toggleClass.call(
  368. [el],
  369. value.call(el, i, el.attribs['class'] || '', stateVal),
  370. stateVal
  371. );
  372. });
  373. }
  374. // Return if no value or not a string or function
  375. if (!value || typeof value !== 'string') return this;
  376. var classNames = value.split(rspace),
  377. numClasses = classNames.length,
  378. state = typeof stateVal === 'boolean' ? stateVal ? 1 : -1 : 0,
  379. numElements = this.length,
  380. elementClasses,
  381. index;
  382. for (var i = 0; i < numElements; i++) {
  383. // If selected element isn't a tag, move on
  384. if (!isTag(this[i])) continue;
  385. elementClasses = splitClass(this[i].attribs.class);
  386. // Check if class already exists
  387. for (var j = 0; j < numClasses; j++) {
  388. // Check if the class name is currently defined
  389. index = elementClasses.indexOf(classNames[j]);
  390. // Add if stateValue === true or we are toggling and there is no value
  391. if (state >= 0 && index < 0) {
  392. elementClasses.push(classNames[j]);
  393. } else if (state <= 0 && index >= 0) {
  394. // Otherwise remove but only if the item exists
  395. elementClasses.splice(index, 1);
  396. }
  397. }
  398. this[i].attribs.class = elementClasses.join(' ');
  399. }
  400. return this;
  401. };
  402. exports.is = function (selector) {
  403. if (selector) {
  404. return this.filter(selector).length > 0;
  405. }
  406. return false;
  407. };