1 | var __extends = (this && this.__extends) || function (d, b) {
|
2 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
|
3 | function __() { this.constructor = d; }
|
4 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
5 | };
|
6 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
7 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
8 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
9 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
10 | return c > 3 && r && Object.defineProperty(target, key, r), r;
|
11 | };
|
12 | var __metadata = (this && this.__metadata) || function (k, v) {
|
13 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
14 | };
|
15 | import { Injectable } from '@angular/core';
|
16 | import { Http } from '@angular/http';
|
17 | import { MdError } from '@angular2-material/core';
|
18 | import { Observable } from 'rxjs/Observable';
|
19 | import 'rxjs/add/observable/forkJoin';
|
20 | import 'rxjs/add/observable/of';
|
21 | import 'rxjs/add/operator/map';
|
22 | import 'rxjs/add/operator/filter';
|
23 | import 'rxjs/add/operator/do';
|
24 | import 'rxjs/add/operator/share';
|
25 | import 'rxjs/add/operator/finally';
|
26 | import 'rxjs/add/operator/catch';
|
27 | /** Exception thrown when attempting to load an icon with a name that cannot be found. */
|
28 | export var MdIconNameNotFoundError = (function (_super) {
|
29 | __extends(MdIconNameNotFoundError, _super);
|
30 | function MdIconNameNotFoundError(iconName) {
|
31 | _super.call(this, "Unable to find icon with the name \"" + iconName + "\"");
|
32 | }
|
33 | return MdIconNameNotFoundError;
|
34 | }(MdError));
|
35 | /**
|
36 | * Exception thrown when attempting to load SVG content that does not contain the expected
|
37 | * <svg> tag.
|
38 | */
|
39 | export var MdIconSvgTagNotFoundError = (function (_super) {
|
40 | __extends(MdIconSvgTagNotFoundError, _super);
|
41 | function MdIconSvgTagNotFoundError() {
|
42 | _super.call(this, '<svg> tag not found');
|
43 | }
|
44 | return MdIconSvgTagNotFoundError;
|
45 | }(MdError));
|
46 | /** Configuration for an icon, including the URL and possibly the cached SVG element. */
|
47 | var SvgIconConfig = (function () {
|
48 | function SvgIconConfig(url) {
|
49 | this.url = url;
|
50 | this.svgElement = null;
|
51 | }
|
52 | return SvgIconConfig;
|
53 | }());
|
54 | /** Returns the cache key to use for an icon namespace and name. */
|
55 | var iconKey = function (namespace, name) { return namespace + ':' + name; };
|
56 | /**
|
57 | * Service to register and display icons used by the <md-icon> component.
|
58 | * - Registers icon URLs by namespace and name.
|
59 | * - Registers icon set URLs by namespace.
|
60 | * - Registers aliases for CSS classes, for use with icon fonts.
|
61 | * - Loads icons from URLs and extracts individual icons from icon sets.
|
62 | */
|
63 | export var MdIconRegistry = (function () {
|
64 | function MdIconRegistry(_http) {
|
65 | this._http = _http;
|
66 | /**
|
67 | * URLs and cached SVG elements for individual icons. Keys are of the format "[namespace]:[icon]".
|
68 | */
|
69 | this._svgIconConfigs = new Map();
|
70 | /**
|
71 | * SvgIconConfig objects and cached SVG elements for icon sets, keyed by namespace.
|
72 | * Multiple icon sets can be registered under the same namespace.
|
73 | */
|
74 | this._iconSetConfigs = new Map();
|
75 | /** Cache for icons loaded by direct URLs. */
|
76 | this._cachedIconsByUrl = new Map();
|
77 | /** In-progress icon fetches. Used to coalesce multiple requests to the same URL. */
|
78 | this._inProgressUrlFetches = new Map();
|
79 | /** Map from font identifiers to their CSS class names. Used for icon fonts. */
|
80 | this._fontCssClassesByAlias = new Map();
|
81 | /**
|
82 | * The CSS class to apply when an <md-icon> component has no icon name, url, or font specified.
|
83 | * The default 'material-icons' value assumes that the material icon font has been loaded as
|
84 | * described at http://google.github.io/material-design-icons/#icon-font-for-the-web
|
85 | */
|
86 | this._defaultFontSetClass = 'material-icons';
|
87 | }
|
88 | /** Registers an icon by URL in the default namespace. */
|
89 | MdIconRegistry.prototype.addSvgIcon = function (iconName, url) {
|
90 | return this.addSvgIconInNamespace('', iconName, url);
|
91 | };
|
92 | /** Registers an icon by URL in the specified namespace. */
|
93 | MdIconRegistry.prototype.addSvgIconInNamespace = function (namespace, iconName, url) {
|
94 | var key = iconKey(namespace, iconName);
|
95 | this._svgIconConfigs.set(key, new SvgIconConfig(url));
|
96 | return this;
|
97 | };
|
98 | /** Registers an icon set by URL in the default namespace. */
|
99 | MdIconRegistry.prototype.addSvgIconSet = function (url) {
|
100 | return this.addSvgIconSetInNamespace('', url);
|
101 | };
|
102 | /** Registers an icon set by URL in the specified namespace. */
|
103 | MdIconRegistry.prototype.addSvgIconSetInNamespace = function (namespace, url) {
|
104 | var config = new SvgIconConfig(url);
|
105 | if (this._iconSetConfigs.has(namespace)) {
|
106 | this._iconSetConfigs.get(namespace).push(config);
|
107 | }
|
108 | else {
|
109 | this._iconSetConfigs.set(namespace, [config]);
|
110 | }
|
111 | return this;
|
112 | };
|
113 | /**
|
114 | * Defines an alias for a CSS class name to be used for icon fonts. Creating an mdIcon
|
115 | * component with the alias as the fontSet input will cause the class name to be applied
|
116 | * to the <md-icon> element.
|
117 | */
|
118 | MdIconRegistry.prototype.registerFontClassAlias = function (alias, className) {
|
119 | if (className === void 0) { className = alias; }
|
120 | this._fontCssClassesByAlias.set(alias, className);
|
121 | return this;
|
122 | };
|
123 | /**
|
124 | * Returns the CSS class name associated with the alias by a previous call to
|
125 | * registerFontClassAlias. If no CSS class has been associated, returns the alias unmodified.
|
126 | */
|
127 | MdIconRegistry.prototype.classNameForFontAlias = function (alias) {
|
128 | return this._fontCssClassesByAlias.get(alias) || alias;
|
129 | };
|
130 | /**
|
131 | * Sets the CSS class name to be used for icon fonts when an <md-icon> component does not
|
132 | * have a fontSet input value, and is not loading an icon by name or URL.
|
133 | */
|
134 | MdIconRegistry.prototype.setDefaultFontSetClass = function (className) {
|
135 | this._defaultFontSetClass = className;
|
136 | return this;
|
137 | };
|
138 | /**
|
139 | * Returns the CSS class name to be used for icon fonts when an <md-icon> component does not
|
140 | * have a fontSet input value, and is not loading an icon by name or URL.
|
141 | */
|
142 | MdIconRegistry.prototype.getDefaultFontSetClass = function () {
|
143 | return this._defaultFontSetClass;
|
144 | };
|
145 | /**
|
146 | * Returns an Observable that produces the icon (as an <svg> DOM element) from the given URL.
|
147 | * The response from the URL may be cached so this will not always cause an HTTP request, but
|
148 | * the produced element will always be a new copy of the originally fetched icon. (That is,
|
149 | * it will not contain any modifications made to elements previously returned).
|
150 | */
|
151 | MdIconRegistry.prototype.getSvgIconFromUrl = function (url) {
|
152 | var _this = this;
|
153 | if (this._cachedIconsByUrl.has(url)) {
|
154 | return Observable.of(cloneSvg(this._cachedIconsByUrl.get(url)));
|
155 | }
|
156 | return this._loadSvgIconFromConfig(new SvgIconConfig(url))
|
157 | .do(function (svg) { return _this._cachedIconsByUrl.set(url, svg); })
|
158 | .map(function (svg) { return cloneSvg(svg); });
|
159 | };
|
160 | /**
|
161 | * Returns an Observable that produces the icon (as an <svg> DOM element) with the given name
|
162 | * and namespace. The icon must have been previously registered with addIcon or addIconSet;
|
163 | * if not, the Observable will throw an MdIconNameNotFoundError.
|
164 | */
|
165 | MdIconRegistry.prototype.getNamedSvgIcon = function (name, namespace) {
|
166 | if (namespace === void 0) { namespace = ''; }
|
167 | // Return (copy of) cached icon if possible.
|
168 | var key = iconKey(namespace, name);
|
169 | if (this._svgIconConfigs.has(key)) {
|
170 | return this._getSvgFromConfig(this._svgIconConfigs.get(key));
|
171 | }
|
172 | // See if we have any icon sets registered for the namespace.
|
173 | var iconSetConfigs = this._iconSetConfigs.get(namespace);
|
174 | if (iconSetConfigs) {
|
175 | return this._getSvgFromIconSetConfigs(name, iconSetConfigs);
|
176 | }
|
177 | return Observable.throw(new MdIconNameNotFoundError(key));
|
178 | };
|
179 | /**
|
180 | * Returns the cached icon for a SvgIconConfig if available, or fetches it from its URL if not.
|
181 | */
|
182 | MdIconRegistry.prototype._getSvgFromConfig = function (config) {
|
183 | if (config.svgElement) {
|
184 | // We already have the SVG element for this icon, return a copy.
|
185 | return Observable.of(cloneSvg(config.svgElement));
|
186 | }
|
187 | else {
|
188 | // Fetch the icon from the config's URL, cache it, and return a copy.
|
189 | return this._loadSvgIconFromConfig(config)
|
190 | .do(function (svg) { return config.svgElement = svg; })
|
191 | .map(function (svg) { return cloneSvg(svg); });
|
192 | }
|
193 | };
|
194 | /**
|
195 | * Attempts to find an icon with the specified name in any of the SVG icon sets.
|
196 | * First searches the available cached icons for a nested element with a matching name, and
|
197 | * if found copies the element to a new <svg> element. If not found, fetches all icon sets
|
198 | * that have not been cached, and searches again after all fetches are completed.
|
199 | * The returned Observable produces the SVG element if possible, and throws
|
200 | * MdIconNameNotFoundError if no icon with the specified name can be found.
|
201 | */
|
202 | MdIconRegistry.prototype._getSvgFromIconSetConfigs = function (name, iconSetConfigs) {
|
203 | var _this = this;
|
204 | // For all the icon set SVG elements we've fetched, see if any contain an icon with the
|
205 | // requested name.
|
206 | var namedIcon = this._extractIconWithNameFromAnySet(name, iconSetConfigs);
|
207 | if (namedIcon) {
|
208 | // We could cache namedIcon in _svgIconConfigs, but since we have to make a copy every
|
209 | // time anyway, there's probably not much advantage compared to just always extracting
|
210 | // it from the icon set.
|
211 | return Observable.of(namedIcon);
|
212 | }
|
213 | // Not found in any cached icon sets. If there are icon sets with URLs that we haven't
|
214 | // fetched, fetch them now and look for iconName in the results.
|
215 | var iconSetFetchRequests = iconSetConfigs
|
216 | .filter(function (iconSetConfig) { return !iconSetConfig.svgElement; })
|
217 | .map(function (iconSetConfig) {
|
218 | return _this._loadSvgIconSetFromConfig(iconSetConfig)
|
219 | .catch(function (err, caught) {
|
220 | // Swallow errors fetching individual URLs so the combined Observable won't
|
221 | // necessarily fail.
|
222 | console.log("Loading icon set URL: " + iconSetConfig.url + " failed: " + err);
|
223 | return Observable.of(null);
|
224 | })
|
225 | .do(function (svg) {
|
226 | // Cache SVG element.
|
227 | if (svg) {
|
228 | iconSetConfig.svgElement = svg;
|
229 | }
|
230 | });
|
231 | });
|
232 | // Fetch all the icon set URLs. When the requests complete, every IconSet should have a
|
233 | // cached SVG element (unless the request failed), and we can check again for the icon.
|
234 | return Observable.forkJoin(iconSetFetchRequests)
|
235 | .map(function (ignoredResults) {
|
236 | var foundIcon = _this._extractIconWithNameFromAnySet(name, iconSetConfigs);
|
237 | if (!foundIcon) {
|
238 | throw new MdIconNameNotFoundError(name);
|
239 | }
|
240 | return foundIcon;
|
241 | });
|
242 | };
|
243 | /**
|
244 | * Searches the cached SVG elements for the given icon sets for a nested icon element whose "id"
|
245 | * tag matches the specified name. If found, copies the nested element to a new SVG element and
|
246 | * returns it. Returns null if no matching element is found.
|
247 | */
|
248 | MdIconRegistry.prototype._extractIconWithNameFromAnySet = function (iconName, iconSetConfigs) {
|
249 | // Iterate backwards, so icon sets added later have precedence.
|
250 | for (var i = iconSetConfigs.length - 1; i >= 0; i--) {
|
251 | var config = iconSetConfigs[i];
|
252 | if (config.svgElement) {
|
253 | var foundIcon = this._extractSvgIconFromSet(config.svgElement, iconName, config);
|
254 | if (foundIcon) {
|
255 | return foundIcon;
|
256 | }
|
257 | }
|
258 | }
|
259 | return null;
|
260 | };
|
261 | /**
|
262 | * Loads the content of the icon URL specified in the SvgIconConfig and creates an SVG element
|
263 | * from it.
|
264 | */
|
265 | MdIconRegistry.prototype._loadSvgIconFromConfig = function (config) {
|
266 | var _this = this;
|
267 | return this._fetchUrl(config.url)
|
268 | .map(function (svgText) { return _this._createSvgElementForSingleIcon(svgText, config); });
|
269 | };
|
270 | /**
|
271 | * Loads the content of the icon set URL specified in the SvgIconConfig and creates an SVG element
|
272 | * from it.
|
273 | */
|
274 | MdIconRegistry.prototype._loadSvgIconSetFromConfig = function (config) {
|
275 | var _this = this;
|
276 | // TODO: Document that icons should only be loaded from trusted sources.
|
277 | return this._fetchUrl(config.url)
|
278 | .map(function (svgText) { return _this._svgElementFromString(svgText); });
|
279 | };
|
280 | /**
|
281 | * Creates a DOM element from the given SVG string, and adds default attributes.
|
282 | */
|
283 | MdIconRegistry.prototype._createSvgElementForSingleIcon = function (responseText, config) {
|
284 | var svg = this._svgElementFromString(responseText);
|
285 | this._setSvgAttributes(svg, config);
|
286 | return svg;
|
287 | };
|
288 | /**
|
289 | * Searches the cached element of the given SvgIconConfig for a nested icon element whose "id"
|
290 | * tag matches the specified name. If found, copies the nested element to a new SVG element and
|
291 | * returns it. Returns null if no matching element is found.
|
292 | */
|
293 | MdIconRegistry.prototype._extractSvgIconFromSet = function (iconSet, iconName, config) {
|
294 | var iconNode = iconSet.querySelector('#' + iconName);
|
295 | if (!iconNode) {
|
296 | return null;
|
297 | }
|
298 | // If the icon node is itself an <svg> node, clone and return it directly. If not, set it as
|
299 | // the content of a new <svg> node.
|
300 | if (iconNode.tagName.toLowerCase() == 'svg') {
|
301 | return this._setSvgAttributes(iconNode.cloneNode(true), config);
|
302 | }
|
303 | // createElement('SVG') doesn't work as expected; the DOM ends up with
|
304 | // the correct nodes, but the SVG content doesn't render. Instead we
|
305 | // have to create an empty SVG node using innerHTML and append its content.
|
306 | // Elements created using DOMParser.parseFromString have the same problem.
|
307 | // http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display
|
308 | var svg = this._svgElementFromString('<svg></svg>');
|
309 | // Clone the node so we don't remove it from the parent icon set element.
|
310 | svg.appendChild(iconNode.cloneNode(true));
|
311 | return this._setSvgAttributes(svg, config);
|
312 | };
|
313 | /**
|
314 | * Creates a DOM element from the given SVG string.
|
315 | */
|
316 | MdIconRegistry.prototype._svgElementFromString = function (str) {
|
317 | // TODO: Is there a better way than innerHTML? Renderer doesn't appear to have a method for
|
318 | // creating an element from an HTML string.
|
319 | var div = document.createElement('DIV');
|
320 | div.innerHTML = str;
|
321 | var svg = div.querySelector('svg');
|
322 | if (!svg) {
|
323 | throw new MdIconSvgTagNotFoundError();
|
324 | }
|
325 | return svg;
|
326 | };
|
327 | /**
|
328 | * Sets the default attributes for an SVG element to be used as an icon.
|
329 | */
|
330 | MdIconRegistry.prototype._setSvgAttributes = function (svg, config) {
|
331 | if (!svg.getAttribute('xmlns')) {
|
332 | svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
333 | }
|
334 | svg.setAttribute('fit', '');
|
335 | svg.setAttribute('height', '100%');
|
336 | svg.setAttribute('width', '100%');
|
337 | svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
338 | svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable.
|
339 | return svg;
|
340 | };
|
341 | /**
|
342 | * Returns an Observable which produces the string contents of the given URL. Results may be
|
343 | * cached, so future calls with the same URL may not cause another HTTP request.
|
344 | */
|
345 | MdIconRegistry.prototype._fetchUrl = function (url) {
|
346 | var _this = this;
|
347 | // Store in-progress fetches to avoid sending a duplicate request for a URL when there is
|
348 | // already a request in progress for that URL. It's necessary to call share() on the
|
349 | // Observable returned by http.get() so that multiple subscribers don't cause multiple XHRs.
|
350 | if (this._inProgressUrlFetches.has(url)) {
|
351 | return this._inProgressUrlFetches.get(url);
|
352 | }
|
353 | // TODO(jelbourn): for some reason, the `finally` operator "loses" the generic type on the
|
354 | // Observable. Figure out why and fix it.
|
355 | var req = this._http.get(url)
|
356 | .map(function (response) { return response.text(); })
|
357 | .finally(function () {
|
358 | _this._inProgressUrlFetches.delete(url);
|
359 | })
|
360 | .share();
|
361 | this._inProgressUrlFetches.set(url, req);
|
362 | return req;
|
363 | };
|
364 | MdIconRegistry = __decorate([
|
365 | Injectable(),
|
366 | __metadata('design:paramtypes', [Http])
|
367 | ], MdIconRegistry);
|
368 | return MdIconRegistry;
|
369 | }());
|
370 | /** Clones an SVGElement while preserving type information. */
|
371 | function cloneSvg(svg) {
|
372 | return svg.cloneNode(true);
|
373 | }
|
374 |
|
375 | //# sourceMappingURL=icon-registry.js.map
|