Browse Source

UI: Segmented button

pull/12142/head
Artem Dzhereleiko 2 years ago
parent
commit
53da8cd47e
  1. 1
      application/src/main/data/json/system/widget_bundles/buttons.json
  2. 1
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  3. 43
      application/src/main/data/json/system/widget_types/segmented_button.json
  4. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  5. 271
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html
  6. 145
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts
  7. 33
      ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.html
  8. 37
      ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.scss
  9. 118
      ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts
  10. 346
      ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts
  11. 263
      ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html
  12. 134
      ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts
  13. 92
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.html
  14. 68
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.scss
  15. 210
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts
  16. 44
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html
  17. 46
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.scss
  18. 169
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts
  19. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  20. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  21. 3
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  22. 42
      ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html
  23. 189
      ui-ngx/src/app/shared/components/button/widget-button-toggle.component.scss
  24. 234
      ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts
  25. 3
      ui-ngx/src/app/shared/shared.module.ts
  26. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  27. 1
      ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg
  28. 1
      ui-ngx/src/assets/widget/segmented-button/squared-layout.svg

1
application/src/main/data/json/system/widget_bundles/buttons.json

@ -11,6 +11,7 @@
"action_button", "action_button",
"command_button", "command_button",
"toggle_button", "toggle_button",
"segmented_button",
"power_button" "power_button"
] ]
} }

1
application/src/main/data/json/system/widget_bundles/control_widgets.json

@ -11,6 +11,7 @@
"single_switch", "single_switch",
"command_button", "command_button",
"toggle_button", "toggle_button",
"segmented_button",
"power_button", "power_button",
"slider", "slider",
"control_widgets.switch_control", "control_widgets.switch_control",

43
application/src/main/data/json/system/widget_types/segmented_button.json

File diff suppressed because one or more lines are too long

6
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -143,6 +143,9 @@ import {
UnreadNotificationBasicConfigComponent UnreadNotificationBasicConfigComponent
} from '@home/components/widget/config/basic/cards/unread-notification-basic-config.component'; } from '@home/components/widget/config/basic/cards/unread-notification-basic-config.component';
import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/basic/scada/scada-symbol-basic-config.component'; import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/basic/scada/scada-symbol-basic-config.component';
import {
SegmentedButtonBasicConfigComponent
} from '@home/components/widget/config/basic/button/segmented-button-basic-config.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -174,6 +177,7 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/
BarChartWithLabelsBasicConfigComponent, BarChartWithLabelsBasicConfigComponent,
SingleSwitchBasicConfigComponent, SingleSwitchBasicConfigComponent,
ActionButtonBasicConfigComponent, ActionButtonBasicConfigComponent,
SegmentedButtonBasicConfigComponent,
CommandButtonBasicConfigComponent, CommandButtonBasicConfigComponent,
PowerButtonBasicConfigComponent, PowerButtonBasicConfigComponent,
SliderBasicConfigComponent, SliderBasicConfigComponent,
@ -227,6 +231,7 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/
BarChartWithLabelsBasicConfigComponent, BarChartWithLabelsBasicConfigComponent,
SingleSwitchBasicConfigComponent, SingleSwitchBasicConfigComponent,
ActionButtonBasicConfigComponent, ActionButtonBasicConfigComponent,
SegmentedButtonBasicConfigComponent,
CommandButtonBasicConfigComponent, CommandButtonBasicConfigComponent,
PowerButtonBasicConfigComponent, PowerButtonBasicConfigComponent,
SliderBasicConfigComponent, SliderBasicConfigComponent,
@ -272,6 +277,7 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-bar-chart-with-labels-basic-config': BarChartWithLabelsBasicConfigComponent, 'tb-bar-chart-with-labels-basic-config': BarChartWithLabelsBasicConfigComponent,
'tb-single-switch-basic-config': SingleSwitchBasicConfigComponent, 'tb-single-switch-basic-config': SingleSwitchBasicConfigComponent,
'tb-action-button-basic-config': ActionButtonBasicConfigComponent, 'tb-action-button-basic-config': ActionButtonBasicConfigComponent,
'tb-segmented-button-basic-config': SegmentedButtonBasicConfigComponent,
'tb-command-button-basic-config': CommandButtonBasicConfigComponent, 'tb-command-button-basic-config': CommandButtonBasicConfigComponent,
'tb-power-button-basic-config': PowerButtonBasicConfigComponent, 'tb-power-button-basic-config': PowerButtonBasicConfigComponent,
'tb-slider-basic-config': SliderBasicConfigComponent, 'tb-slider-basic-config': SliderBasicConfigComponent,

271
ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html

