diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 4c250a4618..3b454a1961 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -116,7 +116,29 @@ export type ScadaSymbolStateRenderFunction = (ctx: ScadaSymbolContext, svg: Svg) export type ScadaSymbolTagStateRenderFunction = (ctx: ScadaSymbolContext, element: Element) => void; // noinspection JSUnusedGlobalSymbols -export type ScadaSymbolActionTrigger = 'click'; +export type ScadaSymbolActionTrigger = 'click' | 'dblclick' | 'contextmenu'; + +export const scadaSymbolActionTriggers: ScadaSymbolActionTrigger[] = ['click', 'dblclick', 'contextmenu']; + +// Max interval between clicks of a double click: a click action is deferred by this amount +// when the same tag also has a dblclick action, so it can be canceled by the second click. +const dblClickDelay = 300; + +export const scadaSymbolActionTriggerTranslations = new Map( + [ + ['click', 'scada.tag.on-click-action'], + ['dblclick', 'scada.tag.on-double-click-action'], + ['contextmenu', 'scada.tag.on-right-click-action'] + ] +); + +export const scadaSymbolActionTriggerHelpIds = new Map( + [ + ['click', 'scada/tag_click_action_fn'], + ['dblclick', 'scada/tag_dblclick_action_fn'], + ['contextmenu', 'scada/tag_contextmenu_action_fn'] + ] +); export type ScadaSymbolActionFunction = (ctx: ScadaSymbolContext, element: Element, event: Event) => void; export interface ScadaSymbolAction { @@ -669,13 +691,29 @@ export class ScadaSymbolObject { for (const tag of this.metadata.tags) { if (tag.actions) { const elements = this.svgShape.find(`[tb\\:tag="${tag.tag}"]`); + const deferClick = !!tag.actions.click && !!tag.actions.dblclick; for (const trigger of Object.keys(tag.actions)) { const action = tag.actions[trigger]; elements.forEach(element => { element.attr('cursor', 'pointer'); - element.on(trigger, (event) => { - action.action(this.context, element, event); - }); + if (trigger === 'click' && deferClick) { + let clickTimeout: ReturnType = null; + element.on('click', (event: MouseEvent) => { + clearTimeout(clickTimeout); + if (event.detail === 1) { + clickTimeout = setTimeout(() => { + action.action(this.context, element, event); + }, dblClickDelay); + } + }); + } else { + element.on(trigger, (event) => { + if (trigger === 'contextmenu') { + event.preventDefault(); + } + action.action(this.context, element, event); + }); + } }); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts index 2e1a7e4d29..9a4869b901 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts @@ -30,10 +30,15 @@ import { WidgetService } from '@core/http/widget.service'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { AceHighlightRules } from '@shared/models/ace/ace.models'; import { - scadaSymbolClickActionHighlightRules, + scadaSymbolActionFunctionHighlightRules, scadaSymbolRenderFunctionHighlightRules } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { JsFuncComponent } from '@shared/components/js-func.component'; +import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggerHelpIds, + scadaSymbolActionTriggerTranslations +} from '@home/components/widget/lib/scada/scada-symbol.models'; @Component({ selector: 'tb-scada-symbol-metadata-tag-function-panel', @@ -51,7 +56,7 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft tagFunction: string; @Input() - tagFunctionType: 'renderFunction' | 'clickAction'; + tagFunctionType: 'renderFunction' | ScadaSymbolActionTrigger; @Input() tag: string; @@ -98,11 +103,11 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft this.tagFunctionArgs = ['ctx', 'element']; this.highlightRules = scadaSymbolRenderFunctionHighlightRules; this.tagFunctionHelpId = 'scada/tag_state_render_fn'; - } else if (this.tagFunctionType === 'clickAction') { - this.panelTitle = 'scada.tag.on-click-action'; + } else { + this.panelTitle = scadaSymbolActionTriggerTranslations.get(this.tagFunctionType); this.tagFunctionArgs = ['ctx', 'element', 'event']; - this.highlightRules = scadaSymbolClickActionHighlightRules; - this.tagFunctionHelpId = 'scada/tag_click_action_fn'; + this.highlightRules = scadaSymbolActionFunctionHighlightRules; + this.tagFunctionHelpId = scadaSymbolActionTriggerHelpIds.get(this.tagFunctionType); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html index b3a64d6c0e..fd8dfae708 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.html @@ -37,16 +37,19 @@ } -
- @if (!disabled || tagFormGroup.get('clickAction').value) { - - } -
+ @for (trigger of actionTriggers; track trigger) { +
+ @if (!disabled || tagFormGroup.get(actionControlName(trigger)).value) { + + } +
+ } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss index 88a0cd4d20..28958522ac 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.scss @@ -15,15 +15,10 @@ */ .tb-scada-symbol-metadata-tag-row { .tb-tag-field { - flex: 1 1 70%; + flex: 1 1 40%; } - .tb-state-render-function-field { + .tb-state-render-function-field, .tb-action-function-field { flex: 1 1 15%; - } - .tb-on-click-action-field { - flex: 1 1 15%; - } - .tb-state-render-function-field, .tb-on-click-action-field { display: flex; justify-content: flex-end; .mdc-button { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts index a1237d87dc..4ab6f2254d 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component.ts @@ -19,8 +19,10 @@ import { forwardRef, Input, OnInit, + QueryList, Renderer2, ViewChild, + ViewChildren, ViewContainerRef, ViewEncapsulation } from '@angular/core'; @@ -34,7 +36,11 @@ import { UntypedFormGroup, Validator } from '@angular/forms'; -import { ScadaSymbolTag } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggers, + ScadaSymbolTag +} from '@home/components/widget/lib/scada/scada-symbol.models'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; @@ -66,8 +72,8 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On @ViewChild('editStateRenderFunctionButton') editStateRenderFunctionButton: MatButton; - @ViewChild('editClickActionButton') - editClickActionButton: MatButton; + @ViewChildren('editActionButton') + editActionButtons: QueryList; @Input() disabled: boolean; @@ -76,7 +82,9 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On elementStateRenderFunctionCompleter: TbEditorCompleter; @Input() - clickActionFunctionCompleter: TbEditorCompleter; + actionFunctionCompleter: TbEditorCompleter; + + actionTriggers = scadaSymbolActionTriggers; tagFormGroup: UntypedFormGroup; @@ -91,11 +99,14 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On } ngOnInit() { - this.tagFormGroup = this.fb.group({ + const controls: {[key: string]: any} = { tag: [null, []], - stateRenderFunction: [null, []], - clickAction: [null, []] - }); + stateRenderFunction: [null, []] + }; + for (const trigger of this.actionTriggers) { + controls[this.actionControlName(trigger)] = [null, []]; + } + this.tagFormGroup = this.fb.group(controls); } registerOnChange(fn: any): void { @@ -116,14 +127,14 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On writeValue(value: ScadaSymbolTag): void { this.modelValue = value; - const clickAction = value?.actions?.click?.actionFunction; - this.tagFormGroup.patchValue( - { - tag: value?.tag, - stateRenderFunction: value?.stateRenderFunction, - clickAction - }, {emitEvent: false} - ); + const patch: {[key: string]: any} = { + tag: value?.tag, + stateRenderFunction: value?.stateRenderFunction + }; + for (const trigger of this.actionTriggers) { + patch[this.actionControlName(trigger)] = value?.actions?.[trigger]?.actionFunction; + } + this.tagFormGroup.patchValue(patch, {emitEvent: false}); } public validate(_c: UntypedFormControl) { @@ -135,15 +146,23 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On }; } + actionControlName(trigger: ScadaSymbolActionTrigger): string { + return trigger + 'Action'; + } + editTagStateRenderFunction(): void { this.openTagFunction('renderFunction', this.editStateRenderFunctionButton); } - editClickAction(): void { - this.openTagFunction('clickAction', this.editClickActionButton); + editAction(trigger: ScadaSymbolActionTrigger): void { + const button = this.editActionButtons + .find(b => b._elementRef.nativeElement.getAttribute('data-trigger') === trigger); + if (button) { + this.openTagFunction(trigger, button); + } } - private openTagFunction(tagFunctionType: 'renderFunction' | 'clickAction', + private openTagFunction(tagFunctionType: 'renderFunction' | ScadaSymbolActionTrigger, button: MatButton) { const trigger = button._elementRef.nativeElement; trigger.scrollIntoView(); @@ -155,9 +174,9 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On if (tagFunctionType === 'renderFunction') { tagFunctionControl = this.tagFormGroup.get('stateRenderFunction'); completer = this.elementStateRenderFunctionCompleter; - } else if (tagFunctionType === 'clickAction') { - tagFunctionControl = this.tagFormGroup.get('clickAction'); - completer = this.clickActionFunctionCompleter; + } else { + tagFunctionControl = this.tagFormGroup.get(this.actionControlName(tagFunctionType)); + completer = this.actionFunctionCompleter; } const scadaSymbolTagFunctionPanelPopover = this.popoverService.displayPopover({ trigger, @@ -189,15 +208,17 @@ export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, On tag: value.tag, stateRenderFunction: value.stateRenderFunction }; - if (value.clickAction) { - this.modelValue.actions = { - click: { - actionFunction: value.clickAction - } - }; - } else { - this.modelValue.actions = null; + let actions: ScadaSymbolTag['actions'] = null; + for (const trigger of this.actionTriggers) { + const actionFunction = value[this.actionControlName(trigger)]; + if (actionFunction) { + actions = actions || {}; + actions[trigger] = { + actionFunction + }; + } } + this.modelValue.actions = actions; this.propagateChange(this.modelValue); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html index 6bda520766..9de4ec14a5 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html @@ -20,7 +20,9 @@
scada.tag.tag
scada.state-render-function
-
scada.tag.on-click-action
+ @for (trigger of actionTriggers; track trigger) { +
{{ actionTriggerTranslations.get(trigger) | translate }}
+ }
@if (tagsFormArray().controls.length) {
@@ -28,7 +30,7 @@
@if (!$last) { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss index 3463b7ae2f..285dee5fe0 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.scss @@ -18,15 +18,10 @@ margin: 12px; .tb-form-table-header-cell { &.tb-tag-header { - flex: 1 1 70%; + flex: 1 1 40%; } - &.tb-state-render-function-header { + &.tb-state-render-function-header, &.tb-action-function-header { flex: 1 1 15%; - } - &.tb-on-click-action-header { - flex: 1 1 15%; - } - &.tb-state-render-function-header, &.tb-on-click-action-header { display: flex; justify-content: flex-end; } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts index 826f585e71..c2d27be179 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.ts @@ -38,7 +38,12 @@ import { UntypedFormGroup, Validator } from '@angular/forms'; -import { ScadaSymbolTag } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggers, + scadaSymbolActionTriggerTranslations, + ScadaSymbolTag +} from '@home/components/widget/lib/scada/scada-symbol.models'; import { ScadaSymbolMetadataTagComponent } from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component'; @@ -46,7 +51,7 @@ import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; const tagIsEmpty = (tag: ScadaSymbolTag): boolean => - !tag.stateRenderFunction && !tag.actions?.click?.actionFunction; + !tag.stateRenderFunction && !scadaSymbolActionTriggers.some(trigger => tag.actions?.[trigger]?.actionFunction); @Component({ selector: 'tb-scada-symbol-metadata-tags', @@ -85,7 +90,11 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O elementStateRenderFunctionCompleter: TbEditorCompleter; @Input() - clickActionFunctionCompleter: TbEditorCompleter; + actionFunctionCompleter: TbEditorCompleter; + + actionTriggers = scadaSymbolActionTriggers; + + actionTriggerTranslations = scadaSymbolActionTriggerTranslations; tagsFormGroup: UntypedFormGroup; @@ -184,12 +193,12 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O }); } - editTagClickAction(tag: string): void { + editTagAction(tag: string, trigger: ScadaSymbolActionTrigger): void { setTimeout(() => { const tags: ScadaSymbolTag[] = this.tagsFormGroup.get('tags').value; const index = tags.findIndex(t => t.tag === tag); const tagComponent = this.metadataTags.get(index); - tagComponent?.editClickAction(); + tagComponent?.editAction(trigger); }); } @@ -204,12 +213,14 @@ export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, O const found = result.find(t => t.tag === tag.tag); if (found) { found.stateRenderFunction = tag.stateRenderFunction; - if (tag.actions?.click?.actionFunction) { - found.actions = { - click: { - actionFunction: tag.actions.click.actionFunction - } - }; + for (const trigger of scadaSymbolActionTriggers) { + const actionFunction = tag.actions?.[trigger]?.actionFunction; + if (actionFunction) { + found.actions = found.actions || {}; + found.actions[trigger] = { + actionFunction + }; + } } } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html index 30dbe4f734..47f8eabc3b 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html @@ -95,7 +95,7 @@ diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index a893100791..e2d0798e82 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -36,7 +36,11 @@ import { Validators } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; -import { emptyMetadata, ScadaSymbolMetadata } from '@home/components/widget/lib/scada/scada-symbol.models'; +import { + emptyMetadata, + ScadaSymbolActionTrigger, + ScadaSymbolMetadata +} from '@home/components/widget/lib/scada/scada-symbol.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; @@ -46,7 +50,7 @@ import { } from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { - clickActionFunctionCompletions, + actionFunctionCompletions, elementStateRenderFunctionCompletions, generalStateRenderFunctionCompletions, scadaSymbolContextCompletion, @@ -122,7 +126,7 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni generalStateRenderFunctionCompleter: TbEditorCompleter; elementStateRenderFunctionCompleter: TbEditorCompleter; - clickActionFunctionCompleter: TbEditorCompleter; + actionFunctionCompleter: TbEditorCompleter; highlightRules = scadaSymbolGeneralStateHighlightRules; @@ -205,9 +209,9 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni this.symbolMetadataTags.editTagStateRenderFunction(tag); } - editTagClickAction(tag: string): void { + editTagAction(tag: string, trigger: ScadaSymbolActionTrigger): void { this.selectedOption = 'tags'; - this.symbolMetadataTags.editTagClickAction(tag); + this.symbolMetadataTags.editTagAction(tag, trigger); } public validate(c: UntypedFormControl) { @@ -240,11 +244,11 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni } else { this.elementStateRenderFunctionCompleter.updateCompletions(elementStateRender); } - const clickAction = clickActionFunctionCompletions(contextCompleter); - if (!this.clickActionFunctionCompleter) { - this.clickActionFunctionCompleter = new TbEditorCompleter(clickAction); + const actionFunction = actionFunctionCompletions(contextCompleter); + if (!this.actionFunctionCompleter) { + this.actionFunctionCompleter = new TbEditorCompleter(actionFunction); } else { - this.clickActionFunctionCompleter.updateCompletions(clickAction); + this.actionFunctionCompleter.updateCompletions(actionFunction); } } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts index fe59eb8dc3..cc3c77baf3 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts @@ -24,6 +24,8 @@ import { setupTagPanelTooltip } from '@home/pages/scada-symbol/scada-symbol-tooltip.components'; import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggers, ScadaSymbolBehavior, ScadaSymbolBehaviorType, scadaSymbolContentData, @@ -51,9 +53,9 @@ export interface ScadaSymbolData { export interface ScadaSymbolEditObjectCallbacks { tagHasStateRenderFunction: (tag: string) => boolean; - tagHasClickAction: (tag: string) => boolean; + tagHasAction: (tag: string, trigger: ScadaSymbolActionTrigger) => boolean; editTagStateRenderFunction: (tag: string) => void; - editTagClickAction: (tag: string) => void; + editTagAction: (tag: string, trigger: ScadaSymbolActionTrigger) => void; tagsUpdated: (tags: string[]) => void; hasHiddenElements?: (hasHidden: boolean) => void; onSymbolEditObjectDirty: (dirty: boolean) => void; @@ -423,16 +425,16 @@ export class ScadaSymbolEditObject { return this.callbacks.tagHasStateRenderFunction(tag); } - public tagHasClickAction(tag: string): boolean { - return this.callbacks.tagHasClickAction(tag); + public tagHasAction(tag: string, trigger: ScadaSymbolActionTrigger): boolean { + return this.callbacks.tagHasAction(tag, trigger); } public editTagStateRenderFunction(tag: string) { this.callbacks.editTagStateRenderFunction(tag); } - public editTagClickAction(tag: string) { - this.callbacks.editTagClickAction(tag); + public editTagAction(tag: string, trigger: ScadaSymbolActionTrigger) { + this.callbacks.editTagAction(tag, trigger); } public setDirty(dirty: boolean) { @@ -645,23 +647,27 @@ export class ScadaSymbolElement { } } - public hasClickAction(): boolean { + public hasAction(trigger: ScadaSymbolActionTrigger): boolean { if (this.hasTag()) { - return this.editObject.tagHasClickAction(this.tag); + return this.editObject.tagHasAction(this.tag, trigger); } else { return false; } } + public hasAnyAction(): boolean { + return scadaSymbolActionTriggers.some(trigger => this.hasAction(trigger)); + } + public editStateRenderFunction() { if (this.hasTag()) { this.editObject.editTagStateRenderFunction(this.tag); } } - public editClickAction() { + public editAction(trigger: ScadaSymbolActionTrigger) { if (this.hasTag()) { - this.editObject.editTagClickAction(this.tag); + this.editObject.editTagAction(this.tag, trigger); } } @@ -1081,7 +1087,7 @@ export const scadaSymbolRenderFunctionHighlightRules: AceHighlightRules = { ...scadaSymbolElementPropertyHighlightRules }; -export const scadaSymbolClickActionHighlightRules: AceHighlightRules = { +export const scadaSymbolActionFunctionHighlightRules: AceHighlightRules = { start: [ scadaSymbolCtxObjectHighlightRule, scadaSymbolElementHighlightRule, @@ -1114,7 +1120,7 @@ export const elementStateRenderFunctionCompletions = (ctxCompletion: TbEditorCom } }); -export const clickActionFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => { +export const actionFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => { const completions = elementStateRenderFunctionCompletions(ctxCompletion); completions.event = { meta: 'argument', diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts index f4065f8f93..1e04d69ac9 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-tooltip.components.ts @@ -31,6 +31,11 @@ import { ViewEncapsulation } from '@angular/core'; import { ScadaSymbolElement } from '@home/pages/scada-symbol/scada-symbol-editor.models'; +import { + ScadaSymbolActionTrigger, + scadaSymbolActionTriggers, + scadaSymbolActionTriggerTranslations +} from '@home/components/widget/lib/scada/scada-symbol.models'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { ENTER } from '@angular/cdk/keycodes'; @@ -241,7 +246,7 @@ class ScadaSymbolTagPanelComponent extends ScadaSymbolPanelComponent implements ngOnInit() { if (this.symbolElement?.readonly) { - this.displayTagSettings = (this.symbolElement?.hasStateRenderFunction() || this.symbolElement?.hasClickAction()); + this.displayTagSettings = (this.symbolElement?.hasStateRenderFunction() || this.symbolElement?.hasAnyAction()); } } @@ -387,26 +392,28 @@ class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent imp action.add } - @if (!symbolElement?.readonly || hasClickAction) { -
scada.tag.on-click-action
- } - @if (hasClickAction) { - - } - @if (!hasClickAction && !symbolElement?.readonly) { - + @for (trigger of actionTriggers; track trigger) { + @if (!symbolElement?.readonly || hasActions[trigger]) { +
{{ actionTriggerTranslations.get(trigger) | translate }}
+ } + @if (hasActions[trigger]) { + + } + @if (!hasActions[trigger] && !symbolElement?.readonly) { + + } }
`, styleUrls: ['./scada-symbol-tooltip.component.scss'], @@ -415,9 +422,13 @@ class ScadaSymbolRemoveTagConfirmComponent extends ScadaSymbolPanelComponent imp }) class ScadaSymbolTagSettingsComponent extends ScadaSymbolPanelComponent implements OnInit, AfterViewInit { + actionTriggers = scadaSymbolActionTriggers; + + actionTriggerTranslations = scadaSymbolActionTriggerTranslations; + hasStateRenderFunction = false; - hasClickAction = false; + hasActions: {[trigger in ScadaSymbolActionTrigger]?: boolean} = {}; constructor(public element: ElementRef) { super(element); @@ -429,15 +440,17 @@ class ScadaSymbolTagSettingsComponent extends ScadaSymbolPanelComponent implemen updateFunctionsState() { this.hasStateRenderFunction = this.symbolElement.hasStateRenderFunction(); - this.hasClickAction = this.symbolElement.hasClickAction(); + for (const trigger of this.actionTriggers) { + this.hasActions[trigger] = this.symbolElement.hasAction(trigger); + } } editStateRenderFunction() { this.symbolElement.editStateRenderFunction(); } - editClickAction() { - this.symbolElement.editClickAction(); + editAction(trigger: ScadaSymbolActionTrigger) { + this.symbolElement.editAction(trigger); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts index f2a358fde7..2a51f0f124 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -33,6 +33,7 @@ import { Subject } from 'rxjs'; import { ScadaSymbolData, ScadaSymbolEditObjectCallbacks } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { parseScadaSymbolMetadataFromContent, + ScadaSymbolActionTrigger, ScadaSymbolMetadata, ScadaSymbolObjectSettings, updateScadaSymbolMetadataInContent @@ -330,11 +331,11 @@ export class ScadaSymbolComponent extends PageComponent return false; } - tagHasClickAction(tag: string): boolean { + tagHasAction(tag: string, trigger: ScadaSymbolActionTrigger): boolean { const metadata: ScadaSymbolMetadata = this.scadaSymbolFormGroup.get('metadata').value; if (metadata.tags) { const found = metadata.tags.find(t => t.tag === tag); - return !!found?.actions?.click?.actionFunction; + return !!found?.actions?.[trigger]?.actionFunction; } return false; } @@ -343,8 +344,8 @@ export class ScadaSymbolComponent extends PageComponent this.symbolMetadata.editTagStateRenderFunction(tag); } - editTagClickAction(tag: string) { - this.symbolMetadata.editTagClickAction(tag); + editTagAction(tag: string, trigger: ScadaSymbolActionTrigger) { + this.symbolMetadata.editTagAction(tag, trigger); } onSymbolEditObjectDirty(dirty: boolean) { diff --git a/ui-ngx/src/assets/help/en_US/scada/tag_contextmenu_action_fn.md b/ui-ngx/src/assets/help/en_US/scada/tag_contextmenu_action_fn.md new file mode 100644 index 0000000000..ca68bb3bf0 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/scada/tag_contextmenu_action_fn.md @@ -0,0 +1,65 @@ +#### Symbol tag right click action function + +
+
+ +*function (ctx, element, event): void* + +A JavaScript function invoked when user right-clicks on SVG element with specific tag. + +**Parameters:** + +
    +
  • ctx: ScadaSymbolContext - Context of the SCADA symbol. +
  • +
  • element: Element - SVG.js element.
    + See the examples below to learn how to manipulate and animate elements.
    +
  • +
  • event: Event - DOM event. +
  • +
+ +
+ +The browser context menu is automatically suppressed on the element, so the configured action is executed instead. +Note that on touch devices the right click action is typically triggered by a long press. + +##### Example + +**Invoke widget right click action** + +
+ +This JavaScript snippet demonstrates triggering a widget action using the ScadaSymbolContext API when the right click event occurs. The widget action will be linked to the behaviorId 'rightClick', which defines the action that will be executed upon the event. +The behavior of this action depends on the type of widget action configured in the ThingsBoard platform (e.g., navigating to a dashboard state, updating the current state, opening a URL, etc.). + +
+ +```javascript +ctx.api.callAction(event, 'rightClick'); // Trigger widget action 'rightClick' on event +{:copy-code} +``` + +
+ +*Example Use Case* + +- **Open details popup:** If the 'rightClick' action is configured to open a popup with device details, right-clicking the element shows contextual information, while the left click keeps its primary action. +- **Custom context menu:** A custom-action widget action can render your own context menu for the element in place of the browser one. + +
+ +You can also execute an action behavior directly and pass a value, for example: + +```javascript +var running = ctx.values.running; // Current running status from context + +// Request device shutdown with observer callbacks for error handling +ctx.api.callAction(event, 'shutdown', undefined, { + error: () => { + // Action failed; keep the current state + ctx.api.setValue('running', running); + } +}); +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/scada/tag_dblclick_action_fn.md b/ui-ngx/src/assets/help/en_US/scada/tag_dblclick_action_fn.md new file mode 100644 index 0000000000..a5a41ebe9c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/scada/tag_dblclick_action_fn.md @@ -0,0 +1,65 @@ +#### Symbol tag double click action function + +
+
+ +*function (ctx, element, event): void* + +A JavaScript function invoked when user double-clicks on SVG element with specific tag. + +**Parameters:** + +
    +
  • ctx: ScadaSymbolContext - Context of the SCADA symbol. +
  • +
  • element: Element - SVG.js element.
    + See the examples below to learn how to manipulate and animate elements.
    +
  • +
  • event: Event - DOM event. +
  • +
+ +
+ +If the same tag also has an *on click* action configured, the click action is automatically deferred for a short interval +and cancelled when a double click is detected — only the double click action is executed. + +##### Example + +**Invoke widget double click action** + +
+ +This JavaScript snippet demonstrates triggering a widget action using the ScadaSymbolContext API when the double click event occurs. The widget action will be linked to the behaviorId 'doubleClick', which defines the action that will be executed upon the event. +The behavior of this action depends on the type of widget action configured in the ThingsBoard platform (e.g., navigating to a dashboard state, updating the current state, opening a URL, etc.). + +
+ +```javascript +ctx.api.callAction(event, 'doubleClick'); // Trigger widget action 'doubleClick' on event +{:copy-code} +``` + +
+ +*Example Use Case* + +- **Navigate to Dashboard State:** If the 'doubleClick' action is configured to navigate to a detailed dashboard state, the user will be redirected to that state when double-clicking on the element, while a single click may be reserved for a quick toggle action. +- **Open URL:** If configured to open a URL, double-clicking on the element will take the user to a specified web resource. + +
+ +You can also execute an action behavior directly and pass a value, for example: + +```javascript +var running = ctx.values.running; // Current running status from context + +// Toggle the device state with observer callbacks for next and error handling +ctx.api.callAction(event, 'switchState', !running, { + error: () => { + // Action failed; keep the current state + ctx.api.setValue('running', running); + } +}); +{:copy-code} +``` 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 1007fa3eea..83cf304d72 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3888,6 +3888,8 @@ "tag": { "tag": "Tag", "on-click-action": "On click action", + "on-double-click-action": "On double click action", + "on-right-click-action": "On right click action", "no-tags": "No tags configured", "delete-tag-text": "Are you sure you want to delete tag
{{tag}} from {{elementType}} element?", "update-tag": "Update tag",