Update tb-icon to accept custom image.

This commit is contained in:
deaflynx 2025-08-08 16:52:48 +03:00
parent a3c28e9db4
commit 20e44ab004
7 changed files with 58 additions and 27 deletions

View File

@ -14,9 +14,8 @@
/// limitations under the License.
///
import { ChangeDetectionStrategy, Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { MenuSection } from '@core/services/menu.models';
import { tbImageIcon } from '@shared/models/custom-menu.models';
@Component({
selector: 'tb-menu-link',
@ -24,27 +23,14 @@ import { tbImageIcon } from '@shared/models/custom-menu.models';
styleUrls: ['./menu-link.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MenuLinkComponent implements OnInit, OnChanges {
export class MenuLinkComponent implements OnInit {
@Input() section: MenuSection;
isCustomIcon: boolean;
constructor() {
}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue) {
if (propName === 'section' && change.currentValue) {
this.isCustomIcon = tbImageIcon(change.currentValue.icon);
}
}
}
}
}

View File

@ -33,6 +33,8 @@ import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { ContentObserver } from '@angular/cdk/observers';
import { isTbImage } from '@shared/models/resource.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
const _TbIconBase = mixinColor(
class {
@ -70,7 +72,7 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
host: {
role: 'img',
class: 'mat-icon notranslate',
'[attr.data-mat-icon-type]': '!_useSvgIcon ? "font" : "svg"',
'[attr.data-mat-icon-type]': '_useSvgIcon ? "svg" : (_useImageIcon ? null : "font")',
'[attr.data-mat-icon-name]': '_svgName',
'[attr.data-mat-icon-namespace]': '_svgNamespace',
'[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"',
@ -99,6 +101,9 @@ export class TbIconComponent extends _TbIconBase
private _textElement = null;
_useImageIcon = false;
private _imageElement = null;
private _previousPath?: string;
private _elementsWithExternalReferences?: Map<Element, {name: string; value: string}[]>;
@ -109,6 +114,7 @@ export class TbIconComponent extends _TbIconBase
private contentObserver: ContentObserver,
private renderer: Renderer2,
private _iconRegistry: MatIconRegistry,
private imagePipe: ImagePipe,
@Inject(MAT_ICON_LOCATION) private _location: MatIconLocation,
private readonly _errorHandler: ErrorHandler) {
super(elementRef);
@ -148,16 +154,29 @@ export class TbIconComponent extends _TbIconBase
private _updateIcon() {
const useSvgIcon = isSvgIcon(this.icon);
const useImageIcon = isTbImage(this.icon);
if (this._useSvgIcon !== useSvgIcon) {
this._useSvgIcon = useSvgIcon;
if (!this._useSvgIcon) {
this._updateSvgIcon(undefined);
} else {
this._updateFontIcon(undefined);
this._updateImageIcon(undefined);
}
}
if (this._useImageIcon !== useImageIcon) {
this._useImageIcon = useImageIcon;
if (!this._useImageIcon) {
this._updateImageIcon(undefined);
} else {
this._updateFontIcon(undefined);
this._updateSvgIcon(undefined);
}
}
if (this._useSvgIcon) {
this._updateSvgIcon(this.icon);
} else if (this._useImageIcon) {
this._updateImageIcon(this.icon);
} else {
this._updateFontIcon(this.icon);
}
@ -278,4 +297,28 @@ export class TbIconComponent extends _TbIconBase
}
}
private _updateImageIcon(rawName: string | undefined) {
if (rawName) {
this._clearImageIcon();
this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe(
imageUrl => {
const imgElement = this.renderer.createElement('img');
this.renderer.setAttribute(imgElement, 'src', imageUrl as string);
const elem: HTMLElement = this._elementRef.nativeElement;
this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement);
this._imageElement = imgElement;
}
);
} else {
this._clearImageIcon();
}
}
private _clearImageIcon() {
const elem: HTMLElement = this._elementRef.nativeElement;
if (this._imageElement !== null) {
this.renderer.removeChild(elem, this._imageElement);
this._imageElement = null;
}
}
}

View File

@ -36,7 +36,7 @@ import { TbPopoverService } from '@shared/components/popover.service';
import { MaterialIconsComponent } from '@shared/components/material-icons.component';
import { MatButton } from '@angular/material/button';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { tbImageIcon } from '@shared/models/custom-menu.models';
import { isTbImage } from '@shared/models/resource.models';
@Component({
selector: 'tb-material-icon-select',
@ -201,7 +201,7 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
private defineIconType(icon: string) {
if (this.allowedCustomIcon) {
this.isCustomIcon = tbImageIcon(icon);
this.isCustomIcon = isTbImage(icon);
}
}
}

View File

@ -16,15 +16,16 @@
-->
<div class="tb-material-icons-panel">
<div *ngIf="showTitle" class="tb-material-icons-title" translate>icon.icons</div>
@if (allowedCustomIcon) {
<div class="flex w-full flex-row items-center justify-end">
<div class="flex w-full">
<span *ngIf="showTitle" class="tb-material-icons-title flex-1" translate>icon.icons</span>
@if (allowedCustomIcon) {
<tb-toggle-select [(ngModel)]="isCustomIcon" [ngModelOptions]="{standalone: true}" (ngModelChange)="selectedIcon = null">
<tb-toggle-option [value]="false">{{ 'resource.system' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'icon.custom' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
}
<span class="flex-1"></span>
}
</div>
@if (!isCustomIcon) {
<mat-form-field class="tb-material-icons-search tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<mat-icon matPrefix>search</mat-icon>

View File

@ -37,7 +37,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion';
import { tbImageIcon } from '@shared/models/custom-menu.models';
import { isTbImage } from '@shared/models/resource.models';
@Component({
selector: 'tb-material-icons',
@ -129,7 +129,7 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
map((data) => data.iconRows),
share()
);
this.isCustomIcon = tbImageIcon(this.selectedIcon)
this.isCustomIcon = isTbImage(this.selectedIcon)
}
clearSearch() {

View File

@ -188,6 +188,7 @@ export const isImageResourceUrl = (url: string): boolean => url && IMAGES_URL_RE
export const isJSResourceUrl = (url: string): boolean => url && RESOURCES_URL_REGEXP.test(url);
export const isJSResource = (url: string): boolean => url?.startsWith(TB_RESOURCE_PREFIX);
export const isTbImage = (url: string): boolean => url?.startsWith(TB_IMAGE_PREFIX);
export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => {
const res = url.match(IMAGES_URL_REGEXP);

View File

@ -896,7 +896,7 @@ pre.tb-highlight {
}
.mat-icon {
svg {
svg, img {
vertical-align: inherit;
}
&.tb-mat-12 {