-
Notifications
You must be signed in to change notification settings - Fork 6.8k
md-icon #281
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
Changes from 21 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,255 @@ | ||
import {Injectable, Renderer} from 'angular2/core'; | ||
import {Http, Response, HTTP_PROVIDERS} from 'angular2/http'; | ||
import {AsyncSubject, Observer, Observable} from 'rxjs/Rx'; | ||
|
||
/** | ||
* Configuration for an icon, possibly including the cached SVG element. | ||
*/ | ||
class IconConfig { | ||
svgElement: SVGElement = null; | ||
constructor(public url: string, public viewBoxSize: number) { | ||
} | ||
} | ||
|
||
@Injectable() | ||
export class MdIconProvider { | ||
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'm trying to come up with a better name for this class, since it would be good to move away from the Angular 1 "provider" concept. What do you think about 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. SG |
||
// IconConfig objects and cached SVG elements for individual icons. | ||
// First level of cache is icon set (which is the empty string for the default set). | ||
// Second level is the icon name within the set. | ||
private _iconConfigs = new Map<string, Map<string, IconConfig>>(); | ||
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. Rather than doing a Map of Map, why not just treat the key as 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. It seemed slightly cleaner to use nested maps rather than encoding the nesting in the key. No strong opinion either way though. |
||
|
||
// IconConfig objects and cached SVG elements for icon sets. | ||
// These are stored only by set name, but multiple URLs can be registered under the same name. | ||
private _iconSetConfigs = new Map<string, [IconConfig]>(); | ||
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. Does the format 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. Oh, I see now that this is a tuple type, not an array (which isn't something I had seen used before). Why is it a tuple? 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 IconConfig[] works. It is just supposed to be an array. |
||
|
||
// Cache for icons loaded by direct URLs. | ||
private _cachedIconsByUrl = new Map<string, SVGElement>(); | ||
|
||
private _fontClassNamesByAlias = new Map<string, string>(); | ||
|
||
private _inProgressUrlFetches = new Map<string, Observable<string>>(); | ||
|
||
private _defaultViewBoxSize = 24; | ||
private _defaultFontSetClass = 'material-icons'; | ||
|
||
constructor(private _http: Http) { | ||
} | ||
|
||
public addIcon(iconName: string, url: string, viewBoxSize:number=0): MdIconProvider { | ||
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. Cool TypeScript trick: you can make your return type public addIcon(name: string, url: string, viewBoxSize: number = 0): this { ... } 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. |
||
return this.addIconInSet('', iconName, url, viewBoxSize); | ||
} | ||
|
||
public addIconInSet( | ||
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 think we can separate the concept of icon namespaces and icon sets. We can treat an icon-set is a bundle of icons all in one file, which can be added to any namespace. Perhaps it makes sense to just use the term "icon bundle". addIcon(name, url, opt_namespace)
addIconBundle(url, opt_namespace) Then again, maybe it does make more sense to always treat a "bundle" as a namespace. Let me run this idea by the rest of the team. 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 basically what it does now, I think. You can do addIconSet('animals', 'animals.svg') and addIconInSet('animals', 'cat', 'cat1.svg') and then svgIcon="animals:cat" will use the individual icon while "animals:dog" will search the set/bundle. And you can pass an empty namespace to addIconSet to add it to the default namespace. I like your names and API, assuming we can get rid of the current optional viewBoxSize parameter. |
||
setName: string, iconName: string, url: string, viewBoxSize:number=0): MdIconProvider { | ||
let iconSetMap = this._iconConfigs.get(setName); | ||
if (!iconSetMap) { | ||
iconSetMap = new Map<string, IconConfig>(); | ||
this._iconConfigs.set(setName, iconSetMap); | ||
} | ||
iconSetMap.set(iconName, new IconConfig(url, viewBoxSize || this._defaultViewBoxSize)); | ||
return this; | ||
} | ||
|
||
public addIconSet(setName: string, url: string, viewBoxSize=0): MdIconProvider { | ||
const config = new IconConfig(url, viewBoxSize || this._defaultViewBoxSize); | ||
if (this._iconSetConfigs.has(setName)) { | ||
this._iconSetConfigs.get(setName).push(config); | ||
} else { | ||
this._iconSetConfigs.set(setName, [config]); | ||
} | ||
return this; | ||
} | ||
|
||
public registerFontSet(alias: string, className?: string): MdIconProvider { | ||
this._fontClassNamesByAlias.set(alias, className || alias); | ||
return this; | ||
} | ||
|
||
public setDefaultViewBoxSize(size: number) { | ||
this._defaultViewBoxSize = size; | ||
return this; | ||
} | ||
|
||
public getDefaultViewBoxSize(): number { | ||
return this._defaultViewBoxSize; | ||
} | ||
|
||
public setDefaultFontSetClass(className: string) { | ||
this._defaultFontSetClass = className; | ||
return this; | ||
} | ||
|
||
public getDefaultFontSetClass(): string { | ||
return this._defaultFontSetClass; | ||
} | ||
|
||
loadIconFromSetByName(setName: string, iconName: string): Observable<SVGElement> { | ||
// Return (copy of) cached icon if possible. | ||
if (this._iconConfigs.has(setName) && this._iconConfigs.get(setName).has(iconName)) { | ||
const config = this._iconConfigs.get(setName).get(iconName); | ||
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._loadIconFromConfig(config) | ||
.do((svg: SVGElement) => config.svgElement = svg) | ||
.map((svg: SVGElement) => svg.cloneNode(true)); | ||
} | ||
} | ||
// See if we have any icon sets registered for the set name. | ||
const iconSetConfigs = this._iconSetConfigs.get(setName); | ||
if (iconSetConfigs) { | ||
// For all the icon set SVG elements we've fetched, see if any contain an icon with the | ||
// requested name. | ||
const namedIcon = this._extractIconWithNameFromAnySet(iconName, iconSetConfigs); | ||
if (namedIcon) { | ||
// We could cache namedSvg in _iconConfigs, 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.cloneNode(true)); | ||
} | ||
// 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.forEach((setConfig) => { | ||
if (!setConfig.svgElement) { | ||
iconSetFetchRequests.push( | ||
this._loadIconSetFromConfig(setConfig) | ||
.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: ${setConfig.url} failed with error: ${err}`); | ||
return Observable.of(null); | ||
}) | ||
.do((svg: SVGElement) => { | ||
// Cache SVG element. | ||
if (svg) { | ||
setConfig.svgElement = svg; | ||
} | ||
}) | ||
); | ||
} | ||
}); | ||
// 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(iconName, iconSetConfigs); | ||
if (!foundIcon) { | ||
throw Error(`Failed to find icon name: ${iconName} in set: ${setName}`); | ||
} | ||
return foundIcon; | ||
}); | ||
} | ||
return Observable.throw(Error(`Unknown icon name: ${iconName} in set: ${setName}`)); | ||
} | ||
|
||
private _extractIconWithNameFromAnySet(iconName: string, setConfigs: [IconConfig]): SVGElement { | ||
// Iterate backwards, so icon sets added later have precedence. | ||
for (let i = setConfigs.length - 1; i >= 0; i--) { | ||
const config = setConfigs[i]; | ||
if (config.svgElement) { | ||
const foundIcon = this._extractSvgIconFromSet(config.svgElement, iconName, config); | ||
if (foundIcon) { | ||
return foundIcon; | ||
} | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
loadIconFromUrl(url: string): Observable<SVGElement> { | ||
if (this._cachedIconsByUrl.has(url)) { | ||
return Observable.of(this._cachedIconsByUrl.get(url).cloneNode(true)); | ||
} | ||
return this._loadIconFromConfig(new IconConfig(url, this._defaultViewBoxSize)) | ||
.do((svg: SVGElement) => this._cachedIconsByUrl.set(url, svg)); | ||
} | ||
|
||
classNameForFontAlias(alias: string): string { | ||
if (!this._fontClassNamesByAlias.has(alias)) { | ||
throw Error('Unknown font alias: ' + alias); | ||
} | ||
return this._fontClassNamesByAlias.get(alias); | ||
} | ||
|
||
private _fetchUrl(url: string): Observable<string> { | ||
// FIXME: This is trying to avoid sending a duplicate request for a URL when there is already | ||
// a request in progress for that URL. But it's not working; even though we return the cached | ||
// Observable, a second request is still sent. | ||
console.log('*** fetchUrl: ' + url); | ||
if (this._inProgressUrlFetches.has(url)) { | ||
console.log("*** Using existing request"); | ||
return this._inProgressUrlFetches.get(url); | ||
} | ||
console.log(`*** Sending request for ${url}`); | ||
const req = this._http.get(url) | ||
.do((response) => { | ||
console.log(`*** Got response for ${url}`); | ||
console.log('*** Removing request: ' + url); | ||
this._inProgressUrlFetches.delete(url); | ||
}) | ||
.map((response) => response.text()); | ||
this._inProgressUrlFetches.set(url, req); | ||
return req; | ||
} | ||
|
||
private _loadIconFromConfig(config: IconConfig): Observable<SVGElement> { | ||
return this._fetchUrl(config.url) | ||
.map(svgText => this._createSvgElementForSingleIcon(svgText, config)); | ||
} | ||
|
||
private _loadIconSetFromConfig(config: IconConfig): Observable<SVGElement> { | ||
return this._fetchUrl(config.url) | ||
.map((svgText) => this._svgElementFromString(svgText)); | ||
} | ||
|
||
private _createSvgElementForSingleIcon(responseText: string, config: IconConfig): SVGElement { | ||
const svg = this._svgElementFromString(responseText); | ||
this._setSvgAttributes(svg, config); | ||
return svg; | ||
} | ||
|
||
private _setSvgAttributes(svg: SVGElement, config: IconConfig) { | ||
const viewBoxSize = config.viewBoxSize || this._defaultViewBoxSize; | ||
if (!svg.getAttribute('xmlns')) { | ||
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('viewBox', | ||
svg.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize)); | ||
svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable. | ||
} | ||
|
||
private _extractSvgIconFromSet( | ||
iconSet: SVGElement, iconName: string, config: IconConfig): SVGElement { | ||
const iconNode = iconSet.querySelector('#' + iconName); | ||
if (!iconNode) { | ||
return null; | ||
} | ||
// 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>'); | ||
svg.appendChild(iconNode); | ||
this._setSvgAttributes(svg, config); | ||
return svg; | ||
} | ||
|
||
private _svgElementFromString(str: string): SVGElement { | ||
// TODO: Is there a better way than innerHTML? Renderer doesn't appear to have a method for | ||
// creating an element from an HTML string. | ||
const div = document.createElement('DIV'); | ||
div.innerHTML = str; | ||
const svg = <SVGElement>div.querySelector('svg'); | ||
if (!svg) { | ||
throw Error('<svg> tag not found'); | ||
} | ||
return svg; | ||
} | ||
} |
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 commentThe reason will be displayed to describe this comment to others. Learn more. Too much indent |
||
} |
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.
Do you know why we do anything with viewBox? I know material1 does the same thing, but I'm iffy on the background.
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.
Yeah it seems like an obscure feature. You can use it to have a version of the icon that's zoomed in or out, but only from one corner because the top and left coordinates are always 0. I don't know of any especially compelling use cases, happy to remove it.