tiny-svg.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  3. typeof define === 'function' && define.amd ? define(['exports'], factory) :
  4. (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. * Clear utility
  163. */
  164. function index(arr, obj) {
  165. if (arr.indexOf) {
  166. return arr.indexOf(obj);
  167. }
  168. for (var i = 0; i < arr.length; ++i) {
  169. if (arr[i] === obj) {
  170. return i;
  171. }
  172. }
  173. return -1;
  174. }
  175. var re = /\s+/;
  176. var toString = Object.prototype.toString;
  177. function defined(o) {
  178. return typeof o !== 'undefined';
  179. }
  180. /**
  181. * Wrap `el` in a `ClassList`.
  182. *
  183. * @param {Element} el
  184. * @return {ClassList}
  185. * @api public
  186. */
  187. function classes(el) {
  188. return new ClassList(el);
  189. }
  190. function ClassList(el) {
  191. if (!el || !el.nodeType) {
  192. throw new Error('A DOM element reference is required');
  193. }
  194. this.el = el;
  195. this.list = el.classList;
  196. }
  197. /**
  198. * Add class `name` if not already present.
  199. *
  200. * @param {String} name
  201. * @return {ClassList}
  202. * @api public
  203. */
  204. ClassList.prototype.add = function(name) {
  205. // classList
  206. if (this.list) {
  207. this.list.add(name);
  208. return this;
  209. }
  210. // fallback
  211. var arr = this.array();
  212. var i = index(arr, name);
  213. if (!~i) {
  214. arr.push(name);
  215. }
  216. if (defined(this.el.className.baseVal)) {
  217. this.el.className.baseVal = arr.join(' ');
  218. } else {
  219. this.el.className = arr.join(' ');
  220. }
  221. return this;
  222. };
  223. /**
  224. * Remove class `name` when present, or
  225. * pass a regular expression to remove
  226. * any which match.
  227. *
  228. * @param {String|RegExp} name
  229. * @return {ClassList}
  230. * @api public
  231. */
  232. ClassList.prototype.remove = function(name) {
  233. if ('[object RegExp]' === toString.call(name)) {
  234. return this.removeMatching(name);
  235. }
  236. // classList
  237. if (this.list) {
  238. this.list.remove(name);
  239. return this;
  240. }
  241. // fallback
  242. var arr = this.array();
  243. var i = index(arr, name);
  244. if (~i) {
  245. arr.splice(i, 1);
  246. }
  247. this.el.className.baseVal = arr.join(' ');
  248. return this;
  249. };
  250. /**
  251. * Remove all classes matching `re`.
  252. *
  253. * @param {RegExp} re
  254. * @return {ClassList}
  255. * @api private
  256. */
  257. ClassList.prototype.removeMatching = function(re) {
  258. var arr = this.array();
  259. for (var i = 0; i < arr.length; i++) {
  260. if (re.test(arr[i])) {
  261. this.remove(arr[i]);
  262. }
  263. }
  264. return this;
  265. };
  266. /**
  267. * Toggle class `name`, can force state via `force`.
  268. *
  269. * For browsers that support classList, but do not support `force` yet,
  270. * the mistake will be detected and corrected.
  271. *
  272. * @param {String} name
  273. * @param {Boolean} force
  274. * @return {ClassList}
  275. * @api public
  276. */
  277. ClassList.prototype.toggle = function(name, force) {
  278. // classList
  279. if (this.list) {
  280. if (defined(force)) {
  281. if (force !== this.list.toggle(name, force)) {
  282. this.list.toggle(name); // toggle again to correct
  283. }
  284. } else {
  285. this.list.toggle(name);
  286. }
  287. return this;
  288. }
  289. // fallback
  290. if (defined(force)) {
  291. if (!force) {
  292. this.remove(name);
  293. } else {
  294. this.add(name);
  295. }
  296. } else {
  297. if (this.has(name)) {
  298. this.remove(name);
  299. } else {
  300. this.add(name);
  301. }
  302. }
  303. return this;
  304. };
  305. /**
  306. * Return an array of classes.
  307. *
  308. * @return {Array}
  309. * @api public
  310. */
  311. ClassList.prototype.array = function() {
  312. var className = this.el.getAttribute('class') || '';
  313. var str = className.replace(/^\s+|\s+$/g, '');
  314. var arr = str.split(re);
  315. if ('' === arr[0]) {
  316. arr.shift();
  317. }
  318. return arr;
  319. };
  320. /**
  321. * Check if class `name` is present.
  322. *
  323. * @param {String} name
  324. * @return {ClassList}
  325. * @api public
  326. */
  327. ClassList.prototype.has =
  328. ClassList.prototype.contains = function(name) {
  329. return (
  330. this.list ?
  331. this.list.contains(name) :
  332. !! ~index(this.array(), name)
  333. );
  334. };
  335. function remove(element) {
  336. var parent = element.parentNode;
  337. if (parent) {
  338. parent.removeChild(element);
  339. }
  340. return element;
  341. }
  342. /**
  343. * Clear utility
  344. */
  345. /**
  346. * Removes all children from the given element
  347. *
  348. * @param {DOMElement} element
  349. * @return {DOMElement} the element (for chaining)
  350. */
  351. function clear(element) {
  352. var child;
  353. while ((child = element.firstChild)) {
  354. remove(child);
  355. }
  356. return element;
  357. }
  358. function clone(element) {
  359. return element.cloneNode(true);
  360. }
  361. var ns = {
  362. svg: 'http://www.w3.org/2000/svg'
  363. };
  364. /**
  365. * DOM parsing utility
  366. */
  367. var SVG_START = '<svg xmlns="' + ns.svg + '"';
  368. function parse(svg) {
  369. var unwrap = false;
  370. // ensure we import a valid svg document
  371. if (svg.substring(0, 4) === '<svg') {
  372. if (svg.indexOf(ns.svg) === -1) {
  373. svg = SVG_START + svg.substring(4);
  374. }
  375. } else {
  376. // namespace svg
  377. svg = SVG_START + '>' + svg + '</svg>';
  378. unwrap = true;
  379. }
  380. var parsed = parseDocument(svg);
  381. if (!unwrap) {
  382. return parsed;
  383. }
  384. var fragment = document.createDocumentFragment();
  385. var parent = parsed.firstChild;
  386. while (parent.firstChild) {
  387. fragment.appendChild(parent.firstChild);
  388. }
  389. return fragment;
  390. }
  391. function parseDocument(svg) {
  392. var parser;
  393. // parse
  394. parser = new DOMParser();
  395. parser.async = false;
  396. return parser.parseFromString(svg, 'text/xml');
  397. }
  398. /**
  399. * Create utility for SVG elements
  400. */
  401. /**
  402. * Create a specific type from name or SVG markup.
  403. *
  404. * @param {String} name the name or markup of the element
  405. * @param {Object} [attrs] attributes to set on the element
  406. *
  407. * @returns {SVGElement}
  408. */
  409. function create(name, attrs) {
  410. var element;
  411. if (name.charAt(0) === '<') {
  412. element = parse(name).firstChild;
  413. element = document.importNode(element, true);
  414. } else {
  415. element = document.createElementNS(ns.svg, name);
  416. }
  417. if (attrs) {
  418. attr(element, attrs);
  419. }
  420. return element;
  421. }
  422. /**
  423. * Events handling utility
  424. */
  425. function on(node, event, listener, useCapture) {
  426. node.addEventListener(event, listener, useCapture);
  427. }
  428. function off(node, event, listener, useCapture) {
  429. node.removeEventListener(event, listener, useCapture);
  430. }
  431. /**
  432. * Geometry helpers
  433. */
  434. // fake node used to instantiate svg geometry elements
  435. var node = create('svg');
  436. function extend(object, props) {
  437. var i, k, keys = Object.keys(props);
  438. for (i = 0; (k = keys[i]); i++) {
  439. object[k] = props[k];
  440. }
  441. return object;
  442. }
  443. function createPoint(x, y) {
  444. var point = node.createSVGPoint();
  445. switch (arguments.length) {
  446. case 0:
  447. return point;
  448. case 2:
  449. x = {
  450. x: x,
  451. y: y
  452. };
  453. break;
  454. }
  455. return extend(point, x);
  456. }
  457. /**
  458. * Create matrix via args.
  459. *
  460. * @example
  461. *
  462. * createMatrix({ a: 1, b: 1 });
  463. * createMatrix();
  464. * createMatrix(1, 2, 0, 0, 30, 20);
  465. *
  466. * @return {SVGMatrix}
  467. */
  468. function createMatrix(a, b, c, d, e, f) {
  469. var matrix = node.createSVGMatrix();
  470. switch (arguments.length) {
  471. case 0:
  472. return matrix;
  473. case 1:
  474. return extend(matrix, a);
  475. case 6:
  476. return extend(matrix, {
  477. a: a,
  478. b: b,
  479. c: c,
  480. d: d,
  481. e: e,
  482. f: f
  483. });
  484. }
  485. }
  486. function createTransform(matrix) {
  487. if (matrix) {
  488. return node.createSVGTransformFromMatrix(matrix);
  489. } else {
  490. return node.createSVGTransform();
  491. }
  492. }
  493. /**
  494. * Serialization util
  495. */
  496. var TEXT_ENTITIES = /([&<>]{1})/g;
  497. var ATTR_ENTITIES = /([\n\r"]{1})/g;
  498. var ENTITY_REPLACEMENT = {
  499. '&': '&amp;',
  500. '<': '&lt;',
  501. '>': '&gt;',
  502. '"': '\''
  503. };
  504. function escape(str, pattern) {
  505. function replaceFn(match, entity) {
  506. return ENTITY_REPLACEMENT[entity] || entity;
  507. }
  508. return str.replace(pattern, replaceFn);
  509. }
  510. function serialize(node, output) {
  511. var i, len, attrMap, attrNode, childNodes;
  512. switch (node.nodeType) {
  513. // TEXT
  514. case 3:
  515. // replace special XML characters
  516. output.push(escape(node.textContent, TEXT_ENTITIES));
  517. break;
  518. // ELEMENT
  519. case 1:
  520. output.push('<', node.tagName);
  521. if (node.hasAttributes()) {
  522. attrMap = node.attributes;
  523. for (i = 0, len = attrMap.length; i < len; ++i) {
  524. attrNode = attrMap.item(i);
  525. output.push(' ', attrNode.name, '="', escape(attrNode.value, ATTR_ENTITIES), '"');
  526. }
  527. }
  528. if (node.hasChildNodes()) {
  529. output.push('>');
  530. childNodes = node.childNodes;
  531. for (i = 0, len = childNodes.length; i < len; ++i) {
  532. serialize(childNodes.item(i), output);
  533. }
  534. output.push('</', node.tagName, '>');
  535. } else {
  536. output.push('/>');
  537. }
  538. break;
  539. // COMMENT
  540. case 8:
  541. output.push('<!--', escape(node.nodeValue, TEXT_ENTITIES), '-->');
  542. break;
  543. // CDATA
  544. case 4:
  545. output.push('<![CDATA[', node.nodeValue, ']]>');
  546. break;
  547. default:
  548. throw new Error('unable to handle node ' + node.nodeType);
  549. }
  550. return output;
  551. }
  552. /**
  553. * innerHTML like functionality for SVG elements.
  554. * based on innerSVG (https://code.google.com/p/innersvg)
  555. */
  556. function set(element, svg) {
  557. var parsed = parse(svg);
  558. // clear element contents
  559. clear(element);
  560. if (!svg) {
  561. return;
  562. }
  563. if (!isFragment(parsed)) {
  564. // extract <svg> from parsed document
  565. parsed = parsed.documentElement;
  566. }
  567. var nodes = slice(parsed.childNodes);
  568. // import + append each node
  569. for (var i = 0; i < nodes.length; i++) {
  570. appendTo(nodes[i], element);
  571. }
  572. }
  573. function get(element) {
  574. var child = element.firstChild,
  575. output = [];
  576. while (child) {
  577. serialize(child, output);
  578. child = child.nextSibling;
  579. }
  580. return output.join('');
  581. }
  582. function isFragment(node) {
  583. return node.nodeName === '#document-fragment';
  584. }
  585. function innerSVG(element, svg) {
  586. if (svg !== undefined) {
  587. try {
  588. set(element, svg);
  589. } catch (e) {
  590. throw new Error('error parsing SVG: ' + e.message);
  591. }
  592. return element;
  593. } else {
  594. return get(element);
  595. }
  596. }
  597. function slice(arr) {
  598. return Array.prototype.slice.call(arr);
  599. }
  600. /**
  601. * Selection utilities
  602. */
  603. function select(node, selector) {
  604. return node.querySelector(selector);
  605. }
  606. function selectAll(node, selector) {
  607. var nodes = node.querySelectorAll(selector);
  608. return [].map.call(nodes, function(element) {
  609. return element;
  610. });
  611. }
  612. /**
  613. * prependTo utility
  614. */
  615. /**
  616. * Prepend a node to a target element and return the prepended node.
  617. *
  618. * @param {SVGElement} node
  619. * @param {SVGElement} target
  620. *
  621. * @return {SVGElement} the prepended node
  622. */
  623. function prependTo(node, target) {
  624. return target.insertBefore(ensureImported(node, target), target.firstChild || null);
  625. }
  626. /**
  627. * prepend utility
  628. */
  629. /**
  630. * Prepend a node to a target element
  631. *
  632. * @param {SVGElement} target
  633. * @param {SVGElement} node
  634. *
  635. * @return {SVGElement} the target element
  636. */
  637. function prepend(target, node) {
  638. prependTo(node, target);
  639. return target;
  640. }
  641. /**
  642. * Replace utility
  643. */
  644. function replace(element, replacement) {
  645. element.parentNode.replaceChild(ensureImported(replacement, element), element);
  646. return replacement;
  647. }
  648. /**
  649. * transform accessor utility
  650. */
  651. function wrapMatrix(transformList, transform) {
  652. if (transform instanceof SVGMatrix) {
  653. return transformList.createSVGTransformFromMatrix(transform);
  654. }
  655. return transform;
  656. }
  657. function setTransforms(transformList, transforms) {
  658. var i, t;
  659. transformList.clear();
  660. for (i = 0; (t = transforms[i]); i++) {
  661. transformList.appendItem(wrapMatrix(transformList, t));
  662. }
  663. }
  664. /**
  665. * Get or set the transforms on the given node.
  666. *
  667. * @param {SVGElement} node
  668. * @param {SVGTransform|SVGMatrix|Array<SVGTransform|SVGMatrix>} [transforms]
  669. *
  670. * @return {SVGTransform} the consolidated transform
  671. */
  672. function transform(node, transforms) {
  673. var transformList = node.transform.baseVal;
  674. if (transforms) {
  675. if (!Array.isArray(transforms)) {
  676. transforms = [ transforms ];
  677. }
  678. setTransforms(transformList, transforms);
  679. }
  680. return transformList.consolidate();
  681. }
  682. exports.append = append;
  683. exports.appendTo = appendTo;
  684. exports.attr = attr;
  685. exports.classes = classes;
  686. exports.clear = clear;
  687. exports.clone = clone;
  688. exports.create = create;
  689. exports.innerSVG = innerSVG;
  690. exports.prepend = prepend;
  691. exports.prependTo = prependTo;
  692. exports.remove = remove;
  693. exports.replace = replace;
  694. exports.transform = transform;
  695. exports.on = on;
  696. exports.off = off;
  697. exports.createPoint = createPoint;
  698. exports.createMatrix = createMatrix;
  699. exports.createTransform = createTransform;
  700. exports.select = select;
  701. exports.selectAll = selectAll;
  702. Object.defineProperty(exports, '__esModule', { value: true });
  703. })));