282 lines
8.6 KiB
TypeScript
282 lines
8.6 KiB
TypeScript
///
|
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
///
|
|
|
|
import { CanColor, mixinColor } from '@angular/material/core';
|
|
import {
|
|
AfterContentInit,
|
|
AfterViewChecked,
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
ElementRef,
|
|
ErrorHandler,
|
|
Inject,
|
|
OnDestroy,
|
|
Renderer2,
|
|
ViewChild,
|
|
ViewEncapsulation
|
|
} from '@angular/core';
|
|
import { MAT_ICON_LOCATION, MatIconLocation, MatIconRegistry } from '@angular/material/icon';
|
|
import { Subscription } from 'rxjs';
|
|
import { take } from 'rxjs/operators';
|
|
import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
|
|
import { ContentObserver } from '@angular/cdk/observers';
|
|
|
|
const _TbIconBase = mixinColor(
|
|
class {
|
|
constructor(public _elementRef: ElementRef) {}
|
|
},
|
|
);
|
|
|
|
const funcIriAttributes = [
|
|
'clip-path',
|
|
'color-profile',
|
|
'src',
|
|
'cursor',
|
|
'fill',
|
|
'filter',
|
|
'marker',
|
|
'marker-start',
|
|
'marker-mid',
|
|
'marker-end',
|
|
'mask',
|
|
'stroke',
|
|
];
|
|
|
|
const funcIriAttributeSelector = funcIriAttributes.map(attr => `[${attr}]`).join(', ');
|
|
|
|
const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
|
|
|
|
@Component({
|
|
template: '<span style="display: none;" #iconNameContent><ng-content></ng-content></span>',
|
|
selector: 'tb-icon',
|
|
exportAs: 'tbIcon',
|
|
styleUrls: [],
|
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
|
inputs: ['color'],
|
|
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
|
|
host: {
|
|
role: 'img',
|
|
class: 'mat-icon notranslate',
|
|
'[attr.data-mat-icon-type]': '!_useSvgIcon ? "font" : "svg"',
|
|
'[attr.data-mat-icon-name]': '_svgName',
|
|
'[attr.data-mat-icon-namespace]': '_svgNamespace',
|
|
'[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"',
|
|
},
|
|
encapsulation: ViewEncapsulation.None,
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class TbIconComponent extends _TbIconBase
|
|
implements AfterContentInit, AfterViewChecked, CanColor, OnDestroy {
|
|
|
|
@ViewChild('iconNameContent', {static: true})
|
|
_iconNameContent: ElementRef;
|
|
|
|
private icon: string;
|
|
|
|
get viewValue(): string {
|
|
return (this._iconNameContent?.nativeElement.textContent || '').trim();
|
|
}
|
|
|
|
private _contentChanges = Subscription.EMPTY;
|
|
private _previousFontSetClass: string[] = [];
|
|
|
|
_useSvgIcon = false;
|
|
_svgName: string | null;
|
|
_svgNamespace: string | null;
|
|
|
|
private _textElement = null;
|
|
|
|
private _previousPath?: string;
|
|
|
|
private _elementsWithExternalReferences?: Map<Element, {name: string; value: string}[]>;
|
|
|
|
private _currentIconFetch = Subscription.EMPTY;
|
|
|
|
constructor(elementRef: ElementRef<HTMLElement>,
|
|
private contentObserver: ContentObserver,
|
|
private renderer: Renderer2,
|
|
private _iconRegistry: MatIconRegistry,
|
|
@Inject(MAT_ICON_LOCATION) private _location: MatIconLocation,
|
|
private readonly _errorHandler: ErrorHandler) {
|
|
super(elementRef);
|
|
}
|
|
|
|
ngAfterContentInit(): void {
|
|
this.icon = this.viewValue;
|
|
this._updateIcon();
|
|
this._contentChanges = this.contentObserver.observe(this._iconNameContent.nativeElement)
|
|
.subscribe(() => {
|
|
const content = this.viewValue;
|
|
if (this.icon !== content) {
|
|
this.icon = content;
|
|
this._updateIcon();
|
|
}
|
|
});
|
|
}
|
|
|
|
ngAfterViewChecked() {
|
|
const cachedElements = this._elementsWithExternalReferences;
|
|
if (cachedElements && cachedElements.size) {
|
|
const newPath = this._location.getPathname();
|
|
if (newPath !== this._previousPath) {
|
|
this._previousPath = newPath;
|
|
this._prependPathToReferences(newPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this._contentChanges.unsubscribe();
|
|
this._currentIconFetch.unsubscribe();
|
|
if (this._elementsWithExternalReferences) {
|
|
this._elementsWithExternalReferences.clear();
|
|
}
|
|
}
|
|
|
|
private _updateIcon() {
|
|
const useSvgIcon = isSvgIcon(this.icon);
|
|
if (this._useSvgIcon !== useSvgIcon) {
|
|
this._useSvgIcon = useSvgIcon;
|
|
if (!this._useSvgIcon) {
|
|
this._updateSvgIcon(undefined);
|
|
} else {
|
|
this._updateFontIcon(undefined);
|
|
}
|
|
}
|
|
if (this._useSvgIcon) {
|
|
this._updateSvgIcon(this.icon);
|
|
} else {
|
|
this._updateFontIcon(this.icon);
|
|
}
|
|
}
|
|
|
|
private _updateFontIcon(rawName: string | undefined) {
|
|
if (rawName) {
|
|
this._clearFontIcon();
|
|
const iconName = splitIconName(rawName)[1];
|
|
this._textElement = this.renderer.createText(iconName);
|
|
const elem: HTMLElement = this._elementRef.nativeElement;
|
|
this.renderer.insertBefore(elem, this._textElement, this._iconNameContent.nativeElement);
|
|
const fontSetClasses = (
|
|
this._iconRegistry.getDefaultFontSetClass()
|
|
).filter(className => className.length > 0);
|
|
fontSetClasses.forEach(className => elem.classList.add(className));
|
|
this._previousFontSetClass = fontSetClasses;
|
|
} else {
|
|
this._clearFontIcon();
|
|
}
|
|
}
|
|
|
|
private _clearFontIcon() {
|
|
const elem: HTMLElement = this._elementRef.nativeElement;
|
|
if (this._textElement !== null) {
|
|
this.renderer.removeChild(elem, this._textElement);
|
|
this._textElement = null;
|
|
}
|
|
this._previousFontSetClass.forEach(className => elem.classList.remove(className));
|
|
this._previousFontSetClass = [];
|
|
}
|
|
|
|
private _updateSvgIcon(rawName: string | undefined) {
|
|
this._svgNamespace = null;
|
|
this._svgName = null;
|
|
this._currentIconFetch.unsubscribe();
|
|
|
|
if (rawName) {
|
|
const [namespace, iconName] = splitIconName(rawName);
|
|
if (namespace) {
|
|
this._svgNamespace = namespace;
|
|
}
|
|
if (iconName) {
|
|
this._svgName = iconName;
|
|
}
|
|
this._iconRegistry.getDefaultFontSetClass();
|
|
this._currentIconFetch = this._iconRegistry
|
|
.getNamedSvgIcon(iconName, namespace)
|
|
.pipe(take(1))
|
|
.subscribe({
|
|
next: (svg) => this._setSvgElement(svg),
|
|
error: (err: Error) => {
|
|
const errorMessage = `Error retrieving icon ${namespace}:${iconName}! ${err.message}`;
|
|
this._errorHandler.handleError(new Error(errorMessage));
|
|
}
|
|
});
|
|
} else {
|
|
this._clearSvgElement();
|
|
}
|
|
}
|
|
|
|
private _setSvgElement(svg: SVGElement) {
|
|
this._clearSvgElement();
|
|
const path = this._location.getPathname();
|
|
this._previousPath = path;
|
|
this._cacheChildrenWithExternalReferences(svg);
|
|
this._prependPathToReferences(path);
|
|
this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement);
|
|
}
|
|
|
|
private _clearSvgElement() {
|
|
const layoutElement: HTMLElement = this._elementRef.nativeElement;
|
|
let childCount = layoutElement.childNodes.length;
|
|
if (this._elementsWithExternalReferences) {
|
|
this._elementsWithExternalReferences.clear();
|
|
}
|
|
while (childCount--) {
|
|
const child = layoutElement.childNodes[childCount];
|
|
if (child.nodeType !== 1 || child.nodeName.toLowerCase() === 'svg') {
|
|
child.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
private _cacheChildrenWithExternalReferences(element: SVGElement) {
|
|
const elementsWithFuncIri = element.querySelectorAll(funcIriAttributeSelector);
|
|
const elements = (this._elementsWithExternalReferences = this._elementsWithExternalReferences || new Map());
|
|
elementsWithFuncIri.forEach(
|
|
(elementWithFuncIri) => {
|
|
funcIriAttributes.forEach(attr => {
|
|
const elementWithReference = elementWithFuncIri;
|
|
const value = elementWithReference.getAttribute(attr);
|
|
const match = value ? value.match(funcIriPattern) : null;
|
|
|
|
if (match) {
|
|
let attributes = elements.get(elementWithReference);
|
|
|
|
if (!attributes) {
|
|
attributes = [];
|
|
elements.set(elementWithReference, attributes);
|
|
}
|
|
|
|
attributes.push({name: attr, value: match[1]});
|
|
}
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
private _prependPathToReferences(path: string) {
|
|
const elements = this._elementsWithExternalReferences;
|
|
if (elements) {
|
|
elements.forEach((attrs, element) => {
|
|
attrs.forEach(attr => {
|
|
element.setAttribute(attr.name, `url('${path}#${attr.value}')`);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|