85 changed files with 2790 additions and 184 deletions
@ -0,0 +1,13 @@ |
|||||
|
{ |
||||
|
"widgetsBundle": { |
||||
|
"alias": "buttons", |
||||
|
"title": "Buttons", |
||||
|
"image": null, |
||||
|
"description": null, |
||||
|
"order": 7500, |
||||
|
"name": "Buttons" |
||||
|
}, |
||||
|
"widgetTypeFqns": [ |
||||
|
"action_button" |
||||
|
] |
||||
|
} |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,77 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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. |
||||
|
|
||||
|
--> |
||||
|
<ng-container [formGroup]="actionButtonWidgetConfigForm"> |
||||
|
<tb-datasources |
||||
|
*ngIf="!widgetEditMode" |
||||
|
[configMode]="basicMode" |
||||
|
hideDatasourceLabel |
||||
|
hideDataKeys |
||||
|
forceSingleDatasource |
||||
|
formControlName="datasources"> |
||||
|
</tb-datasources> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.action-button.behavior</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.action-button.on-click-hint' | translate}}" translate>widgets.action-button.on-click</div> |
||||
|
<tb-widget-action-settings fxFlex |
||||
|
panelTitle="widgets.action-button.on-click" |
||||
|
[callbacks]="callbacks" |
||||
|
[widgetType]="widgetType" |
||||
|
formControlName="onClickAction"> |
||||
|
</tb-widget-action-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.button-state.activated-state-hint' | translate}}" translate>widgets.button-state.activated-state</div> |
||||
|
<tb-get-value-action-settings fxFlex |
||||
|
panelTitle="widgets.button-state.activated-state" |
||||
|
[valueType]="valueType.BOOLEAN" |
||||
|
stateLabel="widgets.button-state.activated" |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
[widgetType]="widgetType" |
||||
|
formControlName="activatedState"></tb-get-value-action-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.button-state.disabled-state-hint' | translate}}" translate>widgets.button-state.disabled-state</div> |
||||
|
<tb-get-value-action-settings fxFlex |
||||
|
panelTitle="widgets.button-state.disabled-state" |
||||
|
[valueType]="valueType.BOOLEAN" |
||||
|
stateLabel="widgets.button-state.disabled" |
||||
|
[aliasController]="aliasController" |
||||
|
[targetDevice]="targetDevice" |
||||
|
[widgetType]="widgetType" |
||||
|
formControlName="disabledState"></tb-get-value-action-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
||||
|
<tb-widget-button-appearance |
||||
|
[borderRadius]="actionButtonWidgetConfigForm.get('borderRadius').value" |
||||
|
formControlName="appearance"> |
||||
|
</tb-widget-button-appearance> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widget-config.card-border-radius' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
@ -0,0 +1,139 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { Component } from '@angular/core'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
||||
|
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
||||
|
import { |
||||
|
actionDescriptorToAction, |
||||
|
Datasource, |
||||
|
defaultWidgetAction, |
||||
|
TargetDevice, |
||||
|
WidgetAction, |
||||
|
WidgetConfig, |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { guid } from '@core/utils'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models'; |
||||
|
import { |
||||
|
actionButtonDefaultSettings, |
||||
|
ActionButtonWidgetSettings |
||||
|
} from '@home/components/widget/lib/button/action-button-widget.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-action-button-basic-config', |
||||
|
templateUrl: './action-button-basic-config.component.html', |
||||
|
styleUrls: ['../basic-config.scss'] |
||||
|
}) |
||||
|
export class ActionButtonBasicConfigComponent extends BasicWidgetConfigComponent { |
||||
|
|
||||
|
get targetDevice(): TargetDevice { |
||||
|
const datasources: Datasource[] = this.actionButtonWidgetConfigForm.get('datasources').value; |
||||
|
return getTargetDeviceFromDatasources(datasources); |
||||
|
} |
||||
|
|
||||
|
valueType = ValueType; |
||||
|
|
||||
|
actionButtonWidgetConfigForm: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected widgetConfigComponent: WidgetConfigComponent, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store, widgetConfigComponent); |
||||
|
} |
||||
|
|
||||
|
protected configForm(): UntypedFormGroup { |
||||
|
return this.actionButtonWidgetConfigForm; |
||||
|
} |
||||
|
|
||||
|
protected onConfigSet(configData: WidgetConfigComponentData) { |
||||
|
const settings: ActionButtonWidgetSettings = {...actionButtonDefaultSettings, ...(configData.config.settings || {})}; |
||||
|
const onClickAction = this.getOnClickAction(configData.config); |
||||
|
this.actionButtonWidgetConfigForm = this.fb.group({ |
||||
|
datasources: [configData.config.datasources, []], |
||||
|
|
||||
|
onClickAction: [onClickAction, []], |
||||
|
activatedState: [settings.activatedState, []], |
||||
|
disabledState: [settings.disabledState, []], |
||||
|
|
||||
|
appearance: [settings.appearance, []], |
||||
|
|
||||
|
borderRadius: [configData.config.borderRadius, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
||||
|
|
||||
|
this.widgetConfig.config.datasources = config.datasources; |
||||
|
this.setOnClickAction(this.widgetConfig.config, config.onClickAction); |
||||
|
|
||||
|
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
||||
|
|
||||
|
this.widgetConfig.config.settings.activatedState = config.activatedState; |
||||
|
this.widgetConfig.config.settings.disabledState = config.disabledState; |
||||
|
|
||||
|
this.widgetConfig.config.settings.appearance = config.appearance; |
||||
|
|
||||
|
this.widgetConfig.config.borderRadius = config.borderRadius; |
||||
|
|
||||
|
return this.widgetConfig; |
||||
|
} |
||||
|
|
||||
|
private getOnClickAction(config: WidgetConfig): WidgetAction { |
||||
|
let clickAction: WidgetAction; |
||||
|
const actions = config.actions; |
||||
|
if (actions && actions.click) { |
||||
|
const descriptors = actions.click; |
||||
|
if (descriptors?.length) { |
||||
|
const descriptor = descriptors[0]; |
||||
|
clickAction = actionDescriptorToAction(descriptor); |
||||
|
} |
||||
|
} |
||||
|
if (!clickAction) { |
||||
|
clickAction = defaultWidgetAction(); |
||||
|
} |
||||
|
return clickAction; |
||||
|
} |
||||
|
|
||||
|
private setOnClickAction(config: WidgetConfig, clickAction: WidgetAction): void { |
||||
|
let actions = config.actions; |
||||
|
if (!actions) { |
||||
|
actions = {}; |
||||
|
config.actions = actions; |
||||
|
} |
||||
|
let descriptors = actions.click; |
||||
|
if (!descriptors) { |
||||
|
descriptors = []; |
||||
|
actions.click = descriptors; |
||||
|
} |
||||
|
let descriptor = descriptors[0]; |
||||
|
if (!descriptor) { |
||||
|
descriptor = { |
||||
|
id: guid(), |
||||
|
name: 'onClick', |
||||
|
icon: 'more_horiz', |
||||
|
...clickAction |
||||
|
}; |
||||
|
descriptors[0] = descriptor; |
||||
|
} else { |
||||
|
descriptors[0] = {...descriptor, ...clickAction}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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-action-widget-error-container { |
||||
|
position: absolute; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
z-index: 1; |
||||
|
.tb-action-widget-error-panel { |
||||
|
display: flex; |
||||
|
padding: 4px 4px 4px 12px; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
border-radius: 4px; |
||||
|
background-color: #fff2f3; |
||||
|
box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2); |
||||
|
.tb-action-widget-error-text { |
||||
|
font-size: 12px; |
||||
|
font-style: normal; |
||||
|
font-weight: 400; |
||||
|
line-height: 16px; |
||||
|
color: rgba(209, 39, 48, 1); |
||||
|
} |
||||
|
.tb-action-widget-error-clear { |
||||
|
color: rgba(209, 39, 48, 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-action-button-widget" [style.pointer-events]="ctx.isEdit ? 'none' : 'all'"> |
||||
|
<div class="tb-action-button-widget-title-panel"> |
||||
|
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
||||
|
</div> |
||||
|
<tb-widget-button |
||||
|
[appearance]="appearance" |
||||
|
[borderRadius]="borderRadius" |
||||
|
[disabled]="disabled" |
||||
|
[activated]="activated" |
||||
|
(clicked)="onClick($event)"> |
||||
|
</tb-widget-button> |
||||
|
<div *ngIf="error" class="tb-action-widget-error-container"> |
||||
|
<div class="tb-action-widget-error-panel"> |
||||
|
<div class="tb-action-widget-error-text" [innerHTML]="error | safe: 'html'"></div> |
||||
|
<button class="tb-action-widget-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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-action-button-widget { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
|
||||
|
> div.tb-action-button-widget-title-panel { |
||||
|
position: absolute; |
||||
|
top: 12px; |
||||
|
left: 12px; |
||||
|
right: 12px; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; |
||||
|
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models'; |
||||
|
import { ImagePipe } from '@shared/pipe/image.pipe'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
import { |
||||
|
actionButtonDefaultSettings, |
||||
|
ActionButtonWidgetSettings |
||||
|
} from '@home/components/widget/lib/button/action-button-widget.models'; |
||||
|
import { WidgetButtonAppearance } from '@shared/components/button/widget-button.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-action-button-widget', |
||||
|
templateUrl: './action-button-widget.component.html', |
||||
|
styleUrls: ['../action/action-widget.scss', './action-button-widget.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class ActionButtonWidgetComponent extends |
||||
|
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { |
||||
|
|
||||
|
settings: ActionButtonWidgetSettings; |
||||
|
|
||||
|
disabled = false; |
||||
|
activated = false; |
||||
|
|
||||
|
appearance: WidgetButtonAppearance; |
||||
|
borderRadius = '4px'; |
||||
|
|
||||
|
constructor(protected imagePipe: ImagePipe, |
||||
|
protected sanitizer: DomSanitizer, |
||||
|
protected cd: ChangeDetectorRef) { |
||||
|
super(cd); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
super.ngOnInit(); |
||||
|
this.settings = {...actionButtonDefaultSettings, ...this.ctx.settings}; |
||||
|
|
||||
|
this.appearance = this.settings.appearance; |
||||
|
|
||||
|
const activatedStateSettings = |
||||
|
{...this.settings.activatedState, actionLabel: this.ctx.translate.instant('widgets.button-state.activated-state')}; |
||||
|
this.createValueGetter(activatedStateSettings, ValueType.BOOLEAN, { |
||||
|
next: (value) => this.onActivated(value) |
||||
|
}); |
||||
|
|
||||
|
const disabledStateSettings = |
||||
|
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.button-state.disabled-state')}; |
||||
|
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, { |
||||
|
next: (value) => this.onDisabled(value) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
super.ngAfterViewInit(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
super.ngOnDestroy(); |
||||
|
} |
||||
|
|
||||
|
public onInit() { |
||||
|
super.onInit(); |
||||
|
this.borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
public onClick($event: MouseEvent) { |
||||
|
if (!this.ctx.isEdit && !this.ctx.isPreview) { |
||||
|
this.ctx.actionsApi.click($event); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onActivated(value: boolean): void { |
||||
|
this.activated = !!value; |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
private onDisabled(value: boolean): void { |
||||
|
this.disabled = !!value; |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
WidgetButtonAppearance, |
||||
|
widgetButtonDefaultAppearance |
||||
|
} from '@shared/components/button/widget-button.models'; |
||||
|
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models'; |
||||
|
|
||||
|
export interface ActionButtonWidgetSettings { |
||||
|
appearance: WidgetButtonAppearance; |
||||
|
activatedState: GetValueSettings<boolean>; |
||||
|
disabledState: GetValueSettings<boolean>; |
||||
|
} |
||||
|
|
||||
|
export const actionButtonDefaultSettings: ActionButtonWidgetSettings = { |
||||
|
appearance: widgetButtonDefaultAppearance, |
||||
|
activatedState: { |
||||
|
action: GetValueAction.DO_NOTHING, |
||||
|
defaultValue: false, |
||||
|
getAttribute: { |
||||
|
key: 'state', |
||||
|
scope: null, |
||||
|
subscribeForUpdates: false |
||||
|
}, |
||||
|
getTimeSeries: { |
||||
|
key: 'state', |
||||
|
subscribeForUpdates: false |
||||
|
}, |
||||
|
dataToValue: { |
||||
|
type: DataToValueType.NONE, |
||||
|
compareToValue: true, |
||||
|
dataToValueFunction: '/* Should return boolean value */\nreturn data;' |
||||
|
} |
||||
|
}, |
||||
|
disabledState: { |
||||
|
action: GetValueAction.DO_NOTHING, |
||||
|
defaultValue: false, |
||||
|
getAttribute: { |
||||
|
key: 'state', |
||||
|
scope: null, |
||||
|
subscribeForUpdates: false |
||||
|
}, |
||||
|
getTimeSeries: { |
||||
|
key: 'state', |
||||
|
subscribeForUpdates: false |
||||
|
}, |
||||
|
dataToValue: { |
||||
|
type: DataToValueType.NONE, |
||||
|
compareToValue: true, |
||||
|
dataToValueFunction: '/* Should return boolean value */\nreturn data;' |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
@ -0,0 +1,42 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-action-settings-panel" [formGroup]="widgetActionFormGroup"> |
||||
|
<div class="tb-action-settings-title">{{ panelTitle | translate }}</div> |
||||
|
<div class="tb-action-settings-panel-content"> |
||||
|
<tb-widget-action |
||||
|
formControlName="widgetAction" |
||||
|
[callbacks]="callbacks" |
||||
|
[widgetType]="widgetType"> |
||||
|
</tb-widget-action> |
||||
|
</div> |
||||
|
<div class="tb-action-settings-panel-buttons"> |
||||
|
<button mat-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="applyWidgetAction()" |
||||
|
[disabled]="widgetActionFormGroup.invalid || !widgetActionFormGroup.dirty"> |
||||
|
{{ 'action.apply' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,88 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { merge } from 'rxjs'; |
||||
|
import { |
||||
|
DataToValueType, |
||||
|
GetValueAction, |
||||
|
getValueActions, |
||||
|
getValueActionTranslations, |
||||
|
GetValueSettings |
||||
|
} from '@shared/models/action-widget-settings.models'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
import { TargetDevice, WidgetAction, widgetType } from '@shared/models/widget.models'; |
||||
|
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { WidgetService } from '@core/http/widget.service'; |
||||
|
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-action-settings-panel', |
||||
|
templateUrl: './widget-action-settings-panel.component.html', |
||||
|
providers: [], |
||||
|
styleUrls: ['./action-settings-panel.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class WidgetActionSettingsPanelComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
widgetAction: WidgetAction; |
||||
|
|
||||
|
@Input() |
||||
|
panelTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
widgetType: widgetType; |
||||
|
|
||||
|
@Input() |
||||
|
callbacks: WidgetActionCallbacks; |
||||
|
|
||||
|
@Input() |
||||
|
popover: TbPopoverComponent<WidgetActionSettingsPanelComponent>; |
||||
|
|
||||
|
@Output() |
||||
|
widgetActionApplied = new EventEmitter<WidgetAction>(); |
||||
|
|
||||
|
widgetActionFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.widgetActionFormGroup = this.fb.group( |
||||
|
{ |
||||
|
widgetAction: [this.widgetAction, []] |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.popover?.hide(); |
||||
|
} |
||||
|
|
||||
|
applyWidgetAction() { |
||||
|
const widgetAction: WidgetAction = this.widgetActionFormGroup.get('widgetAction').getRawValue(); |
||||
|
this.widgetActionApplied.emit(widgetAction); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,136 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
forwardRef, |
||||
|
HostBinding, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
ViewContainerRef, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { MatButton } from '@angular/material/button'; |
||||
|
import { TbPopoverService } from '@shared/components/popover.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { WidgetAction, widgetActionTypeTranslationMap, widgetType } from '@shared/models/widget.models'; |
||||
|
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; |
||||
|
import { |
||||
|
WidgetActionSettingsPanelComponent |
||||
|
} from '@home/components/widget/lib/settings/common/action/widget-action-settings-panel.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-action-settings', |
||||
|
templateUrl: './action-settings-button.component.html', |
||||
|
styleUrls: ['./action-settings-button.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => WidgetActionSettingsComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class WidgetActionSettingsComponent implements OnInit, ControlValueAccessor { |
||||
|
|
||||
|
@HostBinding('style.overflow') |
||||
|
overflow = 'hidden'; |
||||
|
|
||||
|
@Input() |
||||
|
panelTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
widgetType: widgetType; |
||||
|
|
||||
|
@Input() |
||||
|
callbacks: WidgetActionCallbacks; |
||||
|
|
||||
|
@Input() |
||||
|
disabled = false; |
||||
|
|
||||
|
modelValue: WidgetAction; |
||||
|
|
||||
|
displayValue: string; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private translate: TranslateService, |
||||
|
private popoverService: TbPopoverService, |
||||
|
private renderer: Renderer2, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private cd: ChangeDetectorRef) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
if (this.disabled !== isDisabled) { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: WidgetAction): void { |
||||
|
this.modelValue = value; |
||||
|
this.updateDisplayValue(); |
||||
|
} |
||||
|
|
||||
|
openActionSettingsPopup($event: Event, matButton: MatButton) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const trigger = matButton._elementRef.nativeElement; |
||||
|
if (this.popoverService.hasPopover(trigger)) { |
||||
|
this.popoverService.hidePopover(trigger); |
||||
|
} else { |
||||
|
const ctx: any = { |
||||
|
widgetAction: this.modelValue, |
||||
|
panelTitle: this.panelTitle, |
||||
|
widgetType: this.widgetType, |
||||
|
callbacks: this.callbacks |
||||
|
}; |
||||
|
const widgetActionSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
||||
|
this.viewContainerRef, WidgetActionSettingsPanelComponent, |
||||
|
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, |
||||
|
ctx, |
||||
|
{}, |
||||
|
{}, {}, true); |
||||
|
widgetActionSettingsPanelPopover.tbComponentRef.instance.popover = widgetActionSettingsPanelPopover; |
||||
|
widgetActionSettingsPanelPopover.tbComponentRef.instance.widgetActionApplied.subscribe((widgetAction) => { |
||||
|
widgetActionSettingsPanelPopover.hide(); |
||||
|
this.modelValue = widgetAction; |
||||
|
this.updateDisplayValue(); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateDisplayValue() { |
||||
|
this.displayValue = this.translate.instant(widgetActionTypeTranslationMap.get(this.modelValue.type)); |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-form-panel no-padding no-border" [formGroup]="appearanceFormGroup"> |
||||
|
<tb-image-cards-select rowHeight="3:1" |
||||
|
[cols]="{columns: 2, |
||||
|
breakpoints: { |
||||
|
'lt-sm': 1 |
||||
|
}}" |
||||
|
label="{{ 'widgets.button.layout' | translate }}" formControlName="type"> |
||||
|
<tb-image-cards-select-option *ngFor="let type of widgetButtonTypes" |
||||
|
[value]="type" |
||||
|
[image]="widgetButtonTypeImageMap.get(type)"> |
||||
|
{{ widgetButtonTypeTranslationMap.get(type) | translate }} |
||||
|
</tb-image-cards-select-option> |
||||
|
</tb-image-cards-select> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="autoScale"> |
||||
|
{{ 'widgets.button.auto-scale' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel"> |
||||
|
{{ 'widgets.button.label' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> |
||||
|
{{ 'widgets.button.icon' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select> |
||||
|
<tb-material-icon-select asBoxInput |
||||
|
iconClearButton |
||||
|
formControlName="icon"> |
||||
|
</tb-material-icon-select> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.button.color-palette' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.button.main</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="mainColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<mat-divider vertical></mat-divider> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<div translate>widgets.button.background</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="backgroundColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked" formGroupName="customStyle"> |
||||
|
<mat-expansion-panel class="tb-settings"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-form-panel-title" translate>widgets.button.custom-styles</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between" *ngFor="let state of widgetButtonStates"> |
||||
|
<div>{{ widgetButtonStateTranslationMap.get(state) | translate }}</div> |
||||
|
<tb-widget-button-custom-style |
||||
|
[state]="state" |
||||
|
[appearance]="this.appearanceFormGroup.value" |
||||
|
[borderRadius]="borderRadius" |
||||
|
[formControlName]="state"> |
||||
|
</tb-widget-button-custom-style> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,141 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { |
||||
|
WidgetButtonAppearance, |
||||
|
widgetButtonStates, widgetButtonStatesTranslations, |
||||
|
widgetButtonTypeImages, |
||||
|
widgetButtonTypes, |
||||
|
widgetButtonTypeTranslations |
||||
|
} from '@shared/components/button/widget-button.models'; |
||||
|
import { merge } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-button-appearance', |
||||
|
templateUrl: './widget-button-appearance.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => WidgetButtonAppearanceComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class WidgetButtonAppearanceComponent implements OnInit, ControlValueAccessor { |
||||
|
|
||||
|
@Input() |
||||
|
disabled = false; |
||||
|
|
||||
|
@Input() |
||||
|
borderRadius: string; |
||||
|
|
||||
|
widgetButtonTypes = widgetButtonTypes; |
||||
|
|
||||
|
widgetButtonTypeTranslationMap = widgetButtonTypeTranslations; |
||||
|
widgetButtonTypeImageMap = widgetButtonTypeImages; |
||||
|
|
||||
|
widgetButtonStates = widgetButtonStates; |
||||
|
widgetButtonStateTranslationMap = widgetButtonStatesTranslations; |
||||
|
|
||||
|
modelValue: WidgetButtonAppearance; |
||||
|
|
||||
|
appearanceFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.appearanceFormGroup = this.fb.group({ |
||||
|
type: [null, []], |
||||
|
autoScale: [null, []], |
||||
|
showLabel: [null, []], |
||||
|
label: [null, []], |
||||
|
showIcon: [null, []], |
||||
|
icon: [null, []], |
||||
|
iconSize: [null, []], |
||||
|
iconSizeUnit: [null, []], |
||||
|
mainColor: [null, []], |
||||
|
backgroundColor: [null, []] |
||||
|
}); |
||||
|
const customStyle = this.fb.group({}); |
||||
|
for (const state of widgetButtonStates) { |
||||
|
customStyle.addControl(state, this.fb.control(null, [])); |
||||
|
} |
||||
|
this.appearanceFormGroup.addControl('customStyle', customStyle); |
||||
|
this.appearanceFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
merge(this.appearanceFormGroup.get('showLabel').valueChanges, |
||||
|
this.appearanceFormGroup.get('showIcon').valueChanges) |
||||
|
.subscribe(() => { |
||||
|
this.updateValidators(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.appearanceFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.appearanceFormGroup.enable({emitEvent: false}); |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: WidgetButtonAppearance): void { |
||||
|
this.modelValue = value; |
||||
|
this.appearanceFormGroup.patchValue( |
||||
|
value, {emitEvent: false} |
||||
|
); |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
this.modelValue = this.appearanceFormGroup.getRawValue(); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
|
||||
|
private updateValidators(): void { |
||||
|
const showLabel: boolean = this.appearanceFormGroup.get('showLabel').value; |
||||
|
const showIcon: boolean = this.appearanceFormGroup.get('showIcon').value; |
||||
|
if (showLabel) { |
||||
|
this.appearanceFormGroup.get('label').enable(); |
||||
|
} else { |
||||
|
this.appearanceFormGroup.get('label').disable(); |
||||
|
} |
||||
|
if (showIcon) { |
||||
|
this.appearanceFormGroup.get('icon').enable(); |
||||
|
this.appearanceFormGroup.get('iconSize').enable(); |
||||
|
this.appearanceFormGroup.get('iconSizeUnit').enable(); |
||||
|
} else { |
||||
|
this.appearanceFormGroup.get('icon').disable(); |
||||
|
this.appearanceFormGroup.get('iconSize').disable(); |
||||
|
this.appearanceFormGroup.get('iconSizeUnit').disable(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-widget-button-custom-style-panel" [formGroup]="customStyleFormGroup"> |
||||
|
<div class="tb-widget-button-custom-style-title">{{ widgetButtonStateTranslationMap.get(state) | translate }}</div> |
||||
|
<div class="tb-widget-button-custom-style-panel-content"> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="overrideMainColor"> |
||||
|
{{ 'widgets.button.main' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="mainColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="overrideBackgroundColor"> |
||||
|
{{ 'widgets.button.background' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="backgroundColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="overrideDropShadow"> |
||||
|
{{ 'widgets.button.shadow' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<mat-chip-listbox formControlName="dropShadow"> |
||||
|
<mat-chip-option [selectable]="!customStyleFormGroup.get('dropShadow').value" [value]="true">{{ 'widgets.button.enabled' | translate }}</mat-chip-option> |
||||
|
<mat-chip-option [selectable]="customStyleFormGroup.get('dropShadow').value" [value]="false">{{ 'widgets.button.disabled' | translate }}</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
</div> |
||||
|
<div class="tb-widget-button-custom-style-preview"> |
||||
|
<div class="tb-widget-button-custom-style-preview-title" translate> |
||||
|
widgets.button.preview |
||||
|
</div> |
||||
|
<tb-widget-button |
||||
|
[appearance]="previewAppearance" |
||||
|
[borderRadius]="borderRadius" |
||||
|
disableEvents |
||||
|
[hovered]="state === widgetButtonState.hovered" |
||||
|
[pressed]="state === widgetButtonState.pressed" |
||||
|
[activated]="state === widgetButtonState.activated" |
||||
|
[disabled]="state === widgetButtonState.disabled"> |
||||
|
</tb-widget-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-widget-button-custom-style-panel-buttons"> |
||||
|
<button *ngIf="copyFromStates?.length" |
||||
|
#copyStyleButton |
||||
|
class="tb-nowrap" |
||||
|
mat-stroked-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
[matMenuTriggerFor]="styleSourcesMenu" [matMenuTriggerData]="{menuWidth: copyStyleButton._elementRef.nativeElement.clientWidth}"> |
||||
|
{{ 'widgets.button.copy-style-from' | translate }} |
||||
|
</button> |
||||
|
<mat-menu #styleSourcesMenu="matMenu"> |
||||
|
<ng-template matMenuContent let-menuWidth="menuWidth"> |
||||
|
<div [style.min-width.px]="menuWidth"> |
||||
|
<button mat-menu-item *ngFor="let state of copyFromStates" (click)="copyStyle(state)">{{ widgetButtonStateTranslationMap.get(state) | translate }}</button> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-menu> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="applyCustomStyle()" |
||||
|
[disabled]="customStyleFormGroup.invalid || !customStyleFormGroup.dirty"> |
||||
|
{{ 'action.apply' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,68 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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 '../../../../../../../../../scss/constants'; |
||||
|
|
||||
|
.tb-widget-button-custom-style-panel { |
||||
|
width: 530px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16px; |
||||
|
@media #{$mat-lt-md} { |
||||
|
width: 90vw; |
||||
|
} |
||||
|
.tb-widget-button-custom-style-panel-content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16px; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
.tb-widget-button-custom-style-title { |
||||
|
font-size: 16px; |
||||
|
font-weight: 500; |
||||
|
line-height: 24px; |
||||
|
letter-spacing: 0.25px; |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
} |
||||
|
.tb-widget-button-custom-style-preview { |
||||
|
flex: 1; |
||||
|
background: rgba(0, 0, 0, 0.04); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 12px 16px 24px 16px; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
.tb-widget-button-custom-style-preview-title { |
||||
|
align-self: stretch; |
||||
|
font-size: 16px; |
||||
|
font-style: normal; |
||||
|
font-weight: 500; |
||||
|
line-height: 24px; |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
tb-widget-button { |
||||
|
width: 200px; |
||||
|
height: 60px; |
||||
|
} |
||||
|
} |
||||
|
.tb-widget-button-custom-style-panel-buttons { |
||||
|
height: 40px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
gap: 16px; |
||||
|
justify-content: flex-end; |
||||
|
align-items: flex-end; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,171 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
defaultBackgroundColorDisabled, |
||||
|
defaultMainColorDisabled, |
||||
|
WidgetButtonAppearance, |
||||
|
WidgetButtonCustomStyle, |
||||
|
WidgetButtonState, |
||||
|
widgetButtonStates, |
||||
|
widgetButtonStatesTranslations, |
||||
|
WidgetButtonType |
||||
|
} from '@shared/components/button/widget-button.models'; |
||||
|
import { merge } from 'rxjs'; |
||||
|
import { deepClone } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-button-custom-style-panel', |
||||
|
templateUrl: './widget-button-custom-style-panel.component.html', |
||||
|
providers: [], |
||||
|
styleUrls: ['./widget-button-custom-style-panel.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class WidgetButtonCustomStylePanelComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
appearance: WidgetButtonAppearance; |
||||
|
|
||||
|
@Input() |
||||
|
borderRadius: string; |
||||
|
|
||||
|
@Input() |
||||
|
state: WidgetButtonState; |
||||
|
|
||||
|
@Input() |
||||
|
customStyle: WidgetButtonCustomStyle; |
||||
|
|
||||
|
@Input() |
||||
|
popover: TbPopoverComponent<WidgetButtonCustomStylePanelComponent>; |
||||
|
|
||||
|
@Output() |
||||
|
customStyleApplied = new EventEmitter<WidgetButtonCustomStyle>(); |
||||
|
|
||||
|
widgetButtonStateTranslationMap = widgetButtonStatesTranslations; |
||||
|
|
||||
|
widgetButtonState = WidgetButtonState; |
||||
|
|
||||
|
previewAppearance: WidgetButtonAppearance; |
||||
|
|
||||
|
copyFromStates: WidgetButtonState[]; |
||||
|
|
||||
|
customStyleFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
protected store: Store<AppState>, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.copyFromStates = widgetButtonStates.filter(state => |
||||
|
state !== this.state && !!this.appearance.customStyle[state]); |
||||
|
this.customStyleFormGroup = this.fb.group( |
||||
|
{ |
||||
|
overrideMainColor: [false, []], |
||||
|
mainColor: [null, []], |
||||
|
overrideBackgroundColor: [false, []], |
||||
|
backgroundColor: [null, []], |
||||
|
overrideDropShadow: [false, []], |
||||
|
dropShadow: [false, []] |
||||
|
} |
||||
|
); |
||||
|
merge(this.customStyleFormGroup.get('overrideMainColor').valueChanges, |
||||
|
this.customStyleFormGroup.get('overrideBackgroundColor').valueChanges, |
||||
|
this.customStyleFormGroup.get('overrideDropShadow').valueChanges) |
||||
|
.subscribe(() => { |
||||
|
this.updateValidators(); |
||||
|
}); |
||||
|
this.customStyleFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updatePreviewAppearance(); |
||||
|
}); |
||||
|
this.setStyle(this.customStyle); |
||||
|
} |
||||
|
|
||||
|
copyStyle(state: WidgetButtonState) { |
||||
|
this.customStyle = deepClone(this.appearance.customStyle[state]); |
||||
|
this.setStyle(this.customStyle); |
||||
|
this.customStyleFormGroup.markAsDirty(); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.popover?.hide(); |
||||
|
} |
||||
|
|
||||
|
applyCustomStyle() { |
||||
|
const customStyle: WidgetButtonCustomStyle = this.customStyleFormGroup.value; |
||||
|
this.customStyleApplied.emit(customStyle); |
||||
|
} |
||||
|
|
||||
|
private setStyle(customStyle?: WidgetButtonCustomStyle): void { |
||||
|
let mainColor = this.state === WidgetButtonState.disabled ? defaultMainColorDisabled : this.appearance.mainColor; |
||||
|
if (customStyle?.overrideMainColor) { |
||||
|
mainColor = customStyle?.mainColor; |
||||
|
} |
||||
|
let backgroundColor = this.state === WidgetButtonState.disabled ? defaultBackgroundColorDisabled : this.appearance.backgroundColor; |
||||
|
if (customStyle?.overrideBackgroundColor) { |
||||
|
backgroundColor = customStyle?.backgroundColor; |
||||
|
} |
||||
|
let dropShadow = this.appearance.type === WidgetButtonType.basic ? false : true; |
||||
|
if (customStyle?.overrideDropShadow) { |
||||
|
dropShadow = customStyle?.dropShadow; |
||||
|
} |
||||
|
this.customStyleFormGroup.patchValue({ |
||||
|
overrideMainColor: customStyle?.overrideMainColor, |
||||
|
mainColor, |
||||
|
overrideBackgroundColor: customStyle?.overrideBackgroundColor, |
||||
|
backgroundColor, |
||||
|
overrideDropShadow: customStyle?.overrideDropShadow, |
||||
|
dropShadow |
||||
|
}, {emitEvent: false}); |
||||
|
this.updateValidators(); |
||||
|
this.updatePreviewAppearance(); |
||||
|
} |
||||
|
|
||||
|
private updateValidators() { |
||||
|
const overrideMainColor: boolean = this.customStyleFormGroup.get('overrideMainColor').value; |
||||
|
const overrideBackgroundColor: boolean = this.customStyleFormGroup.get('overrideBackgroundColor').value; |
||||
|
const overrideDropShadow: boolean = this.customStyleFormGroup.get('overrideDropShadow').value; |
||||
|
|
||||
|
if (overrideMainColor) { |
||||
|
this.customStyleFormGroup.get('mainColor').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.customStyleFormGroup.get('mainColor').disable({emitEvent: false}); |
||||
|
} |
||||
|
if (overrideBackgroundColor) { |
||||
|
this.customStyleFormGroup.get('backgroundColor').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.customStyleFormGroup.get('backgroundColor').disable({emitEvent: false}); |
||||
|
} |
||||
|
if (overrideDropShadow) { |
||||
|
this.customStyleFormGroup.get('dropShadow').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.customStyleFormGroup.get('dropShadow').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updatePreviewAppearance() { |
||||
|
this.previewAppearance = {...this.appearance}; |
||||
|
this.previewAppearance.customStyle[this.state] = this.customStyleFormGroup.value; |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-widget-button-custom-style"> |
||||
|
<div class="tb-widget-button-preview-panel tb-primary-fill"> |
||||
|
<tb-widget-button |
||||
|
[appearance]="previewAppearance" |
||||
|
[borderRadius]="borderRadius" |
||||
|
disableEvents |
||||
|
[hovered]="state === widgetButtonState.hovered" |
||||
|
[pressed]="state === widgetButtonState.pressed" |
||||
|
[activated]="state === widgetButtonState.activated" |
||||
|
[disabled]="state === widgetButtonState.disabled"> |
||||
|
</tb-widget-button> |
||||
|
<button *ngIf="modelValue" |
||||
|
mat-icon-button |
||||
|
[matTooltip]="'widgets.button.clear-style' | translate" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-mat-32" |
||||
|
(click)="clearStyle()"> |
||||
|
<tb-icon>mdi:broom</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<button mat-icon-button |
||||
|
class="tb-mat-32" |
||||
|
#matIconButton |
||||
|
(click)="openButtonCustomStylePopup($event, matIconButton)"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
@ -0,0 +1,46 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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 '../../../../../../../../../scss/constants'; |
||||
|
|
||||
|
.tb-widget-button-custom-style { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
button.mat-mdc-icon-button { |
||||
|
color: rgba(0,0,0,0.56); |
||||
|
} |
||||
|
.tb-widget-button-preview-panel { |
||||
|
width: 148px; |
||||
|
height: 48px; |
||||
|
padding: 8px 12px; |
||||
|
border-radius: 4px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
tb-widget-button { |
||||
|
width: 84px; |
||||
|
height: 100%; |
||||
|
} |
||||
|
@media #{$mat-gt-xs} { |
||||
|
width: 168px; |
||||
|
tb-widget-button { |
||||
|
width: 104px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,157 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
SimpleChanges, |
||||
|
ViewContainerRef, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { |
||||
|
WidgetButtonAppearance, |
||||
|
WidgetButtonCustomStyle, |
||||
|
WidgetButtonState |
||||
|
} from '@shared/components/button/widget-button.models'; |
||||
|
import { TbPopoverService } from '@shared/components/popover.service'; |
||||
|
import { MatIconButton } from '@angular/material/button'; |
||||
|
import { |
||||
|
WidgetButtonCustomStylePanelComponent |
||||
|
} from '@home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-button-custom-style', |
||||
|
templateUrl: './widget-button-custom-style.component.html', |
||||
|
styleUrls: ['./widget-button-custom-style.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => WidgetButtonCustomStyleComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class WidgetButtonCustomStyleComponent implements OnInit, OnChanges, ControlValueAccessor { |
||||
|
|
||||
|
@Input() |
||||
|
disabled = false; |
||||
|
|
||||
|
@Input() |
||||
|
appearance: WidgetButtonAppearance; |
||||
|
|
||||
|
@Input() |
||||
|
borderRadius: string; |
||||
|
|
||||
|
@Input() |
||||
|
state: WidgetButtonState; |
||||
|
|
||||
|
widgetButtonState = WidgetButtonState; |
||||
|
|
||||
|
modelValue: WidgetButtonCustomStyle; |
||||
|
|
||||
|
previewAppearance: WidgetButtonAppearance; |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private popoverService: TbPopoverService, |
||||
|
private renderer: Renderer2, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private cd: ChangeDetectorRef) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.updatePreviewAppearance(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange) { |
||||
|
if (propName === 'appearance') { |
||||
|
this.updatePreviewAppearance(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(_isDisabled: boolean): void { |
||||
|
} |
||||
|
|
||||
|
writeValue(value: WidgetButtonCustomStyle): void { |
||||
|
this.modelValue = value; |
||||
|
this.updatePreviewAppearance(); |
||||
|
} |
||||
|
|
||||
|
clearStyle() { |
||||
|
this.updateModel(null); |
||||
|
} |
||||
|
|
||||
|
openButtonCustomStylePopup($event: Event, matButton: MatIconButton) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const trigger = matButton._elementRef.nativeElement; |
||||
|
if (this.popoverService.hasPopover(trigger)) { |
||||
|
this.popoverService.hidePopover(trigger); |
||||
|
} else { |
||||
|
const ctx: any = { |
||||
|
appearance: this.appearance, |
||||
|
borderRadius: this.borderRadius, |
||||
|
state: this.state, |
||||
|
customStyle: this.modelValue |
||||
|
}; |
||||
|
const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, |
||||
|
this.viewContainerRef, WidgetButtonCustomStylePanelComponent, |
||||
|
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, |
||||
|
ctx, |
||||
|
{}, |
||||
|
{}, {}, true); |
||||
|
widgetButtonCustomStylePanelPopover.tbComponentRef.instance.popover = widgetButtonCustomStylePanelPopover; |
||||
|
widgetButtonCustomStylePanelPopover.tbComponentRef.instance.customStyleApplied.subscribe((customStyle) => { |
||||
|
widgetButtonCustomStylePanelPopover.hide(); |
||||
|
this.updateModel(customStyle); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateModel(value: WidgetButtonCustomStyle): void { |
||||
|
this.modelValue = value; |
||||
|
this.updatePreviewAppearance(); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
|
||||
|
private updatePreviewAppearance() { |
||||
|
this.previewAppearance = {...this.appearance}; |
||||
|
if (this.modelValue) { |
||||
|
this.previewAppearance.customStyle[this.state] = this.modelValue; |
||||
|
} |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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. |
||||
|
|
||||
|
--> |
||||
|
<button #widgetButton |
||||
|
mat-button |
||||
|
type="button" |
||||
|
class="tb-widget-button" |
||||
|
(click)="clicked.emit($event)" |
||||
|
(mousedown)="mousePressed = true" |
||||
|
(mouseup)="mousePressed = false" |
||||
|
(mouseleave)="mousePressed = false" |
||||
|
[disabled]="disabled" |
||||
|
[class]="'tb-'+appearance.type" |
||||
|
[class.tb-pressed]="mousePressed" |
||||
|
[class.tb-pressed-state]="pressed" |
||||
|
[class.tb-hover-state]="hovered" |
||||
|
[class.tb-active-state]="activated" |
||||
|
[class.tb-disabled-state]="disabled" |
||||
|
[style.border-radius]="borderRadius" |
||||
|
[style.pointer-events]="disableEvents ? 'none' : ''"> |
||||
|
<div #widgetButtonContent class="tb-widget-button-content" *ngIf="appearance.showIcon || appearance.showLabel"> |
||||
|
<tb-icon matButtonIcon *ngIf="appearance.showIcon" [style]="iconStyle">{{ appearance.icon }}</tb-icon> |
||||
|
<span *ngIf="appearance.showLabel" class="tb-widget-button-label">{{ appearance.label }}</span> |
||||
|
</div> |
||||
|
</button> |
||||
@ -0,0 +1,189 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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. |
||||
|
*/ |
||||
|
$defaultMainColor: #3F52DD; |
||||
|
$defaultBackgroundColor: #FFFFFF; |
||||
|
$defaultBoxShadowColor: rgba(0, 0, 0, 0.08); |
||||
|
$defaultDisabledBoxShadowColor: rgba(0, 0, 0, 0); |
||||
|
|
||||
|
$defaultMainColorDisabled: rgba(0, 0, 0, 0.38); |
||||
|
$defaultBackgroundColorDisabled: rgba(0, 0, 0, 0.03); |
||||
|
|
||||
|
$mainColorEnabled: var(--tb-widget-button-main-color-enabled, $defaultMainColor); |
||||
|
$backgroundColorEnabled: var(--tb-widget-button-background-color-enabled, $defaultBackgroundColor); |
||||
|
$boxShadowColorEnabled: var(--tb-widget-button-box-shadow-color-enabled, $defaultBoxShadowColor); |
||||
|
|
||||
|
$mainColorHovered: var(--tb-widget-button-main-color-hovered, $defaultMainColor); |
||||
|
$backgroundColorHovered: var(--tb-widget-button-background-color-hovered, $defaultBackgroundColor); |
||||
|
$boxShadowColorHovered: var(--tb-widget-button-box-shadow-color-hovered, $defaultBoxShadowColor); |
||||
|
$mainColorHoveredFilled: var(--tb-widget-button-main-color-hovered-filled, #263BD7); // main.darken(6) |
||||
|
|
||||
|
$mainColorPressed: var(--tb-widget-button-main-color-pressed, $defaultMainColor); |
||||
|
$backgroundColorPressed: var(--tb-widget-button-background-color-pressed, $defaultBackgroundColor); |
||||
|
$boxShadowColorPressed: var(--tb-widget-button-box-shadow-color-pressed, $defaultBoxShadowColor); |
||||
|
$mainColorPressedFilled: var(--tb-widget-button-main-color-pressed-filled, #2234BD); // main.darken(12) |
||||
|
$mainColorPressedRipple: var(--tb-widget-button-main-color-pressed-ripple, rgba(63, 82, 221, 0.1)); // Alpha(Main, 0.1) |
||||
|
$mainColorPressedRippleFilled: var(--tb-widget-button-main-color-pressed-ripple-filled, #1D2DA3); // main.darken(18) |
||||
|
|
||||
|
$mainColorActivated: var(--tb-widget-button-main-color-activated, $defaultMainColor); |
||||
|
$backgroundColorActivated: var(--tb-widget-button-background-color-activated, $defaultBackgroundColor); |
||||
|
$boxShadowColorActivated: var(--tb-widget-button-box-shadow-color-activated, $defaultBoxShadowColor); |
||||
|
$mainColorActivatedFilled: var(--tb-widget-button-main-color-activated-filled, #2234BD); // main.darken(12) |
||||
|
|
||||
|
$mainColorDisabled: var(--tb-widget-button-main-color-disabled, $defaultMainColorDisabled); |
||||
|
$backgroundColorDisabled: var(--tb-widget-button-background-color-disabled, $defaultBackgroundColorDisabled); |
||||
|
$boxShadowColorDisabled: var(--tb-widget-button-box-shadow-color-activated, $defaultBoxShadowColor); |
||||
|
|
||||
|
|
||||
|
@mixin _tb-widget-button-styles($main, $background, $boxShadow) { |
||||
|
color: $main; |
||||
|
background-color: $background; |
||||
|
box-shadow: 0 4px 8px 0 $boxShadow; |
||||
|
&.tb-outlined { |
||||
|
border: 1px solid $main; |
||||
|
} |
||||
|
&.tb-filled { |
||||
|
color: $background; |
||||
|
background-color: $main; |
||||
|
} |
||||
|
&.tb-underlined { |
||||
|
border-bottom: 2px solid $main; |
||||
|
} |
||||
|
&.tb-basic { |
||||
|
background-color: transparent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.mat-mdc-button.mat-mdc-button-base.tb-widget-button { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
padding: 8px 12px; |
||||
|
.mdc-button__label { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
.tb-widget-button-content { |
||||
|
width: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
gap: 4px; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
.mat-icon { |
||||
|
margin: 0; |
||||
|
} |
||||
|
span.tb-widget-button-label { |
||||
|
line-height: normal; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-button-persistent-ripple::before { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
|
||||
|
@include _tb-widget-button-styles($mainColorEnabled, $backgroundColorEnabled, $boxShadowColorEnabled); |
||||
|
|
||||
|
&:not(:disabled):not(.tb-disabled-state) { |
||||
|
&:hover, &.tb-hover-state { |
||||
|
&:not(:active):not(.tb-active-state) { |
||||
|
&:not(.tb-filled) { |
||||
|
.mat-mdc-button-persistent-ripple::before { |
||||
|
opacity: 0.04; |
||||
|
background-color: $mainColorHovered; |
||||
|
} |
||||
|
} |
||||
|
&.tb-filled { |
||||
|
.mat-mdc-button-persistent-ripple::before { |
||||
|
opacity: 1; |
||||
|
background-color: $mainColorHoveredFilled; |
||||
|
} |
||||
|
} |
||||
|
@include _tb-widget-button-styles($mainColorHovered, $backgroundColorHovered, $boxShadowColorHovered); |
||||
|
} |
||||
|
} |
||||
|
&.tb-pressed-state { |
||||
|
&:not(.tb-filled) { |
||||
|
.mat-mdc-button-ripple { |
||||
|
background-color: $mainColorPressedRipple; |
||||
|
} |
||||
|
} |
||||
|
&.tb-filled { |
||||
|
.mat-mdc-button-ripple { |
||||
|
background-color: $mainColorPressedRippleFilled; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
&.tb-pressed { |
||||
|
&:not(.tb-filled) { |
||||
|
.mat-ripple-element { |
||||
|
background-color: $mainColorPressedRipple; |
||||
|
} |
||||
|
} |
||||
|
&.tb-filled { |
||||
|
.mat-ripple-element { |
||||
|
background-color: $mainColorPressedRippleFilled; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
&.tb-pressed, &.tb-pressed-state { |
||||
|
&:not(.tb-filled) { |
||||
|
.mat-mdc-button-persistent-ripple::before { |
||||
|
opacity: 0.12; |
||||
|
background-color: $mainColorPressed; |
||||
|
} |
||||
|
} |
||||
|
&.tb-filled { |
||||
|
.mat-mdc-button-persistent-ripple::before { |
||||
|
opacity: 1; |
||||
|
background-color: $mainColorPressedFilled; |
||||
|
} |
||||
|
} |
||||
|
@include _tb-widget-button-styles($mainColorPressed, $backgroundColorPressed, $boxShadowColorPressed); |
||||
|
} |
||||
|
&:active, &.tb-active-state { |
||||
|
&:not(.tb-pressed):not(.tb-pressed-state) { |
||||
|
&:not(.tb-filled) { |
||||
|
.mat-mdc-button-persistent-ripple::before { |
||||
|
opacity: 0.12; |
||||
|
background-color: $mainColorActivated; |
||||
|
} |
||||
|
} |
||||
|
&.tb-filled { |
||||
|
.mat-mdc-button-persistent-ripple::before { |
||||
|
opacity: 1; |
||||
|
background-color: $mainColorActivatedFilled; |
||||
|
} |
||||
|
} |
||||
|
@include _tb-widget-button-styles($mainColorActivated, $backgroundColorActivated, $boxShadowColorActivated); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:disabled, &.tb-disabled-state { |
||||
|
&:not(.tb-filled) { |
||||
|
@include _tb-widget-button-styles($mainColorDisabled, $backgroundColorDisabled, $boxShadowColorDisabled); |
||||
|
} |
||||
|
&.tb-filled { |
||||
|
@include _tb-widget-button-styles($backgroundColorDisabled, $mainColorDisabled, $boxShadowColorDisabled); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,178 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
AfterViewInit, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
EventEmitter, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
Output, |
||||
|
Renderer2, |
||||
|
SimpleChanges, ViewChild, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
generateWidgetButtonAppearanceCss, |
||||
|
widgetButtonDefaultAppearance |
||||
|
} from '@shared/components/button/widget-button.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
import { ComponentStyle, iconStyle } from '@shared/models/widget-settings.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { ResizeObserver } from '@juggle/resize-observer'; |
||||
|
|
||||
|
const initialButtonHeight = 60; |
||||
|
const horizontalLayoutPadding = 24; |
||||
|
const verticalLayoutPadding = 16; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget-button', |
||||
|
templateUrl: './widget-button.component.html', |
||||
|
styleUrls: ['./widget-button.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class WidgetButtonComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { |
||||
|
|
||||
|
@ViewChild('widgetButton', {read: ElementRef}) |
||||
|
widgetButton: ElementRef<HTMLElement>; |
||||
|
|
||||
|
@ViewChild('widgetButtonContent', {static: false}) |
||||
|
widgetButtonContent: ElementRef<HTMLElement>; |
||||
|
|
||||
|
@Input() |
||||
|
appearance = widgetButtonDefaultAppearance; |
||||
|
|
||||
|
@Input() |
||||
|
borderRadius = '4px'; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
disabled = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
activated = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
hovered = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
pressed = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
disableEvents = false; |
||||
|
|
||||
|
@Output() |
||||
|
clicked = new EventEmitter<MouseEvent>(); |
||||
|
|
||||
|
iconStyle: ComponentStyle = {}; |
||||
|
|
||||
|
mousePressed = false; |
||||
|
|
||||
|
private buttonResize$: ResizeObserver; |
||||
|
|
||||
|
private appearanceCssClass: string; |
||||
|
|
||||
|
constructor(private renderer: Renderer2, |
||||
|
private elementRef: ElementRef, |
||||
|
private utils: UtilsService) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.updateAppearance(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange) { |
||||
|
if (propName === 'appearance') { |
||||
|
this.updateAppearance(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
this.updateAutoScale(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
if (this.buttonResize$) { |
||||
|
this.buttonResize$.disconnect(); |
||||
|
} |
||||
|
this.clearAppearanceCss(); |
||||
|
} |
||||
|
|
||||
|
private updateAppearance(): void { |
||||
|
this.clearAppearanceCss(); |
||||
|
if (this.appearance.showIcon) { |
||||
|
this.iconStyle = iconStyle(this.appearance.iconSize, this.appearance.iconSizeUnit); |
||||
|
} |
||||
|
const appearanceCss = generateWidgetButtonAppearanceCss(this.appearance); |
||||
|
this.appearanceCssClass = this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, |
||||
|
'tb-widget-button', appearanceCss); |
||||
|
this.updateAutoScale(); |
||||
|
} |
||||
|
|
||||
|
private clearAppearanceCss(): void { |
||||
|
if (this.appearanceCssClass) { |
||||
|
this.utils.clearCssElement(this.renderer, this.appearanceCssClass, this.elementRef?.nativeElement); |
||||
|
this.appearanceCssClass = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateAutoScale() { |
||||
|
if (this.buttonResize$) { |
||||
|
this.buttonResize$.disconnect(); |
||||
|
} |
||||
|
if (this.widgetButton && this.widgetButtonContent) { |
||||
|
if (this.appearance.autoScale) { |
||||
|
this.buttonResize$ = new ResizeObserver(() => { |
||||
|
this.onResize(); |
||||
|
}); |
||||
|
this.buttonResize$.observe(this.widgetButton.nativeElement); |
||||
|
this.onResize(); |
||||
|
} else { |
||||
|
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', 'none'); |
||||
|
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', '100%'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onResize() { |
||||
|
const height = this.widgetButton.nativeElement.getBoundingClientRect().height; |
||||
|
const buttonScale = height / initialButtonHeight; |
||||
|
const paddingScale = Math.min(buttonScale, 1); |
||||
|
const buttonWidth = this.widgetButton.nativeElement.getBoundingClientRect().width - (horizontalLayoutPadding * paddingScale); |
||||
|
const buttonHeight = this.widgetButton.nativeElement.getBoundingClientRect().height - (verticalLayoutPadding * paddingScale); |
||||
|
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', `scale(1)`); |
||||
|
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', 'auto'); |
||||
|
const contentWidth = this.widgetButtonContent.nativeElement.getBoundingClientRect().width; |
||||
|
const contentHeight = this.widgetButtonContent.nativeElement.getBoundingClientRect().height; |
||||
|
const maxScale = Math.max(1, buttonScale); |
||||
|
const scale = Math.min(Math.min(buttonWidth / contentWidth, buttonHeight / contentHeight), maxScale); |
||||
|
const targetWidth = buttonWidth / scale; |
||||
|
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', targetWidth + 'px'); |
||||
|
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', `scale(${scale})`); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,259 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { cssUnit } from '@shared/models/widget-settings.models'; |
||||
|
import tinycolor from 'tinycolor2'; |
||||
|
|
||||
|
const defaultMainColor = '#3F52DD'; |
||||
|
const defaultBackgroundColor = '#FFFFFF'; |
||||
|
|
||||
|
export const defaultMainColorDisabled = 'rgba(0, 0, 0, 0.38)'; |
||||
|
export const defaultBackgroundColorDisabled = 'rgba(0, 0, 0, 0.03)'; |
||||
|
|
||||
|
const defaultBoxShadowColor = 'rgba(0, 0, 0, 0.08)'; |
||||
|
const defaultDisabledBoxShadowColor = 'rgba(0, 0, 0, 0)'; |
||||
|
|
||||
|
export enum WidgetButtonType { |
||||
|
outlined = 'outlined', |
||||
|
filled = 'filled', |
||||
|
underlined = 'underlined', |
||||
|
basic = 'basic' |
||||
|
} |
||||
|
|
||||
|
export const widgetButtonTypes = Object.keys(WidgetButtonType) as WidgetButtonType[]; |
||||
|
|
||||
|
export const widgetButtonTypeTranslations = new Map<WidgetButtonType, string>( |
||||
|
[ |
||||
|
[WidgetButtonType.outlined, 'widgets.button.outlined'], |
||||
|
[WidgetButtonType.filled, 'widgets.button.filled'], |
||||
|
[WidgetButtonType.underlined, 'widgets.button.underlined'], |
||||
|
[WidgetButtonType.basic, 'widgets.button.basic'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export const widgetButtonTypeImages = new Map<WidgetButtonType, string>( |
||||
|
[ |
||||
|
[WidgetButtonType.outlined, 'assets/widget/button/outlined.svg'], |
||||
|
[WidgetButtonType.filled, 'assets/widget/button/filled.svg'], |
||||
|
[WidgetButtonType.underlined, 'assets/widget/button/underlined.svg'], |
||||
|
[WidgetButtonType.basic, 'assets/widget/button/basic.svg'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export enum WidgetButtonState { |
||||
|
enabled = 'enabled', |
||||
|
hovered = 'hovered', |
||||
|
pressed = 'pressed', |
||||
|
activated = 'activated', |
||||
|
disabled = 'disabled' |
||||
|
} |
||||
|
|
||||
|
export const widgetButtonStates = Object.keys(WidgetButtonState) as WidgetButtonState[]; |
||||
|
|
||||
|
export const widgetButtonStatesTranslations = new Map<WidgetButtonState, string>( |
||||
|
[ |
||||
|
[WidgetButtonState.enabled, 'widgets.button-state.enabled'], |
||||
|
[WidgetButtonState.hovered, 'widgets.button-state.hovered'], |
||||
|
[WidgetButtonState.pressed, 'widgets.button-state.pressed'], |
||||
|
[WidgetButtonState.activated, 'widgets.button-state.activated'], |
||||
|
[WidgetButtonState.disabled, 'widgets.button-state.disabled'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export interface WidgetButtonCustomStyle { |
||||
|
overrideMainColor?: boolean; |
||||
|
mainColor?: string; |
||||
|
overrideBackgroundColor?: boolean; |
||||
|
backgroundColor?: string; |
||||
|
overrideDropShadow?: boolean; |
||||
|
dropShadow?: boolean; |
||||
|
} |
||||
|
|
||||
|
export type WidgetButtonCustomStyles = Record<WidgetButtonState, WidgetButtonCustomStyle>; |
||||
|
|
||||
|
export interface WidgetButtonAppearance { |
||||
|
type: WidgetButtonType; |
||||
|
autoScale: boolean; |
||||
|
showLabel: boolean; |
||||
|
label: string; |
||||
|
showIcon: boolean; |
||||
|
icon: string; |
||||
|
iconSize: number; |
||||
|
iconSizeUnit: cssUnit; |
||||
|
mainColor: string; |
||||
|
backgroundColor: string; |
||||
|
customStyle: WidgetButtonCustomStyles; |
||||
|
} |
||||
|
|
||||
|
export const widgetButtonDefaultAppearance: WidgetButtonAppearance = { |
||||
|
type: WidgetButtonType.outlined, |
||||
|
autoScale: true, |
||||
|
showLabel: true, |
||||
|
label: 'Button', |
||||
|
showIcon: true, |
||||
|
icon: 'home', |
||||
|
iconSize: 24, |
||||
|
iconSizeUnit: 'px', |
||||
|
mainColor: defaultMainColor, |
||||
|
backgroundColor: defaultBackgroundColor, |
||||
|
customStyle: { |
||||
|
enabled: null, |
||||
|
hovered: null, |
||||
|
pressed: null, |
||||
|
activated: null, |
||||
|
disabled: null |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const mainColorVarPrefix = '--tb-widget-button-main-color-'; |
||||
|
const backgroundColorVarPrefix = '--tb-widget-button-background-color-'; |
||||
|
const boxShadowColorVarPrefix = '--tb-widget-button-box-shadow-color-'; |
||||
|
|
||||
|
abstract class ButtonStateCssGenerator { |
||||
|
|
||||
|
constructor() {} |
||||
|
|
||||
|
public generateStateCss(appearance: WidgetButtonAppearance): string { |
||||
|
let mainColor = this.getMainColor(appearance); |
||||
|
let backgroundColor = this.getBackgroundColor(appearance); |
||||
|
const shadowEnabledByDefault = appearance.type !== WidgetButtonType.basic; |
||||
|
let shadowColor = shadowEnabledByDefault ? defaultBoxShadowColor : defaultDisabledBoxShadowColor; |
||||
|
const stateCustomStyle = appearance.customStyle[this.state]; |
||||
|
if (stateCustomStyle?.overrideMainColor && stateCustomStyle?.mainColor) { |
||||
|
mainColor = stateCustomStyle.mainColor; |
||||
|
} |
||||
|
if (stateCustomStyle?.overrideBackgroundColor && stateCustomStyle?.backgroundColor) { |
||||
|
backgroundColor = stateCustomStyle.backgroundColor; |
||||
|
} |
||||
|
if (stateCustomStyle?.overrideDropShadow) { |
||||
|
shadowColor = !!stateCustomStyle.dropShadow ? defaultBoxShadowColor : defaultDisabledBoxShadowColor; |
||||
|
} |
||||
|
|
||||
|
let css = `${mainColorVarPrefix}${this.state}: ${mainColor};\n`+ |
||||
|
`${backgroundColorVarPrefix}${this.state}: ${backgroundColor};\n`+ |
||||
|
`${boxShadowColorVarPrefix}${this.state}: ${shadowColor};`; |
||||
|
const additionalCss = this.generateAdditionalStateCss(mainColor, backgroundColor); |
||||
|
if (additionalCss) { |
||||
|
css += `\n${additionalCss}`; |
||||
|
} |
||||
|
return css; |
||||
|
} |
||||
|
|
||||
|
protected abstract get state(): WidgetButtonState; |
||||
|
|
||||
|
protected getMainColor(appearance: WidgetButtonAppearance): string { |
||||
|
return appearance.mainColor || defaultMainColor; |
||||
|
} |
||||
|
|
||||
|
protected getBackgroundColor(appearance: WidgetButtonAppearance): string { |
||||
|
return appearance.backgroundColor || defaultBackgroundColor; |
||||
|
} |
||||
|
|
||||
|
protected generateAdditionalStateCss(_mainColor: string, _backgroundColor: string): string { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class EnabledButtonStateCssGenerator extends ButtonStateCssGenerator { |
||||
|
|
||||
|
protected get state(): WidgetButtonState { |
||||
|
return WidgetButtonState.enabled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class HoveredButtonStateCssGenerator extends ButtonStateCssGenerator { |
||||
|
|
||||
|
protected get state(): WidgetButtonState { |
||||
|
return WidgetButtonState.hovered; |
||||
|
} |
||||
|
|
||||
|
protected generateAdditionalStateCss(mainColor: string): string { |
||||
|
const mainColorHoveredFilled = darkenColor(mainColor, 6); |
||||
|
return `--tb-widget-button-main-color-hovered-filled: ${mainColorHoveredFilled};`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class PressedButtonStateCssGenerator extends ButtonStateCssGenerator { |
||||
|
|
||||
|
protected get state(): WidgetButtonState { |
||||
|
return WidgetButtonState.pressed; |
||||
|
} |
||||
|
|
||||
|
protected generateAdditionalStateCss(mainColor: string): string { |
||||
|
const mainColorPressedFilled = darkenColor(mainColor, 12); |
||||
|
const mainColorInstance = tinycolor(mainColor); |
||||
|
const mainColorPressedRipple = mainColorInstance.setAlpha(mainColorInstance.getAlpha() * 0.1).toRgbString(); |
||||
|
const mainColorPressedRippleFilled = darkenColor(mainColor, 18); |
||||
|
return `--tb-widget-button-main-color-pressed-filled: ${mainColorPressedFilled};\n`+ |
||||
|
`--tb-widget-button-main-color-pressed-ripple: ${mainColorPressedRipple};\n`+ |
||||
|
`--tb-widget-button-main-color-pressed-ripple-filled: ${mainColorPressedRippleFilled};`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class ActivatedButtonStateCssGenerator extends ButtonStateCssGenerator { |
||||
|
|
||||
|
protected get state(): WidgetButtonState { |
||||
|
return WidgetButtonState.activated; |
||||
|
} |
||||
|
|
||||
|
protected generateAdditionalStateCss(mainColor: string): string { |
||||
|
const mainColorActivatedFilled = darkenColor(mainColor, 12); |
||||
|
return `--tb-widget-button-main-color-activated-filled: ${mainColorActivatedFilled};`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class DisabledButtonStateCssGenerator extends ButtonStateCssGenerator { |
||||
|
|
||||
|
protected get state(): WidgetButtonState { |
||||
|
return WidgetButtonState.disabled; |
||||
|
} |
||||
|
|
||||
|
protected getMainColor(): string { |
||||
|
return defaultMainColorDisabled; |
||||
|
} |
||||
|
|
||||
|
protected getBackgroundColor(): string { |
||||
|
return defaultBackgroundColorDisabled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const buttonStateCssGeneratorsMap = new Map<WidgetButtonState, ButtonStateCssGenerator>( |
||||
|
[ |
||||
|
[WidgetButtonState.enabled, new EnabledButtonStateCssGenerator()], |
||||
|
[WidgetButtonState.hovered, new HoveredButtonStateCssGenerator()], |
||||
|
[WidgetButtonState.pressed, new PressedButtonStateCssGenerator()], |
||||
|
[WidgetButtonState.activated, new ActivatedButtonStateCssGenerator()], |
||||
|
[WidgetButtonState.disabled, new DisabledButtonStateCssGenerator()] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
const widgetButtonCssSelector = '.mat-mdc-button.mat-mdc-button-base.tb-widget-button'; |
||||
|
|
||||
|
export const generateWidgetButtonAppearanceCss = (appearance: WidgetButtonAppearance): string => { |
||||
|
let statesCss = ''; |
||||
|
for (const state of widgetButtonStates) { |
||||
|
const generator = buttonStateCssGeneratorsMap.get(state); |
||||
|
statesCss += `\n${generator.generateStateCss(appearance)}`; |
||||
|
} |
||||
|
return `${widgetButtonCssSelector} {\n`+ |
||||
|
`${statesCss}\n`+ |
||||
|
`}`; |
||||
|
}; |
||||
|
|
||||
|
const darkenColor = (inputColor: string, amount: number): string => { |
||||
|
const input = tinycolor(inputColor); |
||||
|
return input.darken(amount).toRgbString(); |
||||
|
}; |
||||
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
Loading…
Reference in new issue