tiny-svg.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  3. typeof define === 'function' && define.amd ? define(['exports'], factory) :
  4. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.TinySVG = {}));
  5. })(this, (function (exports) { 'use strict';
  6. function ensureImported(element, target) {
  7. if (element.ownerDocument !== target.ownerDocument) {
  8. try {
  9. // may fail on webkit
  10. return target.ownerDocument.importNode(element, true);
  11. } catch (e) {
  12. // ignore
  13. }
  14. }
  15. return element;
  16. }
  17. /**
  18. * appendTo utility
  19. */
  20. /**
  21. * Append a node to a target element and return the appended node.
  22. *
  23. * @param {SVGElement} element
  24. * @param {SVGElement} target
  25. *
  26. * @return {SVGElement} the appended node
  27. */
  28. function appendTo(element, target) {
  29. return target.appendChild(ensureImported(element, target));
  30. }
  31. /**
  32. * append utility
  33. */
  34. /**
  35. * Append a node to an element
  36. *
  37. * @param {SVGElement} element
  38. * @param {SVGElement} node
  39. *
  40. * @return {SVGElement} the element
  41. */
  42. function append(target, node) {
  43. appendTo(node, target);
  44. return target;
  45. }
  46. /**
  47. * attribute accessor utility
  48. */
  49. var LENGTH_ATTR = 2;
  50. var CSS_PROPERTIES = {
  51. 'alignment-baseline': 1,
  52. 'baseline-shift': 1,
  53. 'clip': 1,
  54. 'clip-path': 1,
  55. 'clip-rule': 1,
  56. 'color': 1,
  57. 'color-interpolation': 1,
  58. 'color-interpolation-filters': 1,
  59. 'color-profile': 1,
  60. 'color-rendering': 1,
  61. 'cursor': 1,
  62. 'direction': 1,
  63. 'display': 1,
  64. 'dominant-baseline': 1,
  65. 'enable-background': 1,
  66. 'fill': 1,
  67. 'fill-opacity': 1,
  68. 'fill-rule': 1,
  69. 'filter': 1,
  70. 'flood-color': 1,
  71. 'flood-opacity': 1,
  72. 'font': 1,
  73. 'font-family': 1,
  74. 'font-size': LENGTH_ATTR,
  75. 'font-size-adjust': 1,
  76. 'font-stretch': 1,
  77. 'font-style': 1,
  78. 'font-variant': 1,
  79. 'font-weight': 1,
  80. 'glyph-orientation-horizontal': 1,
  81. 'glyph-orientation-vertical': 1,
  82. 'image-rendering': 1,
  83. 'kerning': 1,
  84. 'letter-spacing': 1,
  85. 'lighting-color': 1,
  86. 'marker': 1,
  87. 'marker-end': 1,
  88. 'marker-mid': 1,
  89. 'marker-start': 1,
  90. 'mask': 1,
  91. 'opacity': 1,
  92. 'overflow': 1,
  93. 'pointer-events': 1,
  94. 'shape-rendering': 1,
  95. 'stop-color': 1,
  96. 'stop-opacity': 1,
  97. 'stroke': 1,
  98. 'stroke-dasharray': 1,
  99. 'stroke-dashoffset': 1,
  100. 'stroke-linecap': 1,
  101. 'stroke-linejoin': 1,
  102. 'stroke-miterlimit': 1,
  103. 'stroke-opacity': 1,
  104. 'stroke-width': LENGTH_ATTR,
  105. 'text-anchor': 1,
  106. 'text-decoration': 1,
  107. 'text-rendering': 1,
  108. 'unicode-bidi': 1,
  109. 'visibility': 1,
  110. 'word-spacing': 1,
  111. 'writing-mode': 1
  112. };
  113. function getAttribute(node, name) {
  114. if (CSS_PROPERTIES[name]) {
  115. return node.style[name];
  116. } else {
  117. return node.getAttributeNS(null, name);
  118. }
  119. }
  120. function setAttribute(node, name, value) {
  121. var hyphenated = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  122. var type = CSS_PROPERTIES[hyphenated];
  123. if (type) {
  124. // append pixel unit, unless present
  125. if (type === LENGTH_ATTR && typeof value === 'number') {
  126. value = String(value) + 'px';
  127. }
  128. node.style[hyphenated] = value;
  129. } else {
  130. node.setAttributeNS(null, name, value);
  131. }
  132. }
  133. function setAttributes(node, attrs) {
  134. var names = Object.keys(attrs), i, name;
  135. for (i = 0, name; (name = names[i]); i++) {
  136. setAttribute(node, name, attrs[name]);
  137. }
  138. }
  139. /**
  140. * Gets or sets raw attributes on a node.
  141. *
  142. * @param {SVGElement} node
  143. * @param {Object} [attrs]
  144. * @param {String} [name]
  145. * @param {String} [value]
  146. *
  147. * @return {String}
  148. */
  149. function attr(node, name, value) {
  150. if (typeof name === 'string') {
  151. if (value !== undefined) {
  152. setAttribute(node, name, value);
  153. } else {
  154. return getAttribute(node, name);
  155. }
  156. } else {
  157. setAttributes(node, name);
  158. }
  159. return node;
  160. }
  161. /**
  162. * Taken from https://github.com/component/classes
  163. *
  164. * Without the component bits.
  165. */
  166. /**
  167. * toString reference.
  168. */
  169. const toString = Object.prototype.toString;
  170. /**
  171. * Wrap `el` in a `ClassList`.
  172. *
  173. * @param {Element} el
  174. * @return {ClassList}
  175. * @api public
  176. */
  177. function classes(el) {
  178. return new ClassList(el);
  179. }
  180. function ClassList(el) {
  181. if (!el || !el.nodeType) {
  182. throw new Error('A DOM element reference is required');
  183. }
  184. this.el = el;
  185. this.list = el.classList;
  186. }
  187. /**
  188. * Add class `name` if not already present.
  189. *
  190. * @param {String} name
  191. * @return {ClassList}
  192. * @api public
  193. */
  194. ClassList.prototype.add = function(name) {
  195. this.list.add(name);
  196. return this;
  197. };
  198. /**
  199. * Remove class `name` when present, or
  200. * pass a regular expression to remove
  201. * any which match.
  202. *
  203. * @param {String|RegExp} name
  204. * @return {ClassList}
  205. * @api public
  206. */
  207. ClassList.prototype.remove = function(name) {
  208. if ('[object RegExp]' == toString.call(name)) {
  209. return this.removeMatching(name);
  210. }
  211. this.list.remove(name);
  212. return this;
  213. };
  214. /**
  215. * Remove all classes matching `re`.
  216. *
  217. * @param {RegExp} re
  218. * @return {ClassList}
  219. * @api private
  220. */
  221. ClassList.prototype.removeMatching = function(re) {
  222. const arr = this.array();
  223. for (let i = 0; i < arr.length; i++) {
  224. if (re.test(arr[i])) {
  225. this.remove(arr[i]);
  226. }
  227. }
  228. return this;
  229. };
  230. /**
  231. * Toggle class `name`, can force state via `force`.
  232. *
  233. * For browsers that support classList, but do not support `force` yet,
  234. * the mistake will be detected and corrected.
  235. *
  236. * @param {String} name
  237. * @param {Boolean} force
  238. * @return {ClassList}
  239. * @api public
  240. */
  241. ClassList.prototype.toggle = function(name, force) {
  242. if ('undefined' !== typeof force) {
  243. if (force !== this.list.toggle(name, force)) {
  244. this.list.toggle(name); // toggle again to correct
  245. }
  246. } else {
  247. this.list.toggle(name);
  248. }
  249. return this;
  250. };
  251. /**
  252. * Return an array of classes.
  253. *
  254. * @return {Array}
  255. * @api public
  256. */
  257. ClassList.prototype.array = function() {
  258. return Array.from(this.list);
  259. };
  260. /**
  261. * Check if class `name` is present.
  262. *
  263. * @param {String} name
  264. * @return {ClassList}
  265. * @api public
  266. */
  267. ClassList.prototype.has =
  268. ClassList.prototype.contains = function(name) {
  269. return this.list.contains(name);
  270. };
  271. function remove(element) {
  272. var parent = element.parentNode;
  273. if (parent) {
  274. parent.removeChild(element);
  275. }
  276. return element;
  277. }
  278. /**
  279. * Clear utility
  280. */
  281. /**
  282. * Removes all children from the given element
  283. *
  284. * @param {DOMElement} element
  285. * @return {DOMElement} the element (for chaining)
  286. */
  287. function clear(element) {
  288. var child;
  289. while ((child = element.firstChild)) {
  290. remove(child);
  291. }
  292. return element;
  293. }
  294. function clone(element) {
  295. return element.cloneNode(true);
  296. }
  297. var ns = {
  298. svg: 'http://www.w3.org/2000/svg'
  299. };
  300. /**
  301. * DOM parsing utility
  302. */
  303. var SVG_START = '<svg xmlns="' + ns.svg + '"';
  304. function parse(svg) {
  305. var unwrap = false;
  306. // ensure we import a valid svg document
  307. if (svg.substring(0, 4) === '<svg') {
  308. if (svg.indexOf(ns.svg) === -1) {
  309. svg = SVG_START + svg.substring(4);
  310. }
  311. } else {
  312. // namespace svg
  313. svg = SVG_START + '>' + svg + '</svg>';
  314. unwrap = true;
  315. }
  316. var parsed = parseDocument(svg);
  317. if (!unwrap) {
  318. return parsed;
  319. }
  320. var fragment = document.createDocumentFragment();
  321. var parent = parsed.firstChild;
  322. while (parent.firstChild) {
  323. fragment.appendChild(parent.firstChild);
  324. }
  325. return fragment;
  326. }
  327. function parseDocument(svg) {
  328. var parser;
  329. // parse
  330. parser = new DOMParser();
  331. parser.async = false;
  332. return parser.parseFromString(svg, 'text/xml');
  333. }
  334. /**
  335. * Create utility for SVG elements
  336. */
  337. /**
  338. * Create a specific type from name or SVG markup.
  339. *
  340. * @param {String} name the name or markup of the element
  341. * @param {Object} [attrs] attributes to set on the element
  342. *
  343. * @returns {SVGElement}
  344. */
  345. function create(name, attrs) {
  346. var element;
  347. if (name.charAt(0) === '<') {
  348. element = parse(name).firstChild;
  349. element = document.importNode(element, true);
  350. } else {
  351. element = document.createElementNS(ns.svg, name);
  352. }
  353. if (attrs) {
  354. attr(element, attrs);
  355. }
  356. return element;
  357. }
  358. /**
  359. * Events handling utility
  360. */
  361. function on(node, event, listener, useCapture) {
  362. node.addEventListener(event, listener, useCapture);
  363. }
  364. function off(node, event, listener, useCapture) {
  365. node.removeEventListener(event, listener, useCapture);
  366. }
  367. /**
  368. * Geometry helpers
  369. */
  370. // fake node used to instantiate svg geometry elements
  371. var node = null;
  372. function getNode() {
  373. if (node === null) {
  374. node = create('svg');
  375. }
  376. return node;
  377. }
  378. function extend(object, props) {
  379. var i, k, keys = Object.keys(props);
  380. for (i = 0; (k = keys[i]); i++) {
  381. object[k] = props[k];
  382. }
  383. return object;
  384. }
  385. function createPoint(x, y) {
  386. var point = getNode().createSVGPoint();
  387. switch (arguments.length) {
  388. case 0:
  389. return point;
  390. case 2:
  391. x = {
  392. x: x,
  393. y: y
  394. };
  395. break;
  396. }
  397. return extend(point, x);
  398. }
  399. /**
  400. * Create matrix via args.
  401. *
  402. * @example
  403. *
  404. * createMatrix({ a: 1, b: 1 });
  405. * createMatrix();
  406. * createMatrix(1, 2, 0, 0, 30, 20);
  407. *
  408. * @return {SVGMatrix}
  409. */
  410. function createMatrix(a, b, c, d, e, f) {
  411. var matrix = getNode().createSVGMatrix();
  412. switch (arguments.length) {
  413. case 0:
  414. return matrix;
  415. case 1:
  416. return extend(matrix, a);
  417. case 6:
  418. return extend(matrix, {
  419. a: a,
  420. b: b,
  421. c: c,
  422. d: d,
  423. e: e,
  424. f: f
  425. });
  426. }
  427. }
  428. function createTransform(matrix) {
  429. if (matrix) {
  430. return getNode().createSVGTransformFromMatrix(matrix);
  431. } else {
  432. return getNode().createSVGTransform();
  433. }
  434. }
  435. /**
  436. * Serialization util
  437. */
  438. var TEXT_ENTITIES = /([&<>]{1})/g;
  439. var ATTR_ENTITIES = /([\n\r"]{1})/g;
  440. var ENTITY_REPLACEMENT = {
  441. '&': '&amp;',
  442. '<': '&lt;',
  443. '>': '&gt;',
  444. '"': '\''
  445. };
  446. function escape(str, pattern) {
  447. function replaceFn(match, entity) {
  448. return ENTITY_REPLACEMENT[entity] || entity;
  449. }
  450. return str.replace(pattern, replaceFn);
  451. }
  452. function serialize(node, output) {
  453. var i, len, attrMap, attrNode, childNodes;
  454. switch (node.nodeType) {
  455. // TEXT
  456. case 3:
  457. // replace special XML characters
  458. output.push(escape(node.textContent, TEXT_ENTITIES));
  459. break;
  460. // ELEMENT
  461. case 1:
  462. output.push('<', node.tagName);
  463. if (node.hasAttributes()) {
  464. attrMap = node.attributes;
  465. for (i = 0, len = attrMap.length; i < len; ++i) {
  466. attrNode = attrMap.item(i);
  467. output.push(' ', attrNode.name, '="', escape(attrNode.value, ATTR_ENTITIES), '"');
  468. }
  469. }
  470. if (node.hasChildNodes()) {
  471. output.push('>');
  472. childNodes = node.childNodes;
  473. for (i = 0, len = childNodes.length; i < len; ++i) {
  474. serialize(childNodes.item(i), output);
  475. }
  476. output.push('</', node.tagName, '>');
  477. } else {
  478. output.push('/>');
  479. }
  480. break;
  481. // COMMENT
  482. case 8:
  483. output.push('<!--', escape(node.nodeValue, TEXT_ENTITIES), '-->');
  484. break;
  485. // CDATA
  486. case 4:
  487. output.push('<![CDATA[', node.nodeValue, ']]>');
  488. break;
  489. default:
  490. throw new Error('unable to handle node ' + node.nodeType);
  491. }
  492. return output;
  493. }
  494. /**
  495. * innerHTML like functionality for SVG elements.
  496. * based on innerSVG (https://code.google.com/p/innersvg)
  497. */
  498. function set(element, svg) {
  499. var parsed = parse(svg);
  500. // clear element contents
  501. clear(element);
  502. if (!svg) {
  503. return;
  504. }
  505. if (!isFragment(parsed)) {
  506. // extract <svg> from parsed document
  507. parsed = parsed.documentElement;
  508. }
  509. var nodes = slice(parsed.childNodes);
  510. // import + append each node
  511. for (var i = 0; i < nodes.length; i++) {
  512. appendTo(nodes[i], element);
  513. }
  514. }
  515. function get(element) {
  516. var child = element.firstChild,
  517. output = [];
  518. while (child) {
  519. serialize(child, output);
  520. child = child.nextSibling;
  521. }
  522. return output.join('');
  523. }
  524. function isFragment(node) {
  525. return node.nodeName === '#document-fragment';
  526. }
  527. function innerSVG(element, svg) {
  528. if (svg !== undefined) {
  529. try {
  530. set(element, svg);
  531. } catch (e) {
  532. throw new Error('error parsing SVG: ' + e.message);
  533. }
  534. return element;
  535. } else {
  536. return get(element);
  537. }
  538. }
  539. function slice(arr) {
  540. return Array.prototype.slice.call(arr);
  541. }
  542. /**
  543. * Selection utilities
  544. */
  545. function select(node, selector) {
  546. return node.querySelector(selector);
  547. }
  548. function selectAll(node, selector) {
  549. var nodes = node.querySelectorAll(selector);
  550. return [].map.call(nodes, function(element) {
  551. return element;
  552. });
  553. }
  554. /**
  555. * prependTo utility
  556. */
  557. /**
  558. * Prepend a node to a target element and return the prepended node.
  559. *
  560. * @param {SVGElement} node
  561. * @param {SVGElement} target
  562. *
  563. * @return {SVGElement} the prepended node
  564. */
  565. function prependTo(node, target) {
  566. return target.insertBefore(ensureImported(node, target), target.firstChild || null);
  567. }
  568. /**
  569. * prepend utility
  570. */
  571. /**
  572. * Prepend a node to a target element
  573. *
  574. * @param {SVGElement} target
  575. * @param {SVGElement} node
  576. *
  577. * @return {SVGElement} the target element
  578. */
  579. function prepend(target, node) {
  580. prependTo(node, target);
  581. return target;
  582. }
  583. /**
  584. * Replace utility
  585. */
  586. function replace(element, replacement) {
  587. element.parentNode.replaceChild(ensureImported(replacement, element), element);
  588. return replacement;
  589. }
  590. /**
  591. * transform accessor utility
  592. */
  593. function wrapMatrix(transformList, transform) {
  594. if (transform instanceof SVGMatrix) {
  595. return transformList.createSVGTransformFromMatrix(transform);
  596. }
  597. return transform;
  598. }
  599. function setTransforms(transformList, transforms) {
  600. var i, t;
  601. transformList.clear();
  602. for (i = 0; (t = transforms[i]); i++) {
  603. transformList.appendItem(wrapMatrix(transformList, t));
  604. }
  605. }
  606. /**
  607. * Get or set the transforms on the given node.
  608. *
  609. * @param {SVGElement} node
  610. * @param {SVGTransform|SVGMatrix|Array<SVGTransform|SVGMatrix>} [transforms]
  611. *
  612. * @return {SVGTransform} the consolidated transform
  613. */
  614. function transform(node, transforms) {
  615. var transformList = node.transform.baseVal;
  616. if (transforms) {
  617. if (!Array.isArray(transforms)) {
  618. transforms = [ transforms ];
  619. }
  620. setTransforms(transformList, transforms);
  621. }
  622. return transformList.consolidate();
  623. }
  624. exports.append = append;
  625. exports.appendTo = appendTo;
  626. exports.attr = attr;
  627. exports.classes = classes;
  628. exports.clear = clear;
  629. exports.clone = clone;
  630. exports.create = create;
  631. exports.createMatrix = createMatrix;
  632. exports.createPoint = createPoint;
  633. exports.createTransform = createTransform;
  634. exports.innerSVG = innerSVG;
  635. exports.off = off;
  636. exports.on = on;
  637. exports.prepend = prepend;
  638. exports.prependTo = prependTo;
  639. exports.remove = remove;
  640. exports.replace = replace;
  641. exports.select = select;
  642. exports.selectAll = selectAll;
  643. exports.transform = transform;
  644. Object.defineProperty(exports, '__esModule', { value: true });
  645. }));