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'],
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||||
data: {
|
data: {
|
||||||
icon
|
icon
|
||||||
}
|
},
|
||||||
|
autoFocus: false
|
||||||
}).afterClosed();
|
}).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());
|
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]) {
|
if (this.loadedJsonResources[url]) {
|
||||||
return this.loadedJsonResources[url].asObservable();
|
return this.loadedJsonResources[url].asObservable();
|
||||||
}
|
}
|
||||||
const subject = new ReplaySubject<any>();
|
const subject = new ReplaySubject<any>();
|
||||||
this.loadedJsonResources[url] = subject;
|
this.loadedJsonResources[url] = subject;
|
||||||
this.http.get(url).subscribe(
|
this.http.get<T>(url).subscribe(
|
||||||
{
|
{
|
||||||
next: (o) => {
|
next: (o) => {
|
||||||
|
if (postProcess) {
|
||||||
|
o = postProcess(o);
|
||||||
|
}
|
||||||
this.loadedJsonResources[url].next(o);
|
this.loadedJsonResources[url].next(o);
|
||||||
this.loadedJsonResources[url].complete();
|
this.loadedJsonResources[url].complete();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,32 +21,31 @@ import { Inject, Injectable, NgZone } from '@angular/core';
|
|||||||
import { WINDOW } from '@core/services/window.service';
|
import { WINDOW } from '@core/services/window.service';
|
||||||
import { ExceptionData } from '@app/shared/models/error.models';
|
import { ExceptionData } from '@app/shared/models/error.models';
|
||||||
import {
|
import {
|
||||||
|
base64toObj,
|
||||||
|
base64toString,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
createLabelFromDatasource,
|
createLabelFromDatasource,
|
||||||
deepClone,
|
deepClone,
|
||||||
deleteNullProperties,
|
deleteNullProperties,
|
||||||
guid, hashCode,
|
guid,
|
||||||
|
hashCode,
|
||||||
isDefined,
|
isDefined,
|
||||||
isDefinedAndNotNull,
|
isDefinedAndNotNull,
|
||||||
isString,
|
isString,
|
||||||
isUndefined,
|
isUndefined,
|
||||||
objToBase64,
|
objToBase64,
|
||||||
objToBase64URI,
|
objToBase64URI
|
||||||
base64toString,
|
|
||||||
base64toObj
|
|
||||||
} from '@core/utils';
|
} from '@core/utils';
|
||||||
import { WindowMessage } from '@shared/models/window-message.model';
|
import { WindowMessage } from '@shared/models/window-message.model';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
|
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
|
||||||
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
|
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 { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
|
||||||
import { alarmFields } from '@shared/models/alarm.models';
|
import { alarmFields } from '@shared/models/alarm.models';
|
||||||
import { materialColors } from '@app/shared/models/material.models';
|
import { materialColors } from '@app/shared/models/material.models';
|
||||||
import { WidgetInfo } from '@home/models/widget-component.models';
|
import { WidgetInfo } from '@home/models/widget-component.models';
|
||||||
import jsonSchemaDefaults from 'json-schema-defaults';
|
import jsonSchemaDefaults from 'json-schema-defaults';
|
||||||
import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw';
|
import { Observable } from 'rxjs';
|
||||||
import { Observable, of, ReplaySubject } from 'rxjs';
|
|
||||||
import { publishReplay, refCount } from 'rxjs/operators';
|
import { publishReplay, refCount } from 'rxjs/operators';
|
||||||
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
|
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
|
||||||
import {
|
import {
|
||||||
@ -86,13 +85,6 @@ const defaultAlarmFields: Array<string> = [
|
|||||||
alarmFields.status.keyName
|
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
|
// @dynamic
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -121,8 +113,6 @@ export class UtilsService {
|
|||||||
|
|
||||||
defaultAlarmDataKeys: Array<DataKey> = [];
|
defaultAlarmDataKeys: Array<DataKey> = [];
|
||||||
|
|
||||||
materialIcons: Array<string> = [];
|
|
||||||
|
|
||||||
constructor(@Inject(WINDOW) private window: Window,
|
constructor(@Inject(WINDOW) private window: Window,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private translate: TranslateService) {
|
private translate: TranslateService) {
|
||||||
@ -306,31 +296,6 @@ export class UtilsService {
|
|||||||
return datasources;
|
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) {
|
public getMaterialColor(index: number) {
|
||||||
const colorIndex = index % materialColors.length;
|
const colorIndex = index % materialColors.length;
|
||||||
return materialColors[colorIndex].value;
|
return materialColors[colorIndex].value;
|
||||||
@ -411,7 +376,7 @@ export class UtilsService {
|
|||||||
|
|
||||||
public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string {
|
public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string {
|
||||||
if (str && str.length) {
|
if (str && str.length) {
|
||||||
let hue = hashCode(str) % 360;
|
const hue = hashCode(str) % 360;
|
||||||
return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`;
|
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 ToggleHeaderComponent from '@shared/components/toggle-header.component';
|
||||||
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
|
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
|
||||||
import * as UnitInputComponent from '@shared/components/unit-input.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 AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
|
||||||
import * as EntitiesTableComponent from '@home/components/entity/entities-table.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-header.component': ToggleHeaderComponent,
|
||||||
'@shared/components/toggle-select.component': ToggleSelectComponent,
|
'@shared/components/toggle-select.component': ToggleSelectComponent,
|
||||||
'@shared/components/unit-input.component': UnitInputComponent,
|
'@shared/components/unit-input.component': UnitInputComponent,
|
||||||
|
'@shared/components/material-icons.component': MaterialIconsComponent,
|
||||||
|
|
||||||
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
|
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
|
||||||
'@home/components/entity/entities-table.component': EntitiesTableComponent,
|
'@home/components/entity/entities-table.component': EntitiesTableComponent,
|
||||||
|
|||||||
@ -67,50 +67,4 @@
|
|||||||
color: inherit;
|
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.
|
limitations under the License.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
<form class="tb-material-icons-dialog" style="min-width: 600px;">
|
<div mat-dialog-content>
|
||||||
<mat-toolbar fxLayout="row" color="primary">
|
<button class="tb-close-button"
|
||||||
<h2>{{ 'icon.select-icon' | translate }}</h2>
|
mat-icon-button
|
||||||
<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
|
|
||||||
(click)="cancel()"
|
(click)="cancel()"
|
||||||
type="button">
|
type="button">
|
||||||
<mat-icon class="material-icons">close</mat-icon>
|
<mat-icon>close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-toolbar>
|
<tb-material-icons [selectedIcon]="selectedIcon"
|
||||||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
|
(iconSelected)="selectIcon($event)">
|
||||||
</mat-progress-bar>
|
</tb-material-icons>
|
||||||
<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>
|
||||||
<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>
|
|
||||||
|
|||||||
@ -14,36 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
:host {
|
:host {
|
||||||
.tb-material-icons-dialog {
|
.tb-close-button {
|
||||||
position: relative;
|
position: absolute;
|
||||||
}
|
top: 6px;
|
||||||
.tb-icons-load {
|
right: 6px;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,18 +14,12 @@
|
|||||||
/// limitations under the License.
|
/// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { DialogComponent } from '@shared/components/dialog.component';
|
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 {
|
export interface MaterialIconsDialogData {
|
||||||
icon: string;
|
icon: string;
|
||||||
@ -37,63 +31,16 @@ export interface MaterialIconsDialogData {
|
|||||||
providers: [],
|
providers: [],
|
||||||
styleUrls: ['./material-icons-dialog.component.scss']
|
styleUrls: ['./material-icons-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string>
|
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> {
|
||||||
implements OnInit, AfterViewInit {
|
|
||||||
|
|
||||||
@ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>;
|
|
||||||
|
|
||||||
selectedIcon: string;
|
selectedIcon: string;
|
||||||
icons$: Observable<Array<string>>;
|
|
||||||
loadingIcons$: Observable<boolean>;
|
|
||||||
|
|
||||||
showAllControl: UntypedFormControl;
|
|
||||||
|
|
||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
|
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
|
||||||
private utils: UtilsService,
|
|
||||||
private resourcesService: ResourcesService,
|
|
||||||
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) {
|
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) {
|
||||||
super(store, router, dialogRef);
|
super(store, router, dialogRef);
|
||||||
this.selectedIcon = data.icon;
|
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) {
|
selectIcon(icon: string) {
|
||||||
|
|||||||
@ -29,7 +29,13 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #boxInput>
|
<ng-template #boxInput>
|
||||||
<mat-icon class="icon-box" [ngStyle]="color && !disabled ? { color: color } : {}"
|
<button type="button"
|
||||||
[ngClass]="{'disabled': disabled}"
|
mat-stroked-button
|
||||||
(click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
|
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>
|
</ng-template>
|
||||||
|
|||||||
@ -22,20 +22,23 @@
|
|||||||
border: solid 1px rgba(0, 0, 0, .27);
|
border: solid 1px rgba(0, 0, 0, .27);
|
||||||
box-sizing: initial;
|
box-sizing: initial;
|
||||||
}
|
}
|
||||||
&.icon-box {
|
}
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
}
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
:host ::ng-deep {
|
||||||
box-sizing: border-box;
|
button.mat-mdc-button-base.icon-box {
|
||||||
padding: 8px;
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
font-size: 22px;
|
min-width: 40px;
|
||||||
vertical-align: middle;
|
height: 40px;
|
||||||
&.disabled {
|
padding: 7px;
|
||||||
cursor: initial;
|
&:not(:disabled) {
|
||||||
color: rgba(0, 0, 0, 0.38);
|
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.
|
/// 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 { PageComponent } from '@shared/components/page.component';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
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 { DialogService } from '@core/services/dialog.service';
|
||||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { coerceBoolean } from '@shared/decorators/coercion';
|
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({
|
@Component({
|
||||||
selector: 'tb-material-icon-select',
|
selector: 'tb-material-icon-select',
|
||||||
@ -81,6 +84,9 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
|
|||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
private dialogs: DialogService,
|
private dialogs: DialogService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
|
private popoverService: TbPopoverService,
|
||||||
|
private renderer: Renderer2,
|
||||||
|
private viewContainerRef: ViewContainerRef,
|
||||||
private fb: UntypedFormBuilder,
|
private fb: UntypedFormBuilder,
|
||||||
private cd: ChangeDetectorRef) {
|
private cd: ChangeDetectorRef) {
|
||||||
super(store);
|
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() {
|
clear() {
|
||||||
this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true});
|
this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true});
|
||||||
this.cd.markForCheck();
|
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 { 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 { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { UntypedFormControl } from '@angular/forms';
|
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 {
|
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);
|
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);
|
super(store);
|
||||||
this.searchIconsControl = new UntypedFormControl('');
|
this.searchIconControl = new UntypedFormControl('');
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
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;
|
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
|
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||||
selector: '[tb-popover]',
|
selector: '[tb-popover]',
|
||||||
exportAs: 'tbPopover',
|
exportAs: 'tbPopover',
|
||||||
|
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
|
||||||
host: {
|
host: {
|
||||||
'[class.tb-popover-open]': 'visible'
|
'[class.tb-popover-open]': 'visible'
|
||||||
}
|
}
|
||||||
@ -265,12 +267,20 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
|
|||||||
} else if (delay > 0) {
|
} else if (delay > 0) {
|
||||||
this.delayTimer = setTimeout(() => {
|
this.delayTimer = setTimeout(() => {
|
||||||
this.delayTimer = undefined;
|
this.delayTimer = undefined;
|
||||||
isEnter ? this.show() : this.hide();
|
if (isEnter) {
|
||||||
|
this.show();
|
||||||
|
} else {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
}, delay * 1000);
|
}, delay * 1000);
|
||||||
} else {
|
} else {
|
||||||
// `isOrigin` is used due to the tooltip will not hide immediately
|
// `isOrigin` is used due to the tooltip will not hide immediately
|
||||||
// (may caused by the fade-out animation).
|
// (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>
|
</ng-template>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class TbPopoverComponent implements OnDestroy, OnInit {
|
export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
|
||||||
|
|
||||||
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
|
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
|
||||||
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
|
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
|
||||||
@ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>;
|
@ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
tbContent: string | TemplateRef<void> | null = null;
|
tbContent: string | TemplateRef<void> | null = null;
|
||||||
tbComponentFactory: ComponentFactory<any> | null = null;
|
tbComponentFactory: ComponentFactory<T> | null = null;
|
||||||
tbComponentRef: ComponentRef<any> | null = null;
|
tbComponentRef: ComponentRef<T> | null = null;
|
||||||
tbComponentContext: any;
|
tbComponentContext: any;
|
||||||
tbComponentInjector: Injector | null = null;
|
tbComponentInjector: Injector | null = null;
|
||||||
tbComponentStyle: { [klass: string]: any } = {};
|
tbComponentStyle: { [klass: string]: any } = {};
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export class TbPopoverService {
|
|||||||
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
|
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
|
||||||
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
|
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
|
||||||
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
|
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
|
||||||
showCloseButton = true): TbPopoverComponent {
|
showCloseButton = true): TbPopoverComponent<T> {
|
||||||
const componentRef = this.createPopoverRef(hostView);
|
const componentRef = this.createPopoverRef(hostView);
|
||||||
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
|
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
|
||||||
injector, context, overlayStyle, popoverStyle, style, showCloseButton);
|
injector, context, overlayStyle, popoverStyle, style, showCloseButton);
|
||||||
@ -74,7 +74,7 @@ export class TbPopoverService {
|
|||||||
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
|
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
|
||||||
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
|
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
|
||||||
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
|
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;
|
const component = componentRef.instance;
|
||||||
this.popoverWithTriggers.push({
|
this.popoverWithTriggers.push({
|
||||||
trigger,
|
trigger,
|
||||||
|
|||||||
@ -26,3 +26,4 @@ export * from './resource/resource-autocomplete.component';
|
|||||||
export * from './toggle-header.component';
|
export * from './toggle-header.component';
|
||||||
export * from './toggle-select.component';
|
export * from './toggle-select.component';
|
||||||
export * from './unit-input.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 { ResourcesService } from '@core/services/resources.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { isEmptyStr, isNotEmptyStr } from '@core/utils';
|
import { isNotEmptyStr } from '@core/utils';
|
||||||
|
|
||||||
export interface MaterialIcon {
|
export interface MaterialIcon {
|
||||||
name: string;
|
name: string;
|
||||||
|
displayName?: string;
|
||||||
tags: 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(
|
const searchIcons = (_icons: Array<MaterialIcon>, searchText: string): Array<MaterialIcon> => _icons.filter(
|
||||||
i => i.name.toUpperCase().includes(searchText.toUpperCase()) ||
|
i => i.name.toUpperCase().includes(searchText.toUpperCase()) ||
|
||||||
|
i.displayName.toUpperCase().includes(searchText.toUpperCase()) ||
|
||||||
searchIconTags(i, searchText)
|
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[]> =>
|
export const getMaterialIcons = (resourcesService: ResourcesService, chunkSize = 11,
|
||||||
resourcesService.loadJsonResource<Array<MaterialIcon>>('/assets/metadata/material-icons.json').pipe(
|
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) => {
|
map((icons) => {
|
||||||
if (isNotEmptyStr(searchText)) {
|
if (isNotEmptyStr(searchText)) {
|
||||||
return searchIcons(icons, searchText);
|
return searchIcons(icons, searchText);
|
||||||
} else if (!all) {
|
} else if (!all) {
|
||||||
return getCommonMaterialIcons(icons);
|
return getCommonMaterialIcons(icons, chunkSize);
|
||||||
} else {
|
} else {
|
||||||
return icons;
|
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 { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
|
||||||
import { ToggleSelectComponent } from '@shared/components/toggle-select.component';
|
import { ToggleSelectComponent } from '@shared/components/toggle-select.component';
|
||||||
import { UnitInputComponent } from '@shared/components/unit-input.component';
|
import { UnitInputComponent } from '@shared/components/unit-input.component';
|
||||||
|
import { MaterialIconsComponent } from '@shared/components/material-icons.component';
|
||||||
|
|
||||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||||
return markedOptionsService;
|
return markedOptionsService;
|
||||||
@ -369,6 +370,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
|||||||
ToggleOption,
|
ToggleOption,
|
||||||
ToggleSelectComponent,
|
ToggleSelectComponent,
|
||||||
UnitInputComponent,
|
UnitInputComponent,
|
||||||
|
MaterialIconsComponent,
|
||||||
RuleChainSelectComponent
|
RuleChainSelectComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
@ -600,6 +602,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
|||||||
ToggleOption,
|
ToggleOption,
|
||||||
ToggleSelectComponent,
|
ToggleSelectComponent,
|
||||||
UnitInputComponent,
|
UnitInputComponent,
|
||||||
|
MaterialIconsComponent,
|
||||||
RuleChainSelectComponent
|
RuleChainSelectComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@ -68,7 +68,8 @@
|
|||||||
"less": "Less",
|
"less": "Less",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"reset": "Reset"
|
"reset": "Reset",
|
||||||
|
"show-more": "Show more"
|
||||||
},
|
},
|
||||||
"aggregation": {
|
"aggregation": {
|
||||||
"aggregation": "Aggregation",
|
"aggregation": "Aggregation",
|
||||||
@ -5501,9 +5502,12 @@
|
|||||||
},
|
},
|
||||||
"icon": {
|
"icon": {
|
||||||
"icon": "Icon",
|
"icon": "Icon",
|
||||||
|
"icons": "Icons",
|
||||||
"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",
|
||||||
|
"search-icon": "Search icon",
|
||||||
|
"no-icons-found": "No icons found for '{{iconSearch}}'"
|
||||||
},
|
},
|
||||||
"phone-input": {
|
"phone-input": {
|
||||||
"phone-input-label": "Phone number",
|
"phone-input-label": "Phone number",
|
||||||
|
|||||||
@ -13,6 +13,9 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@import './scss/constants';
|
||||||
|
|
||||||
.tb-default, .tb-dark {
|
.tb-default, .tb-dark {
|
||||||
.tb-form-panel {
|
.tb-form-panel {
|
||||||
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
|
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
|
||||||
@ -177,6 +180,13 @@
|
|||||||
opacity: 0;
|
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) {
|
&:not(.mat-mdc-form-field-has-icon-suffix) {
|
||||||
.mat-mdc-text-field-wrapper {
|
.mat-mdc-text-field-wrapper {
|
||||||
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
|
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
|
||||||
@ -186,7 +196,6 @@
|
|||||||
}
|
}
|
||||||
.mat-mdc-text-field-wrapper {
|
.mat-mdc-text-field-wrapper {
|
||||||
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
|
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
|
||||||
padding-left: 12px;
|
|
||||||
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
|
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
|
||||||
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
|
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
|
||||||
border-color: rgba(0, 0, 0, 0.12);
|
border-color: rgba(0, 0, 0, 0.12);
|
||||||
@ -203,7 +212,7 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.mat-mdc-form-field-icon-suffix {
|
.mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 40px;
|
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