SCADA Symbol editor: Add XML editor mode.
This commit is contained in:
parent
9e8ca44d44
commit
174166d9a1
@ -39,6 +39,11 @@
|
||||
"input": "./node_modules/ace-builds/src-noconflict/",
|
||||
"output": "/"
|
||||
},
|
||||
{
|
||||
"glob": "worker-xml.js",
|
||||
"input": "./node_modules/ace-builds/src-noconflict/",
|
||||
"output": "/"
|
||||
},
|
||||
{
|
||||
"glob": "worker-css.js",
|
||||
"input": "./node_modules/ace-builds/src-noconflict/",
|
||||
|
||||
@ -289,13 +289,24 @@ const updateScadaSymbolMetadataInDom = (svgDoc: Document, metadata: ScadaSymbolM
|
||||
metadataElement.appendChild(cdata);
|
||||
};
|
||||
|
||||
const tbMetadataRegex = /<tb:metadata>.*<\/tb:metadata>/gs;
|
||||
const tbMetadataRegex = /<tb:metadata[^>]*>.*<\/tb:metadata>/gs;
|
||||
|
||||
export interface ScadaSymbolContentData {
|
||||
svgRootNode: string;
|
||||
innerSvg: string;
|
||||
}
|
||||
|
||||
export const removeScadaSymbolMetadata = (svgContent: string): string => {
|
||||
let result = svgContent;
|
||||
tbMetadataRegex.lastIndex = 0;
|
||||
const metadataMatch = tbMetadataRegex.exec(svgContent);
|
||||
if (metadataMatch !== null && metadataMatch.length) {
|
||||
const metadata = metadataMatch[0];
|
||||
result = result.replace(metadata, '');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const scadaSymbolContentData = (svgContent: string): ScadaSymbolContentData => {
|
||||
const result: ScadaSymbolContentData = {
|
||||
svgRootNode: '',
|
||||
@ -517,10 +528,12 @@ export class ScadaSymbolObject {
|
||||
if (this.context) {
|
||||
for (const tag of this.metadata.tags) {
|
||||
const elements = this.context.tags[tag.tag];
|
||||
elements.forEach(element => {
|
||||
element.timeline().stop();
|
||||
element.timeline(null);
|
||||
});
|
||||
if (elements) {
|
||||
elements.forEach(element => {
|
||||
element.timeline().stop();
|
||||
element.timeline(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.svgShape) {
|
||||
|
||||
@ -35,6 +35,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noTags>
|
||||
<span fxLayoutAlign="start center"
|
||||
<span fxLayoutAlign="center center"
|
||||
class="tb-prompt">{{ 'scada.tag.no-tags' | translate }}</span>
|
||||
</ng-template>
|
||||
|
||||
@ -15,13 +15,19 @@
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<div class="tb-scada-symbol-editor-shape" #scadaSymbolShape></div>
|
||||
<div class="tb-scada-symbol-editor-tooltips" #tooltipsContainer>
|
||||
<div class="tb-scada-symbol-editor-shape" [fxShow]="editorMode === 'svg'" #scadaSymbolShape></div>
|
||||
<div class="tb-scada-symbol-editor-tooltips" [fxShow]="editorMode === 'svg'" #tooltipsContainer>
|
||||
<tb-anchor #tooltipsContainerComponent></tb-anchor>
|
||||
</div>
|
||||
<div class="tb-scada-symbol-editor-svg-xml" [fxShow]="editorMode === 'xml'">
|
||||
<tb-svg-xml [formControl]="svgContentFormControl"
|
||||
fillHeight
|
||||
noLabel>
|
||||
</tb-svg-xml>
|
||||
</div>
|
||||
<div class="tb-scada-symbol-editor-buttons">
|
||||
<div class="tb-scada-symbol-editor-view-buttons">
|
||||
<div class="tb-scada-symbol-editor-zoom-buttons tb-primary-fill">
|
||||
<div class="tb-scada-symbol-editor-zoom-buttons tb-primary-fill" [fxShow]="editorMode === 'svg'">
|
||||
<button mat-icon-button
|
||||
[disabled]="zoomInDisabled"
|
||||
(click)="zoomIn()"
|
||||
@ -37,7 +43,7 @@
|
||||
<mat-icon>remove</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="displayShowHidden" class="tb-primary-fill">
|
||||
<div *ngIf="displayShowHidden" [fxShow]="editorMode === 'svg'" class="tb-primary-fill">
|
||||
<button mat-icon-button
|
||||
(click)="toggleShowHidden()"
|
||||
matTooltip="{{ (showHiddenElements ? 'scada.hide-hidden-elements' : 'scada.show-hidden-elements') | translate }}"
|
||||
@ -46,7 +52,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tb-scada-symbol-editor-upload-buttons">
|
||||
<div class="tb-scada-symbol-editor-right-buttons">
|
||||
<div class="tb-primary-fill">
|
||||
<button mat-icon-button
|
||||
(click)="updateScadaSymbol.emit()"
|
||||
@ -63,5 +69,14 @@
|
||||
<mat-icon>download</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<tb-toggle-select appearance="fill"
|
||||
[disabled]="!svgContentFormControl.valid"
|
||||
fillHeight
|
||||
extraPadding
|
||||
primaryBackground
|
||||
[(ngModel)]="editorMode">
|
||||
<tb-toggle-option value="svg">{{ 'scada.mode-svg' | translate }}</tb-toggle-option>
|
||||
<tb-toggle-option value="xml">{{ 'scada.mode-xml' | translate }}</tb-toggle-option>
|
||||
</tb-toggle-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -25,6 +25,11 @@
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tb-scada-symbol-editor-svg-xml {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 88px;
|
||||
}
|
||||
.tb-scada-symbol-editor-buttons {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
@ -64,11 +69,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tb-scada-symbol-editor-upload-buttons {
|
||||
.tb-scada-symbol-editor-right-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
z-index: 101;
|
||||
tb-toggle-select {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -34,11 +34,15 @@ import {
|
||||
ScadaSymbolEditObjectCallbacks
|
||||
} from '@home/pages/scada-symbol/scada-symbol-editor.models';
|
||||
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { removeScadaSymbolMetadata } from '@home/components/widget/lib/scada/scada-symbol.models';
|
||||
|
||||
export interface ScadaSymbolEditorData {
|
||||
scadaSymbolContent: string;
|
||||
}
|
||||
|
||||
type editorModeType = 'svg' | 'xml';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-scada-symbol-editor',
|
||||
templateUrl: './scada-symbol-editor.component.html',
|
||||
@ -84,10 +88,31 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
|
||||
|
||||
displayShowHidden = false;
|
||||
|
||||
svgContentFormControl = new FormControl();
|
||||
|
||||
svgContent: string;
|
||||
|
||||
private editorModeValue: editorModeType = 'svg';
|
||||
|
||||
get editorMode(): editorModeType {
|
||||
return this.editorModeValue;
|
||||
}
|
||||
|
||||
set editorMode(value: editorModeType) {
|
||||
this.updateEditorMode(value);
|
||||
}
|
||||
|
||||
constructor(private cd: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.svgContentFormControl.valueChanges.subscribe((svgContent) => {
|
||||
if (this.svgContent !== svgContent) {
|
||||
this.svgContent = svgContent;
|
||||
this.editObjectCallbacks.onSymbolEditObjectDirty(true);
|
||||
}
|
||||
this.editObjectCallbacks.onSymbolEditObjectValid(this.svgContentFormControl.valid);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@ -114,13 +139,16 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
|
||||
const change = changes[propName];
|
||||
if (!change.firstChange && change.currentValue !== change.previousValue) {
|
||||
if (propName === 'data') {
|
||||
if (this.scadaSymbolEditObject) {
|
||||
setTimeout(() => {
|
||||
this.updateContent(this.data.scadaSymbolContent);
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.updateContent(this.data.scadaSymbolContent);
|
||||
});
|
||||
} else if (propName === 'readonly') {
|
||||
this.scadaSymbolEditObject.setReadOnly(this.readonly);
|
||||
if (this.readonly) {
|
||||
this.svgContentFormControl.disable({emitEvent: false});
|
||||
} else {
|
||||
this.svgContentFormControl.enable({emitEvent: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,7 +159,11 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
|
||||
}
|
||||
|
||||
getContent(): string {
|
||||
return this.scadaSymbolEditObject?.getContent();
|
||||
if (this.editorMode === 'svg') {
|
||||
return this.scadaSymbolEditObject?.getContent();
|
||||
} else {
|
||||
return this.svgContent;
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
@ -148,12 +180,33 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
|
||||
this.scadaSymbolEditObject.showHiddenElements(this.showHiddenElements);
|
||||
}
|
||||
|
||||
private updateEditorMode(mode: editorModeType) {
|
||||
this.editorModeValue = mode;
|
||||
if (mode === 'xml') {
|
||||
this.svgContent = this.scadaSymbolEditObject.getContent();
|
||||
this.svgContentFormControl.setValue(this.svgContent, {emitEvent: false});
|
||||
} else {
|
||||
this.updateEditObjectContent(this.svgContent);
|
||||
}
|
||||
}
|
||||
|
||||
private updateContent(content: string) {
|
||||
this.displayShowHidden = false;
|
||||
this.scadaSymbolEditObject.setContent(content);
|
||||
setTimeout(() => {
|
||||
this.updateZoomButtonsState();
|
||||
});
|
||||
this.svgContent = removeScadaSymbolMetadata(content);
|
||||
if (this.editorMode === 'xml') {
|
||||
this.svgContentFormControl.setValue(this.svgContent, {emitEvent: false});
|
||||
} else {
|
||||
this.updateEditObjectContent(this.svgContent);
|
||||
}
|
||||
}
|
||||
|
||||
private updateEditObjectContent(content: string) {
|
||||
if (this.scadaSymbolEditObject) {
|
||||
this.displayShowHidden = false;
|
||||
this.scadaSymbolEditObject.setContent(content);
|
||||
setTimeout(() => {
|
||||
this.updateZoomButtonsState();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateZoomButtonsState() {
|
||||
|
||||
@ -54,6 +54,7 @@ export interface ScadaSymbolEditObjectCallbacks {
|
||||
tagsUpdated: (tags: string[]) => void;
|
||||
hasHiddenElements?: (hasHidden: boolean) => void;
|
||||
onSymbolEditObjectDirty: (dirty: boolean) => void;
|
||||
onSymbolEditObjectValid: (valid: boolean) => void;
|
||||
onZoom?: () => void;
|
||||
}
|
||||
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
formControlName="metadata">
|
||||
<div class="tb-scada-symbol-metadata-header-prefix">
|
||||
<button fxHide.gt-sm
|
||||
[disabled]="scadaSymbolFormGroup.invalid"
|
||||
[disabled]="scadaSymbolFormGroup.invalid || !symbolEditorValid"
|
||||
mat-button color="primary"
|
||||
(click)="enterPreviewMode()">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
@ -94,7 +94,7 @@
|
||||
</div>
|
||||
<div fxFlex.lt-md class="tb-scada-symbol-metadata-header-suffix" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="12px">
|
||||
<button fxHide.lt-md
|
||||
[disabled]="scadaSymbolFormGroup.invalid"
|
||||
[disabled]="scadaSymbolFormGroup.invalid || !symbolEditorValid"
|
||||
mat-button color="primary"
|
||||
(click)="enterPreviewMode()">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
@ -107,7 +107,7 @@
|
||||
<mat-icon>close</mat-icon>
|
||||
{{ 'action.decline' | translate }}
|
||||
</button>
|
||||
<button [disabled]="!scadaSymbolFormGroup.valid || !(scadaSymbolFormGroup.dirty || symbolEditorDirty)"
|
||||
<button [disabled]="!scadaSymbolFormGroup.valid || !symbolEditorValid || !(scadaSymbolFormGroup.dirty || symbolEditorDirty)"
|
||||
mat-flat-button color="primary"
|
||||
(click)="onApplyScadaSymbolConfig()">
|
||||
<mat-icon>done</mat-icon>
|
||||
|
||||
@ -135,6 +135,8 @@ export class ScadaSymbolComponent extends PageComponent
|
||||
|
||||
symbolEditorDirty = false;
|
||||
|
||||
symbolEditorValid = true;
|
||||
|
||||
private previewScadaSymbolObjectSettings: ScadaSymbolObjectSettings;
|
||||
|
||||
private forcePristine = false;
|
||||
@ -338,6 +340,10 @@ export class ScadaSymbolComponent extends PageComponent
|
||||
this.symbolEditorDirty = dirty;
|
||||
}
|
||||
|
||||
onSymbolEditObjectValid(valid: boolean) {
|
||||
this.symbolEditorValid = valid;
|
||||
}
|
||||
|
||||
updateScadaSymbol() {
|
||||
this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
|
||||
UploadImageDialogResult>(UploadImageDialogComponent, {
|
||||
@ -354,6 +360,7 @@ export class ScadaSymbolComponent extends PageComponent
|
||||
scadaSymbolContent: this.symbolData.scadaSymbolContent
|
||||
};
|
||||
this.symbolEditorDirty = true;
|
||||
this.symbolEditorValid = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -471,6 +478,7 @@ export class ScadaSymbolComponent extends PageComponent
|
||||
}
|
||||
this.scadaSymbolFormGroup.markAsPristine();
|
||||
this.symbolEditorDirty = false;
|
||||
this.symbolEditorValid = true;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
38
ui-ngx/src/app/shared/components/svg-xml.component.html
Normal file
38
ui-ngx/src/app/shared/components/svg-xml.component.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!--
|
||||
|
||||
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-svg-xml" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight, 'no-label': noLabel}"
|
||||
tb-fullscreen
|
||||
[fullscreen]="fullscreen" fxLayout="column">
|
||||
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;" class="tb-svg-xml-toolbar">
|
||||
<label *ngIf="!noLabel" class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (hasErrors || required && !modelValue), 'tb-required': !disabled && required}">{{ label }}</label>
|
||||
<span fxFlex></span>
|
||||
<fieldset style="width: initial">
|
||||
<div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
|
||||
matTooltipPosition="above"
|
||||
style="border-radius: 50%"
|
||||
(click)="fullscreen = !fullscreen">
|
||||
<button type='button' mat-icon-button class="tb-mat-32">
|
||||
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div id="tb-svg-xml-panel" class="tb-svg-xml-content-panel" fxLayout="column">
|
||||
<div #svgXmlEditor id="tb-svg-xml-input" [ngStyle]="fillHeight ? {} : {minHeight: minHeight}" [ngClass]="{'fill-height': fillHeight}"></div>
|
||||
</div>
|
||||
</div>
|
||||
85
ui-ngx/src/app/shared/components/svg-xml.component.scss
Normal file
85
ui-ngx/src/app/shared/components/svg-xml.component.scss
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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-svg-xml {
|
||||
position: relative;
|
||||
|
||||
&.tb-disabled {
|
||||
color: rgba(0, 0, 0, .38);
|
||||
}
|
||||
|
||||
&.fill-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.no-label {
|
||||
.tb-svg-xml-content-panel {
|
||||
height: 100%;
|
||||
}
|
||||
.tb-svg-xml-toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tb-svg-xml-content-panel {
|
||||
height: calc(100% - 40px);
|
||||
border: 1px solid #c0c0c0;
|
||||
|
||||
#tb-svg-xml-input {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.tb-fullscreen) {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.tb-svg-xml-toolbar {
|
||||
& > * {
|
||||
&:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
button.mat-mdc-button-base, button.mat-mdc-button-base.tb-mat-32 {
|
||||
background: rgba(220, 220, 220, .35);
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
min-width: 32px;
|
||||
min-height: 15px;
|
||||
padding: 4px;
|
||||
font-size: .8rem;
|
||||
line-height: 15px;
|
||||
&:not(.tb-help-popup-button) {
|
||||
color: #7b7b7b;
|
||||
}
|
||||
}
|
||||
button.mat-mdc-button-base:not(.mat-mdc-icon-button) {
|
||||
height: 23px;
|
||||
}
|
||||
.tb-help-popup-button-loading {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
211
ui-ngx/src/app/shared/components/svg-xml.component.ts
Normal file
211
ui-ngx/src/app/shared/components/svg-xml.component.ts
Normal file
@ -0,0 +1,211 @@
|
||||
///
|
||||
/// 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,
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
|
||||
import { Ace } from 'ace-builds';
|
||||
import { getAce } from '@shared/models/ace/ace.models';
|
||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { UtilsService } from '@core/services/utils.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
|
||||
import { ResizeObserver } from '@juggle/resize-observer';
|
||||
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-svg-xml',
|
||||
templateUrl: './svg-xml.component.html',
|
||||
styleUrls: ['./svg-xml.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SvgXmlComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => SvgXmlComponent),
|
||||
multi: true,
|
||||
}
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SvgXmlComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
|
||||
|
||||
@ViewChild('svgXmlEditor', {static: true})
|
||||
svgXmlEditorElmRef: ElementRef;
|
||||
|
||||
private svgXmlEditor: Ace.Editor;
|
||||
private editorsResizeCaf: CancelAnimationFrame;
|
||||
private editorResize$: ResizeObserver;
|
||||
private ignoreChange = false;
|
||||
|
||||
@Input() label: string;
|
||||
|
||||
@Input() disabled: boolean;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
fillHeight = false;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
noLabel = false;
|
||||
|
||||
@Input() minHeight = '200px';
|
||||
|
||||
private requiredValue: boolean;
|
||||
get required(): boolean {
|
||||
return this.requiredValue;
|
||||
}
|
||||
@Input()
|
||||
set required(value: boolean) {
|
||||
this.requiredValue = coerceBooleanProperty(value);
|
||||
}
|
||||
|
||||
fullscreen = false;
|
||||
|
||||
modelValue: string;
|
||||
|
||||
hasErrors = false;
|
||||
|
||||
private propagateChange = null;
|
||||
|
||||
constructor(public elementRef: ElementRef,
|
||||
private utils: UtilsService,
|
||||
private translate: TranslateService,
|
||||
protected store: Store<AppState>,
|
||||
private raf: RafService,
|
||||
private cd: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const editorElement = this.svgXmlEditorElmRef.nativeElement;
|
||||
let editorOptions: Partial<Ace.EditorOptions> = {
|
||||
mode: 'ace/mode/svg',
|
||||
showGutter: true,
|
||||
showPrintMargin: true,
|
||||
readOnly: this.disabled
|
||||
};
|
||||
|
||||
const advancedOptions = {
|
||||
enableSnippets: true,
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true
|
||||
};
|
||||
|
||||
editorOptions = {...editorOptions, ...advancedOptions};
|
||||
getAce().subscribe(
|
||||
(ace) => {
|
||||
this.svgXmlEditor = ace.edit(editorElement, editorOptions);
|
||||
this.svgXmlEditor.session.setUseWrapMode(true);
|
||||
this.svgXmlEditor.setValue(this.modelValue ? this.modelValue : '', -1);
|
||||
this.svgXmlEditor.setReadOnly(this.disabled);
|
||||
this.svgXmlEditor.on('change', () => {
|
||||
if (!this.ignoreChange) {
|
||||
this.updateView();
|
||||
}
|
||||
});
|
||||
// @ts-ignore
|
||||
this.svgXmlEditor.session.on('changeAnnotation', () => {
|
||||
const annotations = this.svgXmlEditor.session.getAnnotations();
|
||||
const hasErrors = annotations.filter(annotation => annotation.type === 'error').length > 0;
|
||||
if (this.hasErrors !== hasErrors) {
|
||||
this.hasErrors = hasErrors;
|
||||
this.propagateChange(this.modelValue);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
this.editorResize$ = new ResizeObserver(() => {
|
||||
this.onAceEditorResize();
|
||||
});
|
||||
this.editorResize$.observe(editorElement);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.editorResize$) {
|
||||
this.editorResize$.disconnect();
|
||||
}
|
||||
if (this.svgXmlEditor) {
|
||||
this.svgXmlEditor.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private onAceEditorResize() {
|
||||
if (this.editorsResizeCaf) {
|
||||
this.editorsResizeCaf();
|
||||
this.editorsResizeCaf = null;
|
||||
}
|
||||
this.editorsResizeCaf = this.raf.raf(() => {
|
||||
this.svgXmlEditor.resize();
|
||||
this.svgXmlEditor.renderer.updateFull();
|
||||
});
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
if (this.svgXmlEditor) {
|
||||
this.svgXmlEditor.setReadOnly(this.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
public validate(c: UntypedFormControl) {
|
||||
return (!this.hasErrors) ? null : {
|
||||
svgXml: {
|
||||
valid: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
writeValue(value: string): void {
|
||||
this.modelValue = value;
|
||||
if (this.svgXmlEditor) {
|
||||
this.ignoreChange = true;
|
||||
this.svgXmlEditor.setValue(this.modelValue ? this.modelValue : '', -1);
|
||||
this.ignoreChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateView() {
|
||||
const editorValue = this.svgXmlEditor.getValue();
|
||||
if (this.modelValue !== editorValue) {
|
||||
this.modelValue = editorValue;
|
||||
this.propagateChange(this.modelValue);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,16 +20,26 @@
|
||||
[disabled]="!leftPaginationEnabled"
|
||||
(click)="handlePaginatorClick('before', $event)"
|
||||
(touchstart)="handlePaginatorTouchStart('before', $event)"
|
||||
class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': !isMdLg, 'tb-mat-24': isMdLg}">
|
||||
class="tb-toggle-header-pagination-button"
|
||||
[class.fill-height]="fillHeight"
|
||||
[class.tb-mat-32]="!isMdLg"
|
||||
[class.tb-mat-24]="isMdLg">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<div #toggleGroupContainer class="tb-toggle-container" [class]="{'tb-disable-pagination': disablePagination}" *ngIf="(useSelect$ | async) === false; else select" >
|
||||
<div #toggleGroupContainer class="tb-toggle-container"
|
||||
[class.tb-disable-pagination]="disablePagination"
|
||||
[class.fill-height]="fillHeight"
|
||||
[class.extra-padding]="extraPadding"
|
||||
*ngIf="(useSelect$ | async) === false; else select" >
|
||||
<mat-button-toggle-group #toggleGroup
|
||||
class="tb-toggle-header"
|
||||
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
|
||||
'tb-invert': appearance === 'fill-invert',
|
||||
'tb-ignore-md-lg': ignoreMdLgSize,
|
||||
'tb-disabled': disabled }" [name]="name" [(ngModel)]="value"
|
||||
[class.tb-fill]="(appearance === 'fill' || appearance === 'fill-invert')"
|
||||
[class.tb-invert]="appearance === 'fill-invert'"
|
||||
[class.tb-primary-fill]="primaryBackground"
|
||||
[class.tb-ignore-md-lg]="ignoreMdLgSize"
|
||||
[class.tb-disabled]="disabled"
|
||||
[name]="name"
|
||||
[(ngModel)]="value"
|
||||
(ngModelChange)="valueChange.emit(value)">
|
||||
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value" [disabled]="disabled">{{ option.name }}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
@ -39,11 +49,16 @@
|
||||
[disabled]="!rightPaginationEnabled"
|
||||
(click)="handlePaginatorClick('after', $event)"
|
||||
(touchstart)="handlePaginatorTouchStart('after', $event)"
|
||||
class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': !isMdLg, 'tb-mat-24': isMdLg}">
|
||||
class="tb-toggle-header-pagination-button"
|
||||
[class.fill-height]="fillHeight"
|
||||
[class.tb-mat-32]="!isMdLg"
|
||||
[class.tb-mat-24]="isMdLg">
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
<ng-template #select>
|
||||
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">
|
||||
<mat-form-field appearance="outline" class="tb-toggle-header-select"
|
||||
[class.fill-height]="fillHeight"
|
||||
subscriptSizing="dynamic">
|
||||
<mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)" [disabled]="disabled">
|
||||
<mat-option *ngFor="let option of options" [value]="option.value"> {{ option.name }}</mat-option>
|
||||
</mat-select>
|
||||
|
||||
@ -21,6 +21,9 @@
|
||||
grid-template-columns: min-content minmax(auto, 1fr) min-content;
|
||||
.tb-toggle-header-pagination-button {
|
||||
display: none;
|
||||
&.fill-height {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
&.tb-toggle-header-pagination-controls-enabled {
|
||||
.tb-toggle-header-pagination-button {
|
||||
@ -43,6 +46,25 @@
|
||||
}
|
||||
|
||||
:host ::ng-deep {
|
||||
.tb-toggle-container {
|
||||
&.fill-height {
|
||||
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
|
||||
height: 100%;
|
||||
.mat-button-toggle.mat-button-toggle-appearance-standard {
|
||||
.mat-button-toggle-button {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.extra-padding {
|
||||
.mat-button-toggle.mat-button-toggle-appearance-standard {
|
||||
.mat-button-toggle-label-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
@ -51,6 +73,12 @@
|
||||
padding: 2px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
&.tb-primary-fill {
|
||||
background: none;
|
||||
&:before {
|
||||
border-radius: 100px;
|
||||
}
|
||||
}
|
||||
.mat-button-toggle + .mat-button-toggle {
|
||||
border-left: none;
|
||||
}
|
||||
@ -161,6 +189,16 @@
|
||||
&.mat-mdc-form-field {
|
||||
line-height: 16px;
|
||||
font-size: 12px;
|
||||
&.fill-height {
|
||||
.mat-mdc-text-field-wrapper {
|
||||
.mat-mdc-form-field-flex {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
.mat-mdc-form-field-subscript-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix {
|
||||
min-height: 0;
|
||||
|
||||
@ -187,6 +187,18 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterV
|
||||
@coerceBoolean()
|
||||
disabled = false;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
fillHeight = false;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
extraPadding = false;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
primaryBackground = false;
|
||||
|
||||
get isMdLg(): boolean {
|
||||
return !this.ignoreMdLgSize && this.isMdLgValue;
|
||||
}
|
||||
|
||||
@ -16,10 +16,14 @@
|
||||
|
||||
-->
|
||||
<tb-toggle-header
|
||||
[class.fill-height]="fillHeight"
|
||||
ignoreMdLgSize="true"
|
||||
useSelectOnMdLg="false"
|
||||
[disabled]="disabled"
|
||||
[appearance]="appearance"
|
||||
[fillHeight]="fillHeight"
|
||||
[extraPadding]="extraPadding"
|
||||
[primaryBackground]="primaryBackground"
|
||||
[disablePagination]="disablePagination"
|
||||
[selectMediaBreakpoint]="selectMediaBreakpoint"
|
||||
[options]="options"
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
:host {
|
||||
tb-toggle-header {
|
||||
&.fill-height {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
|
||||
@Component({
|
||||
selector: 'tb-toggle-select',
|
||||
templateUrl: './toggle-select.component.html',
|
||||
styleUrls: [],
|
||||
styleUrls: ['./toggle-select.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
@ -52,6 +52,18 @@ export class ToggleSelectComponent extends _ToggleBase implements ControlValueAc
|
||||
@coerceBoolean()
|
||||
disablePagination = false;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
fillHeight = false;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
extraPadding = false;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
primaryBackground = false;
|
||||
|
||||
modelValue: any;
|
||||
|
||||
private propagateChange = null;
|
||||
|
||||
@ -37,6 +37,8 @@ function loadAceDependencies(): Observable<any> {
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-xml')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-svg')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-c_cpp')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
|
||||
@ -47,6 +49,8 @@ function loadAceDependencies(): Observable<any> {
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/xml')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/svg')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/c_cpp')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/protobuf')));
|
||||
aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate')));
|
||||
|
||||
@ -224,6 +224,7 @@ import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
|
||||
import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component';
|
||||
import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component';
|
||||
import { CountryData } from '@shared/models/country.models';
|
||||
import { SvgXmlComponent } from '@shared/components/svg-xml.component';
|
||||
|
||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||
return markedOptionsService;
|
||||
@ -338,6 +339,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
JsFuncComponent,
|
||||
CssComponent,
|
||||
HtmlComponent,
|
||||
SvgXmlComponent,
|
||||
FabTriggerDirective,
|
||||
FabActionsDirective,
|
||||
FabToolbarComponent,
|
||||
@ -544,6 +546,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
JsFuncComponent,
|
||||
CssComponent,
|
||||
HtmlComponent,
|
||||
SvgXmlComponent,
|
||||
FabTriggerDirective,
|
||||
FabActionsDirective,
|
||||
TbJsonToStringDirective,
|
||||
|
||||
@ -3661,6 +3661,8 @@
|
||||
"update-symbol": "Update SCADA symbol",
|
||||
"edit-symbol": "Edit SCADA symbol",
|
||||
"symbol-details": "SCADA symbol details",
|
||||
"mode-svg": "SVG",
|
||||
"mode-xml": "XML",
|
||||
"no-symbols": "No symbols found",
|
||||
"show-hidden-elements": "Show hidden elements",
|
||||
"hide-hidden-elements": "Hide hidden elements",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user