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>
 | 
			
		||||
            <tb-material-icon-select asBoxInput
 | 
			
		||||
                                     iconClearButton
 | 
			
		||||
                                     allowedCustomIcon
 | 
			
		||||
                                     [color]="widgetSettings.get('iconColor').value"
 | 
			
		||||
              formControlName="titleIcon">
 | 
			
		||||
            </tb-material-icon-select>
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,9 @@ 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';
 | 
			
		||||
import { DomSanitizer } from '@angular/platform-browser';
 | 
			
		||||
 | 
			
		||||
const _TbIconBase = mixinColor(
 | 
			
		||||
  class {
 | 
			
		||||
@ -70,7 +73,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 +102,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 +115,8 @@ export class TbIconComponent extends _TbIconBase
 | 
			
		||||
              private contentObserver: ContentObserver,
 | 
			
		||||
              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);
 | 
			
		||||
@ -148,16 +156,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 +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()
 | 
			
		||||
  iconClearButton = false;
 | 
			
		||||
 | 
			
		||||
  @Input()
 | 
			
		||||
  @coerceBoolean()
 | 
			
		||||
  allowedCustomIcon = false;
 | 
			
		||||
 | 
			
		||||
  private requiredValue: boolean;
 | 
			
		||||
  get required(): boolean {
 | 
			
		||||
    return this.requiredValue;
 | 
			
		||||
@ -169,7 +173,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);
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,17 @@
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<div class="tb-material-icons-panel">
 | 
			
		||||
  <div *ngIf="showTitle" class="tb-material-icons-title" translate>icon.icons</div>
 | 
			
		||||
  <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>
 | 
			
		||||
      <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>
 | 
			
		||||
      <input matInput [formControl]="searchIconControl" placeholder="{{ 'icon.search-icon' | translate }}"/>
 | 
			
		||||
@ -37,7 +47,7 @@
 | 
			
		||||
                  class="tb-select-icon-button"
 | 
			
		||||
                  mat-raised-button
 | 
			
		||||
                  color="primary"
 | 
			
		||||
                (click)="selectIcon(icon)"
 | 
			
		||||
                  (click)="selectIcon(icon.name)"
 | 
			
		||||
                  matTooltip="{{ icon.displayName }}"
 | 
			
		||||
                  matTooltipPosition="above"
 | 
			
		||||
                  type="button">
 | 
			
		||||
@ -46,7 +56,7 @@
 | 
			
		||||
          <button *ngIf="icon.name !== selectedIcon"
 | 
			
		||||
                  class="tb-select-icon-button"
 | 
			
		||||
                  mat-button
 | 
			
		||||
                (click)="selectIcon(icon)"
 | 
			
		||||
                  (click)="selectIcon(icon.name)"
 | 
			
		||||
                  matTooltip="{{ icon.displayName }}"
 | 
			
		||||
                  matTooltipPosition="above"
 | 
			
		||||
                  type="button">
 | 
			
		||||
@ -75,4 +85,17 @@
 | 
			
		||||
        {{ '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>
 | 
			
		||||
 | 
			
		||||
@ -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 { isTbImage } from '@shared/models/resource.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<boolean>(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 = isTbImage(this.selectedIcon)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clearSearch() {
 | 
			
		||||
    this.searchIconControl.patchValue('', {emitEvent: true});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectIcon(icon: MaterialIcon) {
 | 
			
		||||
    this.iconSelected.emit(icon.name);
 | 
			
		||||
  selectIcon(icon: string) {
 | 
			
		||||
    this.iconSelected.emit(icon);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 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);
 | 
			
		||||
 | 
			
		||||
@ -9553,6 +9553,7 @@
 | 
			
		||||
    "icon": {
 | 
			
		||||
        "icon": "Icon",
 | 
			
		||||
        "icons": "Icons",
 | 
			
		||||
        "custom": "Custom",
 | 
			
		||||
        "select-icon": "Select icon",
 | 
			
		||||
        "material-icons": "Material icons",
 | 
			
		||||
        "show-all": "Show all icons",
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,10 @@
 | 
			
		||||
    width: #{$size}px;
 | 
			
		||||
    height: #{$size}px;
 | 
			
		||||
  }
 | 
			
		||||
  img {
 | 
			
		||||
    width: #{$size}px;
 | 
			
		||||
    height: #{$size}px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin tb-mat-icon-button-size($size) {
 | 
			
		||||
 | 
			
		||||
@ -896,7 +896,7 @@ pre.tb-highlight {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .mat-icon {
 | 
			
		||||
    svg {
 | 
			
		||||
    svg, img {
 | 
			
		||||
      vertical-align: inherit;
 | 
			
		||||
    }
 | 
			
		||||
    &.tb-mat-12 {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user