SCADA Symbol editor: Add XML editor mode.

This commit is contained in:
Igor Kulikov 2024-09-06 18:18:10 +03:00
parent 9e8ca44d44
commit 174166d9a1
21 changed files with 584 additions and 35 deletions

View File

@ -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/",

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View 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>

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

View 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();
}
}
}

View File

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

View File

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

View File

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

View File

@ -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"

View File

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

View File

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

View File

@ -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')));

View File

@ -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,

View File

@ -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",