Merge pull request #13972 from deaflynx/feature/custom-icons
Feature custom icon image
This commit is contained in:
commit
5d024e1277
@ -71,6 +71,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<tb-material-icon-select asBoxInput
|
<tb-material-icon-select asBoxInput
|
||||||
iconClearButton
|
iconClearButton
|
||||||
|
allowedCustomIcon
|
||||||
[color]="widgetSettings.get('iconColor').value"
|
[color]="widgetSettings.get('iconColor').value"
|
||||||
formControlName="titleIcon">
|
formControlName="titleIcon">
|
||||||
</tb-material-icon-select>
|
</tb-material-icon-select>
|
||||||
|
|||||||
@ -33,6 +33,9 @@ import { Subscription } from 'rxjs';
|
|||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
|
import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
|
||||||
import { ContentObserver } from '@angular/cdk/observers';
|
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(
|
const _TbIconBase = mixinColor(
|
||||||
class {
|
class {
|
||||||
@ -70,7 +73,7 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
|
|||||||
host: {
|
host: {
|
||||||
role: 'img',
|
role: 'img',
|
||||||
class: 'mat-icon notranslate',
|
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-name]': '_svgName',
|
||||||
'[attr.data-mat-icon-namespace]': '_svgNamespace',
|
'[attr.data-mat-icon-namespace]': '_svgNamespace',
|
||||||
'[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"',
|
'[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"',
|
||||||
@ -99,6 +102,9 @@ export class TbIconComponent extends _TbIconBase
|
|||||||
|
|
||||||
private _textElement = null;
|
private _textElement = null;
|
||||||
|
|
||||||
|
_useImageIcon = false;
|
||||||
|
private _imageElement = null;
|
||||||
|
|
||||||
private _previousPath?: string;
|
private _previousPath?: string;
|
||||||
|
|
||||||
private _elementsWithExternalReferences?: Map<Element, {name: string; value: string}[]>;
|
private _elementsWithExternalReferences?: Map<Element, {name: string; value: string}[]>;
|
||||||
@ -109,6 +115,8 @@ export class TbIconComponent extends _TbIconBase
|
|||||||
private contentObserver: ContentObserver,
|
private contentObserver: ContentObserver,
|
||||||
private renderer: Renderer2,
|
private renderer: Renderer2,
|
||||||
private _iconRegistry: MatIconRegistry,
|
private _iconRegistry: MatIconRegistry,
|
||||||
|
private imagePipe: ImagePipe,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
@Inject(MAT_ICON_LOCATION) private _location: MatIconLocation,
|
@Inject(MAT_ICON_LOCATION) private _location: MatIconLocation,
|
||||||
private readonly _errorHandler: ErrorHandler) {
|
private readonly _errorHandler: ErrorHandler) {
|
||||||
super(elementRef);
|
super(elementRef);
|
||||||
@ -148,16 +156,29 @@ export class TbIconComponent extends _TbIconBase
|
|||||||
|
|
||||||
private _updateIcon() {
|
private _updateIcon() {
|
||||||
const useSvgIcon = isSvgIcon(this.icon);
|
const useSvgIcon = isSvgIcon(this.icon);
|
||||||
|
const useImageIcon = isTbImage(this.icon);
|
||||||
if (this._useSvgIcon !== useSvgIcon) {
|
if (this._useSvgIcon !== useSvgIcon) {
|
||||||
this._useSvgIcon = useSvgIcon;
|
this._useSvgIcon = useSvgIcon;
|
||||||
if (!this._useSvgIcon) {
|
if (!this._useSvgIcon) {
|
||||||
this._updateSvgIcon(undefined);
|
this._updateSvgIcon(undefined);
|
||||||
} else {
|
} else {
|
||||||
this._updateFontIcon(undefined);
|
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) {
|
if (this._useSvgIcon) {
|
||||||
this._updateSvgIcon(this.icon);
|
this._updateSvgIcon(this.icon);
|
||||||
|
} else if (this._useImageIcon) {
|
||||||
|
this._updateImageIcon(this.icon);
|
||||||
} else {
|
} else {
|
||||||
this._updateFontIcon(this.icon);
|
this._updateFontIcon(this.icon);
|
||||||
}
|
}
|
||||||
@ -278,4 +299,49 @@ 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 urlStr = imageUrl as string;
|
||||||
|
const isSvg = urlStr?.startsWith('data:image/svg+xml') || urlStr?.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: () => this._setImageElement(urlStr)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._setImageElement(urlStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this._clearImageIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
this.renderer.insertBefore(this._elementRef.nativeElement, imgElement, this._iconNameContent.nativeElement);
|
||||||
|
this._imageElement = imgElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearImageIcon() {
|
||||||
|
const elem: HTMLElement = this._elementRef.nativeElement;
|
||||||
|
if (this._imageElement !== null) {
|
||||||
|
this.renderer.removeChild(elem, this._imageElement);
|
||||||
|
this._imageElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,6 +71,10 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
|
|||||||
@coerceBoolean()
|
@coerceBoolean()
|
||||||
iconClearButton = false;
|
iconClearButton = false;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
@coerceBoolean()
|
||||||
|
allowedCustomIcon = false;
|
||||||
|
|
||||||
private requiredValue: boolean;
|
private requiredValue: boolean;
|
||||||
get required(): boolean {
|
get required(): boolean {
|
||||||
return this.requiredValue;
|
return this.requiredValue;
|
||||||
@ -169,7 +173,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
|
|||||||
this.viewContainerRef, MaterialIconsComponent, 'left', true, null,
|
this.viewContainerRef, MaterialIconsComponent, 'left', true, null,
|
||||||
{
|
{
|
||||||
selectedIcon: this.materialIconFormGroup.get('icon').value,
|
selectedIcon: this.materialIconFormGroup.get('icon').value,
|
||||||
iconClearButton: this.iconClearButton
|
iconClearButton: this.iconClearButton,
|
||||||
|
allowedCustomIcon: this.allowedCustomIcon,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{}, {}, true);
|
{}, {}, true);
|
||||||
|
|||||||
@ -16,63 +16,86 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
<div class="tb-material-icons-panel">
|
<div class="tb-material-icons-panel">
|
||||||
<div *ngIf="showTitle" class="tb-material-icons-title" translate>icon.icons</div>
|
<div class="flex w-full">
|
||||||
<mat-form-field class="tb-material-icons-search tb-inline-field" appearance="outline" subscriptSizing="dynamic">
|
<span *ngIf="showTitle" class="tb-material-icons-title flex-1" translate>icon.icons</span>
|
||||||
<mat-icon matPrefix>search</mat-icon>
|
@if (allowedCustomIcon) {
|
||||||
<input matInput [formControl]="searchIconControl" placeholder="{{ 'icon.search-icon' | translate }}"/>
|
<tb-toggle-select [(ngModel)]="isCustomIcon" [ngModelOptions]="{standalone: true}" (ngModelChange)="selectedIcon = null">
|
||||||
<button *ngIf="searchIconControl.value"
|
<tb-toggle-option [value]="false">{{ 'resource.system' | translate }}</tb-toggle-option>
|
||||||
type="button"
|
<tb-toggle-option [value]="true">{{ 'icon.custom' | translate }}</tb-toggle-option>
|
||||||
matSuffix mat-icon-button aria-label="Clear"
|
</tb-toggle-select>
|
||||||
(click)="clearSearch()">
|
<span class="flex-1"></span>
|
||||||
<mat-icon class="material-icons">close</mat-icon>
|
}
|
||||||
</button>
|
|
||||||
</mat-form-field>
|
|
||||||
<cdk-virtual-scroll-viewport [class.!hidden]="notFound" #iconsPanel
|
|
||||||
[itemSize]="iconsRowHeight" class="tb-material-icons-viewport"
|
|
||||||
[style.width]="iconsPanelWidth"
|
|
||||||
[style.height]="iconsPanelHeight">
|
|
||||||
<div *cdkVirtualFor="let iconRow of iconRows$ | async" class="tb-material-icons-row">
|
|
||||||
<ng-container *ngFor="let icon of iconRow">
|
|
||||||
<button *ngIf="icon.name === selectedIcon"
|
|
||||||
class="tb-select-icon-button"
|
|
||||||
mat-raised-button
|
|
||||||
color="primary"
|
|
||||||
(click)="selectIcon(icon)"
|
|
||||||
matTooltip="{{ icon.displayName }}"
|
|
||||||
matTooltipPosition="above"
|
|
||||||
type="button">
|
|
||||||
<tb-icon matButtonIcon>{{icon.name}}</tb-icon>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="icon.name !== selectedIcon"
|
|
||||||
class="tb-select-icon-button"
|
|
||||||
mat-button
|
|
||||||
(click)="selectIcon(icon)"
|
|
||||||
matTooltip="{{ icon.displayName }}"
|
|
||||||
matTooltipPosition="above"
|
|
||||||
type="button">
|
|
||||||
<tb-icon matButtonIcon>{{icon.name}}</tb-icon>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</cdk-virtual-scroll-viewport>
|
|
||||||
<ng-container *ngIf="notFound">
|
|
||||||
<div class="tb-no-data-available" [style.width]="iconsPanelWidth">
|
|
||||||
<div class="tb-no-data-bg"></div>
|
|
||||||
<div class="tb-no-data-text">{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<div class="tb-material-icons-panel-buttons" *ngIf="iconClearButton || !showAllSubject.value">
|
|
||||||
<button *ngIf="iconClearButton"
|
|
||||||
mat-button
|
|
||||||
color="primary"
|
|
||||||
type="button"
|
|
||||||
(click)="clearIcon()"
|
|
||||||
[disabled]="!selectedIcon">
|
|
||||||
{{ 'action.clear' | translate }}
|
|
||||||
</button>
|
|
||||||
<span class="flex-1"></span>
|
|
||||||
<button *ngIf="!showAllSubject.value" class="tb-material-icons-show-more" mat-button color="primary" (click)="showAllSubject.next(true)">
|
|
||||||
{{ 'action.show-more' | translate }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
@if (!isCustomIcon) {
|
||||||
|
<mat-form-field class="tb-material-icons-search tb-inline-field" appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-icon matPrefix>search</mat-icon>
|
||||||
|
<input matInput [formControl]="searchIconControl" placeholder="{{ 'icon.search-icon' | translate }}"/>
|
||||||
|
<button *ngIf="searchIconControl.value"
|
||||||
|
type="button"
|
||||||
|
matSuffix mat-icon-button aria-label="Clear"
|
||||||
|
(click)="clearSearch()">
|
||||||
|
<mat-icon class="material-icons">close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
<cdk-virtual-scroll-viewport [class.!hidden]="notFound" #iconsPanel
|
||||||
|
[itemSize]="iconsRowHeight" class="tb-material-icons-viewport"
|
||||||
|
[style.width]="iconsPanelWidth"
|
||||||
|
[style.height]="iconsPanelHeight">
|
||||||
|
<div *cdkVirtualFor="let iconRow of iconRows$ | async" class="tb-material-icons-row">
|
||||||
|
<ng-container *ngFor="let icon of iconRow">
|
||||||
|
<button *ngIf="icon.name === selectedIcon"
|
||||||
|
class="tb-select-icon-button"
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
(click)="selectIcon(icon.name)"
|
||||||
|
matTooltip="{{ icon.displayName }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
type="button">
|
||||||
|
<tb-icon matButtonIcon>{{icon.name}}</tb-icon>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="icon.name !== selectedIcon"
|
||||||
|
class="tb-select-icon-button"
|
||||||
|
mat-button
|
||||||
|
(click)="selectIcon(icon.name)"
|
||||||
|
matTooltip="{{ icon.displayName }}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
type="button">
|
||||||
|
<tb-icon matButtonIcon>{{icon.name}}</tb-icon>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
<ng-container *ngIf="notFound">
|
||||||
|
<div class="tb-no-data-available" [style.width]="iconsPanelWidth">
|
||||||
|
<div class="tb-no-data-bg"></div>
|
||||||
|
<div class="tb-no-data-text">{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<div class="tb-material-icons-panel-buttons" *ngIf="iconClearButton || !showAllSubject.value">
|
||||||
|
<button *ngIf="iconClearButton"
|
||||||
|
mat-button
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
(click)="clearIcon()"
|
||||||
|
[disabled]="!selectedIcon">
|
||||||
|
{{ 'action.clear' | translate }}
|
||||||
|
</button>
|
||||||
|
<span class="flex-1"></span>
|
||||||
|
<button *ngIf="!showAllSubject.value" class="tb-material-icons-show-more" mat-button color="primary" (click)="showAllSubject.next(true)">
|
||||||
|
{{ 'action.show-more' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<tb-gallery-image-input [style.width]="iconsPanelWidth"
|
||||||
|
[(ngModel)]="selectedIcon">
|
||||||
|
</tb-gallery-image-input>
|
||||||
|
<div class="tb-material-icons-panel-buttons">
|
||||||
|
<span class="flex-1"></span>
|
||||||
|
<button class="tb-material-icons-show-more" mat-button color="primary"
|
||||||
|
[disabled]="!selectedIcon"
|
||||||
|
(click)="selectIcon(selectedIcon)">
|
||||||
|
{{ 'action.set' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
|
|||||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||||
import { MediaBreakpoints } from '@shared/models/constants';
|
import { MediaBreakpoints } from '@shared/models/constants';
|
||||||
import { coerceBoolean } from '@shared/decorators/coercion';
|
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||||
|
import { isTbImage } from '@shared/models/resource.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-material-icons',
|
selector: 'tb-material-icons',
|
||||||
@ -61,6 +62,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
|
|||||||
@coerceBoolean()
|
@coerceBoolean()
|
||||||
showTitle = true;
|
showTitle = true;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
@coerceBoolean()
|
||||||
|
allowedCustomIcon = false;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
popover: TbPopoverComponent;
|
popover: TbPopoverComponent;
|
||||||
|
|
||||||
@ -71,6 +76,8 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
|
|||||||
showAllSubject = new BehaviorSubject<boolean>(false);
|
showAllSubject = new BehaviorSubject<boolean>(false);
|
||||||
searchIconControl: UntypedFormControl;
|
searchIconControl: UntypedFormControl;
|
||||||
|
|
||||||
|
isCustomIcon = false;
|
||||||
|
|
||||||
iconsRowHeight = 48;
|
iconsRowHeight = 48;
|
||||||
|
|
||||||
iconsPanelHeight: string;
|
iconsPanelHeight: string;
|
||||||
@ -122,14 +129,15 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
|
|||||||
map((data) => data.iconRows),
|
map((data) => data.iconRows),
|
||||||
share()
|
share()
|
||||||
);
|
);
|
||||||
|
this.isCustomIcon = isTbImage(this.selectedIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
this.searchIconControl.patchValue('', {emitEvent: true});
|
this.searchIconControl.patchValue('', {emitEvent: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
selectIcon(icon: MaterialIcon) {
|
selectIcon(icon: string) {
|
||||||
this.iconSelected.emit(icon.name);
|
this.iconSelected.emit(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearIcon() {
|
clearIcon() {
|
||||||
|
|||||||
@ -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 isJSResourceUrl = (url: string): boolean => url && RESOURCES_URL_REGEXP.test(url);
|
||||||
export const isJSResource = (url: string): boolean => url?.startsWith(TB_RESOURCE_PREFIX);
|
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} => {
|
export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => {
|
||||||
const res = url.match(IMAGES_URL_REGEXP);
|
const res = url.match(IMAGES_URL_REGEXP);
|
||||||
|
|||||||
@ -9553,6 +9553,7 @@
|
|||||||
"icon": {
|
"icon": {
|
||||||
"icon": "Icon",
|
"icon": "Icon",
|
||||||
"icons": "Icons",
|
"icons": "Icons",
|
||||||
|
"custom": "Custom",
|
||||||
"select-icon": "Select icon",
|
"select-icon": "Select icon",
|
||||||
"material-icons": "Material icons",
|
"material-icons": "Material icons",
|
||||||
"show-all": "Show all icons",
|
"show-all": "Show all icons",
|
||||||
|
|||||||
@ -26,6 +26,10 @@
|
|||||||
width: #{$size}px;
|
width: #{$size}px;
|
||||||
height: #{$size}px;
|
height: #{$size}px;
|
||||||
}
|
}
|
||||||
|
img {
|
||||||
|
width: #{$size}px;
|
||||||
|
height: #{$size}px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin tb-mat-icon-button-size($size) {
|
@mixin tb-mat-icon-button-size($size) {
|
||||||
|
|||||||
@ -896,7 +896,7 @@ pre.tb-highlight {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-icon {
|
.mat-icon {
|
||||||
svg {
|
svg, img {
|
||||||
vertical-align: inherit;
|
vertical-align: inherit;
|
||||||
}
|
}
|
||||||
&.tb-mat-12 {
|
&.tb-mat-12 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user