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