@ -0,0 +1,271 @@
<!--
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]="segmentedButtonWidgetConfigForm">
<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.button-state.initial-state-hint' | translate}}" translate>widgets.button-state.initial-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.button-state.initial-state' | translate }}"
[valueType]="valueType.BOOLEAN"
trueLabel="{{ 'widgets.button-state.left' | translate }}"
falseLabel="{{ 'widgets.button-state.right' | translate }}"
stateLabel="{{ 'widgets.button-state.initial' | translate }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.action-button.button-click-hint' | translate}}" translate>widgets.action-button.left-button-click</div>
<tb-widget-action-settings class="flex-1"
panelTitle="{{ 'widgets.action-button.left-button-click' | translate }}"
[callbacks]="callbacks"
[widgetType]="widgetType"
formControlName="leftButtonClick">
</tb-widget-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.action-button.button-click-hint' | translate}}" translate>widgets.action-button.right-button-click</div>
<tb-widget-action-settings class="flex-1"
panelTitle="{{ 'widgets.action-button.right-button-click' | translate }}"
[callbacks]="callbacks"
[widgetType]="widgetType"
formControlName="rightButtonClick">
</tb-widget-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 class="flex-1"
panelTitle="{{ 'widgets.button-state.disabled-state' | translate }}"
[valueType]="valueType.BOOLEAN"
stateLabel="{{ 'widgets.button-state.disabled' | translate }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="3:1"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of segmentedButtonLayouts"
[value]="layout"
[image]="segmentedButtonLayoutImageMap.get(layout)">
{{ segmentedButtonLayoutTranslationMap.get(layout) | 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 space-between">
<div translate>widgets.segmented-button.card-border</div>
<div class="flex flex-row items-center gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="cardBorder" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden">px</div>
</mat-form-field>
<tb-color-input asBoxInput
formControlName="cardBorderColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>widgets.segmented-button.button-appearance</div>
<tb-toggle-select [(ngModel)]="segmentedButtonAppearanceType" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="left">{{ 'widgets.segmented-button.left' | translate }}</tb-toggle-option>
<tb-toggle-option value="right">{{ 'widgets.segmented-button.right' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="leftAppearance" [class.!hidden]="segmentedButtonAppearanceType !== 'left'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.button.label' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.label').value">
</tb-font-settings>
</div>
</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 class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="rightAppearance" [class.!hidden]="segmentedButtonAppearanceType !== 'right'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.button.label' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.label').value">
</tb-font-settings>
</div>
</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 class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>widgets.segmented-button.color-styles</div>
<tb-toggle-select [(ngModel)]="segmentedButtonColorStylesType" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="selected">{{ 'widgets.segmented-button.selected' | translate }}</tb-toggle-option>
<tb-toggle-option value="unselected">{{ 'widgets.segmented-button.unselected' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="selectedStyle" [class.!hidden]="segmentedButtonColorStylesType !== 'selected'">
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.button.color-palette' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.button.main</div>
<tb-color-input asBoxInput
formControlName="mainColor">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<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 class="flex flex-row flex-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 widgetButtonToggleStates">
<div>{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}</div>
<tb-widget-button-toggle-custom-style
[value]="true"
[state]="state"
[appearance]="segmentedButtonWidgetConfigForm.get('appearance').value"
[autoScale]="true"
[formControlName]="state">
</tb-widget-button-toggle-custom-style>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="unselectedStyle" [class.!hidden]="segmentedButtonColorStylesType !== 'unselected'">
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.button.color-palette' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.button.main</div>
<tb-color-input asBoxInput
formControlName="mainColor">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<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 class="flex flex-row flex-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 widgetButtonToggleStates">
<div>{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}</div>
<tb-widget-button-toggle-custom-style
[value]="false"
[state]="state"
[appearance]="segmentedButtonWidgetConfigForm.get('appearance').value"
[autoScale]="true"
[formControlName]="state">
</tb-widget-button-toggle-custom-style>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
</div>
</ng-container>

145
ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts

@ -0,0 +1,145 @@
///
/// 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 { Datasource, TargetDevice, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { ValueType } from '@shared/models/constants';
import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models';
import {
SegmentedButtonAppearanceType,
SegmentedButtonColorStylesType,
segmentedButtonDefaultSettings,
segmentedButtonLayoutBorder,
segmentedButtonLayoutImages,
segmentedButtonLayouts,
segmentedButtonLayoutTranslations,
SegmentedButtonWidgetSettings,
WidgetButtonToggleState,
widgetButtonToggleStatesTranslations
} from '@home/components/widget/lib/button/segmented-button-widget.models';
@Component({
selector: 'tb-segmented-button-basic-config',
templateUrl: './segmented-button-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class SegmentedButtonBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
const datasources: Datasource[] = this.segmentedButtonWidgetConfigForm.get('datasources').value;
return getTargetDeviceFromDatasources(datasources);
}
segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'left';
segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected';
widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[];
widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations;
segmentedButtonLayouts = segmentedButtonLayouts;
segmentedButtonLayoutTranslationMap = segmentedButtonLayoutTranslations;
segmentedButtonLayoutImageMap = segmentedButtonLayoutImages;
segmentedButtonLayoutBorderMap = segmentedButtonLayoutBorder;
valueType = ValueType;
segmentedButtonWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.segmentedButtonWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: SegmentedButtonWidgetSettings = {...segmentedButtonDefaultSettings, ...(configData.config.settings || {})};
this.segmentedButtonWidgetConfigForm = this.fb.group({
datasources: [configData.config.datasources, []],
initialState: [settings.initialState, []],
leftButtonClick: [settings.leftButtonClick, []],
rightButtonClick: [settings.rightButtonClick, []],
disabledState: [settings.disabledState, []],
appearance: this.fb.group({
layout: [settings.appearance.layout, []],
autoScale: [settings.appearance.autoScale, []],
cardBorder: [settings.appearance.cardBorder, []],
cardBorderColor: [settings.appearance.cardBorderColor, []],
leftAppearance: this.fb.group({
showLabel: [settings.appearance.leftAppearance.showLabel, []],
label: [settings.appearance.leftAppearance.label, []],
labelFont: [settings.appearance.leftAppearance.labelFont, []],
showIcon: [settings.appearance.leftAppearance.showIcon, []],
icon: [settings.appearance.leftAppearance.icon, []],
iconSize: [settings.appearance.leftAppearance.iconSize, []],
iconSizeUnit: [settings.appearance.leftAppearance.iconSizeUnit, []],
}),
rightAppearance: this.fb.group({
showLabel: [settings.appearance.rightAppearance.showLabel, []],
label: [settings.appearance.rightAppearance.label, []],
labelFont: [settings.appearance.rightAppearance.labelFont, []],
showIcon: [settings.appearance.rightAppearance.showIcon, []],
icon: [settings.appearance.rightAppearance.icon, []],
iconSize: [settings.appearance.rightAppearance.iconSize, []],
iconSizeUnit: [settings.appearance.rightAppearance.iconSizeUnit, []],
}),
selectedStyle: this.fb.group({
mainColor: [settings.appearance.selectedStyle.mainColor, []],
backgroundColor: [settings.appearance.selectedStyle.backgroundColor, []],
customStyle: this.fb.group({
enabled: [settings.appearance.selectedStyle.customStyle.enabled, []],
hovered: [settings.appearance.selectedStyle.customStyle.hovered, []],
disabled: [settings.appearance.selectedStyle.customStyle.disabled, []],
})
}),
unselectedStyle: this.fb.group({
mainColor: [settings.appearance.unselectedStyle.mainColor, []],
backgroundColor: [settings.appearance.unselectedStyle.backgroundColor, []],
customStyle: this.fb.group({
enabled: [settings.appearance.unselectedStyle.customStyle.enabled, []],
hovered: [settings.appearance.unselectedStyle.customStyle.hovered, []],
disabled: [settings.appearance.unselectedStyle.customStyle.disabled, []],
})
}),
})
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.datasources = config.datasources;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.initialState = config.initialState;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.leftButtonClick = config.leftButtonClick;
this.widgetConfig.config.settings.rightButtonClick = config.rightButtonClick;
this.widgetConfig.config.settings.appearance = config.appearance;
this.widgetConfig.config.borderRadius = this.segmentedButtonLayoutBorderMap.get(config.appearance.layout);
return this.widgetConfig;
}
}

33
ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.html

@ -0,0 +1,33 @@
<!--
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-segmented-button-widget" [class.no-pointer-events]="ctx.isEdit">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-segmented-button-container">
<tb-widget-button-toggle
class="tb-widget-button-toggle"
[appearance]="appearance"
[autoScale]="autoScale"
[disabled]="disabled || (loading$ | async)"
[value]="value"
[ctx]="ctx"
(clicked)="onClick($event)">
</tb-widget-button-toggle>
</div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
</div>

37
ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.scss

@ -0,0 +1,37 @@
/**
* 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-segmented-button-widget {
width: 100%;
height: 100%;
position: relative;
.tb-segmented-button-container {
height: 100%;
.tb-widget-button-toggle {
display: block;
height: 100%;
}
}
> div.tb-segmented-button-widget-title-panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 2;
}
}

118
ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts

@ -0,0 +1,118 @@
///
/// 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 {
ButtonToggleAppearance,
segmentedButtonDefaultSettings,
SegmentedButtonWidgetSettings
} from '@home/components/widget/lib/button/segmented-button-widget.models';
import { ComponentStyle } from '@shared/models/widget-settings.models';
import { MatButtonToggleChange } from '@angular/material/button-toggle';
@Component({
selector: 'tb-segmented-button-widget',
templateUrl: './segmented-button-widget.component.html',
styleUrls: ['../action/action-widget.scss', './segmented-button-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class SegmentedButtonWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
settings: SegmentedButtonWidgetSettings;
overlayStyle: ComponentStyle = {};
value = false;
disabled = false;
autoScale: boolean;
appearance: ButtonToggleAppearance;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
protected cd: ChangeDetectorRef) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...segmentedButtonDefaultSettings, ...this.ctx.settings};
this.autoScale = this.settings.appearance.autoScale;
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onValue(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)
});
this.appearance = this.settings.appearance;
}
ngAfterViewInit(): void {
super.ngAfterViewInit();
}
ngOnDestroy() {
super.ngOnDestroy();
}
public onInit() {
super.onInit();
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
private onValue(value: boolean): void {
const newValue = !!value;
if (this.value !== newValue) {
this.value = newValue;
this.appearance = this.settings.appearance;
this.cd.markForCheck();
}
}
private onDisabled(value: boolean): void {
const newDisabled = !!value;
if (this.disabled !== newDisabled) {
this.disabled = newDisabled;
this.cd.markForCheck();
}
}
public onClick(_$event: MatButtonToggleChange) {
if (!this.ctx.isEdit && !this.ctx.isPreview) {
if (_$event.value) {
this.ctx.actionsApi.onWidgetAction(event, this.settings.leftButtonClick);
} else {
this.ctx.actionsApi.onWidgetAction(event, this.settings.rightButtonClick);
}
}
}
}

346
ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts

@ -0,0 +1,346 @@
///
/// 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 { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
import { cssUnit, Font } from '@shared/models/widget-settings.models';
import { defaultWidgetAction, WidgetAction } from '@shared/models/widget.models';
const defaultMainColor = '#305680';
const defaultBackgroundColor = '#E8E8E8';
export const defaultMainColorDisabled = 'rgba(0, 0, 0, 0.38)';
export const defaultBackgroundColorDisabled = 'rgba(0, 0, 0, 0.03)';
export const defaultBorderColorDisabled = defaultBackgroundColorDisabled;
const defaultBorderColor = defaultBackgroundColor;
export enum SegmentedButtonLayout {
squared = 'squared',
rounded = 'rounded'
}
export type SegmentedButtonAppearanceType = 'left' | 'right';
export type SegmentedButtonColorStylesType = 'selected' | 'unselected';
export const segmentedButtonLayouts = Object.keys(SegmentedButtonLayout) as SegmentedButtonLayout[];
export const segmentedButtonLayoutTranslations = new Map<SegmentedButtonLayout, string>(
[
[SegmentedButtonLayout.squared, 'widgets.segmented-button.layout-squared'],
[SegmentedButtonLayout.rounded, 'widgets.segmented-button.layout-rounded']
]
);
export const segmentedButtonLayoutImages = new Map<SegmentedButtonLayout, string>(
[
[SegmentedButtonLayout.squared, 'assets/widget/segmented-button/squared-layout.svg'],
[SegmentedButtonLayout.rounded, 'assets/widget/segmented-button/rounded-layout.svg']
]
);
export const segmentedButtonLayoutBorder = new Map<SegmentedButtonLayout, string>(
[
[SegmentedButtonLayout.squared, '4px'],
[SegmentedButtonLayout.rounded, '40px']
]
);
export interface SegmentedButtonAppearance {
showLabel: boolean;
label: string;
labelFont: Font;
showIcon: boolean;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
}
export interface SegmentedButtonStyles {
mainColor: string;
backgroundColor: string;
customStyle: WidgetButtonToggleCustomStyles;
}
export interface ButtonToggleAppearance {
layout: SegmentedButtonLayout;
autoScale: boolean;
cardBorder: number;
cardBorderColor: string;
leftAppearance: SegmentedButtonAppearance;
rightAppearance: SegmentedButtonAppearance;
selectedStyle: SegmentedButtonStyles;
unselectedStyle: SegmentedButtonStyles;
}
export interface SegmentedButtonWidgetSettings {
initialState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
leftButtonClick: WidgetAction;
rightButtonClick: WidgetAction;
appearance: ButtonToggleAppearance;
}
export const segmentedButtonDefaultAppearance: ButtonToggleAppearance = {
layout: SegmentedButtonLayout.squared,
autoScale: true,
cardBorder: 1,
cardBorderColor: '#305680',
leftAppearance: {
showLabel: true,
label: 'Traditional',
labelFont: {
family: 'Roboto',
weight: '500',
style: 'normal',
size: 14,
sizeUnit: 'px',
lineHeight: '18px'
},
showIcon: true,
icon: 'home',
iconSize: 24,
iconSizeUnit: 'px',
},
rightAppearance: {
showLabel: true,
label: 'Hi-Perf',
labelFont: {
family: 'Roboto',
weight: '500',
style: 'normal',
size: 14,
sizeUnit: 'px',
lineHeight: '18px'
},
showIcon: true,
icon: 'home',
iconSize: 24,
iconSizeUnit: 'px',
},
selectedStyle: {
mainColor: '#FFFFFF',
backgroundColor: '#00695C',
customStyle: {
enabled: null,
hovered: null,
disabled: null
}
},
unselectedStyle: {
mainColor: '#000000C2',
backgroundColor: '#E8E8E8',
customStyle: {
enabled: null,
hovered: null,
disabled: null
}
}
}
export const segmentedButtonDefaultSettings: SegmentedButtonWidgetSettings = {
initialState: {
action: GetValueAction.DO_NOTHING,
defaultValue: true,
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
getAlarmStatus: {
severityList: null,
typeList: null
},
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
},
getTimeSeries: {
key: 'state'
},
getAlarmStatus: {
severityList: null,
typeList: null
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
leftButtonClick: defaultWidgetAction(),
rightButtonClick: defaultWidgetAction(),
appearance: segmentedButtonDefaultAppearance
};
const mainCheckedColorVarPrefix = '--tb-widget-button-toggle-main-checked-color-';
const backgroundCheckedColorVarPrefix = '--tb-widget-button-toggle-background-checked-color-';
const borderCheckedColorVarPrefix = '--tb-widget-button-toggle-border-checked-color-';
const mainUncheckedColorVarPrefix = '--tb-widget-button-toggle-main-unchecked-color-';
const backgroundUncheckedColorVarPrefix = '--tb-widget-button-toggle-background-unchecked-color-';
const borderUncheckedColorVarPrefix = '--tb-widget-button-toggle-border-unchecked-color-';
export enum WidgetButtonToggleState {
enabled = 'enabled',
hovered = 'hovered',
disabled = 'disabled'
}
export const widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[];
export const widgetButtonToggleStatesTranslations = new Map<WidgetButtonToggleState, string>(
[
[WidgetButtonToggleState.enabled, 'widgets.button-state.enabled'],
[WidgetButtonToggleState.hovered, 'widgets.button-state.hovered'],
[WidgetButtonToggleState.disabled, 'widgets.button-state.disabled']
]
);
export interface WidgetButtonToggleCustomStyle {
overrideMainColor?: boolean;
mainColor?: string;
overrideBackgroundColor?: boolean;
backgroundColor?: string;
overrideBorderColor?: boolean;
borderColor?: string;
}
export type WidgetButtonToggleCustomStyles = Record<WidgetButtonToggleState, WidgetButtonToggleCustomStyle>;
export abstract class ButtonToggleStateCssGenerator {
constructor() {}
public generateStateCss(selectedAppearance: SegmentedButtonStyles, unselectedAppearance: SegmentedButtonStyles): string {
const selectedColor = this.getColors(selectedAppearance);
const unselectedColor = this.getColors(unselectedAppearance);
return `${mainCheckedColorVarPrefix}${this.state}: ${selectedColor.mainColor};\n`+
`${backgroundCheckedColorVarPrefix}${this.state}: ${selectedColor.backgroundColor};\n`+
`${borderCheckedColorVarPrefix}${this.state}: ${selectedColor.borderColor};\n`+
`${mainUncheckedColorVarPrefix}${this.state}: ${unselectedColor.mainColor};\n`+
`${backgroundUncheckedColorVarPrefix}${this.state}: ${unselectedColor.backgroundColor};\n`+
`${borderUncheckedColorVarPrefix}${this.state}: ${unselectedColor.borderColor};`;
}
private getColors(appearance: SegmentedButtonStyles) {
let mainColor = this.getMainColor(appearance);
let backgroundColor = this.getBackgroundColor(appearance);
let borderColor = this.getBorderColor();
const stateCustomStyle = appearance.customStyle[this.state];
if (stateCustomStyle?.overrideMainColor && stateCustomStyle?.mainColor) {
mainColor = stateCustomStyle.mainColor;
}
if (stateCustomStyle?.overrideBackgroundColor && stateCustomStyle?.backgroundColor) {
backgroundColor = stateCustomStyle.backgroundColor;
}
if (stateCustomStyle?.overrideBorderColor && stateCustomStyle?.borderColor) {
borderColor = stateCustomStyle.borderColor;
}
return {
mainColor,
backgroundColor,
borderColor
}
}
protected abstract get state(): WidgetButtonToggleState;
protected getMainColor(appearance: SegmentedButtonStyles): string {
return appearance.mainColor || defaultMainColor;
}
protected getBackgroundColor(appearance: SegmentedButtonStyles): string {
return appearance.backgroundColor || defaultBackgroundColor;
}
protected getBorderColor(): string {
return defaultBorderColor;
}
}
class EnabledButtonStateCssGenerator extends ButtonToggleStateCssGenerator {
protected get state(): WidgetButtonToggleState {
return WidgetButtonToggleState.enabled;
}
}
class HoveredButtonStateCssGenerator extends ButtonToggleStateCssGenerator {
protected get state(): WidgetButtonToggleState {
return WidgetButtonToggleState.hovered;
}
}
class DisabledButtonStateCssGenerator extends ButtonToggleStateCssGenerator {
protected get state(): WidgetButtonToggleState {
return WidgetButtonToggleState.disabled;
}
protected getMainColor(): string {
return defaultMainColorDisabled;
}
protected getBackgroundColor(): string {
return defaultBackgroundColorDisabled;
}
protected getBorderColor(): string {
return defaultBorderColorDisabled;
}
}
const buttonToggleStateCssGeneratorsMap = new Map<WidgetButtonToggleState, ButtonToggleStateCssGenerator>(
[
[WidgetButtonToggleState.enabled, new EnabledButtonStateCssGenerator()],
[WidgetButtonToggleState.hovered, new HoveredButtonStateCssGenerator()],
[WidgetButtonToggleState.disabled, new DisabledButtonStateCssGenerator()]
]
);
const widgetButtonCssSelector = '.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header';
export const generateWidgetButtonToggleAppearanceCss = (selectedAppearance: SegmentedButtonStyles, unselectedAppearance: SegmentedButtonStyles): string => {
let statesCss = '';
for (const state of widgetButtonToggleStates) {
const generator = buttonToggleStateCssGeneratorsMap.get(state);
statesCss += `\n${generator.generateStateCss(selectedAppearance, unselectedAppearance)}`;
}
return `${widgetButtonCssSelector} {\n`+
`${statesCss}\n`+
`}`;
};
export const generateWidgetButtonToggleBorderLayout = (layout: SegmentedButtonLayout): string => {
return `${widgetButtonCssSelector} {\n`+
`--tb-widget-button-toggle-border-radius: ${segmentedButtonLayoutBorder.get(layout)}\n`+
`}`;
};

263
ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html

@ -0,0 +1,263 @@
<!--
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]="segmentedButtonWidgetSettingsForm">
<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.button-state.initial-state-hint' | translate}}" translate>widgets.button-state.initial-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.button-state.initial-state' | translate }}"
[valueType]="valueType.BOOLEAN"
trueLabel="{{ 'widgets.button-state.left' | translate }}"
falseLabel="{{ 'widgets.button-state.right' | translate }}"
stateLabel="{{ 'widgets.button-state.initial' | translate }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.action-button.left-button-click-hint' | translate}}" translate>widgets.action-button.left-button-click</div>
<tb-widget-action-settings class="flex-1"
panelTitle="{{ 'widgets.action-button.left-button-click' | translate }}"
[callbacks]="callbacks"
[widgetType]="widgetType"
formControlName="leftButtonClick">
</tb-widget-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.action-button.right-button-click-hint' | translate}}" translate>widgets.action-button.right-button-click</div>
<tb-widget-action-settings class="flex-1"
panelTitle="{{ 'widgets.action-button.right-button-click' | translate }}"
[callbacks]="callbacks"
[widgetType]="widgetType"
formControlName="rightButtonClick">
</tb-widget-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 class="flex-1"
panelTitle="{{ 'widgets.button-state.disabled-state' | translate }}"
[valueType]="valueType.BOOLEAN"
stateLabel="{{ 'widgets.button-state.disabled' | translate }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="3:1"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.segmented-button.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of segmentedButtonLayouts"
[value]="layout"
[image]="segmentedButtonLayoutImageMap.get(layout)">
{{ segmentedButtonLayoutTranslationMap.get(layout) | 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 space-between">
<div translate>widgets.segmented-button.card-border</div>
<div class="flex flex-row items-center gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="cardBorder" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden">px</div>
</mat-form-field>
<tb-color-input asBoxInput
formControlName="cardBorderColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>widgets.segmented-button.button-appearance</div>
<tb-toggle-select [(ngModel)]="segmentedButtonAppearanceType" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="left">{{ 'widgets.segmented-button.left' | translate }}</tb-toggle-option>
<tb-toggle-option value="right">{{ 'widgets.segmented-button.right' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="leftAppearance" [class.!hidden]="segmentedButtonAppearanceType !== 'left'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.button.label' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.label').value">
</tb-font-settings>
</div>
</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 class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="rightAppearance" [class.!hidden]="segmentedButtonAppearanceType !== 'right'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.button.label' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.label').value">
</tb-font-settings>
</div>
</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 class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>widgets.segmented-button.color-styles</div>
<tb-toggle-select [(ngModel)]="segmentedButtonColorStylesType" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="selected">{{ 'widgets.segmented-button.selected' | translate }}</tb-toggle-option>
<tb-toggle-option value="unselected">{{ 'widgets.segmented-button.unselected' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="selectedStyle" [class.!hidden]="segmentedButtonColorStylesType !== 'selected'">
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.button.color-palette' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.button.main</div>
<tb-color-input asBoxInput
formControlName="mainColor">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<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 class="flex flex-row flex-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 widgetButtonToggleStates">
<div>{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}</div>
<tb-widget-button-toggle-custom-style
[value]="true"
[state]="state"
[appearance]="segmentedButtonWidgetSettingsForm.get('appearance').value"
[autoScale]="true"
[formControlName]="state">
</tb-widget-button-toggle-custom-style>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="unselectedStyle" [class.!hidden]="segmentedButtonColorStylesType !== 'unselected'">
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.button.color-palette' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.button.main</div>
<tb-color-input asBoxInput
formControlName="mainColor">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<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 class="flex flex-row flex-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 widgetButtonToggleStates">
<div>{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}</div>
<tb-widget-button-toggle-custom-style
[value]="false"
[state]="state"
[appearance]="segmentedButtonWidgetSettingsForm.get('appearance').value"
[autoScale]="true"
[formControlName]="state">
</tb-widget-button-toggle-custom-style>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
</div>
</ng-container>

134
ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts

@ -0,0 +1,134 @@
///
/// 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 { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ValueType } from '@shared/models/constants';
import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models';
import {
SegmentedButtonAppearanceType,
SegmentedButtonColorStylesType,
segmentedButtonDefaultSettings,
segmentedButtonLayoutBorder,
segmentedButtonLayoutImages,
segmentedButtonLayouts,
segmentedButtonLayoutTranslations,
WidgetButtonToggleState,
widgetButtonToggleStatesTranslations
} from '@home/components/widget/lib/button/segmented-button-widget.models';
@Component({
selector: 'tb-segmented-button-widget-settings',
templateUrl: './segmented-button-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class SegmentedButtonWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
const datasources = this.widgetConfig?.config?.datasources;
return getTargetDeviceFromDatasources(datasources);
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
get borderRadius(): string {
return this.segmentedButtonLayoutBorderMap.get(this.segmentedButtonWidgetSettingsForm.get('appearance.layout').value);
}
segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'left';
segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected';
widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[];
widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations;
segmentedButtonLayouts = segmentedButtonLayouts;
segmentedButtonLayoutTranslationMap = segmentedButtonLayoutTranslations;
segmentedButtonLayoutImageMap = segmentedButtonLayoutImages;
segmentedButtonLayoutBorderMap = segmentedButtonLayoutBorder;
valueType = ValueType;
segmentedButtonWidgetSettingsForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.segmentedButtonWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...segmentedButtonDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.segmentedButtonWidgetSettingsForm = this.fb.group({
initialState: [settings.initialState, []],
leftButtonClick: [settings.leftButtonClick, []],
rightButtonClick: [settings.rightButtonClick, []],
disabledState: [settings.disabledState, []],
appearance: this.fb.group({
layout: [settings.appearance.layout, []],
autoScale: [settings.appearance.autoScale, []],
cardBorder: [settings.appearance.cardBorder, []],
cardBorderColor: [settings.appearance.cardBorderColor, []],
leftAppearance: this.fb.group({
showLabel: [settings.appearance.leftAppearance.showLabel, []],
label: [settings.appearance.leftAppearance.label, []],
labelFont: [settings.appearance.leftAppearance.labelFont, []],
showIcon: [settings.appearance.leftAppearance.showIcon, []],
icon: [settings.appearance.leftAppearance.icon, []],
iconSize: [settings.appearance.leftAppearance.iconSize, []],
iconSizeUnit: [settings.appearance.leftAppearance.iconSizeUnit, []],
}),
rightAppearance: this.fb.group({
showLabel: [settings.appearance.rightAppearance.showLabel, []],
label: [settings.appearance.rightAppearance.label, []],
labelFont: [settings.appearance.rightAppearance.labelFont, []],
showIcon: [settings.appearance.rightAppearance.showIcon, []],
icon: [settings.appearance.rightAppearance.icon, []],
iconSize: [settings.appearance.rightAppearance.iconSize, []],
iconSizeUnit: [settings.appearance.rightAppearance.iconSizeUnit, []],
}),
selectedStyle: this.fb.group({
mainColor: [settings.appearance.selectedStyle.mainColor, []],
backgroundColor: [settings.appearance.selectedStyle.backgroundColor, []],
customStyle: this.fb.group({
enabled: [settings.appearance.selectedStyle.customStyle.enabled, []],
hovered: [settings.appearance.selectedStyle.customStyle.hovered, []],
disabled: [settings.appearance.selectedStyle.customStyle.disabled, []],
})
}),
unselectedStyle: this.fb.group({
mainColor: [settings.appearance.unselectedStyle.mainColor, []],
backgroundColor: [settings.appearance.unselectedStyle.backgroundColor, []],
customStyle: this.fb.group({
enabled: [settings.appearance.unselectedStyle.customStyle.enabled, []],
hovered: [settings.appearance.unselectedStyle.customStyle.hovered, []],
disabled: [settings.appearance.unselectedStyle.customStyle.disabled, []],
})
}),
})
});
}
}

92
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.html

@ -0,0 +1,92 @@
<!--
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">{{ widgetButtonToggleStatesTranslationsMap.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">
<mat-slide-toggle class="mat-slide" formControlName="overrideBorderColor">
{{ 'widgets.button.border' | translate }}
</mat-slide-toggle>
<tb-color-input asBoxInput
formControlName="borderColor">
</tb-color-input>
</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-toggle
#widgetButtonTogglePreview
[appearance]="previewAppearance"
[autoScale]="autoScale"
[value]="value"
disableEvents
[hovered]="state === WidgetButtonToggleState.hovered"
[disabled]="state === WidgetButtonToggleState.disabled">
</tb-widget-button-toggle>
</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)">{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}</button>
</div>
</ng-template>
</mat-menu>
<span class="flex-1"></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>

68
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.scss

@ -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;
}
}

210
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts

@ -0,0 +1,210 @@
///
/// 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,
ViewChild,
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
} from '@shared/components/button/widget-button.models';
import { merge } from 'rxjs';
import { deepClone } from '@core/utils';
import { WidgetButtonToggleComponent } from '@shared/components/button/widget-button-toggle.component';
import {
ButtonToggleAppearance,
SegmentedButtonStyles,
WidgetButtonToggleCustomStyle,
WidgetButtonToggleState,
widgetButtonToggleStates,
widgetButtonToggleStatesTranslations
} from '@home/components/widget/lib/button/segmented-button-widget.models';
@Component({
selector: 'tb-widget-button-toggle-custom-style-panel',
templateUrl: './widget-button-toggle-custom-style-panel.component.html',
providers: [],
styleUrls: ['./widget-button-toggle-custom-style-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WidgetButtonToggleCustomStylePanelComponent extends PageComponent implements OnInit {
@ViewChild('widgetButtonTogglePreview')
widgetButtonTogglePreview: WidgetButtonToggleComponent;
@Input()
appearance: ButtonToggleAppearance;
@Input()
value: boolean = false;
@Input()
borderRadius: string;
@Input()
autoScale: boolean;
@Input()
state: WidgetButtonToggleState;
@Input()
customStyle: WidgetButtonToggleCustomStyle;
private popoverValue: TbPopoverComponent<WidgetButtonToggleCustomStylePanelComponent>;
@Input()
set popover(popover: TbPopoverComponent<WidgetButtonToggleCustomStylePanelComponent>) {
this.popoverValue = popover;
popover.tbAnimationDone.subscribe(() => {
this.widgetButtonTogglePreview?.validateSize();
});
}
get popover(): TbPopoverComponent<WidgetButtonToggleCustomStylePanelComponent> {
return this.popoverValue;
}
@Output()
customStyleApplied = new EventEmitter<WidgetButtonToggleCustomStyle>();
widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations;
WidgetButtonToggleState = WidgetButtonToggleState;
previewAppearance: ButtonToggleAppearance;
copyFromStates: WidgetButtonToggleState[];
customStyleFormGroup: UntypedFormGroup;
style: SegmentedButtonStyles;
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>,
private cd: ChangeDetectorRef) {
super(store);
}
ngOnInit(): void {
this.style = this.value ? this.appearance.selectedStyle : this.appearance.unselectedStyle;
this.copyFromStates = widgetButtonToggleStates.filter(state =>
state !== this.state && !!this.style.customStyle[state]);
this.customStyleFormGroup = this.fb.group(
{
overrideMainColor: [false, []],
mainColor: [null, []],
overrideBackgroundColor: [false, []],
backgroundColor: [null, []],
overrideBorderColor: [false, []],
borderColor: [null, []]
}
);
merge(this.customStyleFormGroup.get('overrideMainColor').valueChanges,
this.customStyleFormGroup.get('overrideBackgroundColor').valueChanges,
this.customStyleFormGroup.get('overrideBorderColor').valueChanges)
.subscribe(() => {
this.updateValidators();
});
this.customStyleFormGroup.valueChanges.subscribe(() => {
this.updatePreviewAppearance();
});
this.setStyle(this.customStyle);
}
copyStyle(state: WidgetButtonToggleState) {
this.customStyle = deepClone(this.style[state]);
this.setStyle(this.customStyle);
this.customStyleFormGroup.markAsDirty();
}
cancel() {
this.popover?.hide();
}
applyCustomStyle() {
const customStyle: SegmentedButtonStyles = this.customStyleFormGroup.value;
this.customStyleApplied.emit(customStyle);
}
private setStyle(customStyle?: WidgetButtonToggleCustomStyle): void {
let mainColor = this.state === WidgetButtonToggleState.disabled ? defaultMainColorDisabled : this.style.mainColor;
if (customStyle?.overrideMainColor) {
mainColor = customStyle?.mainColor;
}
let backgroundColor = this.state === WidgetButtonToggleState.disabled ? defaultBackgroundColorDisabled : this.style.backgroundColor;
if (customStyle?.overrideBackgroundColor) {
backgroundColor = customStyle?.backgroundColor;
}
let borderColor = this.state === WidgetButtonToggleState.disabled ? defaultBackgroundColorDisabled : this.style.backgroundColor;
if (customStyle?.overrideBorderColor) {
borderColor = customStyle?.borderColor;
}
this.customStyleFormGroup.patchValue({
overrideMainColor: customStyle?.overrideMainColor,
mainColor,
overrideBackgroundColor: customStyle?.overrideBackgroundColor,
backgroundColor,
overrideBorderColor: customStyle?.overrideBorderColor,
borderColor
}, {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 overrideBorderColor: boolean = this.customStyleFormGroup.get('overrideBorderColor').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 (overrideBorderColor) {
this.customStyleFormGroup.get('borderColor').enable({emitEvent: false});
} else {
this.customStyleFormGroup.get('borderColor').disable({emitEvent: false});
}
}
private updatePreviewAppearance() {
this.previewAppearance = deepClone(this.appearance);
if (this.value) {
this.previewAppearance.selectedStyle.customStyle[this.state] = this.customStyleFormGroup.value;
} else {
this.previewAppearance.unselectedStyle.customStyle[this.state] = this.customStyleFormGroup.value;
}
this.cd.markForCheck();
}
}

44
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html

@ -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-toggle
[value]="value"
[appearance]="previewAppearance"
[borderRadius]="borderRadius"
[autoScale]="autoScale"
disableEvents
[hovered]="state === widgetButtonToggleState.hovered"
[disabled]="state === widgetButtonToggleState.disabled">
</tb-widget-button-toggle>
<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>

46
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.scss

@ -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-toggle {
width: 84px;
height: 100%;
}
@media #{$mat-gt-xs} {
width: 188px;
tb-widget-button-toggle {
width: 124px;
}
}
}
}

169
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts

@ -0,0 +1,169 @@
///
/// 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 { TbPopoverService } from '@shared/components/popover.service';
import { MatIconButton } from '@angular/material/button';
import { deepClone } from '@core/utils';
import {
ButtonToggleAppearance,
WidgetButtonToggleCustomStyle,
WidgetButtonToggleState
} from '@home/components/widget/lib/button/segmented-button-widget.models';
import {
WidgetButtonToggleCustomStylePanelComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component';
@Component({
selector: 'tb-widget-button-toggle-custom-style',
templateUrl: './widget-button-toggle-custom-style.component.html',
styleUrls: ['./widget-button-toggle-custom-style.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetButtonToggleCustomStyleComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class WidgetButtonToggleCustomStyleComponent implements OnInit, OnChanges, ControlValueAccessor {
@Input()
disabled = false;
@Input()
value = false;
@Input()
appearance: ButtonToggleAppearance;
@Input()
borderRadius: string;
@Input()
autoScale: boolean;
@Input()
state: WidgetButtonToggleState;
widgetButtonToggleState = WidgetButtonToggleState;
modelValue: WidgetButtonToggleCustomStyle;
previewAppearance: ButtonToggleAppearance;
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: WidgetButtonToggleCustomStyle): 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,
autoScale: this.autoScale,
state: this.state,
value: this.value,
customStyle: this.modelValue
};
const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, WidgetButtonToggleCustomStylePanelComponent,
['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: WidgetButtonToggleCustomStyle): void {
this.modelValue = value;
this.updatePreviewAppearance();
this.propagateChange(this.modelValue);
}
private updatePreviewAppearance() {
this.previewAppearance = deepClone(this.appearance);
if (this.modelValue) {
if (this.value) {
this.previewAppearance.selectedStyle.customStyle[this.state] = this.modelValue;
} else {
this.previewAppearance.unselectedStyle.customStyle[this.state] = this.modelValue;
}
}
this.cd.markForCheck();
}
}

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -158,6 +158,12 @@ import {
import { import {
ScadaSymbolObjectSettingsComponent ScadaSymbolObjectSettingsComponent
} from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component'; } from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component';
import {
WidgetButtonToggleCustomStyleComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component';
import {
WidgetButtonToggleCustomStylePanelComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -196,7 +202,9 @@ import {
WidgetActionSettingsPanelComponent, WidgetActionSettingsPanelComponent,
WidgetButtonAppearanceComponent, WidgetButtonAppearanceComponent,
WidgetButtonCustomStyleComponent, WidgetButtonCustomStyleComponent,
WidgetButtonToggleCustomStyleComponent,
WidgetButtonCustomStylePanelComponent, WidgetButtonCustomStylePanelComponent,
WidgetButtonToggleCustomStylePanelComponent,
TimeSeriesChartAxisSettingsComponent, TimeSeriesChartAxisSettingsComponent,
TimeSeriesChartThresholdsPanelComponent, TimeSeriesChartThresholdsPanelComponent,
TimeSeriesChartThresholdRowComponent, TimeSeriesChartThresholdRowComponent,
@ -261,7 +269,9 @@ import {
WidgetActionSettingsPanelComponent, WidgetActionSettingsPanelComponent,
WidgetButtonAppearanceComponent, WidgetButtonAppearanceComponent,
WidgetButtonCustomStyleComponent, WidgetButtonCustomStyleComponent,
WidgetButtonToggleCustomStyleComponent,
WidgetButtonCustomStylePanelComponent, WidgetButtonCustomStylePanelComponent,
WidgetButtonToggleCustomStylePanelComponent,
TimeSeriesChartAxisSettingsComponent, TimeSeriesChartAxisSettingsComponent,
TimeSeriesChartThresholdsPanelComponent, TimeSeriesChartThresholdsPanelComponent,
TimeSeriesChartThresholdRowComponent, TimeSeriesChartThresholdRowComponent,

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -368,6 +368,9 @@ import {
import { import {
ScadaSymbolWidgetSettingsComponent ScadaSymbolWidgetSettingsComponent
} from '@home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component'; } from '@home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component';
import {
SegmentedButtonWidgetSettingsComponent
} from '@home/components/widget/lib/settings/button/segmented-button-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -483,6 +486,7 @@ ScadaSymbolWidgetSettingsComponent
BarChartWithLabelsWidgetSettingsComponent, BarChartWithLabelsWidgetSettingsComponent,
SingleSwitchWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent, ActionButtonWidgetSettingsComponent,
SegmentedButtonWidgetSettingsComponent,
CommandButtonWidgetSettingsComponent, CommandButtonWidgetSettingsComponent,
PowerButtonWidgetSettingsComponent, PowerButtonWidgetSettingsComponent,
SliderWidgetSettingsComponent, SliderWidgetSettingsComponent,
@ -619,6 +623,7 @@ ScadaSymbolWidgetSettingsComponent
BarChartWithLabelsWidgetSettingsComponent, BarChartWithLabelsWidgetSettingsComponent,
SingleSwitchWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent, ActionButtonWidgetSettingsComponent,
SegmentedButtonWidgetSettingsComponent,
CommandButtonWidgetSettingsComponent, CommandButtonWidgetSettingsComponent,
PowerButtonWidgetSettingsComponent, PowerButtonWidgetSettingsComponent,
SliderWidgetSettingsComponent, SliderWidgetSettingsComponent,
@ -723,6 +728,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-bar-chart-with-labels-widget-settings': BarChartWithLabelsWidgetSettingsComponent, 'tb-bar-chart-with-labels-widget-settings': BarChartWithLabelsWidgetSettingsComponent,
'tb-single-switch-widget-settings': SingleSwitchWidgetSettingsComponent, 'tb-single-switch-widget-settings': SingleSwitchWidgetSettingsComponent,
'tb-action-button-widget-settings': ActionButtonWidgetSettingsComponent, 'tb-action-button-widget-settings': ActionButtonWidgetSettingsComponent,
'tb-segmented-button-widget-settings': SegmentedButtonWidgetSettingsComponent,
'tb-command-button-widget-settings': CommandButtonWidgetSettingsComponent, 'tb-command-button-widget-settings': CommandButtonWidgetSettingsComponent,
'tb-power-button-widget-settings': PowerButtonWidgetSettingsComponent, 'tb-power-button-widget-settings': PowerButtonWidgetSettingsComponent,
'tb-slider-widget-settings': SliderWidgetSettingsComponent, 'tb-slider-widget-settings': SliderWidgetSettingsComponent,

3
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -87,6 +87,7 @@ import {
} from '@home/components/widget/lib/cards/notification-type-filter-panel.component'; } from '@home/components/widget/lib/cards/notification-type-filter-panel.component';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
import { SegmentedButtonWidgetComponent } from '@home/components/widget/lib/button/segmented-button-widget.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -124,6 +125,7 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
BarChartWithLabelsWidgetComponent, BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent, SingleSwitchWidgetComponent,
ActionButtonWidgetComponent, ActionButtonWidgetComponent,
SegmentedButtonWidgetComponent,
CommandButtonWidgetComponent, CommandButtonWidgetComponent,
PowerButtonWidgetComponent, PowerButtonWidgetComponent,
SliderWidgetComponent, SliderWidgetComponent,
@ -185,6 +187,7 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
BarChartWithLabelsWidgetComponent, BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent, SingleSwitchWidgetComponent,
ActionButtonWidgetComponent, ActionButtonWidgetComponent,
SegmentedButtonWidgetComponent,
CommandButtonWidgetComponent, CommandButtonWidgetComponent,
PowerButtonWidgetComponent, PowerButtonWidgetComponent,
SliderWidgetComponent, SliderWidgetComponent,

42
ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html

@ -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 #toggleGroupContainer class="tb-toggle-container">
<mat-button-toggle-group #widgetButton
class="tb-toggle-header"
[ngModel]="value"
(change)="clicked.emit($event)"
[style.border-radius]="computedBorderRadius"
[style.border-color]="disabled ? 'rgba(0,0,0,0.38)': computedBorderColor"
[style.border-width]="computedBorderWidth"
[class.tb-pointer-events]="disableEvents"
[class.tb-disabled-state]="disabled"
[class.tb-disabled]="disabled">
<mat-button-toggle [value]="true" [disabled]="disabled" [class.tb-hover-state]="hovered">
<div #leftButtonContent class="tb-widget-button-content" *ngIf="appearance.leftAppearance.showIcon || appearance.leftAppearance.showLabel">
<tb-icon matButtonIcon *ngIf="appearance.leftAppearance.showIcon" [style]="leftIconStyle">{{ appearance.leftAppearance.icon }}</tb-icon>
<span *ngIf="appearance.leftAppearance.showLabel" [style]="leftLabelStyle" class="tb-widget-button-label">{{ leftLabel$ | async }}</span>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="false" [disabled]="disabled" [class.tb-hover-state]="hovered">
<div #rightButtonContent class="tb-widget-button-content" *ngIf="appearance.rightAppearance.showIcon || appearance.rightAppearance.showLabel">
<tb-icon matButtonIcon *ngIf="appearance.rightAppearance.showIcon" [style]="rightIconStyle">{{ appearance.rightAppearance.icon }}</tb-icon>
<span *ngIf="appearance.rightAppearance.showLabel" [style]="rightLabelStyle" class="tb-widget-button-label">{{ rightLabel$ | async }}</span>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>

189
ui-ngx/src/app/shared/components/button/widget-button-toggle.component.scss

@ -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.
*/
$defaultBorderRadius: 4px;
$defaultCheckedMainColor: #FFFFFF;
$defaultCheckedBackgroundColor: #305680;
$defaultCheckedBorderColor: #305680;
$defaultUncheckedMainColor: rgba(0, 0, 0, 0.76);
$defaultUncheckedBackgroundColor: #E8E8E8;
$defaultUncheckedBorderColor: #E8E8E8;
$defaultCheckedMainColorDisabled: #FFFFFFCC;
$defaultCheckedBackgroundColorDisabled: rgba(0, 0, 0, 0.12);
$defaultCheckedBorderColorDisabled: rgba(0, 0, 0, 0.12);
$defaultUncheckedMainColorDisabled: rgba(0, 0, 0, 0.12);
$defaultUncheckedBackgroundColorDisabled: #E8E8E8;
$defaultUncheckedBorderColorDisabled: #E8E8E8;
$buttonBorderRadius: var(--tb-widget-button-toggle-border-radius, $defaultBorderRadius);
$mainCheckedColorEnabled: var(--tb-widget-button-toggle-main-checked-color-enabled, $defaultCheckedMainColor);
$backgroundCheckedColorEnabled: var(--tb-widget-button-toggle-background-checked-color-enabled, $defaultCheckedBackgroundColor);
$borderCheckedColorEnabled: var(--tb-widget-button-toggle-border-checked-color-enabled, $defaultCheckedBorderColor);
$mainUncheckedColorEnabled: var(--tb-widget-button-toggle-main-unchecked-color-enabled, $defaultUncheckedMainColor);
$backgroundUncheckedColorEnabled: var(--tb-widget-button-toggle-background-unchecked-color-enabled, $defaultUncheckedBackgroundColor);
$borderUncheckedColorEnabled: var(--tb-widget-button-toggle-border-unchecked-color-enabled, $defaultUncheckedBorderColor);
$mainCheckedColorDisabled: var(--tb-widget-button-toggle-main-checked-color-disabled, $defaultCheckedMainColorDisabled);
$backgroundCheckedColorDisabled: var(--tb-widget-button-toggle-background-checked-color-disabled, $defaultCheckedBackgroundColorDisabled);
$borderCheckedColorDisabled: var(--tb-widget-button-toggle-border-checked-color-disabled, $defaultCheckedBorderColorDisabled);
$mainUncheckedColorDisabled: var(--tb-widget-button-toggle-main-unchecked-color-disabled, $defaultUncheckedMainColorDisabled);
$backgroundUncheckedColorDisabled: var(--tb-widget-button-toggle-background-unchecked-color-disabled, $defaultUncheckedBackgroundColorDisabled);
$borderUncheckedColorDisabled: var(--tb-widget-button-toggle-border-unchecked-color-disabled, $defaultUncheckedBorderColorDisabled);
$mainCheckedColorHovered: var(--tb-widget-button-toggle-main-checked-color-hovered, $defaultCheckedMainColor);
$backgroundCheckedColorHovered: var(--tb-widget-button-toggle-background-checked-color-hovered, $defaultCheckedBackgroundColor);
$borderCheckedColorHovered: var(--tb-widget-button-toggle-border-checked-color-hovered, $defaultCheckedBorderColor);
$mainUncheckedColorHovered: var(--tb-widget-button-toggle-main-unchecked-color-hovered, $defaultCheckedMainColor);
$backgroundUncheckedColorHovered: var(--tb-widget-button-toggle-background-unchecked-color-hovered, $defaultCheckedBackgroundColor);
$borderUncheckedColorHovered: var(--tb-widget-button-toggle-border-unchecked-color-hovered, $defaultCheckedBorderColor);
@mixin _tb-widget-button-styles($main, $background, $boxShadow) {
color: $main;
background-color: $background;
border: 1px solid $boxShadow;
}
:host {
max-width: 100%;
.tb-toggle-container {
display: block;
height: 100%;
}
.tb-toggle-header {
transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1);
}
.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;
}
}
}
:host ::ng-deep {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
overflow: visible;
width: 100%;
height: 100%;
padding: 3px;
border: 1px solid;
background: transparent;
.mat-button-toggle + .mat-button-toggle {
border-left: none;
}
.mat-button-toggle.mat-button-toggle-appearance-standard {
flex: 1;
width: 50%;
border-radius: $buttonBorderRadius;
&.tb-hover-state {
.mat-button-toggle-button {
@include _tb-widget-button-styles($mainUncheckedColorHovered, $backgroundUncheckedColorHovered, $borderUncheckedColorHovered);
}
&.mat-button-toggle-checked {
.mat-button-toggle-button {
@include _tb-widget-button-styles($mainCheckedColorHovered, $backgroundCheckedColorHovered, $borderCheckedColorHovered);
}
}
}
.mat-button-toggle-button {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
border-radius: $buttonBorderRadius;
@include _tb-widget-button-styles($mainUncheckedColorEnabled, $backgroundUncheckedColorEnabled, $borderUncheckedColorEnabled);
.mat-button-toggle-label-content {
.mat-pseudo-checkbox {
display: none;
}
}
}
&.mat-button-toggle-checked {
.mat-button-toggle-button {
@include _tb-widget-button-styles($mainCheckedColorEnabled, $backgroundCheckedColorEnabled, $borderCheckedColorEnabled);
}
}
}
&.tb-pointer-events {
.mat-button-toggle.mat-button-toggle-appearance-standard {
.mat-button-toggle-button {
pointer-events: none;
}
.mat-button-toggle-focus-overlay, .mat-button-toggle-ripple {
opacity: 0;
}
}
}
&:not(:disabled):not(.tb-disabled-state):not(.tb-pointer-events) {
.mat-button-toggle.mat-button-toggle-appearance-standard {
&:hover {
.mat-button-toggle-button {
@include _tb-widget-button-styles($mainUncheckedColorHovered, $backgroundUncheckedColorHovered, $borderUncheckedColorHovered);
}
&.mat-button-toggle-checked {
.mat-button-toggle-button {
@include _tb-widget-button-styles($mainCheckedColorHovered, $backgroundCheckedColorHovered, $borderCheckedColorHovered);
}
}
}
}
}
&:disabled, &.tb-disabled-state {
pointer-events: none;
.mat-button-toggle.mat-button-toggle-appearance-standard {
.mat-button-toggle-button {
@include _tb-widget-button-styles($mainUncheckedColorDisabled, $backgroundUncheckedColorDisabled, $borderUncheckedColorDisabled);
}
&.mat-button-toggle-checked {
.mat-button-toggle-button {
@include _tb-widget-button-styles($mainCheckedColorDisabled, $backgroundCheckedColorDisabled, $borderCheckedColorDisabled);
}
}
}
.mat-button-toggle-focus-overlay, .mat-button-toggle-ripple {
opacity: 0;
}
}
.mat-button-toggle-focus-overlay, .mat-button-toggle-ripple {
border-radius: inherit;
}
}
}

234
ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts

@ -0,0 +1,234 @@
///
/// 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
} from '@angular/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { ComponentStyle, iconStyle, textStyle, validateCssSize } from '@shared/models/widget-settings.models';
import { UtilsService } from '@core/services/utils.service';
import { Observable, of } from 'rxjs';
import { WidgetContext } from '@home/models/widget-component.models';
import { isDefinedAndNotNull } from '@core/utils';
import {
generateWidgetButtonToggleAppearanceCss,
generateWidgetButtonToggleBorderLayout,
segmentedButtonDefaultAppearance,
segmentedButtonLayoutBorder
} from '@home/components/widget/lib/button/segmented-button-widget.models';
import { MatButtonToggleChange } from '@angular/material/button-toggle';
const initialButtonHeight = 60;
const horizontalLayoutPadding = 10;
@Component({
selector: 'tb-widget-button-toggle',
templateUrl: './widget-button-toggle.component.html',
styleUrls: ['./widget-button-toggle.component.scss']
})
export class WidgetButtonToggleComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
@ViewChild('toggleGroupContainer', {static: false})
toggleGroupContainer: ElementRef<HTMLElement>;
@ViewChild('widgetButton', {read: ElementRef})
widgetButton: ElementRef<HTMLElement>;
@ViewChild('leftButtonContent', {static: false})
leftButtonContent: ElementRef<HTMLElement>;
@ViewChild('rightButtonContent', {static: false})
rightButtonContent: ElementRef<HTMLElement>;
@Input()
appearance = segmentedButtonDefaultAppearance;
@Input()
borderRadius: string;
@Input()
autoScale: boolean;
@Input()
@coerceBoolean()
value = false;
@Input()
@coerceBoolean()
disabled = false;
@Input()
@coerceBoolean()
hovered = false;
@Input()
@coerceBoolean()
disableEvents = false;
@Input()
ctx: WidgetContext;
@Output()
clicked = new EventEmitter<MatButtonToggleChange>();
leftLabel$: Observable<string>;
rightLabel$: Observable<string>;
leftIconStyle: ComponentStyle = {};
rightIconStyle: ComponentStyle = {};
leftLabelStyle: ComponentStyle = {};
rightLabelStyle: ComponentStyle = {};
computedBorderColor: string;
computedBorderWidth: string;
computedBorderRadius: string;
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();
} else if (propName === 'borderRadius') {
this.updateBorderRadius();
} else if (propName === 'autoScale') {
this.updateAutoScale();
}
}
}
}
ngAfterViewInit(): void {
this.updateAutoScale();
}
ngOnDestroy(): void {
if (this.buttonResize$) {
this.buttonResize$.disconnect();
}
this.clearAppearanceCss();
}
public validateSize() {
if (this.appearance.autoScale && this.widgetButton.nativeElement) {
this.onResize();
}
}
private updateAppearance(): void {
this.clearAppearanceCss();
this.computedBorderColor = this.appearance.cardBorderColor;
this.computedBorderWidth = validateCssSize(this.appearance.cardBorder.toString());
if (this.appearance.leftAppearance.showIcon) {
this.leftIconStyle = iconStyle(this.appearance.leftAppearance.iconSize, this.appearance.leftAppearance.iconSizeUnit);
}
if (this.appearance.rightAppearance.showIcon) {
this.rightIconStyle = iconStyle(this.appearance.rightAppearance.iconSize, this.appearance.rightAppearance.iconSizeUnit);
}
if (this.appearance.leftAppearance.showLabel) {
this.leftLabelStyle = textStyle(this.appearance.leftAppearance.labelFont);
this.leftLabel$ = this.ctx ? this.ctx.registerLabelPattern(this.appearance.leftAppearance.label, this.leftLabel$) : of(this.appearance.leftAppearance.label);
}
if (this.appearance.rightAppearance.showLabel) {
this.rightLabelStyle = textStyle(this.appearance.rightAppearance.labelFont);
this.rightLabel$ = this.ctx ? this.ctx.registerLabelPattern(this.appearance.rightAppearance.label, this.rightLabel$) : of(this.appearance.rightAppearance.label);
}
this.updateBorderRadius();
const appearanceCss = generateWidgetButtonToggleAppearanceCss(this.appearance.selectedStyle, this.appearance.unselectedStyle);
const layoutCss = generateWidgetButtonToggleBorderLayout(this.appearance.layout);
this.appearanceCssClass = this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement,
'tb-widget-button', appearanceCss + layoutCss);
this.updateAutoScale();
}
private updateBorderRadius(): void {
if (this.borderRadius?.length) {
const validatedBorderRadius = validateCssSize(this.borderRadius);
if (validatedBorderRadius) {
this.computedBorderRadius = validatedBorderRadius;
} else {
this.computedBorderRadius = this.borderRadius;
}
} else {
this.computedBorderRadius = segmentedButtonLayoutBorder.get(this.appearance.layout);
}
}
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.rightButtonContent && this.leftButtonContent) {
const autoScale = isDefinedAndNotNull(this.autoScale) ? this.autoScale : this.appearance.autoScale;
if (autoScale) {
this.buttonResize$ = new ResizeObserver(() => {
this.onResize();
});
this.buttonResize$.observe(this.widgetButton.nativeElement);
this.onResize();
} else {
this.renderer.setStyle(this.widgetButton.nativeElement, 'transform', 'none');
this.renderer.setStyle(this.widgetButton.nativeElement, 'width', '100%');
}
}
}
private onResize() {
const height = this.widgetButton.nativeElement.getBoundingClientRect().height;
const buttonScale = height / initialButtonHeight;
const buttonWidth = this.widgetButton.nativeElement.getBoundingClientRect().width;
const buttonHeight = this.widgetButton.nativeElement.getBoundingClientRect().height;
this.renderer.setStyle(this.leftButtonContent.nativeElement, 'transform', `scale(1)`);
this.renderer.setStyle(this.rightButtonContent.nativeElement, 'transform', `scale(1)`);
const contentWidth = this.leftButtonContent.nativeElement.getBoundingClientRect().width;
const contentHeight = this.leftButtonContent.nativeElement.getBoundingClientRect().height;
const maxScale = Math.max(1, buttonScale);
const scale = Math.min(Math.min((buttonWidth / 2 - horizontalLayoutPadding) / contentWidth, buttonHeight / contentHeight), maxScale);
this.renderer.setStyle(this.leftButtonContent.nativeElement, 'transform', `scale(${scale})`);
this.renderer.setStyle(this.rightButtonContent.nativeElement, 'transform', `scale(${scale})`);
}
}

