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