Нет описания
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

svg-injector.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. /**
  2. * SVGInjector v1.1.3-RC - Fast, caching, dynamic inline SVG DOM injection library
  3. * https://github.com/iconic/SVGInjector
  4. *
  5. * Copyright (c) 2014 Waybury <hello@waybury.com>
  6. * @license MIT
  7. */
  8. (function (window, document) {
  9. 'use strict';
  10. // Environment
  11. var isLocal = window.location.protocol === 'file:';
  12. var hasSvgSupport = document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1');
  13. function uniqueClasses(list) {
  14. list = list.split(' ');
  15. var hash = {};
  16. var i = list.length;
  17. var out = [];
  18. while (i--) {
  19. if (!hash.hasOwnProperty(list[i])) {
  20. hash[list[i]] = 1;
  21. out.unshift(list[i]);
  22. }
  23. }
  24. return out.join(' ');
  25. }
  26. /**
  27. * cache (or polyfill for <= IE8) Array.forEach()
  28. * source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
  29. */
  30. var forEach = Array.prototype.forEach || function (fn, scope) {
  31. if (this === void 0 || this === null || typeof fn !== 'function') {
  32. throw new TypeError();
  33. }
  34. /* jshint bitwise: false */
  35. var i, len = this.length >>> 0;
  36. /* jshint bitwise: true */
  37. for (i = 0; i < len; ++i) {
  38. if (i in this) {
  39. fn.call(scope, this[i], i, this);
  40. }
  41. }
  42. };
  43. // SVG Cache
  44. var svgCache = {};
  45. var injectCount = 0;
  46. var injectedElements = [];
  47. // Request Queue
  48. var requestQueue = [];
  49. // Script running status
  50. var ranScripts = {};
  51. var cloneSvg = function (sourceSvg) {
  52. return sourceSvg.cloneNode(true);
  53. };
  54. var queueRequest = function (url, callback) {
  55. requestQueue[url] = requestQueue[url] || [];
  56. requestQueue[url].push(callback);
  57. };
  58. var processRequestQueue = function (url) {
  59. for (var i = 0, len = requestQueue[url].length; i < len; i++) {
  60. // Make these calls async so we avoid blocking the page/renderer
  61. /* jshint loopfunc: true */
  62. (function (index) {
  63. setTimeout(function () {
  64. requestQueue[url][index](cloneSvg(svgCache[url]));
  65. }, 0);
  66. })(i);
  67. /* jshint loopfunc: false */
  68. }
  69. };
  70. var loadSvg = function (url, callback) {
  71. if (svgCache[url] !== undefined) {
  72. if (svgCache[url] instanceof SVGSVGElement) {
  73. // We already have it in cache, so use it
  74. callback(cloneSvg(svgCache[url]));
  75. }
  76. else {
  77. // We don't have it in cache yet, but we are loading it, so queue this request
  78. queueRequest(url, callback);
  79. }
  80. }
  81. else {
  82. if (!window.XMLHttpRequest) {
  83. callback('Browser does not support XMLHttpRequest');
  84. return false;
  85. }
  86. // Seed the cache to indicate we are loading this URL already
  87. svgCache[url] = {};
  88. queueRequest(url, callback);
  89. var httpRequest = new XMLHttpRequest();
  90. httpRequest.onreadystatechange = function () {
  91. // readyState 4 = complete
  92. if (httpRequest.readyState === 4) {
  93. // Handle status
  94. if (httpRequest.status === 404 || httpRequest.responseXML === null) {
  95. callback('Unable to load SVG file: ' + url);
  96. if (isLocal) callback('Note: SVG injection ajax calls do not work locally without adjusting security setting in your browser. Or consider using a local webserver.');
  97. callback();
  98. return false;
  99. }
  100. // 200 success from server, or 0 when using file:// protocol locally
  101. if (httpRequest.status === 200 || (isLocal && httpRequest.status === 0)) {
  102. /* globals Document */
  103. if (httpRequest.responseXML instanceof Document) {
  104. // Cache it
  105. svgCache[url] = httpRequest.responseXML.documentElement;
  106. }
  107. /* globals -Document */
  108. // IE9 doesn't create a responseXML Document object from loaded SVG,
  109. // and throws a "DOM Exception: HIERARCHY_REQUEST_ERR (3)" error when injected.
  110. //
  111. // So, we'll just create our own manually via the DOMParser using
  112. // the the raw XML responseText.
  113. //
  114. // :NOTE: IE8 and older doesn't have DOMParser, but they can't do SVG either, so...
  115. else if (DOMParser && (DOMParser instanceof Function)) {
  116. var xmlDoc;
  117. try {
  118. var parser = new DOMParser();
  119. xmlDoc = parser.parseFromString(httpRequest.responseText, 'text/xml');
  120. }
  121. catch (e) {
  122. xmlDoc = undefined;
  123. }
  124. if (!xmlDoc || xmlDoc.getElementsByTagName('parsererror').length) {
  125. callback('Unable to parse SVG file: ' + url);
  126. return false;
  127. }
  128. else {
  129. // Cache it
  130. svgCache[url] = xmlDoc.documentElement;
  131. }
  132. }
  133. // We've loaded a new asset, so process any requests waiting for it
  134. processRequestQueue(url);
  135. }
  136. else {
  137. callback('There was a problem injecting the SVG: ' + httpRequest.status + ' ' + httpRequest.statusText);
  138. return false;
  139. }
  140. }
  141. };
  142. httpRequest.open('GET', url);
  143. // Treat and parse the response as XML, even if the
  144. // server sends us a different mimetype
  145. if (httpRequest.overrideMimeType) httpRequest.overrideMimeType('text/xml');
  146. httpRequest.send();
  147. }
  148. };
  149. // Inject a single element
  150. var injectElement = function (el, evalScripts, pngFallback, callback) {
  151. // Grab the src or data-src attribute
  152. var imgUrl = el.getAttribute('data-src') || el.getAttribute('src');
  153. // We can only inject SVG
  154. if (!(/\.svg/i).test(imgUrl)) {
  155. callback('Attempted to inject a file with a non-svg extension: ' + imgUrl);
  156. return;
  157. }
  158. // If we don't have SVG support try to fall back to a png,
  159. // either defined per-element via data-fallback or data-png,
  160. // or globally via the pngFallback directory setting
  161. if (!hasSvgSupport) {
  162. var perElementFallback = el.getAttribute('data-fallback') || el.getAttribute('data-png');
  163. // Per-element specific PNG fallback defined, so use that
  164. if (perElementFallback) {
  165. el.setAttribute('src', perElementFallback);
  166. callback(null);
  167. }
  168. // Global PNG fallback directoriy defined, use the same-named PNG
  169. else if (pngFallback) {
  170. el.setAttribute('src', pngFallback + '/' + imgUrl.split('/').pop().replace('.svg', '.png'));
  171. callback(null);
  172. }
  173. // um...
  174. else {
  175. callback('This browser does not support SVG and no PNG fallback was defined.');
  176. }
  177. return;
  178. }
  179. // Make sure we aren't already in the process of injecting this element to
  180. // avoid a race condition if multiple injections for the same element are run.
  181. // :NOTE: Using indexOf() only _after_ we check for SVG support and bail,
  182. // so no need for IE8 indexOf() polyfill
  183. if (injectedElements.indexOf(el) !== -1) {
  184. return;
  185. }
  186. // Remember the request to inject this element, in case other injection
  187. // calls are also trying to replace this element before we finish
  188. injectedElements.push(el);
  189. // Try to avoid loading the orginal image src if possible.
  190. el.setAttribute('src', '');
  191. // Load it up
  192. loadSvg(imgUrl, function (svg) {
  193. if (typeof svg === 'undefined' || typeof svg === 'string') {
  194. callback(svg);
  195. return false;
  196. }
  197. var imgId = el.getAttribute('id');
  198. if (imgId) {
  199. svg.setAttribute('id', imgId);
  200. }
  201. var imgTitle = el.getAttribute('title');
  202. if (imgTitle) {
  203. svg.setAttribute('title', imgTitle);
  204. }
  205. // Concat the SVG classes + 'injected-svg' + the img classes
  206. var classMerge = [].concat(svg.getAttribute('class') || [], 'injected-svg', el.getAttribute('class') || []).join(' ');
  207. svg.setAttribute('class', uniqueClasses(classMerge));
  208. var imgStyle = el.getAttribute('style');
  209. if (imgStyle) {
  210. svg.setAttribute('style', imgStyle);
  211. }
  212. // Copy all the data elements to the svg
  213. var imgData = [].filter.call(el.attributes, function (at) {
  214. return (/^data-\w[\w\-]*$/).test(at.name);
  215. });
  216. forEach.call(imgData, function (dataAttr) {
  217. if (dataAttr.name && dataAttr.value) {
  218. svg.setAttribute(dataAttr.name, dataAttr.value);
  219. }
  220. });
  221. // Make sure any internally referenced clipPath ids and their
  222. // clip-path references are unique.
  223. //
  224. // This addresses the issue of having multiple instances of the
  225. // same SVG on a page and only the first clipPath id is referenced.
  226. //
  227. // Browsers often shortcut the SVG Spec and don't use clipPaths
  228. // contained in parent elements that are hidden, so if you hide the first
  229. // SVG instance on the page, then all other instances lose their clipping.
  230. // Reference: https://bugzilla.mozilla.org/show_bug.cgi?id=376027
  231. var clipPaths = svg.querySelectorAll('defs clipPath[id]');
  232. var newClipPathName;
  233. for (var g = 0, clipPathsLen = clipPaths.length; g < clipPathsLen; g++) {
  234. newClipPathName = clipPaths[g].id + '-' + injectCount;
  235. // :NOTE: using a substring match attr selector here to deal with IE "adding extra quotes in url() attrs"
  236. var usingClipPath = svg.querySelectorAll('[clip-path*="' + clipPaths[g].id + '"]');
  237. for (var h = 0, usingClipPathLen = usingClipPath.length; h < usingClipPathLen; h++) {
  238. usingClipPath[h].setAttribute('clip-path', 'url(#' + newClipPathName + ')');
  239. }
  240. clipPaths[g].id = newClipPathName;
  241. }
  242. // Do the same for masks
  243. var masks = svg.querySelectorAll('defs mask[id]');
  244. var newMaskName;
  245. for (var i = 0, masksLen = masks.length; i < masksLen; i++) {
  246. newMaskName = masks[i].id + '-' + injectCount;
  247. // :NOTE: using a substring match attr selector here to deal with IE "adding extra quotes in url() attrs"
  248. var usingMask = svg.querySelectorAll('[mask*="' + masks[i].id + '"]');
  249. for (var j = 0, usingMaskLen = usingMask.length; j < usingMaskLen; j++) {
  250. usingMask[j].setAttribute('mask', 'url(#' + newMaskName + ')');
  251. }
  252. masks[i].id = newMaskName;
  253. }
  254. // Remove any unwanted/invalid namespaces that might have been added by SVG editing tools
  255. svg.removeAttribute('xmlns:a');
  256. // Post page load injected SVGs don't automatically have their script
  257. // elements run, so we'll need to make that happen, if requested
  258. // Find then prune the scripts
  259. var scripts = svg.querySelectorAll('script');
  260. var scriptsToEval = [];
  261. var script, scriptType;
  262. for (var k = 0, scriptsLen = scripts.length; k < scriptsLen; k++) {
  263. scriptType = scripts[k].getAttribute('type');
  264. // Only process javascript types.
  265. // SVG defaults to 'application/ecmascript' for unset types
  266. if (!scriptType || scriptType === 'application/ecmascript' || scriptType === 'application/javascript') {
  267. // innerText for IE, textContent for other browsers
  268. script = scripts[k].innerText || scripts[k].textContent;
  269. // Stash
  270. scriptsToEval.push(script);
  271. // Tidy up and remove the script element since we don't need it anymore
  272. svg.removeChild(scripts[k]);
  273. }
  274. }
  275. // Run/Eval the scripts if needed
  276. if (scriptsToEval.length > 0 && (evalScripts === 'always' || (evalScripts === 'once' && !ranScripts[imgUrl]))) {
  277. for (var l = 0, scriptsToEvalLen = scriptsToEval.length; l < scriptsToEvalLen; l++) {
  278. // :NOTE: Yup, this is a form of eval, but it is being used to eval code
  279. // the caller has explictely asked to be loaded, and the code is in a caller
  280. // defined SVG file... not raw user input.
  281. //
  282. // Also, the code is evaluated in a closure and not in the global scope.
  283. // If you need to put something in global scope, use 'window'
  284. new Function(scriptsToEval[l])(window); // jshint ignore:line
  285. }
  286. // Remember we already ran scripts for this svg
  287. ranScripts[imgUrl] = true;
  288. }
  289. // :WORKAROUND:
  290. // IE doesn't evaluate <style> tags in SVGs that are dynamically added to the page.
  291. // This trick will trigger IE to read and use any existing SVG <style> tags.
  292. //
  293. // Reference: https://github.com/iconic/SVGInjector/issues/23
  294. var styleTags = svg.querySelectorAll('style');
  295. forEach.call(styleTags, function (styleTag) {
  296. styleTag.textContent += '';
  297. });
  298. // Replace the image with the svg
  299. el.parentNode.replaceChild(svg, el);
  300. // Now that we no longer need it, drop references
  301. // to the original element so it can be GC'd
  302. delete injectedElements[injectedElements.indexOf(el)];
  303. el = null;
  304. // Increment the injected count
  305. injectCount++;
  306. callback(svg);
  307. });
  308. };
  309. /**
  310. * SVGInjector
  311. *
  312. * Replace the given elements with their full inline SVG DOM elements.
  313. *
  314. * :NOTE: We are using get/setAttribute with SVG because the SVG DOM spec differs from HTML DOM and
  315. * can return other unexpected object types when trying to directly access svg properties.
  316. * ex: "className" returns a SVGAnimatedString with the class value found in the "baseVal" property,
  317. * instead of simple string like with HTML Elements.
  318. *
  319. * @param {mixes} Array of or single DOM element
  320. * @param {object} options
  321. * @param {function} callback
  322. * @return {object} Instance of SVGInjector
  323. */
  324. var SVGInjector = function (elements, options, done) {
  325. // Options & defaults
  326. options = options || {};
  327. // Should we run the scripts blocks found in the SVG
  328. // 'always' - Run them every time
  329. // 'once' - Only run scripts once for each SVG
  330. // [false|'never'] - Ignore scripts
  331. var evalScripts = options.evalScripts || 'always';
  332. // Location of fallback pngs, if desired
  333. var pngFallback = options.pngFallback || false;
  334. // Callback to run during each SVG injection, returning the SVG injected
  335. var eachCallback = options.each;
  336. // Do the injection...
  337. if (elements.length !== undefined) {
  338. var elementsLoaded = 0;
  339. forEach.call(elements, function (element) {
  340. injectElement(element, evalScripts, pngFallback, function (svg) {
  341. if (eachCallback && typeof eachCallback === 'function') eachCallback(svg);
  342. if (done && elements.length === ++elementsLoaded) done(elementsLoaded);
  343. });
  344. });
  345. }
  346. else {
  347. if (elements) {
  348. injectElement(elements, evalScripts, pngFallback, function (svg) {
  349. if (eachCallback && typeof eachCallback === 'function') eachCallback(svg);
  350. if (done) done(1);
  351. elements = null;
  352. });
  353. }
  354. else {
  355. if (done) done(0);
  356. }
  357. }
  358. };
  359. /* global module, exports: true, define */
  360. // Node.js or CommonJS
  361. if (typeof module === 'object' && typeof module.exports === 'object') {
  362. module.exports = exports = SVGInjector;
  363. }
  364. // AMD support
  365. else if (typeof define === 'function' && define.amd) {
  366. define(function () {
  367. return SVGInjector;
  368. });
  369. }
  370. // Otherwise, attach to window as global
  371. else if (typeof window === 'object') {
  372. window.SVGInjector = SVGInjector;
  373. }
  374. /* global -module, -exports, -define */
  375. }(window, document));