From bebd07c505fd78e01335916d603f0afebc82e61d Mon Sep 17 00:00:00 2001 From: Tony Childs Date: Sat, 12 Mar 2016 17:11:56 -0600 Subject: [PATCH] fix(icon): Allow using data URLs Refs #6531. Fixes #4126. Closes #7547 --- .../icon/demoLoadSvgIconsFromUrl/index.html | 22 ++++++++ .../icon/demoLoadSvgIconsFromUrl/script.js | 4 ++ src/components/icon/icon.spec.js | 53 +++++++++++++++++-- src/components/icon/js/iconService.js | 32 ++++++++--- 4 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/components/icon/demoLoadSvgIconsFromUrl/index.html b/src/components/icon/demoLoadSvgIconsFromUrl/index.html index a6bae8daf96..f6423a665d5 100644 --- a/src/components/icon/demoLoadSvgIconsFromUrl/index.html +++ b/src/components/icon/demoLoadSvgIconsFromUrl/index.html @@ -12,5 +12,27 @@

+ +

Use data URLs (base64 or un-encoded):

+

+ + + + + + + + + +

diff --git a/src/components/icon/demoLoadSvgIconsFromUrl/script.js b/src/components/icon/demoLoadSvgIconsFromUrl/script.js index ad367a18eef..6b07c2ed0fb 100644 --- a/src/components/icon/demoLoadSvgIconsFromUrl/script.js +++ b/src/components/icon/demoLoadSvgIconsFromUrl/script.js @@ -6,4 +6,8 @@ angular.module('appDemoSvgIcons', ['ngMaterial']) $scope.getAndroid = function() { return 'img/icons/android.svg'; } + /* Returns base64 encoded SVG. */ + $scope.getAndroidEncoded = function() { + return 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGcgaWQ9ImFuZHJvaWQiPjxwYXRoIGQ9Ik02IDE4YzAgLjU1LjQ1IDEgMSAxaDF2My41YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNVYxOWgydjMuNWMwIC44My42NyAxLjUgMS41IDEuNXMxLjUtLjY3IDEuNS0xLjVWMTloMWMuNTUgMCAxLS40NSAxLTFWOEg2djEwek0zLjUgOEMyLjY3IDggMiA4LjY3IDIgOS41djdjMCAuODMuNjcgMS41IDEuNSAxLjVTNSAxNy4zMyA1IDE2LjV2LTdDNSA4LjY3IDQuMzMgOCAzLjUgOHptMTcgMGMtLjgzIDAtMS41LjY3LTEuNSAxLjV2N2MwIC44My42NyAxLjUgMS41IDEuNXMxLjUtLjY3IDEuNS0xLjV2LTdjMC0uODMtLjY3LTEuNS0xLjUtMS41em0tNC45Ny01Ljg0bDEuMy0xLjNjLjItLjIuMi0uNTEgMC0uNzEtLjItLjItLjUxLS4yLS43MSAwbC0xLjQ4IDEuNDhDMTMuODUgMS4yMyAxMi45NSAxIDEyIDFjLS45NiAwLTEuODYuMjMtMi42Ni42M0w3Ljg1LjE1Yy0uMi0uMi0uNTEtLjItLjcxIDAtLjIuMi0uMi41MSAwIC43MWwxLjMxIDEuMzFDNi45NyAzLjI2IDYgNS4wMSA2IDdoMTJjMC0xLjk5LS45Ny0zLjc1LTIuNDctNC44NHpNMTAgNUg5VjRoMXYxem01IDBoLTFWNGgxdjF6Ii8+PC9nPjwvc3ZnPg=='; + } }); diff --git a/src/components/icon/icon.spec.js b/src/components/icon/icon.spec.js index 5d7e6b6cd04..2e94c57928f 100644 --- a/src/components/icon/icon.spec.js +++ b/src/components/icon/icon.spec.js @@ -177,11 +177,20 @@ describe('mdIcon directive', function() { return { then: function(fn) { switch(id) { - case 'android' : fn(''); - case 'cake' : fn(''); - case 'android.svg' : fn(''); - case 'cake.svg' : fn(''); - case 'image:android': fn(''); + case 'android' : fn(''); + break; + case 'cake' : fn(''); + break; + case 'android.svg' : fn(''); + break; + case 'cake.svg' : fn(''); + break; + case 'image:android' : fn(''); + break; + default : + if (/^data:/.test(id)) { + fn(window.atob(id.split(',')[1])); + } } } } @@ -240,6 +249,17 @@ describe('mdIcon directive', function() { expect(el.html()).toEqual(''); })); + describe('with a data URL', function() { + it('should set mdSvgSrc from a function expression', inject(function() { + var svgData = ''; + $scope.getData = function() { + return 'data:image/svg+xml;base64,' + window.btoa(svgData); + } + el = make(''); + $scope.$digest(); + expect(el[0].innerHTML).toEqual(svgData); + })); + }) }); describe('with ARIA support', function() { @@ -419,6 +439,29 @@ describe('mdIcon service', function() { $scope.$digest(); }); + describe('and the URL is a data URL', function() { + var svgData = ''; + + describe('and the data is base64 encoded', function() { + it('should return correct SVG markup', function() { + var data = 'data:image/svg+xml;base64,' + btoa(svgData); + $mdIcon(data).then(function(el) { + expect(el.outerHTML).toEqual( updateDefaults(svgData) ); + }) + $scope.$digest(); + }); + }); + + describe('and the data is un-encoded', function() { + it('should return correct SVG markup', function() { + var data = 'data:image/svg+xml,' + svgData; + $mdIcon(data).then(function(el) { + expect(el.outerHTML).toEqual( updateDefaults(svgData) ); + }) + $scope.$digest(); + }); + }); + }); }); describe('icon set URL is not found', function() { diff --git a/src/components/icon/js/iconService.js b/src/components/icon/js/iconService.js index 053fec1d25a..35050ef11a5 100644 --- a/src/components/icon/js/iconService.js +++ b/src/components/icon/js/iconService.js @@ -375,7 +375,8 @@ /* @ngInject */ function MdIconService(config, $http, $q, $log, $templateCache) { var iconCache = {}; - var urlRegex = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i; + var urlRegex = /[-\w@:%\+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%\+.~#?&//=]*)?/i; + var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i; Icon.prototype = { clone : cloneSVG, prepare: prepareAndStyle }; getIcon.fontSet = findRegisteredFontSet; @@ -392,8 +393,8 @@ // If already loaded and cached, use a clone of the cached icon. // Otherwise either load by URL, or lookup in the registry and then load by URL, and cache. - if ( iconCache[id] ) return $q.when( iconCache[id].clone() ); - if ( urlRegex.test(id) ) return loadByURL(id).then( cacheIcon(id) ); + if ( iconCache[id] ) return $q.when( iconCache[id].clone() ); + if ( urlRegex.test(id) || dataUrlRegex.test(id) ) return loadByURL(id).then( cacheIcon(id) ); if ( id.indexOf(':') == -1 ) id = '$default:' + id; var load = config[id] ? loadByID : loadFromIconSet; @@ -481,11 +482,26 @@ * Extract the data for later conversion to Icon */ function loadByURL(url) { - return $http - .get(url, { cache: $templateCache }) - .then(function(response) { - return angular.element('
').append(response.data).find('svg')[0]; - }).catch(announceNotFound); + /* Load the icon from embedded data URL. */ + function loadByDataUrl(url) { + var results = dataUrlRegex.exec(url); + var isBase64 = /base64/i.test(url); + var data = isBase64 ? window.atob(results[2]) : results[2]; + return $q.when(angular.element(data)[0]); + } + + /* Load the icon by URL using HTTP. */ + function loadByHttpUrl(url) { + return $http + .get(url, { cache: $templateCache }) + .then(function(response) { + return angular.element('
').append(response.data).find('svg')[0]; + }).catch(announceNotFound); + } + + return dataUrlRegex.test(url) + ? loadByDataUrl(url) + : loadByHttpUrl(url); } /**