3
ui-ngx/src/app/shared/shared.module.ts

@ -218,6 +218,7 @@ import { CountryAutocompleteComponent } from '@shared/components/country-autocom
import { CountryData } from '@shared/models/country.models'; import { CountryData } from '@shared/models/country.models';
import { SvgXmlComponent } from '@shared/components/svg-xml.component'; import { SvgXmlComponent } from '@shared/components/svg-xml.component';
import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component'; import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component';
import { WidgetButtonToggleComponent } from '@shared/components/button/widget-button-toggle.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService; return markedOptionsService;
@ -419,6 +420,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
EmbedImageDialogComponent, EmbedImageDialogComponent,
ImageGalleryDialogComponent, ImageGalleryDialogComponent,
WidgetButtonComponent, WidgetButtonComponent,
WidgetButtonToggleComponent,
HexInputComponent, HexInputComponent,
ScadaSymbolInputComponent ScadaSymbolInputComponent
], ],
@ -676,6 +678,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
EmbedImageDialogComponent, EmbedImageDialogComponent,
ImageGalleryDialogComponent, ImageGalleryDialogComponent,
WidgetButtonComponent, WidgetButtonComponent,
WidgetButtonToggleComponent,
ScadaSymbolInputComponent ScadaSymbolInputComponent
] ]
}) })

