From 2e1064effa3f086fc555f65f2a14d8f6005af7dd Mon Sep 17 00:00:00 2001 From: rhenck <richard.henck@iqb.hu-berlin.de> Date: Mon, 17 Jun 2024 19:54:12 +0200 Subject: [PATCH] Add new compound element: Table --- .../table/table-child-overlay.component.ts | 68 ++++++++++ .../table/table.component.ts | 121 ++++++++++++++++++ .../input-elements/checkbox.component.ts | 57 +++++---- .../input-elements/text-field.component.ts | 106 ++++++++++----- .../directives/compound-element.directive.ts | 3 +- .../elements/compound-elements/table/table.ts | 95 ++++++++++++++ projects/common/models/elements/element.ts | 2 +- projects/common/pipes/measure.pipe.ts | 3 +- projects/common/shared.module.ts | 2 - projects/common/util/element.factory.ts | 8 +- .../dialogs/table-edit-dialog.component.ts | 75 +++++++++++ .../ui-element-toolbox.component.ts | 2 + .../element-properties-panel.component.html | 4 +- .../ele-specific-props.component.ts | 10 +- .../table-properties.component.ts | 95 ++++++++++++++ .../canvas/canvas-element-overlay.ts | 28 +++- .../editor/src/app/services/dialog.service.ts | 10 ++ .../editor/src/app/services/id.service.ts | 3 +- .../src/app/services/selection.service.ts | 16 ++- .../services/unit-services/element.service.ts | 15 +++ .../services/unit-services/unit.service.ts | 1 + 21 files changed, 647 insertions(+), 77 deletions(-) create mode 100644 projects/common/components/compound-elements/table/table-child-overlay.component.ts create mode 100644 projects/common/components/compound-elements/table/table.component.ts create mode 100644 projects/common/models/elements/compound-elements/table/table.ts create mode 100644 projects/editor/src/app/components/dialogs/table-edit-dialog.component.ts create mode 100644 projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/table-properties.component.ts diff --git a/projects/common/components/compound-elements/table/table-child-overlay.component.ts b/projects/common/components/compound-elements/table/table-child-overlay.component.ts new file mode 100644 index 000000000..4e0a488d1 --- /dev/null +++ b/projects/common/components/compound-elements/table/table-child-overlay.component.ts @@ -0,0 +1,68 @@ +import { + ChangeDetectorRef, Component, ComponentRef, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef +} from '@angular/core'; +import { UIElement } from 'common/models/elements/element'; +import { ElementComponent } from 'common/directives/element-component.directive'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { TextFieldComponent } from 'common/components/input-elements/text-field.component'; +import { CheckboxComponent } from 'common/components/input-elements/checkbox.component'; + +@Component({ + selector: 'aspect-table-child-overlay', + standalone: true, + imports: [ + MatButtonModule, + MatIconModule + ], + template: ` + <div class="wrapper" + [style.border]="isSelected ? 'purple solid 1px' : ''" + (click)="elementSelected.emit(this); $event.stopPropagation();"> + <ng-template #elementContainer></ng-template> + </div> + `, + styles: ` + .wrapper {display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;} + button {position: absolute; opacity: 0;} + button:hover {opacity: 1;} + ` +}) +export class TableChildOverlay implements OnInit { + @Input() element!: UIElement; + @Output() elementSelected = new EventEmitter<TableChildOverlay>(); + @ViewChild('elementContainer', { read: ViewContainerRef, static: true }) private elementContainer!: ViewContainerRef; + childComponent!: ComponentRef<ElementComponent>; + + isSelected: boolean = false; + + constructor(private cdr: ChangeDetectorRef) { } + + ngOnInit(): void { + this.childComponent = this.elementContainer.createComponent(this.element.getElementComponent()); + this.childComponent.instance.elementModel = this.element; + + this.childComponent.changeDetectorRef.detectChanges(); // this fires onInit, which initializes the FormControl + + if (this.childComponent.instance instanceof TextFieldComponent || + this.childComponent.instance instanceof CheckboxComponent) { + this.childComponent.instance.tableMode = true; + } + // this.childComponent.location.nativeElement.style.pointerEvents = 'none'; + if (this.element.type !== 'text') { + this.childComponent.location.nativeElement.style.width = '100%'; + this.childComponent.location.nativeElement.style.height = '100%'; + } + if (this.element.type === 'text') { + this.childComponent.location.nativeElement.style.margin = '5px'; + } + if (this.element.type === 'drop-list') { + this.childComponent.setInput('clozeContext', true); + } + } + + setSelected(newValue: boolean): void { + this.isSelected = newValue; + this.cdr.detectChanges(); + } +} diff --git a/projects/common/components/compound-elements/table/table.component.ts b/projects/common/components/compound-elements/table/table.component.ts new file mode 100644 index 000000000..42d0e54c1 --- /dev/null +++ b/projects/common/components/compound-elements/table/table.component.ts @@ -0,0 +1,121 @@ +import { CompoundElementComponent } from 'common/directives/compound-element.directive'; +import { TableElement } from 'common/models/elements/compound-elements/table/table'; +import { + Component, OnInit, + Input, Output, EventEmitter, + QueryList, ViewChildren +} from '@angular/core'; +import { ElementComponent } from 'common/directives/element-component.directive'; +import { SharedModule } from 'common/shared.module'; +import { PositionedUIElement, UIElementType } from 'common/models/elements/element'; +import { TableChildOverlay } from 'common/components/compound-elements/table/table-child-overlay.component'; +import { MatMenuModule } from '@angular/material/menu'; +import { MeasurePipe } from 'common/pipes/measure.pipe'; + +@Component({ + selector: 'aspect-table', + standalone: true, + imports: [ + SharedModule, + TableChildOverlay, + MatMenuModule, + MeasurePipe + ], + template: ` + <div [style.display]="'grid'" + [style.grid-template-columns]="elementModel.gridColumnSizes | measure" + [style.grid-template-rows]="elementModel.gridRowSizes | measure" + [style.grid-auto-columns]="'auto'" + [style.grid-auto-rows]="'auto'"> + <ng-container *ngFor="let row of elementGrid; let i = index;"> + <div *ngFor="let column of row; let j = index;" + class="cell-container" + [style.border-style]="elementModel.styling.borderStyle" + [style.border-top-style]="(!elementModel.tableEdgesEnabled && i === 0) || (i > 0) ? + 'none' : elementModel.styling.borderStyle" + [style.border-bottom-style]="(!elementModel.tableEdgesEnabled && i === elementGrid.length - 1) ? + 'none' : elementModel.styling.borderStyle" + [style.border-left-style]="(!elementModel.tableEdgesEnabled && j === 0) || (j > 0) ? + 'none' : elementModel.styling.borderStyle" + [style.border-right-style]="(!elementModel.tableEdgesEnabled && j === row.length - 1) ? + 'none' : elementModel.styling.borderStyle" + [style.border-width.px]="elementModel.styling.borderWidth" + [style.border-color]="elementModel.styling.borderColor" + [style.border-radius.px]="elementModel.styling.borderRadius" + [style.grid-row-start]="i + 1" + [style.grid-column-start]="j + 1"> + <ng-container *ngIf="elementGrid[i][j] === undefined && editorMode"> + <button mat-mini-fab color="primary" + [matMenuTriggerFor]="menu"> + <mat-icon>add</mat-icon> + </button> + <mat-menu #menu="matMenu"> + <button mat-menu-item (click)="addElement('text', i, j)">Text</button> + <button mat-menu-item (click)="addElement('text-field', i, j)">Eingabefeld</button> + <button mat-menu-item (click)="addElement('checkbox', i, j)">Kontrollkästchen</button> + <button mat-menu-item (click)="addElement('drop-list', i, j)">Ablegeliste</button> + <button mat-menu-item (click)="addElement('image', i, j)">Bild</button> + <button mat-menu-item (click)="addElement('audio', i, j)">Audio</button> + </mat-menu> + </ng-container> + <div *ngIf="elementGrid[i][j] !== undefined" class="element-container"> + <button *ngIf="editorMode" class="delete-button" mat-mini-fab color="primary" + (click)="removeElement(i, j)"> + <mat-icon>remove</mat-icon> + </button> + <aspect-table-child-overlay [element]="$any(elementGrid[i][j])" + (elementSelected)="childElementSelected.emit($event)"> + </aspect-table-child-overlay> + </div> + </div> + </ng-container> + </div> + `, + styles: [` + .cell-container {display: flex; align-items: center; justify-content: center; min-height: 50px; min-width: 50px;} + .delete-button {position: absolute; opacity: 0;} + .element-container:hover .delete-button {opacity: 1;} + .element-container {width: 100%; height: 100%; display: flex; justify-content: center;} + aspect-table-child-overlay {width: 100%; height: 100%;} + `] +}) +export class TableComponent extends CompoundElementComponent implements OnInit { + @Input() elementModel!: TableElement; + @Input() editorMode: boolean = false; + @Output() elementAdded = new EventEmitter<{ elementType: UIElementType, row: number, col: number }>(); + @Output() elementRemoved = new EventEmitter<number>(); + @Output() childElementSelected = new EventEmitter<TableChildOverlay>(); + @ViewChildren(TableChildOverlay) compoundChildren!: QueryList<TableChildOverlay>; + + elementGrid: (PositionedUIElement | undefined)[][] = []; + + ngOnInit(): void { + this.initElementGrid(); + } + + private initElementGrid(): void { + this.elementGrid = new Array(this.elementModel.gridRowSizes.length).fill(undefined) + .map(() => new Array(this.elementModel.gridColumnSizes.length).fill(undefined)); + this.elementModel.elements.forEach(el => { + this.elementGrid[(el.position.gridRow as number) - 1][(el.position.gridColumn as number) - 1] = el; + }); + } + + addElement(elementType: UIElementType, row: number, col: number): void { + this.elementAdded.emit({ elementType, row, col }); + } + + getFormElementChildrenComponents(): ElementComponent[] { + return this.compoundChildren.toArray().map((child: TableChildOverlay) => child.childComponent.instance); + } + + refresh(): void { + this.initElementGrid(); + } + + removeElement(row: number, col: number): void { + this.elementRemoved.emit(this.elementGrid.flat() + .findIndex(el => el?.position.gridRow === row && el?.position.gridColumn === col)); + this.refresh(); + } +} diff --git a/projects/common/components/input-elements/checkbox.component.ts b/projects/common/components/input-elements/checkbox.component.ts index a07249d5a..1ac235c9c 100644 --- a/projects/common/components/input-elements/checkbox.component.ts +++ b/projects/common/components/input-elements/checkbox.component.ts @@ -1,30 +1,39 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { CheckboxElement } from 'common/models/elements/input-elements/checkbox'; import { FormElementComponent } from '../../directives/form-element-component.directive'; @Component({ selector: 'aspect-checkbox', template: ` - <div class="mat-form-field" - [style.width.%]="100" - [style.height.%]="100" - [style.background-color]="elementModel.styling.backgroundColor"> - <mat-checkbox #checkbox class="example-margin" - [formControl]="elementFormControl" - [checked]="$any(elementModel.value)" - [class.cross-out]="elementModel.crossOutChecked && elementFormControl.value" - [style.color]="elementModel.styling.fontColor" - [style.font-size.px]="elementModel.styling.fontSize" - [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" - [style.font-style]="elementModel.styling.italic ? 'italic' : ''" - [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" - (click)="elementModel.readOnly ? $event.preventDefault() : null"> - <div [innerHTML]="elementModel.label | safeResourceHTML"></div> - </mat-checkbox> - <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched" - class="error-message"> - {{elementFormControl.errors | errorTransform: elementModel}} - </mat-error> + <ng-container *ngIf="!tableMode"> + <div class="mat-form-field" + [style.width.%]="100" + [style.height.%]="100" + [style.background-color]="elementModel.styling.backgroundColor"> + <mat-checkbox #checkbox class="example-margin" + [formControl]="elementFormControl" + [checked]="$any(elementModel.value)" + [class.cross-out]="elementModel.crossOutChecked && elementFormControl.value" + [style.color]="elementModel.styling.fontColor" + [style.font-size.px]="elementModel.styling.fontSize" + [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" + [style.font-style]="elementModel.styling.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" + (click)="elementModel.readOnly ? $event.preventDefault() : null"> + <div [innerHTML]="elementModel.label | safeResourceHTML"></div> + </mat-checkbox> + <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched" + class="error-message"> + {{elementFormControl.errors | errorTransform: elementModel}} + </mat-error> + </div> + </ng-container> + <div *ngIf="tableMode" class="svg-checkbox" + (click)="this.elementFormControl.setValue(!this.elementFormControl.value)"> + <svg class="svg-checkbox-cross" [style.opacity]="this.elementFormControl.value ? 1 : 0" viewBox='0 0 100 100'> + <path d='M1 0 L0 1 L99 100 L100 99' fill='black' stroke="black" stroke-width="2" /> + <path d='M0 99 L99 0 L100 1 L1 100' fill='black' stroke="black" stroke-width="1" /> + </svg> </div> `, styles: [` @@ -48,8 +57,12 @@ import { FormElementComponent } from '../../directives/form-element-component.di text-decoration: line-through; text-decoration-thickness: 3px; } + .svg-checkbox {width: 100%; height: 100%;} + .svg-checkbox:hover {background-color: lightgrey;} + .svg-checkbox-cross {width: 100%; height: 100%;} `] }) -export class CheckboxComponent extends FormElementComponent { +export class CheckboxComponent extends FormElementComponent implements OnInit { @Input() elementModel!: CheckboxElement; + tableMode: boolean = true; } diff --git a/projects/common/components/input-elements/text-field.component.ts b/projects/common/components/input-elements/text-field.component.ts index 487719a82..eaa6b187c 100644 --- a/projects/common/components/input-elements/text-field.component.ts +++ b/projects/common/components/input-elements/text-field.component.ts @@ -5,46 +5,75 @@ import { TextInputComponent } from 'common/directives/text-input-component.direc @Component({ selector: 'aspect-text-field', template: ` - <mat-form-field [class.small-input]="elementModel.label === ''" - [style.width.%]="100" - [style.height.%]="100" - [style.line-height.%]="elementModel.styling.lineHeight" - [style.color]="elementModel.styling.fontColor" - [style.font-size.px]="elementModel.styling.fontSize" - [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" - [style.font-style]="elementModel.styling.italic ? 'italic' : ''" - [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" - [style.--backgroundColor]="elementModel.styling.backgroundColor" - [appearance]="$any(elementModel.appearance)"> - <mat-label>{{elementModel.label}}</mat-label> - <input matInput #input - autocomplete="off" - autocapitalize="none" - autocorrect="off" - spellcheck="false" - value="{{elementModel.value}}" + <ng-container *ngIf="!tableMode"> + <mat-form-field [class.small-input]="elementModel.label === ''" + [style.width.%]="100" + [style.height.%]="100" + [style.line-height.%]="elementModel.styling.lineHeight" + [style.color]="elementModel.styling.fontColor" + [style.font-size.px]="elementModel.styling.fontSize" + [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" + [style.font-style]="elementModel.styling.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" + [style.--backgroundColor]="elementModel.styling.backgroundColor" + [appearance]="$any(elementModel.appearance)"> + <mat-label>{{elementModel.label}}</mat-label> + <input matInput #input + autocomplete="off" + autocapitalize="none" + autocorrect="off" + spellcheck="false" + value="{{elementModel.value}}" + [attr.inputmode]="elementModel.showSoftwareKeyboard || elementModel.hideNativeKeyboard ? 'none' : 'text'" + [formControl]="elementFormControl" + [pattern]="$any(elementModel.pattern)" + [readonly]="elementModel.readOnly" + (paste)="elementModel.isLimitedToMaxLength && elementModel.maxLength ? $event.preventDefault() : null" + (keydown)="onKeyDown.emit({keyboardEvent: $event, inputElement: input})" + (focus)="focusChanged.emit({ inputElement: input, focused: true })" + (blur)="focusChanged.emit({ inputElement: input, focused: false })"> + <div matSuffix + class="fx-row-center-baseline"> + <!-- TODO nicht zu sehen--> + <mat-icon *ngIf="!elementFormControl.touched && elementModel.hasKeyboardIcon">keyboard_outline</mat-icon> + <button *ngIf="elementModel.clearable" + type="button" + mat-icon-button aria-label="Clear" + (click)="elementFormControl.setValue('')"> + <mat-icon>close</mat-icon> + </button> + </div> + <mat-error *ngIf="elementFormControl.errors"> + {{elementFormControl.errors | errorTransform: elementModel}} + </mat-error> + </mat-form-field> + </ng-container> + + <ng-container *ngIf="tableMode"> + <aspect-cloze-child-error-message *ngIf="elementFormControl.errors && elementFormControl.touched" + [elementModel]="elementModel" + [elementFormControl]="elementFormControl"> + </aspect-cloze-child-error-message> + <input #input + class="table-child" + autocomplete="off" autocapitalize="none" autocorrect="off" spellcheck="false" + [class.errors]="elementFormControl.errors && elementFormControl.touched" [attr.inputmode]="elementModel.showSoftwareKeyboard || elementModel.hideNativeKeyboard ? 'none' : 'text'" - [formControl]="elementFormControl" - [pattern]="$any(elementModel.pattern)" + [style.line-height.%]="elementModel.styling.lineHeight" + [style.color]="elementModel.styling.fontColor" + [style.font-size.px]="elementModel.styling.fontSize" + [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" + [style.font-style]="elementModel.styling.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" + [style.background-color]="elementModel.styling.backgroundColor" [readonly]="elementModel.readOnly" + [formControl]="elementFormControl" + [value]="elementModel.value" (paste)="elementModel.isLimitedToMaxLength && elementModel.maxLength ? $event.preventDefault() : null" (keydown)="onKeyDown.emit({keyboardEvent: $event, inputElement: input})" (focus)="focusChanged.emit({ inputElement: input, focused: true })" (blur)="focusChanged.emit({ inputElement: input, focused: false })"> - <div matSuffix - class="fx-row-center-baseline"> - <mat-icon *ngIf="!elementFormControl.touched && elementModel.hasKeyboardIcon">keyboard_outline</mat-icon> - <button *ngIf="elementModel.clearable" - type="button" - mat-icon-button aria-label="Clear" - (click)="elementFormControl.setValue('')"> - <mat-icon>close</mat-icon> - </button> - </div> - <mat-error *ngIf="elementFormControl.errors"> - {{elementFormControl.errors | errorTransform: elementModel}} - </mat-error> - </mat-form-field> + </ng-container> `, styles: [` :host ::ng-deep .small-input div.mdc-notched-outline { @@ -71,8 +100,17 @@ import { TextInputComponent } from 'common/directives/text-input-component.direc justify-content: center; align-items: baseline; } + .table-child { + width: 100%; + height: 100%; + box-sizing: border-box; + border: none; + padding: 0 10px; + font-family: inherit; + } `] }) export class TextFieldComponent extends TextInputComponent { @Input() elementModel!: TextFieldElement; + tableMode: boolean = false; } diff --git a/projects/common/directives/compound-element.directive.ts b/projects/common/directives/compound-element.directive.ts index 03b1438ee..fce0d2552 100644 --- a/projects/common/directives/compound-element.directive.ts +++ b/projects/common/directives/compound-element.directive.ts @@ -7,12 +7,13 @@ import { ElementComponent } from './element-component.directive'; import { ClozeChildOverlay } from '../components/compound-elements/cloze/cloze-child-overlay.component'; import { LikertRadioButtonGroupComponent } from '../components/compound-elements/likert/likert-radio-button-group.component'; +import { TableChildOverlay } from 'common/components/compound-elements/table/table-child-overlay.component'; @Directive() export abstract class CompoundElementComponent extends ElementComponent implements AfterViewInit { @Output() childrenAdded = new EventEmitter<ElementComponent[]>(); @Input() parentForm!: UntypedFormGroup; - compoundChildren!: QueryList<ClozeChildOverlay | LikertRadioButtonGroupComponent>; + compoundChildren!: QueryList<ClozeChildOverlay | LikertRadioButtonGroupComponent | TableChildOverlay>; ngAfterViewInit(): void { this.childrenAdded.emit(this.getFormElementChildrenComponents()); diff --git a/projects/common/models/elements/compound-elements/table/table.ts b/projects/common/models/elements/compound-elements/table/table.ts new file mode 100644 index 000000000..db1ae4857 --- /dev/null +++ b/projects/common/models/elements/compound-elements/table/table.ts @@ -0,0 +1,95 @@ +import { + UIElement, CompoundElement, PositionedUIElement, + UIElementProperties, UIElementType, UIElementValue +} from 'common/models/elements/element'; +import { + BasicStyles, BorderStyles, PositionProperties, + PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { Type } from '@angular/core'; +import { ElementComponent } from 'common/directives/element-component.directive'; +import { InstantiationEror } from 'common/util/errors'; +import { environment } from 'common/environment'; +import { TableComponent } from 'common/components/compound-elements/table/table.component'; +import { ElementFactory } from 'common/util/element.factory'; +import { ClozeDocument } from 'common/models/elements/compound-elements/cloze/cloze'; + +export class TableElement extends CompoundElement implements PositionedUIElement, TableProperties { + type: UIElementType = 'table'; + gridColumnSizes: { value: number; unit: string }[] = [{ value: 1, unit: 'fr' }, { value: 1, unit: 'fr' }]; + gridRowSizes: { value: number; unit: string }[] = [{ value: 1, unit: 'fr' }, { value: 1, unit: 'fr' }]; + elements: PositionedUIElement[] = []; + tableEdgesEnabled: boolean = false; + position: PositionProperties; + styling: BasicStyles & BorderStyles; + + static title: string = 'Tabelle'; + static icon: string = 'table'; + + constructor(element?: TableProperties) { + super(element); + if (element && isValid(element)) { + this.gridColumnSizes = element.gridColumnSizes; + this.gridRowSizes = element.gridRowSizes; + this.elements = element.elements + .map(el => ElementFactory.createElement(el)) as PositionedUIElement[]; + this.tableEdgesEnabled = element.tableEdgesEnabled; + this.position = { ...element.position }; + this.styling = { ...element.styling }; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Cloze instantiation', element); + } + if (element?.gridColumnSizes !== undefined) this.gridColumnSizes = element.gridColumnSizes; + if (element?.gridRowSizes !== undefined) this.gridRowSizes = element.gridRowSizes; + this.elements = element?.elements !== undefined ? + element.elements.map(el => ElementFactory.createElement(el)) as PositionedUIElement[] : + []; + if (element?.tableEdgesEnabled !== undefined) this.tableEdgesEnabled = element.tableEdgesEnabled; + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + ...PropertyGroupGenerators.generateBorderStylingProps({ + borderWidth: 1, + ...element?.styling + }) + }; + } + } + + setProperty(property: string, value: UIElementValue): void { + // Don't preserve original array, so Component gets updated + this[property] = value; + } + + getElementComponent(): Type<ElementComponent> { + return TableComponent; + } + + getDuplicate(): UIElement { + return new TableElement(this); + } + + getChildElements(): UIElement[] { + return this.elements; + } +} + +export interface TableProperties extends UIElementProperties { + gridColumnSizes: { value: number; unit: string }[]; + gridRowSizes: { value: number; unit: string }[]; + elements: UIElement[]; + tableEdgesEnabled: boolean; + position: PositionProperties; + styling: BasicStyles & BorderStyles; +} + +function isValid(blueprint?: TableProperties): boolean { + if (!blueprint) return false; + return blueprint.gridColumnSizes !== undefined && + blueprint. gridRowSizes !== undefined && + blueprint.elements !== undefined && + blueprint.tableEdgesEnabled !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling); +} diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts index ef065c13e..e1bca1329 100644 --- a/projects/common/models/elements/element.ts +++ b/projects/common/models/elements/element.ts @@ -25,7 +25,7 @@ import { VariableInfo } from '@iqb/responses'; export type UIElementType = 'text' | 'button' | 'text-field' | 'text-field-simple' | 'text-area' | 'checkbox' | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert-row' | 'radio-group-images' | 'hotspot-image' | 'drop-list' | 'cloze' | 'spell-correct' | 'slider' | 'frame' | 'toggle-button' | 'geometry' -| 'math-field' | 'math-table' | 'text-area-math' | 'trigger'; +| 'math-field' | 'math-table' | 'text-area-math' | 'trigger' | 'table'; export interface OptionElement extends UIElement { getNewOptionLabel(optionText: string): Label; diff --git a/projects/common/pipes/measure.pipe.ts b/projects/common/pipes/measure.pipe.ts index 874d5890e..dcabbc0dc 100644 --- a/projects/common/pipes/measure.pipe.ts +++ b/projects/common/pipes/measure.pipe.ts @@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'; import { Measurement } from 'common/models/elements/element'; @Pipe({ - name: 'measure' + name: 'measure', + standalone: true }) export class MeasurePipe implements PipeTransform { transform(gridSizes: Measurement[]): string { diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts index e889bbfc4..14e85373e 100644 --- a/projects/common/shared.module.ts +++ b/projects/common/shared.module.ts @@ -160,7 +160,6 @@ import { DraggableDirective } from './components/input-elements/drop-list/dragga GetValuePipe, MathFieldComponent, DynamicRowsDirective, - MeasurePipe, TextImagePanelComponent, UnitDefErrorDialogComponent, TooltipComponent, @@ -211,7 +210,6 @@ import { DraggableDirective } from './components/input-elements/drop-list/dragga ImageComponent, GeometryComponent, MathFieldComponent, - MeasurePipe, TextImagePanelComponent, TextAreaMathComponent, MathTableComponent diff --git a/projects/common/util/element.factory.ts b/projects/common/util/element.factory.ts index b0c4f9aef..ee99a262b 100644 --- a/projects/common/util/element.factory.ts +++ b/projects/common/util/element.factory.ts @@ -1,4 +1,4 @@ -import { UIElement } from 'common/models/elements/element'; +import { UIElement, UIElementType } from 'common/models/elements/element'; import { Type } from '@angular/core'; import { TextElement } from 'common/models/elements/text/text'; import { ButtonElement } from 'common/models/elements/button/button'; @@ -27,6 +27,7 @@ import { MathFieldElement } from 'common/models/elements/input-elements/math-fie import { MathTableElement } from 'common/models/elements/input-elements/math-table'; import { TextAreaMathElement } from 'common/models/elements/input-elements/text-area-math'; import { TriggerElement } from 'common/models/elements/trigger/trigger'; +import { TableElement } from 'common/models/elements/compound-elements/table/table'; export abstract class ElementFactory { static ELEMENT_CLASSES: Record<string, Type<UIElement>> = { @@ -54,10 +55,11 @@ export abstract class ElementFactory { 'hotspot-image': HotspotImageElement, 'math-field': MathFieldElement, 'math-table': MathTableElement, - 'text-area-math': TextAreaMathElement + 'text-area-math': TextAreaMathElement, + table: TableElement }; - static createElement(element: { type: string } & Partial<UIElement>): UIElement { + static createElement(element: { type: UIElementType } & Partial<UIElement>): UIElement { return new ElementFactory.ELEMENT_CLASSES[element.type](element); } } diff --git a/projects/editor/src/app/components/dialogs/table-edit-dialog.component.ts b/projects/editor/src/app/components/dialogs/table-edit-dialog.component.ts new file mode 100644 index 000000000..2a16b4dfd --- /dev/null +++ b/projects/editor/src/app/components/dialogs/table-edit-dialog.component.ts @@ -0,0 +1,75 @@ +import { Component, Inject, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { TableElement } from 'common/models/elements/compound-elements/table/table'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatButtonModule } from '@angular/material/button'; +import { TableComponent } from 'common/components/compound-elements/table/table.component'; +import { ElementFactory } from 'common/util/element.factory'; +import { PositionedUIElement, UIElementProperties, UIElementType } from 'common/models/elements/element'; +import { + PositionProperties, + PropertyGroupGenerators +} from 'common/models/elements/property-group-interfaces'; +import { FileService } from 'common/services/file.service'; +import { AudioProperties } from 'common/models/elements/media-elements/audio'; +import { ImageProperties } from 'common/models/elements/media-elements/image'; + +@Component({ + selector: 'aspect-editor-table-edit-dialog', + standalone: true, + imports: [ + MatDialogModule, + TranslateModule, + MatButtonModule, + TableComponent + ], + template: ` + <div mat-dialog-title>Tabellenelemente</div> + <mat-dialog-content> + <aspect-table [elementModel]="newTable" [editorMode]="true" + (elementAdded)="addElement($event)" + (elementRemoved)="removeElement($event)"></aspect-table> + </mat-dialog-content> + <mat-dialog-actions> + <button mat-button [mat-dialog-close]="newTable.elements">{{'save' | translate }}</button> + <button mat-button mat-dialog-close>{{'cancel' | translate }}</button> + </mat-dialog-actions> + ` +}) +export class TableEditDialogComponent { + @ViewChild(TableComponent) tableComp!: TableComponent; + newTable: TableElement; + + constructor(@Inject(MAT_DIALOG_DATA) public data: { table: TableElement }) { + this.newTable = new TableElement(data.table); + } + + async addElement(el: { elementType: UIElementType, row: number, col: number }): Promise<void> { + const extraProps: Partial<UIElementProperties> = {}; + if (el.elementType === 'image') (extraProps as ImageProperties).src = await FileService.loadImage(); + if (el.elementType === 'audio') { + (extraProps as AudioProperties).src = await FileService.loadAudio(); + (extraProps as AudioProperties).player = + PropertyGroupGenerators.generatePlayerProps({ + progressBar: false, + interactiveProgressbar: false, + volumeControl: false, + muteControl: false, + showRestTime: false + }); + } + this.newTable.elements.push(ElementFactory.createElement({ + type: el.elementType, + position: { + gridRow: el.row + 1, + gridColumn: el.col + 1 + } as PositionProperties, + ...extraProps + }) as PositionedUIElement); + this.tableComp.refresh(); + } + + removeElement(index: number): void { + this.newTable.elements.splice(index, 1); + } +} diff --git a/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.ts b/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.ts index fad6580ab..3a14a6536 100644 --- a/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.ts +++ b/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.ts @@ -24,6 +24,7 @@ import { GeometryElement } from 'common/models/elements/geometry/geometry'; import { TriggerElement } from 'common/models/elements/trigger/trigger'; import { TextElement } from 'common/models/elements/text/text'; import { DragNDropService } from 'editor/src/app/services/drag-n-drop.service'; +import { TableElement } from 'common/models/elements/compound-elements/table/table'; @Component({ selector: 'aspect-ui-element-toolbox', @@ -78,4 +79,5 @@ export class UiElementToolboxComponent { protected readonly GeometryElement = GeometryElement; protected readonly TriggerElement = TriggerElement; protected readonly TextElement = TextElement; + protected readonly TableElement = TableElement; } diff --git a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.html b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.html index 0d75d9921..23001de9f 100644 --- a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.html +++ b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.html @@ -48,11 +48,11 @@ (change)="setElementInteractionEnabled($event.checked)"> {{'propertiesPanel.setElementInteractionEnabled' | translate }} </mat-checkbox> - <button mat-raised-button [disabled]="selectedElements.length > 1 || selectionService.isClozeChildSelected" + <button mat-raised-button [disabled]="selectedElements.length > 1 || selectionService.isCompoundChildSelected" (click)="duplicateElement()"> {{'propertiesPanel.duplicateElement' | translate }} </button> - <button mat-raised-button color="warn" [disabled]="selectionService.isClozeChildSelected" + <button mat-raised-button color="warn" [disabled]="selectionService.isCompoundChildSelected" (click)="deleteElement()"> {{'propertiesPanel.deleteElement' | translate }} </button> diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific-props.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific-props.component.ts index e3af66f40..814e01a16 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific-props.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific-props.component.ts @@ -31,6 +31,9 @@ import { import { TextPropsComponent } from 'editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/text-properties-field-set.component'; +import { + TablePropertiesComponent +} from 'editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/table-properties.component'; @Component({ selector: 'aspect-ele-specific-props', @@ -46,7 +49,8 @@ import { GeometryPropsComponent, HotspotPropsComponent, SliderPropertiesComponent, - TextPropsComponent + TextPropsComponent, + TablePropertiesComponent ], template: ` <aspect-math-field-props *ngIf="combinedProperties.type === 'math-field'" @@ -90,6 +94,10 @@ import { <aspect-text-props [combinedProperties]="combinedProperties" (updateModel)="updateModel.emit($event)"> </aspect-text-props> + + <aspect-table-properties *ngIf="combinedProperties.type === 'table'" + [combinedProperties]="combinedProperties" + (updateModel)="updateModel.emit($event)"></aspect-table-properties> ` }) export class EleSpecificPropsComponent { diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/table-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/table-properties.component.ts new file mode 100644 index 000000000..24153bbda --- /dev/null +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/table-properties.component.ts @@ -0,0 +1,95 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { UIElement } from 'common/models/elements/element'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { NgForOf, NgIf } from '@angular/common'; +import { SizeInputPanelComponent } from 'editor/src/app/components/util/size-input-panel.component'; +import { TranslateModule } from '@ngx-translate/core'; + +@Component({ + selector: 'aspect-table-properties', + standalone: true, + imports: [ + MatCheckboxModule, + MatFormFieldModule, + MatInputModule, + NgForOf, + NgIf, + SizeInputPanelComponent, + TranslateModule + ], + template: ` + <fieldset> + <legend>{{'section-menu.rows' | translate }}</legend> + <mat-form-field appearance="outline"> + <mat-label>{{'section-menu.rowCount' | translate }}</mat-label> + <input matInput type="number" + [value]="$any(combinedProperties.gridRowSizes).length" + (click)="$any($event).stopPropagation()" + (change)="modifySizeArray('gridRowSizes', $any($event).target.value || 0)"> + </mat-form-field> + <ng-container *ngFor="let size of $any(combinedProperties.gridRowSizes); let i = index"> + <aspect-size-input-panel [label]="('section-menu.height' | translate) + ' ' + (i + 1)" + [value]="size.value" + [unit]="size.unit" + [allowedUnits]="['px', 'fr']" + (valueUpdated)="changeGridSize('gridRowSizes', i, $event)"> + </aspect-size-input-panel> + </ng-container> + </fieldset> + <fieldset> + <legend>{{'section-menu.columns' | translate }}</legend> + <mat-form-field appearance="outline"> + <mat-label>{{'section-menu.columnCount' | translate }}</mat-label> + <input matInput type="number" + [value]="$any(combinedProperties.gridColumnSizes).length" + (click)="$any($event).stopPropagation()" + (change)="modifySizeArray('gridColumnSizes', $any($event).target.value || 0)"> + </mat-form-field> + <ng-container *ngFor="let size of $any(combinedProperties.gridColumnSizes); let i = index"> + <aspect-size-input-panel [label]="('section-menu.width' | translate) + ' ' + (i + 1)" + [value]="size.value" + [unit]="size.unit" + [allowedUnits]="['px', 'fr']" + (valueUpdated)="changeGridSize('gridColumnSizes', i, $event)"> + </aspect-size-input-panel> + </ng-container> + </fieldset> + <mat-checkbox [checked]="$any(combinedProperties).tableEdgesEnabled" + (change)="updateModel.emit({ property: 'tableEdgesEnabled', value: $event.checked })"> + Tabellenränder zeichnen + </mat-checkbox> + ` +}) +export class TablePropertiesComponent { + @Input() combinedProperties!: UIElement; + @Output() updateModel = + new EventEmitter<{ property: string; value: boolean | { value: number; unit: string }[] | null }>(); + + /* Add or remove elements to size array. Default value 1fr. */ + modifySizeArray(property: 'gridColumnSizes' | 'gridRowSizes', newLength: number): void { + const sizeArray: { value: number; unit: string }[] = property === 'gridColumnSizes' ? + (this.combinedProperties.gridColumnSizes as { value: number; unit: string }[]) : + (this.combinedProperties.gridRowSizes as { value: number; unit: string }[]); + + let newArray: { value: number; unit: string }[] = []; + if (newLength < sizeArray.length) { + newArray = sizeArray.slice(0, newLength); + } else { + newArray.push( + ...sizeArray, + ...Array(newLength - sizeArray.length).fill({ value: 1, unit: 'fr' }) + ); + } + this.updateModel.emit({ property, value: newArray }); + } + + changeGridSize(property: string, index: number, newValue: { value: number; unit: string }): void { + const sizeArray: { value: number; unit: string }[] = property === 'gridColumnSizes' ? + (this.combinedProperties.gridColumnSizes as { value: number; unit: string }[]) : + (this.combinedProperties.gridRowSizes as { value: number; unit: string }[]); + sizeArray[index] = newValue; + this.updateModel.emit({ property, value: [...sizeArray] }); + } +} diff --git a/projects/editor/src/app/components/unit-view/canvas/canvas-element-overlay.ts b/projects/editor/src/app/components/unit-view/canvas/canvas-element-overlay.ts index eb626de34..3245bc0bd 100644 --- a/projects/editor/src/app/components/unit-view/canvas/canvas-element-overlay.ts +++ b/projects/editor/src/app/components/unit-view/canvas/canvas-element-overlay.ts @@ -16,6 +16,8 @@ import { UnitService } from '../../../services/unit-services/unit.service'; import { SelectionService } from '../../../services/selection.service'; import { ElementService } from 'editor/src/app/services/unit-services/element.service'; import { DragNDropService } from 'editor/src/app/services/drag-n-drop.service'; +import { TableComponent } from 'common/components/compound-elements/table/table.component'; +import { TableChildOverlay } from 'common/components/compound-elements/table/table-child-overlay.component'; @Directive() export abstract class CanvasElementOverlay implements OnInit, OnDestroy { @@ -40,7 +42,7 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy { this.childComponent = this.elementContainer.createComponent(this.element.getElementComponent()); this.childComponent.instance.elementModel = this.element; - this.preventInteraction = this.element.type !== 'cloze'; + this.preventInteraction = this.element.type !== 'cloze' && this.element.type !== 'table'; this.childComponent.changeDetectorRef.detectChanges(); // this fires onInit, which initializes the FormControl @@ -56,7 +58,19 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy { .pipe(takeUntil(this.ngUnsubscribe)) .subscribe((elementSelectionEvent: ClozeChildOverlay) => { this.selectionService.selectElement({ elementComponent: elementSelectionEvent, multiSelect: false }); - this.selectionService.isClozeChildSelected = true; + this.selectionService.isCompoundChildSelected = true; + }); + } + + if (this.childComponent.instance instanceof TableComponent) { + // make element children clickable to access child elements + this.childComponent.location.nativeElement.style.pointerEvents = 'unset'; + this.childComponent.instance.childElementSelected + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe((selectedElementComponent: TableChildOverlay) => { + console.log('CanvasElementOverlay: elementSelected received'); + this.selectionService.selectElement({ elementComponent: selectedElementComponent, multiSelect: false }); + this.selectionService.isCompoundChildSelected = true; }); } @@ -82,6 +96,16 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy { } } ); + + this.unitService.tablePropUpdated + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe( + (elementID: string) => { + if (this.element.type === 'table' && this.element.id === elementID) { + (this.childComponent.instance as TableComponent).refresh(); + } + } + ); } setSelected(newValue: boolean): void { diff --git a/projects/editor/src/app/services/dialog.service.ts b/projects/editor/src/app/services/dialog.service.ts index 4eabc9fe6..3c332d769 100644 --- a/projects/editor/src/app/services/dialog.service.ts +++ b/projects/editor/src/app/services/dialog.service.ts @@ -36,6 +36,8 @@ import { PlayerEditDialogComponent } from '../components/dialogs/player-edit-dia import { LikertRowEditDialogComponent } from '../components/dialogs/likert-row-edit-dialog.component'; import { DropListOptionEditDialogComponent } from '../components/dialogs/drop-list-option-edit-dialog.component'; import { DeleteReferenceDialogComponent } from '../components/dialogs/delete-reference-dialog.component'; +import { TableEditDialogComponent } from 'editor/src/app/components/dialogs/table-edit-dialog.component'; +import { TableElement } from 'common/models/elements/compound-elements/table/table'; @Injectable({ providedIn: 'root' @@ -164,6 +166,14 @@ export class DialogService { return dialogRef.afterClosed(); } + showTableEditDialog(table: TableElement): Observable<any> { + const dialogRef = this.dialog.open(TableEditDialogComponent, { + data: { table }, + autoFocus: false + }); + return dialogRef.afterClosed(); + } + showVisibilityRulesDialog(visibilityRules: VisibilityRule[], logicalConnectiveOfRules: 'disjunction' | 'conjunction', controlIds: string[], diff --git a/projects/editor/src/app/services/id.service.ts b/projects/editor/src/app/services/id.service.ts index 5e910c588..68023817c 100644 --- a/projects/editor/src/app/services/id.service.ts +++ b/projects/editor/src/app/services/id.service.ts @@ -40,7 +40,8 @@ export class IDService { value: 0, 'state-variable': 0, 'text-area-math': 0, - trigger: 0 + trigger: 0, + table: 0 }; constructor(private messageService: MessageService, private translateService: TranslateService) { } diff --git a/projects/editor/src/app/services/selection.service.ts b/projects/editor/src/app/services/selection.service.ts index b9813562f..5e906109a 100644 --- a/projects/editor/src/app/services/selection.service.ts +++ b/projects/editor/src/app/services/selection.service.ts @@ -5,6 +5,7 @@ import { CanvasElementOverlay } from 'editor/src/app/components/unit-view/canvas import { ClozeChildOverlay } from 'common/components/compound-elements/cloze/cloze-child-overlay.component'; +import { TableChildOverlay } from 'common/components/compound-elements/table/table-child-overlay.component'; @Injectable({ providedIn: 'root' @@ -13,8 +14,8 @@ export class SelectionService { selectedPageIndex: number = 0; selectedSectionIndex: number = 0; private _selectedElements!: BehaviorSubject<UIElement[]>; - selectedElementComponents: (CanvasElementOverlay | ClozeChildOverlay)[] = []; - isClozeChildSelected: boolean = false; + selectedElementComponents: (CanvasElementOverlay | ClozeChildOverlay | TableChildOverlay)[] = []; + isCompoundChildSelected: boolean = false; constructor() { this._selectedElements = new BehaviorSubject([] as UIElement[]); @@ -28,20 +29,21 @@ export class SelectionService { return this._selectedElements.value; } - selectElement(event: { elementComponent: CanvasElementOverlay | ClozeChildOverlay; multiSelect: boolean }): void { + selectElement(event: { elementComponent: CanvasElementOverlay | ClozeChildOverlay | TableChildOverlay; multiSelect: boolean }): void { if (!event.multiSelect) { this.clearElementSelection(); } - this.isClozeChildSelected = false; + this.isCompoundChildSelected = false; this.selectedElementComponents.push(event.elementComponent); event.elementComponent.setSelected(true); this._selectedElements.next(this.selectedElementComponents.map(componentElement => componentElement.element)); } clearElementSelection(): void { - this.selectedElementComponents.forEach((overlayComponent: CanvasElementOverlay | ClozeChildOverlay) => { - overlayComponent.setSelected(false); - }); + this.selectedElementComponents + .forEach((overlayComponent: CanvasElementOverlay | ClozeChildOverlay | TableChildOverlay) => { + overlayComponent.setSelected(false); + }); this.selectedElementComponents = []; this._selectedElements.next([]); } diff --git a/projects/editor/src/app/services/unit-services/element.service.ts b/projects/editor/src/app/services/unit-services/element.service.ts index c5e93903f..0f6b1a00a 100644 --- a/projects/editor/src/app/services/unit-services/element.service.ts +++ b/projects/editor/src/app/services/unit-services/element.service.ts @@ -30,6 +30,7 @@ import { TextElement } from 'common/models/elements/text/text'; import { ClozeDocument, ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; import { DomSanitizer } from '@angular/platform-browser'; import { DragNDropValueObject } from 'common/models/elements/label-interfaces'; +import { TableElement } from 'common/models/elements/compound-elements/table/table'; @Injectable({ providedIn: 'root' @@ -151,6 +152,7 @@ export class ElementService { } updateElementsProperty(elements: UIElement[], property: string, value: unknown): void { + console.log('updateElementsProperty ', elements, property, value); elements.forEach(element => { if (property === 'id') { if (this.idService.validateAndAddNewID(value as string, element.id)) { @@ -164,8 +166,10 @@ export class ElementService { element.setProperty(property, value); if (element.type === 'geometry' && property !== 'trackedVariables') this.unitService.geometryElementPropertyUpdated.next(element.id); if (element.type === 'math-table') this.unitService.mathTableElementPropertyUpdated.next(element.id); + if (element.type === 'table') this.unitService.tablePropUpdated.next(element.id); } }); + this.unitService.elementPropertyUpdated.next(); this.unitService.updateUnitDefinition(); } @@ -320,6 +324,17 @@ export class ElementService { ); }); break; + case 'table': + this.dialogService.showTableEditDialog(element as TableElement) + .subscribe((result: UIElement[]) => { + if (result) { + result.forEach(el => { + if (el.id === 'id-placeholder') el.id = this.idService.getAndRegisterNewID(el.type); + }); + this.updateElementsProperty([element], 'elements', result); + } + }); + break; // no default } } diff --git a/projects/editor/src/app/services/unit-services/unit.service.ts b/projects/editor/src/app/services/unit-services/unit.service.ts index 86de02da0..356215ca9 100644 --- a/projects/editor/src/app/services/unit-services/unit.service.ts +++ b/projects/editor/src/app/services/unit-services/unit.service.ts @@ -28,6 +28,7 @@ export class UnitService { elementPropertyUpdated: Subject<void> = new Subject<void>(); geometryElementPropertyUpdated: Subject<string> = new Subject<string>(); mathTableElementPropertyUpdated: Subject<string> = new Subject<string>(); + tablePropUpdated: Subject<string> = new Subject<string>(); referenceManager: ReferenceManager; savedSectionCode: string | undefined; -- GitLab