-
Notifications
You must be signed in to change notification settings - Fork 6.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
md-icon #281
md-icon #281
Changes from 34 commits
62e82fe
8a188e4
8b6f5ff
9518ca2
ec761da
776b755
d88144e
159d53d
e423582
3ce1334
22a92e1
0e6f396
014be77
ea3beea
72d167f
53aeed2
7390015
86d86aa
17f40c5
87dd4ac
82bd25e
e4d568f
af2f94b
154b5e0
cef1277
30f8cce
73a9a3f
03bcbed
e87af2e
062307e
d3682b7
8e2ff62
4b02d27
195acd3
6391cdb
437ab0c
364391a
6dc1af2
796d37f
a4ea23e
df6415b
04b23fa
37d8362
6927d52
08e8cbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,370 @@ | ||
import {Injectable} from 'angular2/core'; | ||
import {BaseException} from 'angular2/src/facade/exceptions'; | ||
import {Http} from 'angular2/http'; | ||
import {Observable} from 'rxjs/Rx'; | ||
|
||
|
||
/** Exception thrown when attmepting to load an icon with a name that cannot be found. */ | ||
export class MdIconNameNotFoundException extends BaseException { | ||
constructor(iconName: string) { | ||
super(`Unable to find icon with the name "${name}"`); | ||
} | ||
} | ||
|
||
/** | ||
* Exception thrown when attmepting to load SVG content that does not contain the expected | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: attmepting --> attempting |
||
* <svg> tag. | ||
*/ | ||
export class MdIconSvgTagNotFoundException extends BaseException { | ||
constructor() { | ||
super('<svg> tag not found'); | ||
} | ||
} | ||
|
||
/** | ||
* Configuration for an icon, including the URL and possibly the cached SVG element. | ||
* @internal | ||
*/ | ||
class SvgIconConfig { | ||
svgElement: SVGElement = null; | ||
constructor(public url: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: empty functions can close on the same line constructor(public url: string) { } (here and elsewhere) |
||
} | ||
} | ||
|
||
/** | ||
* Service to register and display icons used by the <md-icon> component. | ||
* - Registers icon URLs by namespace and name. | ||
* - Registers icon set URLs by namespace. | ||
* - Registers aliases for CSS classes, for use with icon fonts. | ||
* - Loads icons from URLs and extracts individual icons from icon sets. | ||
*/ | ||
@Injectable() | ||
export class MdIconRegistry { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need description on |
||
/** | ||
* URLs and cached SVG elements for individual icons. Keys are of the format "[namespace]:[icon]". | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional: I'm cool with single-lining description blocks when they fit on one line. /** Like this */ |
||
private _svgIconConfigs = new Map<string, SvgIconConfig>(); | ||
|
||
/** | ||
* SvgIconConfig objects and cached SVG elements for icon sets, keyed by namespace. | ||
* Multiple icon sets can be registered under the same namespace. | ||
*/ | ||
private _iconSetConfigs = new Map<string, SvgIconConfig[]>(); | ||
|
||
/** | ||
* Cache for icons loaded by direct URLs. | ||
*/ | ||
private _cachedIconsByUrl = new Map<string, SVGElement>(); | ||
|
||
/** | ||
* In-progress icon fetches. Used to coalesce multiple requests to the same URL. | ||
*/ | ||
private _inProgressUrlFetches = new Map<string, Observable<string>>(); | ||
|
||
/** | ||
* Map from font identifiers to their CSS class names. Used for icon fonts. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accurate to say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could have a custom icon font that uses ligatures and still set an alias for its CSS class and use it like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's right, never mind me. |
||
*/ | ||
private _fontCssClassesByAlias = new Map<string, string>(); | ||
|
||
/** | ||
* The CSS class to apply when an <md-icon> component has no icon name, url, or font specified. | ||
* The default 'material-icons' value assumes that the material icon font has been loaded as | ||
* described at http://google.github.io/material-design-icons/#icon-font-for-the-web | ||
*/ | ||
private _defaultFontSetClass = 'material-icons'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need descriptions for these members |
||
|
||
constructor(private _http: Http) {} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add method descriptions |
||
/** | ||
* Registers an icon by URL in the default namespace. | ||
*/ | ||
addSvgIcon(iconName: string, url: string): this { | ||
return this.addSvgIconInNamespace('', iconName, url); | ||
} | ||
|
||
/** | ||
* Registers an icon by URL in the specified namespace. | ||
*/ | ||
addSvgIconInNamespace(namespace: string, iconName: string, url: string): this { | ||
const iconKey = namespace + ':' + iconName; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should have one place for this, something like |
||
this._svgIconConfigs.set(iconKey, new SvgIconConfig(url)); | ||
return this; | ||
} | ||
|
||
/** | ||
* Registers an icon set by URL in the default namespace. | ||
*/ | ||
addSvgIconSet(url: string): this { | ||
return this.addSvgIconSetInNamespace('', url); | ||
} | ||
|
||
/** | ||
* Registers an icon set by URL in the specified namespace. | ||
*/ | ||
addSvgIconSetInNamespace(namespace: string, url: string): this { | ||
const config = new SvgIconConfig(url); | ||
if (this._iconSetConfigs.has(namespace)) { | ||
this._iconSetConfigs.get(namespace).push(config); | ||
} else { | ||
this._iconSetConfigs.set(namespace, [config]); | ||
} | ||
return this; | ||
} | ||
|
||
/** | ||
* Defines an alias for a CSS class name to be used for icon fonts. Creating an mdIcon | ||
* component with the alias as the fontSet input will cause the class name to be applied | ||
* to the <md-icon> element. | ||
*/ | ||
registerFontClassAlias(alias: string, className = alias): this { | ||
this._fontCssClassesByAlias.set(alias, className); | ||
return this; | ||
} | ||
|
||
/** | ||
* Returns the CSS class name associated with the alias by a previous call to | ||
* registerFontClassAlias. If no CSS class has been associated, returns the alias unmodified. | ||
*/ | ||
classNameForFontAlias(alias: string): string { | ||
return this._fontCssClassesByAlias.get(alias) || alias; | ||
} | ||
|
||
/** | ||
* Sets the CSS class name to be used for icon fonts when an <md-icon> component does not | ||
* have a fontSet input value, and is not loading an icon by name or URL. | ||
*/ | ||
setDefaultFontSetClass(className: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing return value type ( |
||
this._defaultFontSetClass = className; | ||
return this; | ||
} | ||
|
||
/** | ||
* Returns the CSS class name to be used for icon fonts when an <md-icon> component does not | ||
* have a fontSet input value, and is not loading an icon by name or URL. | ||
*/ | ||
getDefaultFontSetClass(): string { | ||
return this._defaultFontSetClass; | ||
} | ||
|
||
/** | ||
* Returns an Observable that produces the icon (as an <svg> DOM element) from the given URL. | ||
* The response from the URL may be cached so this will not always cause an HTTP request, but | ||
* the produced element will always be a new copy of the originally fetched icon. (That is, | ||
* it will not contain any modifications made to elements previously returned). | ||
*/ | ||
getSvgIconFromUrl(url: string): Observable<SVGElement> { | ||
if (this._cachedIconsByUrl.has(url)) { | ||
return Observable.of(this._cachedIconsByUrl.get(url).cloneNode(true)); | ||
} | ||
return this._loadSvgIconFromConfig(new SvgIconConfig(url)) | ||
.do(svg => this._cachedIconsByUrl.set(url, svg)) | ||
.map(svg => svg.cloneNode(true)); | ||
} | ||
|
||
/** | ||
* Returns an Observable that produces the icon (as an <svg> DOM element) with the given name | ||
* and namespace. The icon must have been previously registered with addIcon or addIconSet; | ||
* if not, the Observable will throw an MdIconNameNotFoundException. | ||
*/ | ||
getNamedSvgIcon(name: string, namespace = ''): Observable<SVGElement> { | ||
// Return (copy of) cached icon if possible. | ||
const iconKey = namespace + ':' + name; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: Use template string here rather than concat? i.e. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually think the concatenation is cleaner in this particular case. |
||
if (this._svgIconConfigs.has(iconKey)) { | ||
return this._getSvgFromConfig(this._svgIconConfigs.get(iconKey)); | ||
} | ||
// See if we have any icon sets registered for the namespace. | ||
const iconSetConfigs = this._iconSetConfigs.get(namespace); | ||
if (iconSetConfigs) { | ||
return this._getSvgFromIconSetConfigs(name, this._iconSetConfigs.get(namespace)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason we're not using the return this._getSvgFromIconSetConfigs(name, iconSetConfigs); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No reason at all, changed. |
||
} | ||
return Observable.throw(new MdIconNameNotFoundException(iconKey)); | ||
} | ||
|
||
/** | ||
* Returns the cached icon for a SvgIconConfig if available, or fetches it from its URL if not. | ||
*/ | ||
private _getSvgFromConfig(config: SvgIconConfig): Observable<SVGElement> { | ||
if (config.svgElement) { | ||
// We already have the SVG element for this icon, return a copy. | ||
return Observable.of(config.svgElement.cloneNode(true)); | ||
} else { | ||
// Fetch the icon from the config's URL, cache it, and return a copy. | ||
return this._loadSvgIconFromConfig(config) | ||
.do((svg: SVGElement) => config.svgElement = svg) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the explicit |
||
.map((svg: SVGElement) => svg.cloneNode(true)); | ||
} | ||
} | ||
|
||
/** | ||
* Attempts to find an icon with the specified name in any of the SVG icon sets. | ||
* First searches the available cached icons for a nested element with a matching name, and | ||
* if found copies the element to a new <svg> element. If not found, fetches all icon sets | ||
* that have not been cached, and searches again after all fetches are completed. | ||
* The returned Observable produces the SVG element if possible, and throws | ||
* MdIconNameNotFoundException if no icon with the specified name can be found. | ||
*/ | ||
private _getSvgFromIconSetConfigs(name: string, iconSetConfigs: SvgIconConfig[]): | ||
Observable<SVGElement> { | ||
// For all the icon set SVG elements we've fetched, see if any contain an icon with the | ||
// requested name. | ||
const namedIcon = this._extractIconWithNameFromAnySet(name, iconSetConfigs); | ||
if (namedIcon) { | ||
// We could cache namedSvg in _svgIconConfigs, but since we have to make a copy every | ||
// time anyway, there's probably not much advantage compared to just always extracting | ||
// it from the icon set. | ||
return Observable.of(namedIcon); | ||
} | ||
// Not found in any cached icon sets. If there are icon sets with URLs that we haven't | ||
// fetched, fetch them now and look for iconName in the results. | ||
const iconSetFetchRequests: Observable<SVGElement>[] = iconSetConfigs | ||
.filter(iconSetConfig => !iconSetConfig.svgElement) | ||
.map(iconSetConfig => | ||
this._loadSvgIconSetFromConfig(iconSetConfig) | ||
.catch((err: any, source: any, caught: any): Observable<SVGElement> => { | ||
// Swallow errors fetching individual URLs so the combined Observable won't | ||
// necessarily fail. | ||
console.log(`Loading icon set URL: ${iconSetConfig.url} failed: ${err}`); | ||
return Observable.of(null); | ||
}) | ||
.do((svg: SVGElement) => { | ||
// Cache SVG element. | ||
if (svg) { | ||
iconSetConfig.svgElement = svg; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional nitpick: collapse onto one line?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually prefer always having braces and newlines. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really? Don't like the waste of vertical space :-p But completely subjective! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I reluctantly agree with @jelbourn. (On a related note, I'm very pleased with the 100-char line length). |
||
} | ||
})); | ||
// Fetch all the icon set URLs. When the requests complete, every IconSet should have a | ||
// cached SVG element (unless the request failed), and we can check again for the icon. | ||
return Observable.forkJoin(iconSetFetchRequests) | ||
.map((ignoredResults: any) => { | ||
const foundIcon = this._extractIconWithNameFromAnySet(name, iconSetConfigs); | ||
if (!foundIcon) { | ||
throw new MdIconNameNotFoundException(name); | ||
} | ||
return foundIcon; | ||
}); | ||
} | ||
|
||
/** | ||
* Searches the cached SVG elements for the given icon sets for a nested icon element whose "id" | ||
* tag matches the specified name. If found, copies the nested element to a new SVG element and | ||
* returns it. Returns null if no matching element is found. | ||
*/ | ||
private _extractIconWithNameFromAnySet(iconName: string, iconSetConfigs: SvgIconConfig[]): | ||
SVGElement { | ||
// Iterate backwards, so icon sets added later have precedence. | ||
for (let i = iconSetConfigs.length - 1; i >= 0; i--) { | ||
const config = iconSetConfigs[i]; | ||
if (config.svgElement) { | ||
const foundIcon = this._extractSvgIconFromSet(config.svgElement, iconName, config); | ||
if (foundIcon) { | ||
return foundIcon; | ||
} | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Loads the content of the icon URL specified in the SvgIconConfig and creates an SVG element | ||
* from it. | ||
*/ | ||
private _loadSvgIconFromConfig(config: SvgIconConfig): Observable<SVGElement> { | ||
return this._fetchUrl(config.url) | ||
.map(svgText => this._createSvgElementForSingleIcon(svgText, config)); | ||
} | ||
|
||
/** | ||
* Loads the content of the icon set URL specified in the SvgIconConfig and creates an SVG element | ||
* from it. | ||
*/ | ||
private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable<SVGElement> { | ||
return this._fetchUrl(config.url) | ||
.map((svgText) => this._svgElementFromString(svgText)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My XSS sense is tingling here. I'm trying to imagine a situation where an app would use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that felt sketchy to me also. Although I don't think it's any worse than ngMaterial 1.x. |
||
} | ||
|
||
/** | ||
* Creates a DOM element from the given SVG string, and adds default attributes. | ||
*/ | ||
private _createSvgElementForSingleIcon(responseText: string, config: SvgIconConfig): SVGElement { | ||
const svg = this._svgElementFromString(responseText); | ||
this._setSvgAttributes(svg, config); | ||
return svg; | ||
} | ||
|
||
/** | ||
* Searches the cached element of the given SvgIconConfig for a nested icon element whose "id" | ||
* tag matches the specified name. If found, copies the nested element to a new SVG element and | ||
* returns it. Returns null if no matching element is found. | ||
*/ | ||
private _extractSvgIconFromSet( | ||
iconSet: SVGElement, iconName: string, config: SvgIconConfig): SVGElement { | ||
const iconNode = iconSet.querySelector('#' + iconName); | ||
if (!iconNode) { | ||
return null; | ||
} | ||
// If the icon node is itself an <svg> node, clone and return it directly. If not, set it as | ||
// the content of a new <svg> node. | ||
if (iconNode.tagName.toLowerCase() == 'svg') { | ||
return this._setSvgAttributes(<SVGElement>iconNode.cloneNode(true), config); | ||
} | ||
// createElement('SVG') doesn't work as expected; the DOM ends up with | ||
// the correct nodes, but the SVG content doesn't render. Instead we | ||
// have to create an empty SVG node using innerHTML and append its content. | ||
// http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display | ||
const svg = this._svgElementFromString('<svg></svg>'); | ||
// Clone the node so we don't remove it from the parent icon set element. | ||
svg.appendChild(iconNode.cloneNode(true)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this making any assumptions about the element type of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, although I added a check so that elements are used directly rather than wrapped in another . Should we require it to be or ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's good- checking for a direct |
||
return this._setSvgAttributes(svg, config); | ||
} | ||
|
||
/** | ||
* Creates a DOM element from the given SVG string. | ||
*/ | ||
private _svgElementFromString(str: string): SVGElement { | ||
// TODO: Is there a better way than innerHTML? Renderer doesn't appear to have a method for | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p = new DOMParser();
p.parseFromString('<a>hello<b>world</a>', 'text/html').body There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, I had never seen this API before. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neat, and it specifically supports SVG documents. Requires IE10 per https://developer.mozilla.org/en-US/docs/Web/API/DOMParser, is that ok? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, we only support IE11+ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. We support IE11 and up. You might want to create the DOMParser once. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've played with this for a while, but it appears that using DOMParser to create an element has the same problem as document.createElement('svg'), where the DOM content looks correct but the icon doesn't render. (similar to the referenced http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display) |
||
// creating an element from an HTML string. | ||
const div = document.createElement('DIV'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha |
||
div.innerHTML = str; | ||
const svg = <SVGElement>div.querySelector('svg'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This prompted me to file an issue on the TypeScript repo, since it could statically infer what the type is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be cool. |
||
if (!svg) { | ||
throw new MdIconSvgTagNotFoundException(); | ||
} | ||
return svg; | ||
} | ||
|
||
/** | ||
* Sets the default attributes for an SVG element to be used as an icon. | ||
*/ | ||
private _setSvgAttributes(svg: SVGElement, config: SvgIconConfig): SVGElement { | ||
if (!svg.getAttribute('xmlns')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason this isn't going through the renderer? e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a service, for which there isn't a renderer instance. (each There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whoops, forgot where I was! |
||
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); | ||
} | ||
svg.setAttribute('fit', ''); | ||
svg.setAttribute('height', '100%'); | ||
svg.setAttribute('width', '100%'); | ||
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); | ||
svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable. | ||
return svg; | ||
} | ||
|
||
/** | ||
* Returns an Observable which produces the string contents of the given URL. Results may be | ||
* cached, so future calls with the same URL may not cause another HTTP request. | ||
*/ | ||
private _fetchUrl(url: string): Observable<string> { | ||
// Store in-progress fetches to avoid sending a duplicate request for a URL when there is | ||
// already a request in progress for that URL. It's necessary to call share() on the | ||
// Observable returned by http.get() so that multiple subscribers don't cause multiple XHRs. | ||
if (this._inProgressUrlFetches.has(url)) { | ||
return this._inProgressUrlFetches.get(url); | ||
} | ||
const req = this._http.get(url) | ||
.map((response) => response.text()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forget the whole thing. Apparently HTTP doesn't have a way to do that. Note that a |
||
.finally(() => { | ||
this._inProgressUrlFetches.delete(url); | ||
}) | ||
.share(); | ||
this._inProgressUrlFetches.set(url, req); | ||
return req; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
@import "variables"; | ||
@import "default-theme"; | ||
|
||
/** The width/height of the icon element. */ | ||
$md-icon-size: 24px !default; | ||
|
||
/** | ||
This works because we're using ViewEncapsulation.None. If we used the default | ||
encapsulation, the selector would need to be ":host". | ||
*/ | ||
md-icon { | ||
background-repeat: no-repeat; | ||
display: inline-block; | ||
fill: currentColor; | ||
height: $md-icon-size; | ||
margin: auto; | ||
vertical-align: middle; | ||
width: $md-icon-size; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: attmepting --> attempting