diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html index 1e9bd9616d..3e0684fd75 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.html @@ -60,35 +60,35 @@
{{ property.subLabel | customTranslate }}
- +
{{ property.fieldSuffix | customTranslate }}
- - - +
{{ property.fieldSuffix | customTranslate }}
- - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts index 58d4346e53..7073d034ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.component.ts @@ -30,13 +30,13 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { defaultIotSvgObjectSettings, - parseIotSvgMetadataFromContent, IotSvgBehaviorType, IotSvgMetadata, - IotSvgObjectSettings + IotSvgObjectSettings, + IotSvgPropertyType, + parseIotSvgMetadataFromContent } from '@home/components/widget/lib/svg/iot-svg.models'; import { HttpClient } from '@angular/common/http'; -import { ValueType } from '@shared/models/constants'; import { IAliasController } from '@core/api/widget-api.models'; import { TargetDevice, widgetType } from '@shared/models/widget.models'; import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; @@ -69,6 +69,8 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control IotSvgBehaviorType = IotSvgBehaviorType; + IotSvgPropertyType = IotSvgPropertyType; + @Input() disabled: boolean; @@ -205,7 +207,7 @@ export class IotSvgObjectSettingsComponent implements OnInit, OnChanges, Control if (property.required) { validators.push(Validators.required); } - if (property.type === 'number') { + if (property.type === IotSvgPropertyType.number) { if (isDefinedAndNotNull(property.min)) { validators.push(Validators.min(property.min)); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts index ebb8b908a9..807dffcc56 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/svg/iot-svg-object-settings.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { IotSvgProperty } from '@home/components/widget/lib/svg/iot-svg.models'; +import { IotSvgProperty, IotSvgPropertyType } from '@home/components/widget/lib/svg/iot-svg.models'; export interface IotSvgPropertyRow { label: string; @@ -35,7 +35,7 @@ export const toPropertyRows = (properties: IotSvgProperty[]): IotSvgPropertyRow[ }; result.push(propertyRow); } - if (property.type === 'switch') { + if (property.type === IotSvgPropertyType.switch) { propertyRow.switch = property; } else { propertyRow.properties.push(property); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts index fc443bcd39..52f40152a0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg-widget.component.ts @@ -19,18 +19,18 @@ import { ChangeDetectorRef, Component, ElementRef, + Input, OnDestroy, OnInit, Renderer2, + TemplateRef, ViewChild, 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 { HttpClient } from '@angular/common/http'; -import { IotSvgObject } from '@home/components/widget/lib/svg/iot-svg.models'; -import { ResizeObserver } from '@juggle/resize-observer'; +import { IotSvgObject, IotSvgObjectCallbacks } from '@home/components/widget/lib/svg/iot-svg.models'; import { iotSvgWidgetDefaultSettings, IotSvgWidgetSettings @@ -40,6 +40,7 @@ import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/wi import { ImageService } from '@core/http/image.service'; import { WidgetComponent } from '@home/components/widget/widget.component'; import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; +import { WidgetContext } from '@home/models/widget-component.models'; @Component({ selector: 'tb-iot-svg-widget', @@ -47,110 +48,79 @@ import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; styleUrls: ['../action/action-widget.scss', './iot-svg-widget.component.scss'], encapsulation: ViewEncapsulation.None }) -export class IotSvgWidgetComponent extends - BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { +export class IotSvgWidgetComponent implements OnInit, AfterViewInit, OnDestroy, IotSvgObjectCallbacks { @ViewChild('iotSvgShape', {static: false}) iotSvgShape: ElementRef; + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + private settings: IotSvgWidgetSettings; + private svgContent$: Observable; backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; iotSvgObject: IotSvgObject; - private autoScale = true; - - private shapeResize$: ResizeObserver; - constructor(public widgetComponent: WidgetComponent, protected imagePipe: ImagePipe, protected sanitizer: DomSanitizer, - private renderer: Renderer2, private imageService: ImageService, protected cd: ChangeDetectorRef, private http: HttpClient) { - super(cd); } ngOnInit(): void { - super.ngOnInit(); + this.ctx.$scope.actionWidget = this; this.settings = mergeDeep({} as IotSvgWidgetSettings, iotSvgWidgetDefaultSettings, this.ctx.settings || {}); this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); - let svgContent$: Observable; if (this.settings.iotSvgContent) { - svgContent$ = of(this.settings.iotSvgContent); + this.svgContent$ = of(this.settings.iotSvgContent); } else if (this.settings.iotSvgUrl) { - svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl); + this.svgContent$ = this.imageService.getImageString(this.settings.iotSvgUrl); } else { - svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'}); - } - - svgContent$.subscribe( - (content) => { - this.initObject(content); - } - ); - } - - private initObject(svgContent: string) { - const simulated = this.ctx.utilsService.widgetEditMode || - this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false); - this.iotSvgObject = new IotSvgObject(this.ctx, svgContent, this.settings.iotSvgObject, simulated); - this.iotSvgObject.onError((error) => { - this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); - }); - this.iotSvgObject.onMessage((message) => { - this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true); - }); - this.iotSvgObject.init(); - if (this.iotSvgShape) { - this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); - if (this.autoScale) { - this.onResize(); - } + this.svgContent$ = this.http.get(this.settings.iotSvg, {responseType: 'text'}); } } ngAfterViewInit(): void { - if (this.iotSvgObject) { - this.iotSvgObject.addTo(this.iotSvgShape.nativeElement); - } - if (this.autoScale) { - this.shapeResize$ = new ResizeObserver(() => { - this.onResize(); - }); - this.shapeResize$.observe(this.iotSvgShape.nativeElement); - this.onResize(); - } - super.ngAfterViewInit(); + this.svgContent$.subscribe((content) => { + this.initObject(this.iotSvgShape.nativeElement, content); + }); } ngOnDestroy() { - if (this.shapeResize$) { - this.shapeResize$.disconnect(); - } if (this.iotSvgObject) { this.iotSvgObject.destroy(); } - super.ngOnDestroy(); } public onInit() { - super.onInit(); const borderRadius = this.ctx.$widgetElement.css('borderRadius'); this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; this.cd.detectChanges(); } - private onResize() { - const shapeWidth = this.iotSvgShape.nativeElement.getBoundingClientRect().width; - const shapeHeight = this.iotSvgShape.nativeElement.getBoundingClientRect().height; - if (this.iotSvgObject) { - this.iotSvgObject.setSize(shapeWidth, shapeHeight); - } + onSvgObjectError(error: string) { + this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); } + + onSvgObjectMessage(message: string) { + this.ctx.showSuccessToast(message, 3000, 'bottom', 'center', this.ctx.toastTargetId, true); + } + + private initObject(rootElement: HTMLElement, + svgContent: string) { + const simulated = this.ctx.utilsService.widgetEditMode || + this.ctx.isPreview || (isDefinedAndNotNull(this.settings.simulated) ? this.settings.simulated : false); + this.iotSvgObject = new IotSvgObject(rootElement, this.ctx, svgContent, this.settings.iotSvgObject, this, simulated); + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index 1302afefd1..5c463b7165 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -15,7 +15,7 @@ /// import { ValueType } from '@shared/models/constants'; -import { Box, Element, Runner, SVG, Svg, Text } from '@svgdotjs/svg.js'; +import { Box, Element, Runner, SVG, Svg, Text, Timeline } from '@svgdotjs/svg.js'; import '@svgdotjs/svg.panzoom.js'; import { DataToValueType, @@ -33,7 +33,7 @@ import { mergeDeep, parseFunction } from '@core/utils'; -import { BehaviorSubject, forkJoin, Observable, Observer, of } from 'rxjs'; +import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs'; import { share } from 'rxjs/operators'; import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; @@ -41,6 +41,7 @@ import { ColorProcessor, constantColor, Font } from '@shared/models/widget-setti import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetAction, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; +import { ResizeObserver } from '@juggle/resize-observer'; export interface IotSvgApi { formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; @@ -112,14 +113,37 @@ export interface IotSvgBehaviorValue extends IotSvgBehaviorBase { } export interface IotSvgBehaviorAction extends IotSvgBehaviorBase { + valueType: ValueType; valueToDataType: ValueToDataType; constantValue: any; valueToDataFunction: string; } -export type IotSvgBehavior = IotSvgBehaviorValue | IotSvgBehaviorAction; +export type IotSvgBehavior = IotSvgBehaviorValue & IotSvgBehaviorAction; + +export enum IotSvgPropertyType { + text = 'text', + number = 'number', + switch = 'switch', + color = 'color', + color_settings = 'color_settings', + font = 'font', + units = 'units' +} + +export const iotSvgPropertyTypes = Object.keys(IotSvgPropertyType) as IotSvgPropertyType[]; -export type IotSvgPropertyType = 'string' | 'number' | 'color' | 'color-settings' | 'font' | 'units' | 'switch'; +export const iotSvgPropertyTypeTranslations = new Map( + [ + [IotSvgPropertyType.text, 'scada.property.type-text'], + [IotSvgPropertyType.number, 'scada.property.type-number'], + [IotSvgPropertyType.switch, 'scada.property.type-switch'], + [IotSvgPropertyType.color, 'scada.property.type-color'], + [IotSvgPropertyType.color_settings, 'scada.property.type-color-settings'], + [IotSvgPropertyType.font, 'scada.property.type-font'], + [IotSvgPropertyType.units, 'scada.property.type-units'] + ] +); export interface IotSvgPropertyBase { id: string; @@ -320,17 +344,19 @@ export type IotSvgObjectSettings = { const parseError = (ctx: WidgetContext, err: any): string => ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; +export interface IotSvgObjectCallbacks { + onSvgObjectError: (error: string) => void; + onSvgObjectMessage: (message: string) => void; +} + export class IotSvgObject { private metadata: IotSvgMetadata; private settings: IotSvgObjectSettings; private context: IotSvgContext; - private rootElement: HTMLElement; private svgShape: Svg; private box: Box; - private targetWidth: number; - private targetHeight: number; private loadingSubject = new BehaviorSubject(false); private valueGetters: ValueGetter[] = []; @@ -339,19 +365,22 @@ export class IotSvgObject { private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; - loading$ = this.loadingSubject.asObservable().pipe(share()); + private readonly shapeResize$: ResizeObserver; + private scale = 1; - private _onError: (error: string) => void = () => {}; + private performInit = true; - private _onMessage: (message: string) => void = () => {}; + loading$ = this.loadingSubject.asObservable().pipe(share()); - constructor(private ctx: WidgetContext, + constructor(private rootElement: HTMLElement, + private ctx: WidgetContext, private svgContent: string, private inputSettings: IotSvgObjectSettings, + private callbacks: IotSvgObjectCallbacks, private simulated: boolean) { - } - - public init() { + this.shapeResize$ = new ResizeObserver(() => { + this.resize(); + }); const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml'); this.metadata = parseIotSvgMetadataFromDom(doc); const defaults = defaultIotSvgObjectSettings(this.metadata); @@ -359,25 +388,13 @@ export class IotSvgObject { defaults, this.inputSettings || {} as IotSvgObjectSettings); this.prepareMetadata(); this.prepareSvgShape(doc); - this.initialize(); - } - - public onError(onError: (error: string) => void) { - this._onError = onError; - } - - public onMessage(onMessage: (message: string) => void) { - this._onMessage = onMessage; - } - - public addTo(element: HTMLElement) { - this.rootElement = element; - if (this.svgShape) { - this.svgShape.addTo(element); - } + this.shapeResize$.observe(this.rootElement); } public destroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } for (const stateValueId of Object.keys(this.stateValueSubjects)) { this.stateValueSubjects[stateValueId].complete(); this.stateValueSubjects[stateValueId].unsubscribe(); @@ -385,16 +402,15 @@ export class IotSvgObject { this.valueActions.forEach(v => v.destroy()); this.loadingSubject.complete(); this.loadingSubject.unsubscribe(); - if (this.svgShape) { - this.svgShape.remove(); + for (const tag of this.metadata.tags) { + const elements = this.context.tags[tag.tag]; + elements.forEach(element => { + element.timeline().finish(); + element.timeline(null); + }); } - } - - public setSize(targetWidth: number, targetHeight: number) { - this.targetWidth = targetWidth; - this.targetHeight = targetHeight; if (this.svgShape) { - this.resize(); + this.svgShape.remove(); } } @@ -421,15 +437,10 @@ export class IotSvgObject { this.svgShape.node.style['user-select'] = 'none'; this.box = this.svgShape.bbox(); this.svgShape.size(this.box.width, this.box.height); - if (this.rootElement) { - this.svgShape.addTo(this.rootElement); - } - if (this.targetWidth && this.targetHeight) { - this.resize(); - } + this.svgShape.addTo(this.rootElement); } - private initialize() { + private init() { this.context = { api: { formatValue, @@ -447,7 +458,7 @@ export class IotSvgObject { }; const taggedElements = this.svgShape.find(`[tb\\:tag]`); for (const element of taggedElements) { - const tag = element.attr('tb:tag'); + const tag: string = element.attr('tb:tag'); let elements = this.context.tags[tag]; if (!elements) { elements = []; @@ -489,7 +500,7 @@ export class IotSvgObject { next: (val) => {this.onValue(getBehavior.id, val);}, error: (err) => { const message = parseError(this.ctx, err); - this._onError(message); + this.onError(message); } }, this.simulated); this.valueGetters.push(valueGetter); @@ -526,6 +537,14 @@ export class IotSvgObject { } } + private onError(error: string) { + this.callbacks.onSvgObjectError(error); + } + + private onMessage(message: string) { + this.callbacks.onSvgObjectMessage(message); + } + private callAction(event: Event, behaviorId: string, value?: any, observer?: Partial>) { const behavior = this.metadata.behavior.find(b => b.id === behaviorId); if (behavior) { @@ -547,7 +566,7 @@ export class IotSvgObject { observer.error(err); } const message = parseError(this.ctx, err); - this._onError(message); + this.onError(message); } } ); @@ -557,22 +576,38 @@ export class IotSvgObject { if (this.simulated) { const translatedType = this.ctx.translate.instant(widgetActionTypeTranslationMap.get(widgetAction.type)); const message = this.ctx.translate.instant('scada.preview-widget-action-text', {type: translatedType}); - this._onMessage(message); + this.onMessage(message); } else { this.ctx.actionsApi.onWidgetAction(event, widgetAction); } + if (observer?.next) { + observer.next(); + } } } } private resize() { - let scale: number; - if (this.targetWidth < this.targetHeight) { - scale = this.targetWidth / this.box.width; - } else { - scale = this.targetHeight / this.box.height; + if (this.svgShape) { + const targetWidth = this.rootElement.getBoundingClientRect().width; + const targetHeight = this.rootElement.getBoundingClientRect().height; + if (targetWidth && targetHeight) { + let scale: number; + if (targetWidth < targetHeight) { + scale = targetWidth / this.box.width; + } else { + scale = targetHeight / this.box.height; + } + if (this.scale !== scale) { + this.scale = scale; + this.svgShape.node.style.transform = `scale(${this.scale})`; + } + if (this.performInit) { + this.performInit = false; + this.init(); + } + } } - this.svgShape.node.style.transform = `scale(${scale})`; } private onValue(id: string, value: any) { @@ -598,7 +633,7 @@ export class IotSvgObject { private renderState(): void { this.metadata.stateRender(this.svgShape, this.context); for (const tag of this.metadata.tags) { - const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); + const elements = this.context.tags[tag.tag];// this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); elements.forEach(element => { tag.stateRender(element, this.context); }); @@ -664,6 +699,7 @@ export class IotSvgObject { private animate(element: Element, duration: number): Runner { element.timeline().finish(); + element.timeline(new Timeline()); return element.animate(duration, 0, 'now'); } @@ -692,9 +728,9 @@ export class IotSvgObject { if (property) { const value = this.settings.properties[id]; if (isDefinedAndNotNull(value)) { - if (property.type === 'color-settings') { + if (property.type === IotSvgPropertyType.color_settings) { return ColorProcessor.fromSettings(value); - } else if (property.type === 'string') { + } else if (property.type === IotSvgPropertyType.text) { const result = this.ctx.utilsService.customTranslation(value, value); const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo(); return createLabelFromSubscriptionEntityInfo(entityInfo, result); @@ -702,13 +738,13 @@ export class IotSvgObject { return value; } else { switch (property.type) { - case 'string': + case IotSvgPropertyType.text: return ''; - case 'number': + case IotSvgPropertyType.number: return 0; - case 'color': + case IotSvgPropertyType.color: return '#000'; - case 'color-settings': + case IotSvgPropertyType.color_settings: return ColorProcessor.fromSettings(constantColor('#000')); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html new file mode 100644 index 0000000000..12d308ed7c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.html @@ -0,0 +1,138 @@ + +
+
{{ panelTitle | translate }}
+
+
+
scada.behavior.id
+ + + +
+
+
scada.behavior.name
+ + + +
+
+
scada.behavior.hint
+ + + +
+
+
scada.behavior.type
+ + + + {{ iotSvgBehaviorTypeTranslations.get(type) | translate }} + + + +
+
+
scada.behavior.value-type
+ + + +
+ + {{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} +
+
+ + + {{ valueTypesMap.get(valueType).name | translate }} + +
+
+
+ +
+
scada.behavior.default-value
+ +
+ +
+
scada.behavior.true-label
+ + + +
+
+
scada.behavior.false-label
+ + + +
+
+
scada.behavior.state-label
+ + + +
+
+
+ +
+
+
{{ 'scada.behavior.default-payload' | translate }}
+ + {{ 'widgets.value-action.converter-value' | translate }} + {{ 'widgets.value-action.converter-constant' | translate }} + {{ 'widgets.value-action.converter-function' | translate }} + {{ 'widgets.value-action.converter-none' | translate }} + +
+ + + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss new file mode 100644 index 0000000000..3fbbd245a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.scss @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../scss/constants'; + +.tb-scada-symbol-behavior-settings-panel { + width: 540px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-scada-symbol-behavior-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + .mat-mdc-form-field.tb-value-type { + .mat-mdc-form-field-infix { + max-height: 40px; + } + mat-select-trigger { + .tb-value-type-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 10px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + vertical-align: bottom; + } + } + } + } + } + .tb-scada-symbol-behavior-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-scada-symbol-behavior-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts new file mode 100644 index 0000000000..bdb2827dac --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component.ts @@ -0,0 +1,148 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { merge } from 'rxjs'; +import { + IotSvgBehavior, + IotSvgBehaviorType, + iotSvgBehaviorTypes, + iotSvgBehaviorTypeTranslations +} from '@app/modules/home/components/widget/lib/svg/iot-svg.models'; +import { ValueType, valueTypesMap } from '@shared/models/constants'; +import { ValueToDataType } from '@shared/models/action-widget-settings.models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-scada-symbol-behavior-panel', + templateUrl: './scada-symbol-behavior-panel.component.html', + styleUrls: ['./scada-symbol-behavior-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolBehaviorPanelComponent implements OnInit { + + IotSvgBehaviorType = IotSvgBehaviorType; + + ValueType = ValueType; + + ValueToDataType = ValueToDataType; + + iotSvgBehaviorTypes = iotSvgBehaviorTypes; + iotSvgBehaviorTypeTranslations = iotSvgBehaviorTypeTranslations; + + valueTypes = Object.keys(ValueType) as ValueType[]; + + valueTypesMap = valueTypesMap; + + @Input() + isAdd = false; + + @Input() + behavior: IotSvgBehavior; + + @Input() + popover: TbPopoverComponent; + + @Output() + behaviorSettingsApplied = new EventEmitter(); + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + panelTitle: string; + + behaviorFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService) { + } + + ngOnInit(): void { + this.panelTitle = this.isAdd ? 'scada.behavior.add-behavior' : 'scada.behavior.behavior-settings'; + this.behaviorFormGroup = this.fb.group( + { + id: [this.behavior.id, [Validators.required]], + name: [this.behavior.name, [Validators.required]], + hint: [this.behavior.hint, []], + type: [this.behavior.type, [Validators.required]], + valueType: [this.behavior.valueType, [Validators.required]], + defaultValue: [this.behavior.defaultValue, [Validators.required]], + trueLabel: [this.behavior.trueLabel, []], + falseLabel: [this.behavior.falseLabel, []], + stateLabel: [this.behavior.stateLabel, []], + valueToDataType: [this.behavior.valueToDataType, [Validators.required]], + constantValue: [this.behavior.constantValue, [Validators.required]], + valueToDataFunction: [this.behavior.valueToDataFunction, [Validators.required]] + } + ); + merge(this.behaviorFormGroup.get('type').valueChanges, + this.behaviorFormGroup.get('valueType').valueChanges, + this.behaviorFormGroup.get('valueToDataType').valueChanges).subscribe(() => { + this.updateValidators(); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + applyBehaviorSettings() { + const behavior = this.behaviorFormGroup.getRawValue(); + this.behaviorSettingsApplied.emit(behavior); + } + + private updateValidators() { + const type: IotSvgBehaviorType = this.behaviorFormGroup.get('type').value; + const valueType: ValueType = this.behaviorFormGroup.get('valueType').value; + let valueToDataType: ValueToDataType = this.behaviorFormGroup.get('valueToDataType').value; + this.behaviorFormGroup.disable({emitEvent: false}); + this.behaviorFormGroup.get('id').enable({emitEvent: false}); + this.behaviorFormGroup.get('name').enable({emitEvent: false}); + this.behaviorFormGroup.get('type').enable({emitEvent: false}); + this.behaviorFormGroup.get('hint').enable({emitEvent: false}); + switch (type) { + case IotSvgBehaviorType.value: + this.behaviorFormGroup.get('valueType').enable({emitEvent: false}); + this.behaviorFormGroup.get('defaultValue').enable({emitEvent: false}); + if (valueType === ValueType.BOOLEAN) { + this.behaviorFormGroup.get('trueLabel').enable({emitEvent: false}); + this.behaviorFormGroup.get('falseLabel').enable({emitEvent: false}); + this.behaviorFormGroup.get('stateLabel').enable({emitEvent: false}); + } + break; + case IotSvgBehaviorType.action: + if (valueType === ValueType.BOOLEAN && valueToDataType === ValueToDataType.VALUE) { + this.behaviorFormGroup.patchValue({valueToDataType: ValueToDataType.CONSTANT}, {emitEvent: false}); + valueToDataType = ValueToDataType.CONSTANT; + } else if (valueType !== ValueType.BOOLEAN && valueToDataType === ValueToDataType.CONSTANT) { + this.behaviorFormGroup.patchValue({valueToDataType: ValueToDataType.VALUE}, {emitEvent: false}); + valueToDataType = ValueToDataType.VALUE; + } + this.behaviorFormGroup.get('valueType').enable({emitEvent: false}); + this.behaviorFormGroup.get('valueToDataType').enable({emitEvent: false}); + if (valueToDataType === ValueToDataType.CONSTANT) { + this.behaviorFormGroup.get('constantValue').enable({emitEvent: false}); + } else if (valueToDataType === ValueToDataType.FUNCTION) { + this.behaviorFormGroup.get('valueToDataFunction').enable({emitEvent: false}); + } + break; + case IotSvgBehaviorType.widgetAction: + break; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html index 5b9c4d2cf9..766cce736b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.html @@ -32,7 +32,8 @@
-
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts index c34da44d23..3cb586218b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts @@ -74,13 +74,14 @@ export class ScadaSymbolEditObject { this.scale = 1; const contentData = iotSvgContentData(svgContent); this.svgRootNodePart = contentData.svgRootNode; - this.svgShape = SVG().addTo(this.rootElement).svg(contentData.innerSvg); + this.svgShape = SVG().svg(contentData.innerSvg); this.svgShape.node.style.overflow = 'visible'; this.svgShape.node.style['user-select'] = 'none'; this.box = this.svgShape.bbox(); this.svgShape.size(this.box.width, this.box.height); this.svgShape.viewbox(`0 0 ${this.box.width} ${this.box.height}`); this.svgShape.style().attr('tb:inner', true).rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'}); + this.svgShape.addTo(this.rootElement); this.updateHoverFilterStyle(); this.performSetup = true; this.shapeResize$.observe(this.rootElement); @@ -245,22 +246,24 @@ export class ScadaSymbolEditObject { if (this.svgShape) { const targetWidth = this.rootElement.getBoundingClientRect().width; const targetHeight = this.rootElement.getBoundingClientRect().height; - let scale: number; - if (targetWidth < targetHeight) { - scale = targetWidth / this.box.width; - } else { - scale = targetHeight / this.box.height; - } - if (this.scale !== scale) { - this.scale = scale; - this.svgShape.node.style.transform = `scale(${this.scale})`; - this.updateHoverFilterStyle(); - this.updateZoomOptions(); - this.updateTooltipPositions(); - } - if (this.performSetup) { - this.performSetup = false; - this.doSetup(); + if (targetWidth && targetHeight) { + let scale: number; + if (targetWidth < targetHeight) { + scale = targetWidth / this.box.width; + } else { + scale = targetHeight / this.box.height; + } + if (this.scale !== scale) { + this.scale = scale; + this.svgShape.node.style.transform = `scale(${this.scale})`; + this.updateHoverFilterStyle(); + this.updateZoomOptions(); + this.updateTooltipPositions(); + } + if (this.performSetup) { + this.performSetup = false; + this.doSetup(); + } } } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 8ac12e376e..42c5a644c2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3432,7 +3432,31 @@ "type-value": "Value", "type-widget-action": "Widget action", "behavior-settings": "Behavior settings", - "remove-behavior": "Remove behavior" + "remove-behavior": "Remove behavior", + "hint": "Hint", + "value-type": "Value type", + "default-value": "Default value", + "true-label": "True label", + "false-label": "False label", + "state-label": "State label", + "default-payload": "Default payload" + }, + "property": { + "property": "Property", + "id": "Id", + "name": "Name", + "type": "Type", + "type-text": "Text", + "type-number": "Number", + "type-switch": "Switch", + "type-color": "Color", + "type-color-settings": "Color settings", + "type-font": "Font", + "type-units": "Units", + "no-properties": "No properties configured", + "add-property": "Add property", + "property-settings": "Property settings", + "remove-property": "Remove property" } }, "item": { diff --git a/ui-ngx/src/assets/widget/svg/drawing.svg b/ui-ngx/src/assets/widget/svg/drawing.svg index 5f64fc6643..23759d17a5 100644 --- a/ui-ngx/src/assets/widget/svg/drawing.svg +++ b/ui-ngx/src/assets/widget/svg/drawing.svg @@ -1,307 +1,310 @@ - - { - "title": "My first IoT SVG Object", - "stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}", - "tags": [ - { - "tag": "level", - "stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});" - }, - { - "tag": "levelTitle", - "stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}" - }, - { - "tag": "levelValue", - "stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}", - "actions": { - "click": { - "actionFunction": "ctx.api.callAction(event, 'levelValueClick');" - } - } - }, - { - "tag": "fan", - "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n element.addClass('fan-rotate');\n} else {\n element.removeClass('fan-rotate');\n}\n" - }, - { - "tag": "onButton", - "actions": { - "click": { - "actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});" - } - } - }, - { - "tag": "offButton", - "actions": { - "click": { - "actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n" - } - } - }, - { - "tag": "levelUpButton", - "actions": { - "click": { - "actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" - } - } - }, - { - "tag": "levelDownButton", - "actions": { - "click": { - "actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" - } - } +<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" version="1.1" viewBox="0 0 100 100"> +<tb:metadata xmlns=""><![CDATA[{ + "title": "Level and Fan12", + "stateRenderFunction": "var showMinMaxLevel = ctx.properties.showMinMaxLevel;\nvar minLevelElement = ctx.tags.minLevel[0];\nvar maxLevelElement = ctx.tags.maxLevel[0];\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nif (showMinMaxLevel) {\n \n var minMaxLevelFont = ctx.properties.minMaxLevelFont;\n var minMaxLevelColor = ctx.properties.minMaxLevelColor;\n \n ctx.api.text(minLevelElement, minLevel);\n ctx.api.text(maxLevelElement, maxLevel);\n \n ctx.api.font(minLevelElement, minMaxLevelFont, minMaxLevelColor);\n ctx.api.font(maxLevelElement, minMaxLevelFont, minMaxLevelColor);\n \n} else {\n minLevelElement.hide();\n maxLevelElement.hide();\n}\n\nvar disabled = ctx.values.disabled;\nvar on = ctx.values.on;\nvar level = ctx.values.level;\n\nvar onButton = ctx.tags.onButton;\nvar offButton = ctx.tags.offButton;\nvar levelUpButton = ctx.tags.levelUpButton;\nvar levelDownButton = ctx.tags.levelDownButton;\n\nvar onButtonEnabled = !disabled && !on;\nvar offButtonEnabled = !disabled && on;\nvar levelUpEnabled = !disabled && level < maxLevel;\nvar levelDownEnabled = !disabled && level > minLevel;\n\nif (onButtonEnabled) {\n ctx.api.enable(onButton);\n onButton[0].findOne('rect').attr({fill: '#12ed19'});\n} else {\n ctx.api.disable(onButton);\n onButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (offButtonEnabled) {\n ctx.api.enable(offButton);\n offButton[0].findOne('rect').attr({fill: '#ed121f'});\n} else {\n ctx.api.disable(offButton);\n offButton[0].findOne('rect').attr({fill: '#777'});\n} \n\n\nif (levelUpEnabled) {\n ctx.api.enable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelUpButton);\n levelUpButton[0].findOne('rect').attr({fill: '#777'});\n}\n \nif (levelDownEnabled) {\n ctx.api.enable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#fff'});\n} else {\n ctx.api.disable(levelDownButton);\n levelDownButton[0].findOne('rect').attr({fill: '#777'});\n}", + "tags": [ + { + "tag": "level", + "stateRenderFunction": "var level = ctx.values.level; \nvar disabled = ctx.values.disabled;\nvar minLevel = ctx.properties.minLevel; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar height = (level - minLevel) / (maxLevel - minLevel);\nheight = Math.max(0, Math.min(1, height))*80; \n\nctx.api.animate(element, 200).attr({height: height});\n\nvar fill;\nif (disabled) {\n fill = ctx.properties.disabledLevelBackground;\n} else {\n var colorProcessor = ctx.properties.levelBackground;\n colorProcessor.update(level);\n fill = colorProcessor.color;\n}\n\nelement.attr({fill: fill});" + }, + { + "tag": "levelTitle", + "stateRenderFunction": "var showLevelTitle = ctx.properties.showLevelTitle;\n\nif (showLevelTitle) {\n var levelTitle = ctx.properties.levelTitle;\n var levelTitleFont = ctx.properties.levelTitleFont;\n var levelTitleColor = ctx.properties.levelTitleColor;\n \n ctx.api.text(element, levelTitle);\n ctx.api.font(element, levelTitleFont, levelTitleColor);\n \n} else {\n element.hide();\n}" + }, + { + "tag": "levelValue", + "stateRenderFunction": "var showValue = ctx.properties.showValue;\n\nif (showValue) {\n var valueFont = ctx.properties.valueFont;\n var valueColor = ctx.properties.valueColor;\n var level = ctx.values.level; \n \n var levelText = ctx.api.formatValue(level, ctx.properties.valueDecimals, ctx.properties.valueUnits, false);\n\n ctx.api.font(element, valueFont, valueColor);\n ctx.api.text(element, levelText);\n\n} else {\n element.hide();\n ctx.tags.levelValueBackground[0].hide();\n}", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'levelValueClick');" } - ], - "behavior": [ - { - "id": "level", - "name": "Level", - "type": "value", - "valueType": "DOUBLE", - "defaultValue": 0 - }, - { - "id": "disabled", - "name": "{i18n:widgets.rpc-state.disabled-state}", - "hint": "{i18n:widgets.rpc-state.disabled-state-hint}", - "stateLabel": "{i18n:widgets.rpc-state.disabled}", - "type": "value", - "valueType": "BOOLEAN", - "defaultValue": false - }, - { - "id": "on", - "name": "On/Off state", - "trueLabel": "{i18n:widgets.rpc-state.on}", - "falseLabel": "{i18n:widgets.rpc-state.off}", - "stateLabel": "{i18n:widgets.rpc-state.on}", - "type": "value", - "valueType": "BOOLEAN", - "defaultValue": false - }, - { - "id": "onUpdateState", - "name": "{i18n:widgets.rpc-state.turn-on}", - "hint": "{i18n:widgets.rpc-state.turn-on-hint}", - "type": "action", - "valueType": "BOOLEAN", - "valueToDataType": "CONSTANT", - "constantValue": true - }, - { - "id": "offUpdateState", - "name": "{i18n:widgets.rpc-state.turn-off}", - "hint": "{i18n:widgets.rpc-state.turn-off-hint}", - "type": "action", - "valueType": "BOOLEAN", - "valueToDataType": "CONSTANT", - "constantValue": false - }, - { - "id": "levelUpdateState", - "name": "Update level", - "type": "action", - "valueType": "DOUBLE", - "valueToDataType": "VALUE", - "constantValue": 0 - }, - { - "id": "levelValueClick", - "name": "On level value click", - "type": "widgetAction" + } + }, + { + "tag": "fan", + "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n var level = ctx.values.level; \n var minLevel = ctx.properties.minLevel; \n var maxLevel = ctx.properties.maxLevel;\n\n var speed = (level - minLevel) / (maxLevel - minLevel);\n speed = Math.max(0, Math.min(1, speed))*2; \n\n var t = element.timeline();\n if (!t.active()) {\n ctx.api.animate(element, 1000).ease('-').rotate(360).loop();\n t = element.timeline();\n }\n t.speed(speed);\n} else {\n element.timeline().stop();\n}\n", + "actions": null + }, + { + "tag": "onButton", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'onUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', true);\n }\n});\n\n" } - ], - "properties": [ - { - "id": "showLevelTitle", - "name": "Level title", - "type": "switch", - "default": true, - "rowClass": "column-xs" - }, - { - "id": "levelTitle", - "name": "Level title", - "type": "string", - "default": "{i18n:widgets.battery-level.value}", - "disableOnProperty": "showLevelTitle", - "fieldClass": "flex" - }, - { - "id": "levelTitleFont", - "name": "Level title", - "type": "font", - "default": { - "size": 6, - "sizeUnit": "px", - "family": "Roboto", - "weight": "normal", - "style": "normal" - }, - "disableOnProperty": "showLevelTitle" - }, - { - "id": "levelTitleColor", - "name": "Level title", - "type": "color", - "default": "#000000", - "disableOnProperty": "showLevelTitle" - }, - { - "id": "showValue", - "name": "Value", - "type": "switch", - "default": true, - "rowClass": "column-xs" - }, - { - "id": "valueUnits", - "name": "Value", - "type": "units", - "default": "", - "disableOnProperty": "showValue", - "fieldClass": "flex" - }, - { - "id": "valueDecimals", - "name": "Value", - "type": "number", - "default": 2, - "min": 0, - "max": 15, - "step": 1, - "fieldSuffix": "decimals", - "disableOnProperty": "showValue", - "fieldClass": "flex" - }, - { - "id": "valueFont", - "name": "Value", - "type": "font", - "default": { - "size": 6, - "sizeUnit": "px", - "family": "Roboto", - "weight": "normal", - "style": "normal" - }, - "disableOnProperty": "showValue" - }, - { - "id": "valueColor", - "name": "Value", - "type": "color", - "default": "#000000", - "disableOnProperty": "showValue" - }, - { - "id": "minLevel", - "name": "Level range", - "subLabel": "min", - "type": "number", - "required": true, - "default": 0, - "min": 0, - "rowClass": "column-xs", - "fieldClass": "flex-xs" - }, - { - "id": "maxLevel", - "name": "Level range", - "subLabel": "max", - "type": "number", - "required": true, - "default": 100, - "min": 0, - "fieldClass": "flex-xs" - }, - { - "id": "showMinMaxLevel", - "name": "Min/Max label", - "type": "switch", - "default": true - }, - { - "id": "minMaxLevelFont", - "name": "Min/Max label", - "type": "font", - "default": { - "size": 6, - "sizeUnit": "px", - "family": "Roboto", - "weight": "normal", - "style": "normal" - }, - "disableOnProperty": "showMinMaxLevel" - }, - { - "id": "minMaxLevelColor", - "name": "Min/Max label", - "type": "color", - "default": "#666", - "disableOnProperty": "showMinMaxLevel" - }, - { - "id": "levelBackground", - "name": "Level background", - "subLabel": "Enabled", - "type": "color-settings", - "default": { - "type": "constant", - "color": "#1abb48", - "colorFunction": "return value &gt; 70 ? '#d5280d' : '#1abb48';" - }, - "divider": true, - "rowClass": "column-xs" - }, - { - "id": "disabledLevelBackground", - "name": "Level background", - "subLabel": "Disabled", - "type": "color", - "default": "#ccc" - } - ] - } - - - - - min - - + } + }, + { + "tag": "offButton", + "actions": { + "click": { + "actionFunction": "ctx.api.callAction(event, 'offUpdateState', undefined, {\n next: () => {\n ctx.api.setValue('on', false);\n }\n});\n" + } + } + }, + { + "tag": "levelUpButton", + "actions": { + "click": { + "actionFunction": "var level = ctx.values.level; \nvar maxLevel = ctx.properties.maxLevel;\n\nvar newLevel = Math.min(maxLevel, level + 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" + } + } + }, + { + "tag": "levelDownButton", + "actions": { + "click": { + "actionFunction": "var level = ctx.values.level; \nvar minLevel = ctx.properties.minLevel;\n\nvar newLevel = Math.max(minLevel, level - 5);\nctx.api.setValue('level', newLevel);\nctx.api.callAction(event, 'levelUpdateState', newLevel, {\n error: () => {\n ctx.api.setValue('level', level);\n }\n});" + } + } + }, + { + "tag": "ramka", + "stateRenderFunction": "var on = ctx.values.on;\nif (on) {\n if (!element.timeline().active()) {\n ctx.api.animate(element, 1000).scale(1.1).loop(0, true);\n }\n} else {\n element.timeline().stop();\n}", + "actions": null + } + ], + "behavior": [ + { + "id": "level", + "name": "Level", + "hint": null, + "type": "value", + "valueType": "DOUBLE", + "defaultValue": 10, + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "valueToDataType": null, + "constantValue": null, + "valueToDataFunction": null + }, + { + "id": "disabled", + "name": "{i18n:widgets.rpc-state.disabled-state}", + "hint": "{i18n:widgets.rpc-state.disabled-state-hint}", + "type": "value", + "valueType": "BOOLEAN", + "defaultValue": false, + "trueLabel": "", + "falseLabel": "", + "stateLabel": "{i18n:widgets.rpc-state.disabled}", + "valueToDataType": null, + "constantValue": null, + "valueToDataFunction": null + }, + { + "id": "on", + "name": "On/Off state", + "hint": null, + "type": "value", + "valueType": "BOOLEAN", + "defaultValue": true, + "trueLabel": "{i18n:widgets.rpc-state.on}", + "falseLabel": "{i18n:widgets.rpc-state.off}", + "stateLabel": "{i18n:widgets.rpc-state.on}", + "valueToDataType": null, + "constantValue": null, + "valueToDataFunction": null + }, + { + "id": "onUpdateState", + "name": "{i18n:widgets.rpc-state.turn-on}", + "hint": "{i18n:widgets.rpc-state.turn-on-hint}", + "type": "action", + "valueType": "BOOLEAN", + "valueToDataType": "CONSTANT", + "constantValue": true + }, + { + "id": "offUpdateState", + "name": "{i18n:widgets.rpc-state.turn-off}", + "hint": "{i18n:widgets.rpc-state.turn-off-hint}", + "type": "action", + "valueType": "BOOLEAN", + "valueToDataType": "CONSTANT", + "constantValue": false + }, + { + "id": "levelUpdateState", + "name": "Update level", + "type": "action", + "valueType": "DOUBLE", + "valueToDataType": "VALUE", + "constantValue": 0 + }, + { + "id": "levelValueClick", + "name": "On level value click", + "type": "widgetAction" + } + ], + "properties": [ + { + "id": "showLevelTitle", + "name": "Level title", + "type": "switch", + "default": true, + "rowClass": "column-xs" + }, + { + "id": "levelTitle", + "name": "Level title", + "type": "text", + "default": "{i18n:widgets.battery-level.value}", + "disableOnProperty": "showLevelTitle", + "fieldClass": "flex" + }, + { + "id": "levelTitleFont", + "name": "Level title", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showLevelTitle" + }, + { + "id": "levelTitleColor", + "name": "Level title", + "type": "color", + "default": "#000000", + "disableOnProperty": "showLevelTitle" + }, + { + "id": "showValue", + "name": "Value", + "type": "switch", + "default": true, + "rowClass": "column-xs" + }, + { + "id": "valueUnits", + "name": "Value", + "type": "units", + "default": "", + "disableOnProperty": "showValue", + "fieldClass": "flex" + }, + { + "id": "valueDecimals", + "name": "Value", + "type": "number", + "default": 2, + "min": 0, + "max": 15, + "step": 1, + "fieldSuffix": "decimals", + "disableOnProperty": "showValue", + "fieldClass": "flex" + }, + { + "id": "valueFont", + "name": "Value", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showValue" + }, + { + "id": "valueColor", + "name": "Value", + "type": "color", + "default": "#000000", + "disableOnProperty": "showValue" + }, + { + "id": "minLevel", + "name": "Level range", + "subLabel": "min", + "type": "number", + "required": true, + "default": 0, + "min": 0, + "rowClass": "column-xs", + "fieldClass": "flex-xs" + }, + { + "id": "maxLevel", + "name": "Level range", + "subLabel": "max", + "type": "number", + "required": true, + "default": 100, + "min": 0, + "fieldClass": "flex-xs" + }, + { + "id": "showMinMaxLevel", + "name": "Min/Max label", + "type": "switch", + "default": true + }, + { + "id": "minMaxLevelFont", + "name": "Min/Max label", + "type": "font", + "default": { + "size": 6, + "sizeUnit": "px", + "family": "Roboto", + "weight": "normal", + "style": "normal" + }, + "disableOnProperty": "showMinMaxLevel" + }, + { + "id": "minMaxLevelColor", + "name": "Min/Max label", + "type": "color", + "default": "#666", + "disableOnProperty": "showMinMaxLevel" + }, + { + "id": "levelBackground", + "name": "Level background", + "subLabel": "Enabled", + "type": "color_settings", + "default": { + "type": "constant", + "color": "#1abb48", + "colorFunction": "return value > 70 ? '#d5280d' : '#1abb48';" + }, + "divider": true, + "rowClass": "column-xs" + }, + { + "id": "disabledLevelBackground", + "name": "Level background", + "subLabel": "Disabled", + "type": "color", + "default": "#ccc" + } + ] +}]]> + min N/A Level max - - - - + On - - + Off - - + - - + - + \ No newline at end of file