75 changed files with 7752 additions and 2823 deletions
File diff suppressed because it is too large
@ -0,0 +1,30 @@ |
|||
<!-- |
|||
|
|||
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-color-picker-panel"> |
|||
<div class="tb-color-picker-title" translate>color.color</div> |
|||
<tb-color-picker [formControl]="colorPickerControl"></tb-color-picker> |
|||
<div class="tb-color-picker-panel-buttons"> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="selectColor()" |
|||
[disabled]="colorPickerControl.invalid || !colorPickerControl.dirty"> |
|||
{{ 'action.select' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,36 @@ |
|||
/** |
|||
* 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-color-picker-panel { |
|||
width: 328px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
.tb-color-picker-title { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
line-height: 24px; |
|||
letter-spacing: 0.25px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
} |
|||
.tb-color-picker-panel-buttons { |
|||
height: 60px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
gap: 16px; |
|||
justify-content: flex-end; |
|||
align-items: flex-end; |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
///
|
|||
/// 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 { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { UntypedFormControl } from '@angular/forms'; |
|||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-color-picker-panel', |
|||
templateUrl: './color-picker-panel.component.html', |
|||
providers: [], |
|||
styleUrls: ['./color-picker-panel.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class ColorPickerPanelComponent extends PageComponent implements OnInit { |
|||
|
|||
@Input() |
|||
color: string; |
|||
|
|||
@Input() |
|||
popover: TbPopoverComponent<ColorPickerPanelComponent>; |
|||
|
|||
@Output() |
|||
colorSelected = new EventEmitter<string>(); |
|||
|
|||
colorPickerControl: UntypedFormControl; |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.colorPickerControl = new UntypedFormControl(this.color); |
|||
} |
|||
|
|||
selectColor() { |
|||
this.colorSelected.emit(this.colorPickerControl.value); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
:host { |
|||
.tb-close-button { |
|||
position: absolute; |
|||
top: 6px; |
|||
right: 6px; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +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 { |
|||
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, 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 { |
|||
|
|||
@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; |
|||
|
|||
iconsRowHeight = 48; |
|||
|
|||
iconsPanelHeight: string; |
|||
iconsPanelWidth: string; |
|||
|
|||
notFound = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private resourcesService: ResourcesService, |
|||
private breakpointObserver: BreakpointObserver, |
|||
private cd: ChangeDetectorRef) { |
|||
super(store); |
|||
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(); |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
///
|
|||
/// 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 { isNotEmptyStr } from '@core/utils'; |
|||
|
|||
export interface MaterialIcon { |
|||
name: string; |
|||
displayName?: string; |
|||
tags: string[]; |
|||
} |
|||
|
|||
export const iconByName = (icons: Array<MaterialIcon>, name: string): MaterialIcon => icons.find(i => i.name === name); |
|||
|
|||
const searchIconTags = (icon: MaterialIcon, searchText: string): boolean => |
|||
!!icon.tags.find(t => t.toUpperCase().includes(searchText.toUpperCase())); |
|||
|
|||
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>, chunkSize: number): Array<MaterialIcon> => icons.slice(0, chunkSize * 4); |
|||
|
|||
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, chunkSize); |
|||
} else { |
|||
return icons; |
|||
} |
|||
}), |
|||
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; |
|||
}) |
|||
); |
|||
Binary file not shown.
File diff suppressed because it is too large
Loading…
Reference in new issue