UI: Improve material icons selector
This commit is contained in:
parent
a00656abec
commit
d44f5fda5f
@ -114,7 +114,8 @@ export class DialogService {
|
||||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||
data: {
|
||||
icon
|
||||
}
|
||||
},
|
||||
autoFocus: false
|
||||
}).afterClosed();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -62,15 +62,18 @@ export class ResourcesService {
|
||||
this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache());
|
||||
}
|
||||
|
||||
public loadJsonResource<T>(url: string): Observable<T> {
|
||||
public loadJsonResource<T>(url: string, postProcess?: (data: T) => T): Observable<T> {
|
||||
if (this.loadedJsonResources[url]) {
|
||||
return this.loadedJsonResources[url].asObservable();
|
||||
}
|
||||
const subject = new ReplaySubject<any>();
|
||||
this.loadedJsonResources[url] = subject;
|
||||
this.http.get(url).subscribe(
|
||||
this.http.get<T>(url).subscribe(
|
||||
{
|
||||
next: (o) => {
|
||||
if (postProcess) {
|
||||
o = postProcess(o);
|
||||
}
|
||||
this.loadedJsonResources[url].next(o);
|
||||
this.loadedJsonResources[url].complete();
|
||||
},
|
||||
|
||||
@ -21,32 +21,31 @@ import { Inject, Injectable, NgZone } from '@angular/core';
|
||||
import { WINDOW } from '@core/services/window.service';
|
||||
import { ExceptionData } from '@app/shared/models/error.models';
|
||||
import {
|
||||
base64toObj,
|
||||
base64toString,
|
||||
baseUrl,
|
||||
createLabelFromDatasource,
|
||||
deepClone,
|
||||
deleteNullProperties,
|
||||
guid, hashCode,
|
||||
guid,
|
||||
hashCode,
|
||||
isDefined,
|
||||
isDefinedAndNotNull,
|
||||
isString,
|
||||
isUndefined,
|
||||
objToBase64,
|
||||
objToBase64URI,
|
||||
base64toString,
|
||||
base64toObj
|
||||
objToBase64URI
|
||||
} from '@core/utils';
|
||||
import { WindowMessage } from '@shared/models/window-message.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
|
||||
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
|
||||
import { EntityType } from '@shared/models/entity-type.models';
|
||||
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
|
||||
import { alarmFields } from '@shared/models/alarm.models';
|
||||
import { materialColors } from '@app/shared/models/material.models';
|
||||
import { WidgetInfo } from '@home/models/widget-component.models';
|
||||
import jsonSchemaDefaults from 'json-schema-defaults';
|
||||
import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw';
|
||||
import { Observable, of, ReplaySubject } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { publishReplay, refCount } from 'rxjs/operators';
|
||||
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
|
||||
import {
|
||||
@ -86,13 +85,6 @@ const defaultAlarmFields: Array<string> = [
|
||||
alarmFields.status.keyName
|
||||
];
|
||||
|
||||
const commonMaterialIcons: Array<string> = ['more_horiz', 'more_vert', 'open_in_new',
|
||||
'visibility', 'play_arrow', 'arrow_back', 'arrow_downward',
|
||||
'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
|
||||
'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
|
||||
'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
|
||||
'share', 'add', 'edit', 'done', 'delete'];
|
||||
|
||||
// @dynamic
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -121,8 +113,6 @@ export class UtilsService {
|
||||
|
||||
defaultAlarmDataKeys: Array<DataKey> = [];
|
||||
|
||||
materialIcons: Array<string> = [];
|
||||
|
||||
constructor(@Inject(WINDOW) private window: Window,
|
||||
private zone: NgZone,
|
||||
private translate: TranslateService) {
|
||||
@ -306,31 +296,6 @@ export class UtilsService {
|
||||
return datasources;
|
||||
}
|
||||
|
||||
public getMaterialIcons(): Observable<Array<string>> {
|
||||
if (this.materialIcons.length) {
|
||||
return of(this.materialIcons);
|
||||
} else {
|
||||
const materialIconsSubject = new ReplaySubject<Array<string>>();
|
||||
this.zone.runOutsideAngular(() => {
|
||||
const codepointsArray = materialIconsCodepoints
|
||||
.split('\n')
|
||||
.filter((codepoint) => codepoint && codepoint.length);
|
||||
codepointsArray.forEach((codepoint) => {
|
||||
const values = codepoint.split(' ');
|
||||
if (values && values.length === 2) {
|
||||
this.materialIcons.push(values[0]);
|
||||
}
|
||||
});
|
||||
materialIconsSubject.next(this.materialIcons);
|
||||
});
|
||||
return materialIconsSubject.asObservable();
|
||||
}
|
||||
}
|
||||
|
||||
public getCommonMaterialIcons(): Array<string> {
|
||||
return commonMaterialIcons;
|
||||
}
|
||||
|
||||
public getMaterialColor(index: number) {
|
||||
const colorIndex = index % materialColors.length;
|
||||
return materialColors[colorIndex].value;
|
||||
@ -411,7 +376,7 @@ export class UtilsService {
|
||||
|
||||
public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string {
|
||||
if (str && str.length) {
|
||||
let hue = hashCode(str) % 360;
|
||||
const hue = hashCode(str) % 360;
|
||||
return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,6 +181,7 @@ import * as StringItemsListComponent from '@shared/components/string-items-list.
|
||||
import * as ToggleHeaderComponent from '@shared/components/toggle-header.component';
|
||||
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
|
||||
import * as UnitInputComponent from '@shared/components/unit-input.component';
|
||||
import * as MaterialIconsComponent from '@shared/components/material-icons.component';
|
||||
|
||||
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
|
||||
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
|
||||
@ -482,6 +483,7 @@ class ModulesMap implements IModulesMap {
|
||||
'@shared/components/toggle-header.component': ToggleHeaderComponent,
|
||||
'@shared/components/toggle-select.component': ToggleSelectComponent,
|
||||
'@shared/components/unit-input.component': UnitInputComponent,
|
||||
'@shared/components/material-icons.component': MaterialIconsComponent,
|
||||
|
||||
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
|
||||
'@home/components/entity/entities-table.component': EntitiesTableComponent,
|
||||
|
||||
@ -67,50 +67,4 @@
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.tb-no-data-available {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tb-no-data-bg {
|
||||
margin: 10px;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-height: 100px;
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #305680;
|
||||
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-position: center;
|
||||
mask-image: url(/assets/home/no_data_folder_bg.svg);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tb-no-data-text {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
@media #{$mat-md-lg} {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,63 +15,14 @@
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<form class="tb-material-icons-dialog" style="min-width: 600px;">
|
||||
<mat-toolbar fxLayout="row" color="primary">
|
||||
<h2>{{ 'icon.select-icon' | translate }}</h2>
|
||||
<span fxFlex></span>
|
||||
<section fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
|
||||
<mat-slide-toggle [formControl]="showAllControl">
|
||||
</mat-slide-toggle>
|
||||
<label translate>icon.show-all</label>
|
||||
</section>
|
||||
<button mat-icon-button
|
||||
<div mat-dialog-content>
|
||||
<button class="tb-close-button"
|
||||
mat-icon-button
|
||||
(click)="cancel()"
|
||||
type="button">
|
||||
<mat-icon class="material-icons">close</mat-icon>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
|
||||
</mat-progress-bar>
|
||||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
|
||||
<div class="tb-absolute-fill tb-icons-load" *ngIf="loadingIcons$ | async" fxLayout="column" fxLayoutAlign="center center">
|
||||
<mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner>
|
||||
</div>
|
||||
<div mat-dialog-content>
|
||||
<div class="mat-content mat-padding" fxLayout="column">
|
||||
<fieldset [disabled]="(isLoading$ | async)">
|
||||
<ng-template ngFor let-icon [ngForOf]="icons$ | async" let-last="last">
|
||||
<ng-container #iconButtons>
|
||||
<button *ngIf="icon === selectedIcon"
|
||||
class="tb-select-icon-button"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="selectIcon(icon)"
|
||||
matTooltip="{{ icon }}"
|
||||
matTooltipPosition="above"
|
||||
type="button">
|
||||
<mat-icon>{{icon}}</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="icon !== selectedIcon"
|
||||
class="tb-select-icon-button"
|
||||
mat-button
|
||||
(click)="selectIcon(icon)"
|
||||
matTooltip="{{ icon }}"
|
||||
matTooltipPosition="above"
|
||||
type="button">
|
||||
<mat-icon>{{icon}}</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions fxLayout="row">
|
||||
<span fxFlex></span>
|
||||
<button mat-button
|
||||
type="button"
|
||||
[disabled]="(isLoading$ | async)"
|
||||
(click)="cancel()">
|
||||
{{ 'action.cancel' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<tb-material-icons [selectedIcon]="selectedIcon"
|
||||
(iconSelected)="selectIcon($event)">
|
||||
</tb-material-icons>
|
||||
</div>
|
||||
|
||||
@ -14,36 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
:host {
|
||||
.tb-material-icons-dialog {
|
||||
position: relative;
|
||||
}
|
||||
.tb-icons-load {
|
||||
top: 64px;
|
||||
z-index: 3;
|
||||
background: rgba(255, 255, 255, .75);
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep {
|
||||
.tb-material-icons-dialog {
|
||||
button.mat-mdc-button-base.tb-select-icon-button {
|
||||
width: 56px;
|
||||
min-width: 56px;
|
||||
height: 56px;
|
||||
padding: 16px;
|
||||
margin: 10px;
|
||||
border: solid 1px #ffa500;
|
||||
border-radius: 0;
|
||||
line-height: 0;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
.mat-icon {
|
||||
width: 24px;
|
||||
margin: 0;
|
||||
height: 24px;
|
||||
vertical-align: initial;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.tb-close-button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,18 +14,12 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { AfterViewInit, Component, Inject, OnInit, QueryList, ViewChildren } from '@angular/core';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { Router } from '@angular/router';
|
||||
import { DialogComponent } from '@shared/components/dialog.component';
|
||||
import { UtilsService } from '@core/services/utils.service';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { delay, map, mapTo, mergeMap, share, startWith, tap } from 'rxjs/operators';
|
||||
import { ResourcesService } from '@core/services/resources.service';
|
||||
import { getMaterialIcons } from '@shared/models/icon.models';
|
||||
|
||||
export interface MaterialIconsDialogData {
|
||||
icon: string;
|
||||
@ -37,63 +31,16 @@ export interface MaterialIconsDialogData {
|
||||
providers: [],
|
||||
styleUrls: ['./material-icons-dialog.component.scss']
|
||||
})
|
||||
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string>
|
||||
implements OnInit, AfterViewInit {
|
||||
|
||||
@ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>;
|
||||
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> {
|
||||
|
||||
selectedIcon: string;
|
||||
icons$: Observable<Array<string>>;
|
||||
loadingIcons$: Observable<boolean>;
|
||||
|
||||
showAllControl: UntypedFormControl;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
protected router: Router,
|
||||
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
|
||||
private utils: UtilsService,
|
||||
private resourcesService: ResourcesService,
|
||||
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) {
|
||||
super(store, router, dialogRef);
|
||||
this.selectedIcon = data.icon;
|
||||
this.showAllControl = new UntypedFormControl(false);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.icons$ = this.showAllControl.valueChanges.pipe(
|
||||
map((showAll) => ({firstTime: false, showAll})),
|
||||
startWith<{firstTime: boolean; showAll: boolean}>({firstTime: true, showAll: false}),
|
||||
mergeMap((data) => {
|
||||
const res = getMaterialIcons(this.resourcesService, data.showAll, '');
|
||||
if (data.showAll) {
|
||||
return res.pipe(delay(100));
|
||||
} else {
|
||||
return data.firstTime ? res : res.pipe(delay(50));
|
||||
}
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.loadingIcons$ = merge(
|
||||
this.showAllControl.valueChanges.pipe(
|
||||
mapTo(true),
|
||||
),
|
||||
this.iconButtons.changes.pipe(
|
||||
delay(100),
|
||||
mapTo( false),
|
||||
)
|
||||
).pipe(
|
||||
tap((loadingIcons) => {
|
||||
if (loadingIcons) {
|
||||
this.showAllControl.disable({emitEvent: false});
|
||||
} else {
|
||||
this.showAllControl.enable({emitEvent: false});
|
||||
}
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
selectIcon(icon: string) {
|
||||
|
||||
@ -29,7 +29,13 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<ng-template #boxInput>
|
||||
<mat-icon class="icon-box" [ngStyle]="color && !disabled ? { color: color } : {}"
|
||||
[ngClass]="{'disabled': disabled}"
|
||||
(click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
|
||||
<button type="button"
|
||||
mat-stroked-button
|
||||
class="icon-box"
|
||||
[ngStyle]="color && !disabled ? { color: color } : {}"
|
||||
[disabled]="disabled"
|
||||
#matButton
|
||||
(click)="openIconPopup($event, matButton)">
|
||||
<mat-icon>{{materialIconFormGroup.get('icon').value}}</mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@ -22,20 +22,23 @@
|
||||
border: solid 1px rgba(0, 0, 0, .27);
|
||||
box-sizing: initial;
|
||||
}
|
||||
&.icon-box {
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
font-size: 22px;
|
||||
vertical-align: middle;
|
||||
&.disabled {
|
||||
cursor: initial;
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep {
|
||||
button.mat-mdc-button-base.icon-box {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 7px;
|
||||
&:not(:disabled) {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
> .mat-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,15 +14,18 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||
import { TbPopoverService } from '@shared/components/popover.service';
|
||||
import { MaterialIconsComponent } from '@shared/components/material-icons.component';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-material-icon-select',
|
||||
@ -81,6 +84,9 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
|
||||
constructor(protected store: Store<AppState>,
|
||||
private dialogs: DialogService,
|
||||
private translate: TranslateService,
|
||||
private popoverService: TbPopoverService,
|
||||
private renderer: Renderer2,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private fb: UntypedFormBuilder,
|
||||
private cd: ChangeDetectorRef) {
|
||||
super(store);
|
||||
@ -142,6 +148,32 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
|
||||
}
|
||||
}
|
||||
|
||||
openIconPopup($event: Event, matButton: MatButton) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
const trigger = matButton._elementRef.nativeElement;
|
||||
if (this.popoverService.hasPopover(trigger)) {
|
||||
this.popoverService.hidePopover(trigger);
|
||||
} else {
|
||||
const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer,
|
||||
this.viewContainerRef, MaterialIconsComponent, 'left', true, null,
|
||||
{
|
||||
selectedIcon: this.materialIconFormGroup.get('icon').value
|
||||
},
|
||||
{},
|
||||
{}, {}, true);
|
||||
materialIconsPopover.tbComponentRef.instance.popover = materialIconsPopover;
|
||||
materialIconsPopover.tbComponentRef.instance.iconSelected.subscribe((icon) => {
|
||||
materialIconsPopover.hide();
|
||||
this.materialIconFormGroup.patchValue(
|
||||
{icon}, {emitEvent: true}
|
||||
);
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true});
|
||||
this.cd.markForCheck();
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2023 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.
|
||||
|
||||
-->
|
||||
<div class="tb-material-icons-panel">
|
||||
<div class="tb-material-icons-title" translate>icon.icons</div>
|
||||
<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 [fxShow]="!notFound" #iconsPanel
|
||||
[itemSize]="iconsRowHeight" class="tb-material-icons-viewport" [ngStyle]="{width: iconsPanelWidth, 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">
|
||||
<mat-icon>{{icon.name}}</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="icon.name !== selectedIcon"
|
||||
class="tb-select-icon-button"
|
||||
mat-button
|
||||
(click)="selectIcon(icon)"
|
||||
matTooltip="{{ icon.displayName }}"
|
||||
matTooltipPosition="above"
|
||||
type="button">
|
||||
<mat-icon>{{icon.name}}</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
<button *ngIf="!showAllSubject.value" class="tb-material-icons-show-more" mat-button color="primary" (click)="showAllSubject.next(true)">
|
||||
{{ 'action.show-more' | translate }}
|
||||
</button>
|
||||
<ng-container *ngIf="notFound">
|
||||
<div class="tb-no-data-available" [ngStyle]="{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>
|
||||
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.
|
||||
*/
|
||||
.tb-material-icons-panel {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
.tb-material-icons-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.25px;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
.tb-material-icons-title, .tb-material-icons-search, .tb-material-icons-show-more {
|
||||
width: 100%;
|
||||
}
|
||||
.tb-material-icons-viewport {
|
||||
min-height: 144px;
|
||||
}
|
||||
.tb-material-icons-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
.tb-material-icons-row + .tb-material-icons-row {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.tb-no-data-available {
|
||||
min-height: 144px;
|
||||
}
|
||||
button.mat-mdc-button-base.tb-select-icon-button {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 6px;
|
||||
&:not(.mat-primary) {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
> .mat-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,135 @@
|
||||
///
|
||||
/// Copyright © 2016-2023 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 { PageComponent } from '@shared/components/page.component';
|
||||
import { OnInit } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, debounce, Observable, of, timer } from 'rxjs';
|
||||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||
import { getMaterialIcons, MaterialIcon } from '@shared/models/icon.models';
|
||||
import { distinctUntilChanged, map, mergeMap, share, startWith, tap } from 'rxjs/operators';
|
||||
import { ResourcesService } from '@core/services/resources.service';
|
||||
import { TbPopoverComponent } from '@shared/components/popover.component';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { MediaBreakpoints } from '@shared/models/constants';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-material-icons',
|
||||
templateUrl: './material-icons.component.html',
|
||||
providers: [],
|
||||
styleUrls: ['./material-icons.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MaterialIconsComponent extends PageComponent implements OnInit {
|
||||
|
||||
searchIconsControl: UntypedFormControl;
|
||||
@ViewChild('iconsPanel')
|
||||
iconsPanel: CdkVirtualScrollViewport;
|
||||
|
||||
@Input()
|
||||
selectedIcon: string;
|
||||
|
||||
@Input()
|
||||
popover: TbPopoverComponent<MaterialIconsComponent>;
|
||||
|
||||
@Output()
|
||||
iconSelected = new EventEmitter<string>();
|
||||
|
||||
iconRows$: Observable<MaterialIcon[][]>;
|
||||
showAllSubject = new BehaviorSubject<boolean>(false);
|
||||
searchIconControl: UntypedFormControl;
|
||||
|
||||
icons$: Observable<Array<string>>;
|
||||
iconsRowHeight = 48;
|
||||
|
||||
constructor(protected store: Store<AppState>) {
|
||||
iconsPanelHeight: string;
|
||||
iconsPanelWidth: string;
|
||||
|
||||
notFound = false;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private resourcesService: ResourcesService,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
private cd: ChangeDetectorRef) {
|
||||
super(store);
|
||||
this.searchIconsControl = new UntypedFormControl('');
|
||||
this.searchIconControl = new UntypedFormControl('');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
const iconsRowSize = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? 8 : 11;
|
||||
this.calculatePanelSize(iconsRowSize);
|
||||
const iconsRowSizeObservable = this.breakpointObserver
|
||||
.observe(MediaBreakpoints['lt-md']).pipe(
|
||||
map((state) => state.matches ? 8 : 11),
|
||||
startWith(iconsRowSize),
|
||||
);
|
||||
this.iconRows$ = combineLatest({showAll: this.showAllSubject.asObservable(),
|
||||
rowSize: iconsRowSizeObservable,
|
||||
searchText: this.searchIconControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
debounce((searchText) => searchText ? timer(150) : of({})),
|
||||
)}).pipe(
|
||||
map((data) => {
|
||||
if (data.searchText && !data.showAll) {
|
||||
data.showAll = true;
|
||||
this.showAllSubject.next(true);
|
||||
}
|
||||
return data;
|
||||
}),
|
||||
distinctUntilChanged((p, c) => c.showAll === p.showAll && c.searchText === p.searchText && c.rowSize === p.rowSize),
|
||||
mergeMap((data) => getMaterialIcons(this.resourcesService, data.rowSize, data.showAll, data.searchText).pipe(
|
||||
map(iconRows => ({iconRows, iconsRowSize: data.rowSize}))
|
||||
)),
|
||||
tap((data) => {
|
||||
this.notFound = !data.iconRows.length;
|
||||
this.calculatePanelSize(data.iconsRowSize, data.iconRows.length);
|
||||
this.cd.markForCheck();
|
||||
setTimeout(() => {
|
||||
this.checkSize();
|
||||
}, 0);
|
||||
}),
|
||||
map((data) => data.iconRows),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
clearSearch() {
|
||||
this.searchIconControl.patchValue('', {emitEvent: true});
|
||||
}
|
||||
|
||||
selectIcon(icon: MaterialIcon) {
|
||||
this.iconSelected.emit(icon.name);
|
||||
}
|
||||
|
||||
private calculatePanelSize(iconsRowSize: number, iconRows = 4) {
|
||||
this.iconsPanelHeight = Math.min(iconRows * this.iconsRowHeight, 10 * this.iconsRowHeight) + 'px';
|
||||
this.iconsPanelWidth = (iconsRowSize * 36 + (iconsRowSize - 1) * 12 + 6) + 'px';
|
||||
}
|
||||
|
||||
private checkSize() {
|
||||
this.iconsPanel?.checkViewportSize();
|
||||
this.popover?.updatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,8 +63,10 @@ import { coerceBoolean } from '@shared/decorators/coercion';
|
||||
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
|
||||
|
||||
@Directive({
|
||||
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||
selector: '[tb-popover]',
|
||||
exportAs: 'tbPopover',
|
||||
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
|
||||
host: {
|
||||
'[class.tb-popover-open]': 'visible'
|
||||
}
|
||||
@ -265,12 +267,20 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
|
||||
} else if (delay > 0) {
|
||||
this.delayTimer = setTimeout(() => {
|
||||
this.delayTimer = undefined;
|
||||
isEnter ? this.show() : this.hide();
|
||||
if (isEnter) {
|
||||
this.show();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}, delay * 1000);
|
||||
} else {
|
||||
// `isOrigin` is used due to the tooltip will not hide immediately
|
||||
// (may caused by the fade-out animation).
|
||||
isEnter && isOrigin ? this.show() : this.hide();
|
||||
if (isEnter && isOrigin) {
|
||||
this.show();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,15 +355,15 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
|
||||
</ng-template>
|
||||
`
|
||||
})
|
||||
export class TbPopoverComponent implements OnDestroy, OnInit {
|
||||
export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
|
||||
|
||||
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
|
||||
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
|
||||
@ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>;
|
||||
|
||||
tbContent: string | TemplateRef<void> | null = null;
|
||||
tbComponentFactory: ComponentFactory<any> | null = null;
|
||||
tbComponentRef: ComponentRef<any> | null = null;
|
||||
tbComponentFactory: ComponentFactory<T> | null = null;
|
||||
tbComponentRef: ComponentRef<T> | null = null;
|
||||
tbComponentContext: any;
|
||||
tbComponentInjector: Injector | null = null;
|
||||
tbComponentStyle: { [klass: string]: any } = {};
|
||||
|
||||
@ -65,7 +65,7 @@ export class TbPopoverService {
|
||||
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
|
||||
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
|
||||
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
|
||||
showCloseButton = true): TbPopoverComponent {
|
||||
showCloseButton = true): TbPopoverComponent<T> {
|
||||
const componentRef = this.createPopoverRef(hostView);
|
||||
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
|
||||
injector, context, overlayStyle, popoverStyle, style, showCloseButton);
|
||||
@ -74,7 +74,7 @@ export class TbPopoverService {
|
||||
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
|
||||
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
|
||||
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
|
||||
popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent {
|
||||
popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent<T> {
|
||||
const component = componentRef.instance;
|
||||
this.popoverWithTriggers.push({
|
||||
trigger,
|
||||
|
||||
@ -26,3 +26,4 @@ export * from './resource/resource-autocomplete.component';
|
||||
export * from './toggle-header.component';
|
||||
export * from './toggle-select.component';
|
||||
export * from './unit-input.component';
|
||||
export * from './material-icons.component';
|
||||
|
||||
@ -1,11 +1,27 @@
|
||||
import { Unit, units } from '@shared/models/unit.models';
|
||||
///
|
||||
/// Copyright © 2016-2023 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 { ResourcesService } from '@core/services/resources.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { isEmptyStr, isNotEmptyStr } from '@core/utils';
|
||||
import { isNotEmptyStr } from '@core/utils';
|
||||
|
||||
export interface MaterialIcon {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
@ -16,21 +32,41 @@ const searchIconTags = (icon: MaterialIcon, searchText: string): boolean =>
|
||||
|
||||
const searchIcons = (_icons: Array<MaterialIcon>, searchText: string): Array<MaterialIcon> => _icons.filter(
|
||||
i => i.name.toUpperCase().includes(searchText.toUpperCase()) ||
|
||||
i.displayName.toUpperCase().includes(searchText.toUpperCase()) ||
|
||||
searchIconTags(i, searchText)
|
||||
);
|
||||
|
||||
const getCommonMaterialIcons = (icons: Array<MaterialIcon>): Array<MaterialIcon> => icons.slice(0, 44);
|
||||
const getCommonMaterialIcons = (icons: Array<MaterialIcon>, chunkSize: number): Array<MaterialIcon> => icons.slice(0, chunkSize * 4);
|
||||
|
||||
export const getMaterialIcons = (resourcesService: ResourcesService, all = false, searchText: string): Observable<string[]> =>
|
||||
resourcesService.loadJsonResource<Array<MaterialIcon>>('/assets/metadata/material-icons.json').pipe(
|
||||
export const getMaterialIcons = (resourcesService: ResourcesService, chunkSize = 11,
|
||||
all = false, searchText: string): Observable<MaterialIcon[][]> =>
|
||||
resourcesService.loadJsonResource<Array<MaterialIcon>>('/assets/metadata/material-icons.json',
|
||||
(icons) => {
|
||||
for (const icon of icons) {
|
||||
const words = icon.name.replace(/_/g, ' ').split(' ');
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i].charAt(0).toUpperCase() + words[i].slice(1);
|
||||
}
|
||||
icon.displayName = words.join(' ');
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
).pipe(
|
||||
map((icons) => {
|
||||
if (isNotEmptyStr(searchText)) {
|
||||
return searchIcons(icons, searchText);
|
||||
} else if (!all) {
|
||||
return getCommonMaterialIcons(icons);
|
||||
return getCommonMaterialIcons(icons, chunkSize);
|
||||
} else {
|
||||
return icons;
|
||||
}
|
||||
}),
|
||||
map((icons) => icons.map(icon => icon.name))
|
||||
map((icons) => {
|
||||
const iconChunks: MaterialIcon[][] = [];
|
||||
for (let i = 0; i < icons.length; i += chunkSize) {
|
||||
const chunk = icons.slice(i, i + chunkSize);
|
||||
iconChunks.push(chunk);
|
||||
}
|
||||
return iconChunks;
|
||||
})
|
||||
);
|
||||
|
||||
@ -195,6 +195,7 @@ import { ToggleHeaderComponent, ToggleOption } from '@shared/components/toggle-h
|
||||
import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
|
||||
import { ToggleSelectComponent } from '@shared/components/toggle-select.component';
|
||||
import { UnitInputComponent } from '@shared/components/unit-input.component';
|
||||
import { MaterialIconsComponent } from '@shared/components/material-icons.component';
|
||||
|
||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||
return markedOptionsService;
|
||||
@ -369,6 +370,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
ToggleOption,
|
||||
ToggleSelectComponent,
|
||||
UnitInputComponent,
|
||||
MaterialIconsComponent,
|
||||
RuleChainSelectComponent
|
||||
],
|
||||
imports: [
|
||||
@ -600,6 +602,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
ToggleOption,
|
||||
ToggleSelectComponent,
|
||||
UnitInputComponent,
|
||||
MaterialIconsComponent,
|
||||
RuleChainSelectComponent
|
||||
]
|
||||
})
|
||||
|
||||
@ -68,7 +68,8 @@
|
||||
"less": "Less",
|
||||
"skip": "Skip",
|
||||
"send": "Send",
|
||||
"reset": "Reset"
|
||||
"reset": "Reset",
|
||||
"show-more": "Show more"
|
||||
},
|
||||
"aggregation": {
|
||||
"aggregation": "Aggregation",
|
||||
@ -5501,9 +5502,12 @@
|
||||
},
|
||||
"icon": {
|
||||
"icon": "Icon",
|
||||
"icons": "Icons",
|
||||
"select-icon": "Select icon",
|
||||
"material-icons": "Material icons",
|
||||
"show-all": "Show all icons"
|
||||
"show-all": "Show all icons",
|
||||
"search-icon": "Search icon",
|
||||
"no-icons-found": "No icons found for '{{iconSearch}}'"
|
||||
},
|
||||
"phone-input": {
|
||||
"phone-input-label": "Phone number",
|
||||
|
||||
@ -13,6 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import './scss/constants';
|
||||
|
||||
.tb-default, .tb-dark {
|
||||
.tb-form-panel {
|
||||
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
|
||||
@ -177,6 +180,13 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
&:not(.mat-mdc-form-field-has-icon-prefix) {
|
||||
.mat-mdc-text-field-wrapper {
|
||||
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.mat-mdc-form-field-has-icon-suffix) {
|
||||
.mat-mdc-text-field-wrapper {
|
||||
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
|
||||
@ -186,7 +196,6 @@
|
||||
}
|
||||
.mat-mdc-text-field-wrapper {
|
||||
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
|
||||
padding-left: 12px;
|
||||
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
|
||||
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
@ -203,7 +212,7 @@
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
.mat-mdc-form-field-icon-suffix {
|
||||
.mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix {
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
@ -336,4 +345,49 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tb-no-data-available {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tb-no-data-bg {
|
||||
margin: 10px;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-height: 100px;
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #305680;
|
||||
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-position: center;
|
||||
mask-image: url(/assets/home/no_data_folder_bg.svg);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tb-no-data-text {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
@media #{$mat-md-lg} {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user