openrat-cms

# OpenRat Content Management System
git clone http://git.code.weiherhei.de/openrat-cms.git
Log | Files | Refs

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));