jsAPI.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. 'use strict';
  2. var cssSelect = require('css-select');
  3. var svgoCssSelectAdapter = require('./css-select-adapter');
  4. var cssSelectOpts = {
  5. xmlMode: true,
  6. adapter: svgoCssSelectAdapter
  7. };
  8. var JSAPI = module.exports = function(data, parentNode) {
  9. Object.assign(this, data);
  10. if (parentNode) {
  11. Object.defineProperty(this, 'parentNode', {
  12. writable: true,
  13. value: parentNode
  14. });
  15. }
  16. };
  17. /**
  18. * Perform a deep clone of this node.
  19. *
  20. * @return {Object} element
  21. */
  22. JSAPI.prototype.clone = function() {
  23. var node = this;
  24. var nodeData = {};
  25. Object.keys(node).forEach(function(key) {
  26. if (key !== 'class' && key !== 'style' && key !== 'content') {
  27. nodeData[key] = node[key];
  28. }
  29. });
  30. // Deep-clone node data.
  31. nodeData = JSON.parse(JSON.stringify(nodeData));
  32. // parentNode gets set to a proper object by the parent clone,
  33. // but it needs to be true/false now to do the right thing
  34. // in the constructor.
  35. var clonedNode = new JSAPI(nodeData, !!node.parentNode);
  36. if (node.class) {
  37. clonedNode.class = node.class.clone(clonedNode);
  38. }
  39. if (node.style) {
  40. clonedNode.style = node.style.clone(clonedNode);
  41. }
  42. if (node.content) {
  43. clonedNode.content = node.content.map(function(childNode) {
  44. var clonedChild = childNode.clone();
  45. clonedChild.parentNode = clonedNode;
  46. return clonedChild;
  47. });
  48. }
  49. return clonedNode;
  50. };
  51. /**
  52. * Determine if item is an element
  53. * (any, with a specific name or in a names array).
  54. *
  55. * @param {String|Array} [param] element name or names arrays
  56. * @return {Boolean}
  57. */
  58. JSAPI.prototype.isElem = function(param) {
  59. if (!param) return !!this.elem;
  60. if (Array.isArray(param)) return !!this.elem && (param.indexOf(this.elem) > -1);
  61. return !!this.elem && this.elem === param;
  62. };
  63. /**
  64. * Renames an element
  65. *
  66. * @param {String} name new element name
  67. * @return {Object} element
  68. */
  69. JSAPI.prototype.renameElem = function(name) {
  70. if (name && typeof name === 'string')
  71. this.elem = this.local = name;
  72. return this;
  73. };
  74. /**
  75. * Determine if element is empty.
  76. *
  77. * @return {Boolean}
  78. */
  79. JSAPI.prototype.isEmpty = function() {
  80. return !this.content || !this.content.length;
  81. };
  82. /**
  83. * Find the closest ancestor of the current element.
  84. * @param elemName
  85. *
  86. * @return {?Object}
  87. */
  88. JSAPI.prototype.closestElem = function(elemName) {
  89. var elem = this;
  90. while ((elem = elem.parentNode) && !elem.isElem(elemName));
  91. return elem;
  92. };
  93. /**
  94. * Changes content by removing elements and/or adding new elements.
  95. *
  96. * @param {Number} start Index at which to start changing the content.
  97. * @param {Number} n Number of elements to remove.
  98. * @param {Array|Object} [insertion] Elements to add to the content.
  99. * @return {Array} Removed elements.
  100. */
  101. JSAPI.prototype.spliceContent = function(start, n, insertion) {
  102. if (arguments.length < 2) return [];
  103. if (!Array.isArray(insertion))
  104. insertion = Array.apply(null, arguments).slice(2);
  105. insertion.forEach(function(inner) { inner.parentNode = this }, this);
  106. return this.content.splice.apply(this.content, [start, n].concat(insertion));
  107. };
  108. /**
  109. * Determine if element has an attribute
  110. * (any, or by name or by name + value).
  111. *
  112. * @param {String} [name] attribute name
  113. * @param {String} [val] attribute value (will be toString()'ed)
  114. * @return {Boolean}
  115. */
  116. JSAPI.prototype.hasAttr = function(name, val) {
  117. if (!this.attrs || !Object.keys(this.attrs).length) return false;
  118. if (!arguments.length) return !!this.attrs;
  119. if (val !== undefined) return !!this.attrs[name] && this.attrs[name].value === val.toString();
  120. return !!this.attrs[name];
  121. };
  122. /**
  123. * Determine if element has an attribute by local name
  124. * (any, or by name or by name + value).
  125. *
  126. * @param {String} [localName] local attribute name
  127. * @param {Number|String|RegExp|Function} [val] attribute value (will be toString()'ed or executed, otherwise ignored)
  128. * @return {Boolean}
  129. */
  130. JSAPI.prototype.hasAttrLocal = function(localName, val) {
  131. if (!this.attrs || !Object.keys(this.attrs).length) return false;
  132. if (!arguments.length) return !!this.attrs;
  133. var callback;
  134. switch (val != null && val.constructor && val.constructor.name) {
  135. case 'Number': // same as String
  136. case 'String': callback = stringValueTest; break;
  137. case 'RegExp': callback = regexpValueTest; break;
  138. case 'Function': callback = funcValueTest; break;
  139. default: callback = nameTest;
  140. }
  141. return this.someAttr(callback);
  142. function nameTest(attr) {
  143. return attr.local === localName;
  144. }
  145. function stringValueTest(attr) {
  146. return attr.local === localName && val == attr.value;
  147. }
  148. function regexpValueTest(attr) {
  149. return attr.local === localName && val.test(attr.value);
  150. }
  151. function funcValueTest(attr) {
  152. return attr.local === localName && val(attr.value);
  153. }
  154. };
  155. /**
  156. * Get a specific attribute from an element
  157. * (by name or name + value).
  158. *
  159. * @param {String} name attribute name
  160. * @param {String} [val] attribute value (will be toString()'ed)
  161. * @return {Object|Undefined}
  162. */
  163. JSAPI.prototype.attr = function(name, val) {
  164. if (!this.hasAttr() || !arguments.length) return undefined;
  165. if (val !== undefined) return this.hasAttr(name, val) ? this.attrs[name] : undefined;
  166. return this.attrs[name];
  167. };
  168. /**
  169. * Get computed attribute value from an element
  170. *
  171. * @param {String} name attribute name
  172. * @return {Object|Undefined}
  173. */
  174. JSAPI.prototype.computedAttr = function(name, val) {
  175. /* jshint eqnull: true */
  176. if (!arguments.length) return;
  177. for (var elem = this; elem && (!elem.hasAttr(name) || !elem.attr(name).value); elem = elem.parentNode);
  178. if (val != null) {
  179. return elem ? elem.hasAttr(name, val) : false;
  180. } else if (elem && elem.hasAttr(name)) {
  181. return elem.attrs[name].value;
  182. }
  183. };
  184. /**
  185. * Remove a specific attribute.
  186. *
  187. * @param {String|Array} name attribute name
  188. * @param {String} [val] attribute value
  189. * @return {Boolean}
  190. */
  191. JSAPI.prototype.removeAttr = function(name, val, recursive) {
  192. if (!arguments.length) return false;
  193. if (Array.isArray(name)) {
  194. name.forEach(this.removeAttr, this);
  195. return false;
  196. }
  197. if (!this.hasAttr(name)) return false;
  198. if (!recursive && val && this.attrs[name].value !== val) return false;
  199. delete this.attrs[name];
  200. if (!Object.keys(this.attrs).length) delete this.attrs;
  201. return true;
  202. };
  203. /**
  204. * Add attribute.
  205. *
  206. * @param {Object} [attr={}] attribute object
  207. * @return {Object|Boolean} created attribute or false if no attr was passed in
  208. */
  209. JSAPI.prototype.addAttr = function(attr) {
  210. attr = attr || {};
  211. if (attr.name === undefined ||
  212. attr.prefix === undefined ||
  213. attr.local === undefined
  214. ) return false;
  215. this.attrs = this.attrs || {};
  216. this.attrs[attr.name] = attr;
  217. if(attr.name === 'class') { // newly added class attribute
  218. this.class.hasClass();
  219. }
  220. if(attr.name === 'style') { // newly added style attribute
  221. this.style.hasStyle();
  222. }
  223. return this.attrs[attr.name];
  224. };
  225. /**
  226. * Iterates over all attributes.
  227. *
  228. * @param {Function} callback callback
  229. * @param {Object} [context] callback context
  230. * @return {Boolean} false if there are no any attributes
  231. */
  232. JSAPI.prototype.eachAttr = function(callback, context) {
  233. if (!this.hasAttr()) return false;
  234. for (var name in this.attrs) {
  235. callback.call(context, this.attrs[name]);
  236. }
  237. return true;
  238. };
  239. /**
  240. * Tests whether some attribute passes the test.
  241. *
  242. * @param {Function} callback callback
  243. * @param {Object} [context] callback context
  244. * @return {Boolean} false if there are no any attributes
  245. */
  246. JSAPI.prototype.someAttr = function(callback, context) {
  247. if (!this.hasAttr()) return false;
  248. for (var name in this.attrs) {
  249. if (callback.call(context, this.attrs[name])) return true;
  250. }
  251. return false;
  252. };
  253. /**
  254. * Evaluate a string of CSS selectors against the element and returns matched elements.
  255. *
  256. * @param {String} selectors CSS selector(s) string
  257. * @return {Array} null if no elements matched
  258. */
  259. JSAPI.prototype.querySelectorAll = function(selectors) {
  260. var matchedEls = cssSelect(selectors, this, cssSelectOpts);
  261. return matchedEls.length > 0 ? matchedEls : null;
  262. };
  263. /**
  264. * Evaluate a string of CSS selectors against the element and returns only the first matched element.
  265. *
  266. * @param {String} selectors CSS selector(s) string
  267. * @return {Array} null if no element matched
  268. */
  269. JSAPI.prototype.querySelector = function(selectors) {
  270. return cssSelect.selectOne(selectors, this, cssSelectOpts);
  271. };
  272. /**
  273. * Test if a selector matches a given element.
  274. *
  275. * @param {String} selector CSS selector string
  276. * @return {Boolean} true if element would be selected by selector string, false if it does not
  277. */
  278. JSAPI.prototype.matches = function(selector) {
  279. return cssSelect.is(this, selector, cssSelectOpts);
  280. };