From a3c28e9db4527f0374ad170e19b718e144d686b4 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 5 Sep 2025 12:51:31 +0300 Subject: [PATCH 1/8] Feature custom icon in Custom icon init implementation. --- .../modules/home/menu/menu-link.component.ts | 18 ++- .../material-icon-select.component.html | 6 +- .../material-icon-select.component.ts | 18 ++- .../components/material-icons.component.html | 134 ++++++++++-------- .../components/material-icons.component.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 1 + 6 files changed, 127 insertions(+), 62 deletions(-) diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts index da8a583183..da7a21632b 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -14,8 +14,9 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { MenuSection } from '@core/services/menu.models'; +import { tbImageIcon } from '@shared/models/custom-menu.models'; @Component({ selector: 'tb-menu-link', @@ -23,14 +24,27 @@ import { MenuSection } from '@core/services/menu.models'; styleUrls: ['./menu-link.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class MenuLinkComponent implements OnInit { +export class MenuLinkComponent implements OnInit, OnChanges { @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); + } + } + } + } + } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html index 25aa51d30d..13bf46d1e0 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -37,6 +37,10 @@ [disabled]="disabled" #matButton (click)="openIconPopup($event, matButton)"> - {{materialIconFormGroup.get('icon').value}} + @if (!isCustomIcon) { + {{materialIconFormGroup.get('icon').value}} + } @else { + icon + } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 9432b3c96e..97c5519ed3 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -36,6 +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'; @Component({ selector: 'tb-material-icon-select', @@ -71,6 +72,12 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @coerceBoolean() iconClearButton = false; + @Input() + @coerceBoolean() + allowedCustomIcon = false; + + isCustomIcon = false; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -131,11 +138,13 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.patchValue( { icon: this.modelValue }, {emitEvent: false} ); + this.defineIconType(value); } private updateModel() { const icon: string = this.materialIconFormGroup.get('icon').value; if (this.modelValue !== icon) { + this.defineIconType(icon); this.modelValue = icon; this.propagateChange(this.modelValue); } @@ -169,7 +178,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.viewContainerRef, MaterialIconsComponent, 'left', true, null, { selectedIcon: this.materialIconFormGroup.get('icon').value, - iconClearButton: this.iconClearButton + iconClearButton: this.iconClearButton, + allowedCustomIcon: this.allowedCustomIcon, }, {}, {}, {}, true); @@ -188,4 +198,10 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); this.cd.markForCheck(); } + + private defineIconType(icon: string) { + if (this.allowedCustomIcon) { + this.isCustomIcon = tbImageIcon(icon); + } + } } diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index d4d9fe93a6..dcd285a99d 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -17,62 +17,84 @@ -->
icon.icons
- - search - - - - -
- - - - + @if (allowedCustomIcon) { +
+ + {{ 'resource.system' | translate }} + {{ 'icon.custom' | translate }} +
- - -
-
-
{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
+ } + @if (!isCustomIcon) { + + search + + + + +
+ + + + +
+
+ +
+
+
{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
+
+
+
+ + +
- -
- - - -
+ } @else { + + +
+ + +
+ }
diff --git a/ui-ngx/src/app/shared/components/material-icons.component.ts b/ui-ngx/src/app/shared/components/material-icons.component.ts index ece1a99108..6fe4c995e5 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.ts +++ b/ui-ngx/src/app/shared/components/material-icons.component.ts @@ -37,6 +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'; @Component({ selector: 'tb-material-icons', @@ -61,6 +62,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { @coerceBoolean() showTitle = true; + @Input() + @coerceBoolean() + allowedCustomIcon = false; + @Input() popover: TbPopoverComponent; @@ -71,6 +76,8 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { showAllSubject = new BehaviorSubject(false); searchIconControl: UntypedFormControl; + isCustomIcon = false; + iconsRowHeight = 48; iconsPanelHeight: string; @@ -122,14 +129,15 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { map((data) => data.iconRows), share() ); + this.isCustomIcon = tbImageIcon(this.selectedIcon) } clearSearch() { this.searchIconControl.patchValue('', {emitEvent: true}); } - selectIcon(icon: MaterialIcon) { - this.iconSelected.emit(icon.name); + selectIcon(icon: string) { + this.iconSelected.emit(icon); } clearIcon() { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index fd368e2853..bb17471cbc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -9507,6 +9507,7 @@ "icon": { "icon": "Icon", "icons": "Icons", + "custom": "Custom", "select-icon": "Select icon", "material-icons": "Material icons", "show-all": "Show all icons", From 20e44ab004f728282ef68bcfe0f67570b3a6232c Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 8 Aug 2025 16:52:48 +0300 Subject: [PATCH 2/8] Update tb-icon to accept custom image. --- .../modules/home/menu/menu-link.component.ts | 18 +------- .../app/shared/components/icon.component.ts | 45 ++++++++++++++++++- .../material-icon-select.component.ts | 4 +- .../components/material-icons.component.html | 11 ++--- .../components/material-icons.component.ts | 4 +- .../src/app/shared/models/resource.models.ts | 1 + ui-ngx/src/styles.scss | 2 +- 7 files changed, 58 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts index da7a21632b..da8a583183 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -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); - } - } - } - } - } diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 2b6170b71c..36c4e2e0e8 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -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; @@ -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; + } + } } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 97c5519ed3..c2e904d8d0 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -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); } } } diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index dcd285a99d..558ffd5e0a 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -16,15 +16,16 @@ -->
-
icon.icons
- @if (allowedCustomIcon) { -
+
+ icon.icons + @if (allowedCustomIcon) { {{ 'resource.system' | translate }} {{ 'icon.custom' | translate }} -
- } + + } +
@if (!isCustomIcon) { search diff --git a/ui-ngx/src/app/shared/components/material-icons.component.ts b/ui-ngx/src/app/shared/components/material-icons.component.ts index 6fe4c995e5..d144fd45e6 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.ts +++ b/ui-ngx/src/app/shared/components/material-icons.component.ts @@ -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() { diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 0419e40a7c..c1e692e04f 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -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); diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index a8cf53534e..6d49e9f698 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -896,7 +896,7 @@ pre.tb-highlight { } .mat-icon { - svg { + svg, img { vertical-align: inherit; } &.tb-mat-12 { From f2e66ca01280bcf5998a10b8e46adc57ea21a6cc Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 8 Aug 2025 17:06:02 +0300 Subject: [PATCH 3/8] Feature custom image icon fixes after review. --- ui-ngx/src/app/shared/components/icon.component.ts | 1 + .../components/material-icon-select.component.html | 6 +----- .../components/material-icon-select.component.ts | 11 ----------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 36c4e2e0e8..dfc7944716 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -303,6 +303,7 @@ export class TbIconComponent extends _TbIconBase this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( imageUrl => { const imgElement = this.renderer.createElement('img'); + this.renderer.addClass(imgElement, 'mat-icon'); this.renderer.setAttribute(imgElement, 'src', imageUrl as string); const elem: HTMLElement = this._elementRef.nativeElement; this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html index 13bf46d1e0..25aa51d30d 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -37,10 +37,6 @@ [disabled]="disabled" #matButton (click)="openIconPopup($event, matButton)"> - @if (!isCustomIcon) { - {{materialIconFormGroup.get('icon').value}} - } @else { - icon - } + {{materialIconFormGroup.get('icon').value}} diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index c2e904d8d0..2bbc5b7528 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -36,7 +36,6 @@ 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 { isTbImage } from '@shared/models/resource.models'; @Component({ selector: 'tb-material-icon-select', @@ -76,8 +75,6 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @coerceBoolean() allowedCustomIcon = false; - isCustomIcon = false; - private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -138,13 +135,11 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.patchValue( { icon: this.modelValue }, {emitEvent: false} ); - this.defineIconType(value); } private updateModel() { const icon: string = this.materialIconFormGroup.get('icon').value; if (this.modelValue !== icon) { - this.defineIconType(icon); this.modelValue = icon; this.propagateChange(this.modelValue); } @@ -198,10 +193,4 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); this.cd.markForCheck(); } - - private defineIconType(icon: string) { - if (this.allowedCustomIcon) { - this.isCustomIcon = isTbImage(icon); - } - } } From a4cf5f7d32d9084fb4db9c912972a62a36b5f4c5 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 8 Aug 2025 21:54:20 +0300 Subject: [PATCH 4/8] Minor fix in tb-icon custom image feature - add alt attribute. --- ui-ngx/src/app/shared/components/icon.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index dfc7944716..0aba7bed93 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -304,6 +304,7 @@ export class TbIconComponent extends _TbIconBase imageUrl => { const imgElement = this.renderer.createElement('img'); this.renderer.addClass(imgElement, 'mat-icon'); + this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); this.renderer.setAttribute(imgElement, 'src', imageUrl as string); const elem: HTMLElement = this._elementRef.nativeElement; this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); From b976e052569c8179939409854428b19b5031a0a4 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 19 Aug 2025 12:37:24 +0300 Subject: [PATCH 5/8] Support custom icons in custom menu popover components Edit menu item; Add custom menu item. Update mixins to handle sizing for image icons. --- ui-ngx/src/scss/mixins.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-ngx/src/scss/mixins.scss b/ui-ngx/src/scss/mixins.scss index 8e60483cb1..fb4a51ebce 100644 --- a/ui-ngx/src/scss/mixins.scss +++ b/ui-ngx/src/scss/mixins.scss @@ -26,6 +26,10 @@ width: #{$size}px; height: #{$size}px; } + img { + width: #{$size}px; + height: #{$size}px; + } } @mixin tb-mat-icon-button-size($size) { From 34a50f800918303444516923f9e00fd8124f81fd Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 3 Sep 2025 16:21:32 +0300 Subject: [PATCH 6/8] Custom icon feature svg support. --- .../app/shared/components/icon.component.ts | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 0aba7bed93..4fe6bb26aa 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -35,6 +35,7 @@ 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'; +import { DomSanitizer } from '@angular/platform-browser'; const _TbIconBase = mixinColor( class { @@ -115,6 +116,7 @@ export class TbIconComponent extends _TbIconBase private renderer: Renderer2, private _iconRegistry: MatIconRegistry, private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, @Inject(MAT_ICON_LOCATION) private _location: MatIconLocation, private readonly _errorHandler: ErrorHandler) { super(elementRef); @@ -302,13 +304,26 @@ export class TbIconComponent extends _TbIconBase this._clearImageIcon(); this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( imageUrl => { - const imgElement = this.renderer.createElement('img'); - this.renderer.addClass(imgElement, 'mat-icon'); - this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); - 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; + const urlStr = imageUrl as string; + const isSvg = rawName?.endsWith('.svg'); + if (isSvg) { + const safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urlStr); + this._iconRegistry + .getSvgIconFromUrl(safeUrl) + .pipe(take(1)) + .subscribe({ + next: (svg) => { + this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement); + this._imageElement = svg; + }, + error: (err: Error) => { + console.log('err', err) + this._setImageElement(urlStr); + } + }); + } else { + this._setImageElement(urlStr); + } } ); } else { @@ -316,6 +331,16 @@ export class TbIconComponent extends _TbIconBase } } + private _setImageElement(urlStr: string) { + const imgElement = this.renderer.createElement('img'); + this.renderer.addClass(imgElement, 'mat-icon'); + this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); + this.renderer.setAttribute(imgElement, 'src', urlStr); + const elem: HTMLElement = this._elementRef.nativeElement; + this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); + this._imageElement = imgElement; + } + private _clearImageIcon() { const elem: HTMLElement = this._elementRef.nativeElement; if (this._imageElement !== null) { From 3c2e289cb3f01c3830d6daa07efb3f82c47a7af3 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 4 Sep 2025 17:03:10 +0300 Subject: [PATCH 7/8] Refactor feature custom image icon. --- ui-ngx/src/app/shared/components/icon.component.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 4fe6bb26aa..20ee9c4a72 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -305,7 +305,7 @@ export class TbIconComponent extends _TbIconBase this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( imageUrl => { const urlStr = imageUrl as string; - const isSvg = rawName?.endsWith('.svg'); + const isSvg = urlStr?.startsWith('data:image/svg+xml') || urlStr?.endsWith('.svg'); if (isSvg) { const safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urlStr); this._iconRegistry @@ -316,10 +316,7 @@ export class TbIconComponent extends _TbIconBase this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement); this._imageElement = svg; }, - error: (err: Error) => { - console.log('err', err) - this._setImageElement(urlStr); - } + error: () => this._setImageElement(urlStr) }); } else { this._setImageElement(urlStr); @@ -336,8 +333,7 @@ export class TbIconComponent extends _TbIconBase this.renderer.addClass(imgElement, 'mat-icon'); this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); this.renderer.setAttribute(imgElement, 'src', urlStr); - const elem: HTMLElement = this._elementRef.nativeElement; - this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); + this.renderer.insertBefore(this._elementRef.nativeElement, imgElement, this._iconNameContent.nativeElement); this._imageElement = imgElement; } From 8af4c70e1389438faae2e1424445c1ca448afc6b Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 5 Sep 2025 11:54:42 +0300 Subject: [PATCH 8/8] Support custom icons in widget config. --- .../modules/home/components/widget/widget-config.component.html | 1 + ui-ngx/src/app/shared/components/material-icons.component.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 8d46ede134..35c6fde3f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -71,6 +71,7 @@ diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index 558ffd5e0a..b6880e4ffe 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -86,7 +86,7 @@
} @else { -