27
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -5621,7 +5621,12 @@
"action-button": { "action-button": {
"behavior": "Behavior", "behavior": "Behavior",
"on-click": "On click", "on-click": "On click",
"on-click-hint": "Action triggered when the button is clicked" "on-click-hint": "Action triggered when the button is clicked",
"left-button-click": "Left button click",
"left-button-click-hint": "Action while pressing on left button.",
"right-button-click": "Right button click",
"right-button-click-hint": "Action while pressing on right button.",
"button-click-hint": "Action while pressing on widget."
}, },
"command-button": { "command-button": {
"behavior": "Behavior", "behavior": "Behavior",
@ -5666,6 +5671,18 @@
"vertical-fill": "Vertical fill", "vertical-fill": "Vertical fill",
"button-appearance": "Button appearance" "button-appearance": "Button appearance"
}, },
"segmented-button": {
"layout": "Layout",
"layout-squared": "Squared",
"layout-rounded": "Rounded",
"card-border": "Card border",
"button-appearance": "Button appearance",
"left": "Left",
"right": "Right",
"color-styles": "Color styles",
"selected": "Selected",
"unselected": "Unselected"
},
"button": { "button": {
"layout": "Layout", "layout": "Layout",
"outlined": "Outlined", "outlined": "Outlined",
@ -5679,6 +5696,7 @@
"color-palette": "Color palette", "color-palette": "Color palette",
"main": "Main", "main": "Main",
"background": "Background", "background": "Background",
"border": "Border",
"custom-styles": "Custom styles", "custom-styles": "Custom styles",
"clear-style": "Clear style", "clear-style": "Clear style",
"shadow": "Shadow", "shadow": "Shadow",
@ -5692,11 +5710,16 @@
"activated-state-hint": "Configure condition under which the button is active.", "activated-state-hint": "Configure condition under which the button is active.",
"disabled-state": "Disabled state", "disabled-state": "Disabled state",
"disabled-state-hint": "Configure condition under which the button is disabled.", "disabled-state-hint": "Configure condition under which the button is disabled.",
"initial-state": "Initial button",
"initial-state-hint": "Action to get the initial state of the component",
"enabled": "Enabled", "enabled": "Enabled",
"hovered": "Hovered", "hovered": "Hovered",
"pressed": "Pressed", "pressed": "Pressed",
"activated": "Activated", "activated": "Activated",
"disabled": "Disabled" "disabled": "Disabled",
"initial": "Left button",
"left": "Left",
"right": "Right"
}, },
"background": { "background": {
"background": "Background", "background": "Background",

1
ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

1
ui-ngx/src/assets/widget/segmented-button/squared-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

Loading…
Cancel
Save