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