angular-svg-icon
Version:
Angular 8 component for inlining SVGs allowing them to be easily styled with CSS.
463 lines (457 loc) • 13.3 kB
JavaScript
import { Injectable, InjectionToken, Inject, PLATFORM_ID, Optional, SkipSelf, Component, ElementRef, KeyValueDiffers, Renderer2, ChangeDetectorRef, Input, NgModule } from '@angular/core';
import { DOCUMENT, CommonModule } from '@angular/common';
import { of, throwError } from 'rxjs';
import { map, tap, catchError, finalize, share } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @abstract
*/
class SvgLoader {
}
class SvgHttpLoader extends SvgLoader {
/**
* @param {?} http
*/
constructor(http) {
super();
this.http = http;
}
/**
* @param {?} url
* @return {?}
*/
getSvg(url) {
return this.http.get(url, { responseType: 'text' });
}
}
SvgHttpLoader.decorators = [
{ type: Injectable }
];
/** @nocollapse */
SvgHttpLoader.ctorParameters = () => [
{ type: HttpClient }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const SERVER_URL = new InjectionToken('SERVER_URL');
class SvgIconRegistryService {
/**
* @param {?} loader
* @param {?} platformId
* @param {?} serverUrl
* @param {?} _document
*/
constructor(loader, platformId, serverUrl, _document) {
this.loader = loader;
this.platformId = platformId;
this.serverUrl = serverUrl;
this._document = _document;
this.iconsByUrl = new Map();
this.iconsLoadingByUrl = new Map();
this.document = this._document;
}
/**
* Add a SVG to the registry by passing a name and the SVG.
* @param {?} name
* @param {?} data
* @return {?}
*/
addSvg(name, data) {
if (!this.iconsByUrl.has(name)) {
/** @type {?} */
const div = this.document.createElement('DIV');
div.innerHTML = data;
/** @type {?} */
const svg = (/** @type {?} */ (div.querySelector('svg')));
this.iconsByUrl.set(name, svg);
}
}
/**
* Load a SVG to the registry from a URL.
* @param {?} url
* @param {?=} name
* @return {?}
*/
loadSvg(url, name = url) {
// not sure if there should be a possibility to use name for server usage
// so overriding it for now if provided
// maybe should separate functionality for url and name use-cases
if (this.serverUrl && url.match(/^(http(s)?):/) === null) {
url = this.serverUrl + url;
name = url;
}
if (this.iconsByUrl.has(name)) {
return of(this.iconsByUrl.get(name));
}
else if (this.iconsLoadingByUrl.has(name)) {
return this.iconsLoadingByUrl.get(name);
}
/** @type {?} */
const o = (/** @type {?} */ (this.loader.getSvg(url).pipe(map((/**
* @param {?} svg
* @return {?}
*/
svg => {
/** @type {?} */
const div = this.document.createElement('DIV');
div.innerHTML = svg;
return (/** @type {?} */ (div.querySelector('svg')));
})), tap((/**
* @param {?} svg
* @return {?}
*/
svg => this.iconsByUrl.set(name, svg))), catchError((/**
* @param {?} err
* @return {?}
*/
err => {
console.error(err);
return throwError(err);
})), finalize((/**
* @return {?}
*/
() => this.iconsLoadingByUrl.delete(name))), share())));
this.iconsLoadingByUrl.set(name, o);
return o;
}
/**
* Get loaded SVG from registry by name. (also works by url because of blended map)
* @param {?} name
* @return {?}
*/
getSvgByName(name) {
if (this.iconsByUrl.has(name)) {
return of(this.iconsByUrl.get(name));
}
else if (this.iconsLoadingByUrl.has(name)) {
return this.iconsLoadingByUrl.get(name);
}
return throwError(`No svg with name '${name}' has been loaded`);
}
/**
* Remove a SVG from the registry by URL (or name).
* @param {?} url
* @return {?}
*/
unloadSvg(url) {
if (this.iconsByUrl.has(url)) {
this.iconsByUrl.delete(url);
}
}
}
SvgIconRegistryService.decorators = [
{ type: Injectable }
];
/** @nocollapse */
SvgIconRegistryService.ctorParameters = () => [
{ type: SvgLoader },
{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] },
{ type: String, decorators: [{ type: Optional }, { type: Inject, args: [SERVER_URL,] }] },
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [DOCUMENT,] }] }
];
/**
* @param {?} parentRegistry
* @param {?} loader
* @param {?} platformId
* @param {?=} serverUrl
* @param {?=} document
* @return {?}
*/
function SVG_ICON_REGISTRY_PROVIDER_FACTORY(parentRegistry, loader, platformId, serverUrl, document) {
return parentRegistry || new SvgIconRegistryService(loader, platformId, serverUrl, document);
}
/** @type {?} */
const SVG_ICON_REGISTRY_PROVIDER = {
provide: SvgIconRegistryService,
deps: [[new Optional(), new SkipSelf(), SvgIconRegistryService], SvgLoader, [(/** @type {?} */ (PLATFORM_ID))],
[new Optional(), (/** @type {?} */ (SERVER_URL))], [new Optional(), (/** @type {?} */ (DOCUMENT))]
],
useFactory: SVG_ICON_REGISTRY_PROVIDER_FACTORY
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class SvgIconComponent {
/**
* @param {?} element
* @param {?} differs
* @param {?} renderer
* @param {?} iconReg
* @param {?} cdr
*/
constructor(element, differs, renderer, iconReg, cdr) {
this.element = element;
this.differs = differs;
this.renderer = renderer;
this.iconReg = iconReg;
this.cdr = cdr;
this.stretch = false;
this.applyCss = false;
}
// Adapted from ngStyle
/**
* @param {?} v
* @return {?}
*/
set svgStyle(v) {
this._svgStyle = v;
if (!this.differ && v) {
this.differ = this.differs.find(v).create();
}
}
/**
* @return {?}
*/
ngOnInit() {
this.init();
}
/**
* @return {?}
*/
ngOnDestroy() {
this.destroy();
}
/**
* @param {?} changeRecord
* @return {?}
*/
ngOnChanges(changeRecord) {
if (changeRecord.src || changeRecord.name) {
if (this.svg) {
this.destroy();
}
this.init();
}
if (changeRecord.stretch) {
this.stylize();
}
}
/**
* @return {?}
*/
ngDoCheck() {
if (this.svg && this.differ) {
/** @type {?} */
const changes = this.differ.diff(this._svgStyle);
if (changes) {
this.applyChanges(changes);
}
}
}
/**
* @private
* @return {?}
*/
init() {
if (this.name) {
this.icnSub = this.iconReg.getSvgByName(this.name).subscribe(this.initSvg.bind(this));
}
else if (this.src) {
this.icnSub = this.iconReg.loadSvg(this.src).subscribe(this.initSvg.bind(this));
}
}
/**
* @private
* @param {?} svg
* @return {?}
*/
initSvg(svg) {
this.setSvg(svg);
this.resetDiffer();
}
/**
* @private
* @return {?}
*/
destroy() {
this.svg = undefined;
this.differ = undefined;
if (this.icnSub) {
this.icnSub.unsubscribe();
}
}
/**
* @private
* @return {?}
*/
resetDiffer() {
if (this._svgStyle && !this.differ) {
this.differ = this.differs.find(this._svgStyle).create();
}
}
/**
* @private
* @param {?} svg
* @return {?}
*/
setSvg(svg) {
if (svg) {
this.svg = svg;
/** @type {?} */
const icon = (/** @type {?} */ (svg.cloneNode(true)));
/** @type {?} */
const elem = this.element.nativeElement;
if (this.applyCss) {
this.copyNgContentAttribute(elem, icon);
}
elem.innerHTML = '';
this.renderer.appendChild(elem, icon);
this.stylize();
this.cdr.markForCheck();
}
}
/**
* @private
* @param {?} hostElem
* @param {?} icon
* @return {?}
*/
copyNgContentAttribute(hostElem, icon) {
/** @type {?} */
const attributes = (/** @type {?} */ (hostElem.attributes));
/** @type {?} */
const len = attributes.length;
for (let i = 0; i < len; i += 1) {
/** @type {?} */
const attribute = attributes.item(i);
if (attribute.name.startsWith('_ngcontent')) {
this.setNgContentAttribute(icon, attribute.name);
break;
}
}
}
/**
* @private
* @param {?} parent
* @param {?} attributeName
* @return {?}
*/
setNgContentAttribute(parent, attributeName) {
this.renderer.setAttribute(parent, attributeName, '');
/** @type {?} */
const len = parent.childNodes.length;
for (let i = 0; i < len; i += 1) {
/** @type {?} */
const child = parent.childNodes[i];
if (child instanceof Element) {
this.setNgContentAttribute(child, attributeName);
}
}
}
/**
* @private
* @return {?}
*/
stylize() {
if (this.svg) {
/** @type {?} */
const svg = this.element.nativeElement.firstChild;
if (this.stretch === true) {
this.renderer.setAttribute(svg, 'preserveAspectRatio', 'none');
}
else if (this.stretch === false) {
this.renderer.removeAttribute(svg, 'preserveAspectRatio');
}
}
}
/**
* @private
* @param {?} changes
* @return {?}
*/
applyChanges(changes) {
changes.forEachRemovedItem((/**
* @param {?} record
* @return {?}
*/
(record) => this.setStyle(record.key, null)));
changes.forEachAddedItem((/**
* @param {?} record
* @return {?}
*/
(record) => this.setStyle(record.key, record.currentValue)));
changes.forEachChangedItem((/**
* @param {?} record
* @return {?}
*/
(record) => this.setStyle(record.key, record.currentValue)));
}
/**
* @private
* @param {?} nameAndUnit
* @param {?} value
* @return {?}
*/
setStyle(nameAndUnit, value) {
const [name, unit] = nameAndUnit.split('.');
value = value !== null && unit ? `${value}${unit}` : value;
/** @type {?} */
const svg = this.element.nativeElement.firstChild;
if (value !== null) {
this.renderer.setStyle(svg, name, (/** @type {?} */ (value)));
}
else {
this.renderer.removeStyle(svg, name);
}
}
}
SvgIconComponent.decorators = [
{ type: Component, args: [{
selector: 'svg-icon',
template: '<ng-content></ng-content>'
}] }
];
/** @nocollapse */
SvgIconComponent.ctorParameters = () => [
{ type: ElementRef },
{ type: KeyValueDiffers },
{ type: Renderer2 },
{ type: SvgIconRegistryService },
{ type: ChangeDetectorRef }
];
SvgIconComponent.propDecorators = {
src: [{ type: Input }],
name: [{ type: Input }],
stretch: [{ type: Input }],
applyCss: [{ type: Input }],
svgStyle: [{ type: Input }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class AngularSvgIconModule {
/**
* @param {?=} config
* @return {?}
*/
static forRoot(config = {}) {
return {
ngModule: AngularSvgIconModule,
providers: [
config.loader || { provide: SvgLoader, useClass: SvgHttpLoader },
SVG_ICON_REGISTRY_PROVIDER
]
};
}
}
AngularSvgIconModule.decorators = [
{ type: NgModule, args: [{
imports: [
CommonModule,
],
declarations: [SvgIconComponent],
providers: [SVG_ICON_REGISTRY_PROVIDER, { provide: SvgLoader, useClass: SvgHttpLoader }],
exports: [SvgIconComponent]
},] }
];
export { AngularSvgIconModule, SERVER_URL, SVG_ICON_REGISTRY_PROVIDER, SVG_ICON_REGISTRY_PROVIDER_FACTORY, SvgHttpLoader, SvgIconComponent, SvgIconRegistryService, SvgLoader };
//# sourceMappingURL=angular-svg-icon.js.map