From 960f731f331aed4c7860bc491081df2a0d2ca98f Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Wed, 24 May 2023 18:42:22 +0200
Subject: [PATCH] Change element-margin and section-size properties to object
 'Measure'

Measure holds the value and a unit for CSS properties.
---
 docs/unit_definition_changelog.txt            |   6 +
 package.json                                  |   2 +-
 projects/common/models/elements/element.ts    |  37 +++--
 .../radio-button-group-complex.ts             |   2 +-
 .../input-elements/radio-button-group.ts      |   2 +-
 projects/common/models/page.ts                |   4 +-
 projects/common/models/section.ts             |  54 +++++--
 projects/common/pipes/grid-size.ts            |  11 ++
 .../common/services/sanitization.service.ts   |  13 +-
 projects/common/shared.module.ts              |   7 +-
 projects/editor/src/app/app.module.ts         |   4 +-
 .../dynamic-section-helper-grid.component.ts  |  12 +-
 .../canvas/section-dynamic.component.ts       |  12 +-
 .../canvas/section-menu.component.ts          | 153 +++++-------------
 .../position-field-set.component.ts           | 142 ++++++----------
 .../util/size-input-panel.component.ts        |  45 ++++++
 .../editor/src/app/services/unit.service.ts   |   6 +-
 17 files changed, 258 insertions(+), 254 deletions(-)
 create mode 100644 projects/common/pipes/grid-size.ts
 create mode 100644 projects/editor/src/app/components/util/size-input-panel.component.ts

diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt
index 9d26d2448..551b0a2f7 100644
--- a/docs/unit_definition_changelog.txt
+++ b/docs/unit_definition_changelog.txt
@@ -72,3 +72,9 @@ iqb-aspect-definition@1.0.0
 3.10.0
 - DropList: +allowReplacement
 - DragAndDropValueObject: -returnToOriginOnReplacement; originListID and originListIndex are now mandatory
+
+3.11.0
+- Element.position: margin properties now have a unit attached and are therefore an
+  object ({value: number; unit: string})
+- Section.gridColumnSizes and Section.gridRowSizes now have a unit attached and are therefore an object
+  ({value: number; unit: string})
diff --git a/package.json b/package.json
index 04ddf4a04..55118ad02 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "config": {
     "player_version": "1.32.0",
     "editor_version": "1.39.0",
-    "unit_definition_version": "3.10.0"
+    "unit_definition_version": "3.11.0"
   },
   "scripts": {
     "ng": "ng",
diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts
index 8d66352c6..f25d9a04b 100644
--- a/projects/common/models/elements/element.ts
+++ b/projects/common/models/elements/element.ts
@@ -11,7 +11,7 @@ export type UIElementType = 'text' | 'button' | 'text-field' | 'text-field-simpl
 
 export type UIElementValue = string | number | boolean | undefined | UIElementType | InputElementValue |
 TextLabel | TextLabel[] | ClozeDocument | LikertRowElement[] | Hotspot[] |
-PositionProperties | PlayerProperties | BasicStyles;
+PositionProperties | PlayerProperties | BasicStyles | Measurement | Measurement[];
 
 export type InputAssistancePreset = null | 'french' | 'numbers' | 'numbersAndOperators' | 'numbersAndBasicOperators'
 | 'comparisonOperators' | 'squareDashDot' | 'placeValue' | 'space' | 'comma' | 'custom';
@@ -66,7 +66,8 @@ export abstract class UIElement {
 
   abstract getElementComponent(): Type<ElementComponent>;
 
-  static initPositionProps(defaults: Partial<PositionProperties> = {}): PositionProperties {
+  static initPositionProps(properties: Partial<PositionProperties> = {}): PositionProperties {
+    const defaults = UIElement.sanitizePositionProps(properties);
     return {
       fixedSize: defaults.fixedSize !== undefined ? defaults.fixedSize as boolean : false,
       dynamicPositioning: defaults.dynamicPositioning !== undefined ? defaults.dynamicPositioning as boolean : true,
@@ -77,14 +78,25 @@ export abstract class UIElement {
       gridColumnRange: defaults.gridColumnRange !== undefined ? defaults.gridColumnRange as number : 1,
       gridRow: defaults.gridRow !== undefined ? defaults.gridRow as number : null,
       gridRowRange: defaults.gridRowRange !== undefined ? defaults.gridRowRange as number : 1,
-      marginLeft: defaults.marginLeft !== undefined ? defaults.marginLeft as number : 0,
-      marginRight: defaults.marginRight !== undefined ? defaults.marginRight as number : 0,
-      marginTop: defaults.marginTop !== undefined ? defaults.marginTop as number : 0,
-      marginBottom: defaults.marginBottom !== undefined ? defaults.marginBottom as number : 0,
+      marginLeft: defaults.marginLeft !== undefined ? defaults.marginLeft as Measurement : { value: 0, unit: 'px' },
+      marginRight: defaults.marginRight !== undefined ? defaults.marginRight as Measurement : { value: 0, unit: 'px' },
+      marginTop: defaults.marginTop !== undefined ? defaults.marginTop as Measurement : { value: 0, unit: 'px' },
+      marginBottom: defaults.marginBottom !== undefined ? defaults.marginBottom as Measurement : { value: 0, unit: 'px' },
       zIndex: defaults.zIndex !== undefined ? defaults.zIndex as number : 0
     };
   }
 
+  static sanitizePositionProps(properties: Record<string, any> = {}): Partial<PositionProperties> {
+    const newProperties = { ...properties };
+    if (typeof newProperties.marginLeft === 'number') {
+      newProperties.marginLeft = { value: properties.marginLeft, unit: 'px' };
+      newProperties.marginRight = { value: properties.marginRight, unit: 'px' };
+      newProperties.marginTop = { value: properties.marginTop, unit: 'px' };
+      newProperties.marginBottom = { value: properties.marginBottom, unit: 'px' };
+    }
+    return newProperties;
+  }
+
   static initStylingProps<T>(defaults?: Partial<BasicStyles> & T): BasicStyles & T {
     return {
       ...defaults as T,
@@ -248,10 +260,10 @@ export interface PositionProperties {
   gridColumnRange: number;
   gridRow: number | null;
   gridRowRange: number;
-  marginLeft: number;
-  marginRight: number;
-  marginTop: number;
-  marginBottom: number;
+  marginLeft: Measurement;
+  marginRight: Measurement;
+  marginTop: Measurement;
+  marginBottom: Measurement;
   zIndex: number;
 }
 
@@ -355,3 +367,8 @@ export interface DragNDropValueObject extends TextImageLabel {
 }
 
 export type Label = TextLabel | TextImageLabel | DragNDropValueObject;
+
+export interface Measurement {
+  value: number;
+  unit: string
+}
diff --git a/projects/common/models/elements/input-elements/radio-button-group-complex.ts b/projects/common/models/elements/input-elements/radio-button-group-complex.ts
index 9bea4d8ef..604b51249 100644
--- a/projects/common/models/elements/input-elements/radio-button-group-complex.ts
+++ b/projects/common/models/elements/input-elements/radio-button-group-complex.ts
@@ -17,7 +17,7 @@ export class RadioButtonGroupComplexElement extends InputElement implements Posi
     super({ height: 100, ...element });
     if (element.options) this.options = [...element.options];
     this.itemsPerRow = element.itemsPerRow !== undefined ? element.itemsPerRow : null;
-    this.position = UIElement.initPositionProps({ marginBottom: 40, ...element.position });
+    this.position = UIElement.initPositionProps({ marginBottom: { value: 40, unit: 'px' }, ...element.position });
     this.styling = {
       ...UIElement.initStylingProps({ backgroundColor: 'transparent', ...element.styling })
     };
diff --git a/projects/common/models/elements/input-elements/radio-button-group.ts b/projects/common/models/elements/input-elements/radio-button-group.ts
index 2bb84414d..1cd321d1d 100644
--- a/projects/common/models/elements/input-elements/radio-button-group.ts
+++ b/projects/common/models/elements/input-elements/radio-button-group.ts
@@ -20,7 +20,7 @@ export class RadioButtonGroupElement extends InputElement implements PositionedU
     if (element.options) this.options = [...element.options];
     if (element.alignment) this.alignment = element.alignment;
     if (element.strikeOtherOptions) this.strikeOtherOptions = element.strikeOtherOptions;
-    this.position = UIElement.initPositionProps({ marginBottom: 30, ...element.position });
+    this.position = UIElement.initPositionProps({ marginBottom: { value: 30, unit: 'px' }, ...element.position });
     this.styling = {
       ...UIElement.initStylingProps({
         backgroundColor: 'transparent',
diff --git a/projects/common/models/page.ts b/projects/common/models/page.ts
index 24619f439..5abb4230d 100644
--- a/projects/common/models/page.ts
+++ b/projects/common/models/page.ts
@@ -12,7 +12,7 @@ export class Page {
   alwaysVisiblePagePosition: 'left' | 'right' | 'top' | 'bottom' = 'left';
   alwaysVisibleAspectRatio: number = 50;
 
-  constructor(page?: Partial<Page>) {
+  constructor(page?: Record<string, any>) {
     if (page?.hasMaxWidth) this.hasMaxWidth = page.hasMaxWidth;
     if (page?.maxWidth) this.maxWidth = page.maxWidth;
     if (page?.margin !== undefined) this.margin = page.margin;
@@ -20,7 +20,7 @@ export class Page {
     if (page?.alwaysVisible) this.alwaysVisible = page.alwaysVisible;
     if (page?.alwaysVisiblePagePosition) this.alwaysVisiblePagePosition = page.alwaysVisiblePagePosition;
     if (page?.alwaysVisibleAspectRatio) this.alwaysVisibleAspectRatio = page.alwaysVisibleAspectRatio;
-    this.sections = page?.sections?.map(section => new Section(section)) || [new Section()];
+    this.sections = page?.sections?.map((section: Record<string, any>) => new Section(section)) || [new Section()];
   }
 
   getAllElements(elementType?: string): UIElement[] {
diff --git a/projects/common/models/section.ts b/projects/common/models/section.ts
index c96778320..a498275cd 100644
--- a/projects/common/models/section.ts
+++ b/projects/common/models/section.ts
@@ -1,5 +1,12 @@
 import {
-  CompoundElement, PositionedUIElement, UIElement, UIElementValue, AnswerScheme, PlayerElement, InputElement
+  CompoundElement,
+  PositionedUIElement,
+  UIElement,
+  UIElementValue,
+  AnswerScheme,
+  PlayerElement,
+  InputElement,
+  Measurement
 } from 'common/models/elements/element';
 import { TextElement } from 'common/models/elements/text/text';
 import { ImageElement } from 'common/models/elements/media-elements/image';
@@ -13,23 +20,24 @@ export class Section {
   dynamicPositioning: boolean = true;
   autoColumnSize: boolean = true;
   autoRowSize: boolean = true;
-  gridColumnSizes: string = '1fr 1fr';
-  gridRowSizes: string = '1fr';
+  gridColumnSizes: { value: number; unit: string }[] = [{ value: 1, unit: 'fr' }, { value: 1, unit: 'fr' }];
+  gridRowSizes: { value: number; unit: string }[] = [{ value: 1, unit: 'fr' }];
   activeAfterID: string | null = null;
   activeAfterIdDelay: number = 0;
 
-  constructor(section?: Partial<Section>) {
-    if (section?.height) this.height = section.height;
-    if (section?.backgroundColor) this.backgroundColor = section.backgroundColor;
-    if (section?.dynamicPositioning !== undefined) this.dynamicPositioning = section.dynamicPositioning;
-    if (section?.autoColumnSize !== undefined) this.autoColumnSize = section.autoColumnSize;
-    if (section?.autoRowSize !== undefined) this.autoRowSize = section.autoRowSize;
-    if (section?.gridColumnSizes !== undefined) this.gridColumnSizes = section.gridColumnSizes;
-    if (section?.gridRowSizes !== undefined) this.gridRowSizes = section.gridRowSizes;
-    if (section?.activeAfterID) this.activeAfterID = section.activeAfterID;
-    if (section?.activeAfterIdDelay) this.activeAfterIdDelay = section.activeAfterIdDelay;
+  constructor(blueprint?: Record<string, any>) {
+    const sanitizedBlueprint = Section.sanitizeBlueprint(blueprint);
+    if (sanitizedBlueprint.height) this.height = sanitizedBlueprint.height;
+    if (sanitizedBlueprint.backgroundColor) this.backgroundColor = sanitizedBlueprint.backgroundColor;
+    if (sanitizedBlueprint.dynamicPositioning !== undefined) this.dynamicPositioning = sanitizedBlueprint.dynamicPositioning;
+    if (sanitizedBlueprint.autoColumnSize !== undefined) this.autoColumnSize = sanitizedBlueprint.autoColumnSize;
+    if (sanitizedBlueprint.autoRowSize !== undefined) this.autoRowSize = sanitizedBlueprint.autoRowSize;
+    if (sanitizedBlueprint.gridColumnSizes !== undefined) this.gridColumnSizes = sanitizedBlueprint.gridColumnSizes;
+    if (sanitizedBlueprint.gridRowSizes !== undefined) this.gridRowSizes = sanitizedBlueprint.gridRowSizes;
+    if (sanitizedBlueprint.activeAfterID) this.activeAfterID = sanitizedBlueprint.activeAfterID;
+    if (sanitizedBlueprint.activeAfterIdDelay) this.activeAfterIdDelay = sanitizedBlueprint.activeAfterIdDelay;
     this.elements =
-      section?.elements?.map(element => ElementFactory.createElement({
+      sanitizedBlueprint.elements?.map(element => ElementFactory.createElement({
         ...element,
         position: UIElement.initPositionProps(element.position)
       }) as PositionedUIElement) ||
@@ -63,4 +71,22 @@ export class Section {
         (element as InputElement).getAnswerScheme(dropLists) :
         (element as InputElement | PlayerElement | TextElement | ImageElement).getAnswerScheme()));
   }
+
+  static sanitizeBlueprint(blueprint?: Record<string, UIElementValue>): Partial<Section> {
+    if (!blueprint) return {};
+
+    return {
+      ...blueprint,
+      gridColumnSizes: typeof blueprint.gridColumnSizes === 'string' ?
+        (blueprint.gridColumnSizes as string)
+          .split(' ')
+          .map(size => ({ value: Number(size.slice(0, -2)), unit: size.slice(-2) })) :
+        blueprint.gridColumnSizes as Measurement[],
+      gridRowSizes: typeof blueprint.gridRowSizes === 'string' ?
+        (blueprint.gridRowSizes as string)
+          .split(' ')
+          .map(size => ({ value: Number(size.slice(0, -2)), unit: size.slice(-2) })) :
+        blueprint.gridRowSizes as Measurement[]
+    };
+  }
 }
diff --git a/projects/common/pipes/grid-size.ts b/projects/common/pipes/grid-size.ts
new file mode 100644
index 000000000..874d5890e
--- /dev/null
+++ b/projects/common/pipes/grid-size.ts
@@ -0,0 +1,11 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { Measurement } from 'common/models/elements/element';
+
+@Pipe({
+  name: 'measure'
+})
+export class MeasurePipe implements PipeTransform {
+  transform(gridSizes: Measurement[]): string {
+    return gridSizes.map(size => String(size.value) + size.unit).join(' ');
+  }
+}
diff --git a/projects/common/services/sanitization.service.ts b/projects/common/services/sanitization.service.ts
index 292a91939..3f9bca475 100644
--- a/projects/common/services/sanitization.service.ts
+++ b/projects/common/services/sanitization.service.ts
@@ -122,9 +122,20 @@ export class SanitizationService {
     };
   }
 
-  private sanitizeSection(section: Section): Section {
+  /* Transform grid sizes from string to array with value and unit. */
+  private sanitizeSection(section: any): Section {
     return {
       ...section,
+      gridColumnSizes: typeof section.gridColumnSizes === 'string' ?
+        (section.gridColumnSizes as string)
+          .split(' ')
+          .map(size => ({ value: size.slice(0, -2), unit: size.slice(-2) })) :
+        section.gridColumnSizes,
+      gridRowSizes: typeof section.gridRowSizes === 'string' ?
+        (section.gridRowSizes as string)
+          .split(' ')
+          .map(size => ({ value: size.slice(0, -2), unit: size.slice(-2) })) :
+        section.gridRowSizes,
       elements: section.elements.map((element: UIElement) => (
         this.sanitizeElement(
           element as Record<string, UIElementValue>,
diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts
index c3403814a..d6b814884 100644
--- a/projects/common/shared.module.ts
+++ b/projects/common/shared.module.ts
@@ -74,6 +74,7 @@ import { MathDegreesPipe } from './pipes/math-degrees.pipe';
 import { ArrayIncludesPipe } from './pipes/array-includes.pipe';
 import { SpinnerComponent } from './components/spinner/spinner.component';
 import { GetValuePipe, MathFieldComponent } from './components/input-elements/math-field.component';
+import { MeasurePipe } from 'common/pipes/grid-size';
 
 @NgModule({
   imports: [
@@ -140,7 +141,8 @@ import { GetValuePipe, MathFieldComponent } from './components/input-elements/ma
     SpinnerComponent,
     GetValuePipe,
     MathFieldComponent,
-    DynamicRowsDirective
+    DynamicRowsDirective,
+    MeasurePipe
   ],
   exports: [
     CommonModule,
@@ -179,7 +181,8 @@ import { GetValuePipe, MathFieldComponent } from './components/input-elements/ma
     FrameComponent,
     ImageComponent,
     GeometryComponent,
-    MathFieldComponent
+    MathFieldComponent,
+    MeasurePipe
   ]
 })
 export class SharedModule {
diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts
index cac70db0e..826e8928b 100644
--- a/projects/editor/src/app/app.module.ts
+++ b/projects/editor/src/app/app.module.ts
@@ -97,6 +97,7 @@ import {
   BorderPropertiesComponent
 } from './components/properties-panel/model-properties-tab/input-groups/border-properties.component';
 import { GeogebraAppDefinitionDialogComponent } from './components/dialogs/geogebra-app-definition-dialog.component';
+import { SizeInputPanelComponent } from './components/util/size-input-panel.component';
 
 @NgModule({
   declarations: [
@@ -153,7 +154,8 @@ import { GeogebraAppDefinitionDialogComponent } from './components/dialogs/geoge
     GeogebraAppDefinitionDialogComponent,
     GetValidDropListsPipe,
     GetAnchorIdsPipe,
-    ScrollPageIndexPipe
+    ScrollPageIndexPipe,
+    SizeInputPanelComponent
   ],
   imports: [
     BrowserModule,
diff --git a/projects/editor/src/app/components/canvas/dynamic-section-helper-grid.component.ts b/projects/editor/src/app/components/canvas/dynamic-section-helper-grid.component.ts
index 7985f302e..177d7e531 100644
--- a/projects/editor/src/app/components/canvas/dynamic-section-helper-grid.component.ts
+++ b/projects/editor/src/app/components/canvas/dynamic-section-helper-grid.component.ts
@@ -35,8 +35,8 @@ import { UnitService } from '../../services/unit.service';
 export class DynamicSectionHelperGridComponent implements OnInit, OnChanges {
   @Input() autoColumnSize!: boolean;
   @Input() autoRowSize!: boolean;
-  @Input() gridColumnSizes!: string;
-  @Input() gridRowSizes!: string;
+  @Input() gridColumnSizes!: { value: number; unit: string }[];
+  @Input() gridRowSizes!: { value: number; unit: string }[];
   @Input() section!: Section;
   @Input() sectionIndex!: number;
   @Output() transferElement = new EventEmitter<{ previousSectionIndex: number, newSectionIndex: number }>();
@@ -52,9 +52,7 @@ export class DynamicSectionHelperGridComponent implements OnInit, OnChanges {
   }
 
   ngOnChanges(changes: SimpleChanges): void {
-    if (changes.autoColumnSize ||
-      changes.gridColumnSizes ||
-      changes.gridRowSizes) {
+    if (changes.autoColumnSize || changes.gridColumnSizes || changes.gridRowSizes) {
       this.calculateColumnCount();
       this.calculateRowCount();
     }
@@ -76,7 +74,7 @@ export class DynamicSectionHelperGridComponent implements OnInit, OnChanges {
         ),
         0) - 1;
     } else {
-      numberOfColumns = this.gridColumnSizes.split(' ').length;
+      numberOfColumns = this.gridColumnSizes.length;
     }
     this.columnCountArray = Array(Math.max(numberOfColumns, 1));
   }
@@ -92,7 +90,7 @@ export class DynamicSectionHelperGridComponent implements OnInit, OnChanges {
         ),
         0) - 1;
     } else {
-      numberOfRows = this.gridRowSizes.split(' ').length;
+      numberOfRows = this.gridRowSizes.length;
     }
     this.rowCountArray = Array(Math.max(numberOfRows, 1));
   }
diff --git a/projects/editor/src/app/components/canvas/section-dynamic.component.ts b/projects/editor/src/app/components/canvas/section-dynamic.component.ts
index 686f737f8..ce7498c94 100644
--- a/projects/editor/src/app/components/canvas/section-dynamic.component.ts
+++ b/projects/editor/src/app/components/canvas/section-dynamic.component.ts
@@ -10,8 +10,8 @@ import { DynamicSectionHelperGridComponent } from './dynamic-section-helper-grid
   selector: 'aspect-section-dynamic',
   template: `
     <div [style.display]="'grid'"
-         [style.grid-template-columns]="section.autoColumnSize ? '' : section.gridColumnSizes"
-         [style.grid-template-rows]="section.autoRowSize ? '' : section.gridRowSizes"
+         [style.grid-template-columns]="section.autoColumnSize ? '' : section.gridColumnSizes | measure"
+         [style.grid-template-rows]="section.autoRowSize ? '' : section.gridRowSizes | measure"
          [style.grid-auto-columns]="'auto'"
          [style.grid-auto-rows]="'auto'"
          [style.border]="isSelected ? '2px solid #ff4081': '1px dotted'"
@@ -34,10 +34,10 @@ import { DynamicSectionHelperGridComponent } from './dynamic-section-helper-grid
                                      [element]="$any(element)"
                                      [style.min-width.px]="element.width"
                                      [style.min-height.px]="element.position.useMinHeight ? element.height : null"
-                                     [style.margin-left.px]="element.position.marginLeft"
-                                     [style.margin-right.px]="element.position.marginRight"
-                                     [style.margin-top.px]="element.position.marginTop"
-                                     [style.margin-bottom.px]="element.position.marginBottom"
+                                     [style.margin-left]="[element.position.marginLeft] | measure"
+                                     [style.margin-right]="[element.position.marginRight] | measure"
+                                     [style.margin-top]="[element.position.marginTop] | measure"
+                                     [style.margin-bottom]="[element.position.marginBottom] | measure"
                                      [style.grid-column-start]="element.position.gridColumn"
                                      [style.grid-column-end]="element.position.gridColumn ?
                                        element.position.gridColumn + element.position.gridColumnRange :
diff --git a/projects/editor/src/app/components/canvas/section-menu.component.ts b/projects/editor/src/app/components/canvas/section-menu.component.ts
index 08627ba8f..8be8f550e 100644
--- a/projects/editor/src/app/components/canvas/section-menu.component.ts
+++ b/projects/editor/src/app/components/canvas/section-menu.component.ts
@@ -1,5 +1,5 @@
 import {
-  Component, OnInit, OnDestroy, Input, Output, EventEmitter,
+  Component, OnDestroy, Input, Output, EventEmitter,
   ViewChild, ElementRef
 } from '@angular/core';
 import { Subject } from 'rxjs';
@@ -97,28 +97,18 @@ import { SelectionService } from '../../services/selection.service';
               <mat-form-field appearance="outline">
                 <mat-label>{{'section-menu.columnCount' | translate }}</mat-label>
                 <input matInput type="number"
-                       [value]="$any(section.gridColumnSizes.split(' ').length)"
+                       [value]="$any(section.gridColumnSizes.length)"
                        (click)="$any($event).stopPropagation()"
                        (change)="modifySizeArray('gridColumnSizes', $any($event).target.value)">
               </mat-form-field>
-              <div *ngFor="let size of columnSizes ; let i = index" class="size-inputs">
-                <mat-form-field class="size-item">
-                  <mat-label>{{'section-menu.width' | translate }} {{i + 1}}</mat-label>
-                  <input matInput type="number"
-                         [value]="size.value"
-                         (click)="$any($event).stopPropagation()"
-                         (change)="changeGridSize('gridColumnSizes', i, false, $any($event).target.value)">
-                </mat-form-field>
-                <mat-form-field class="size-item-unit">
-                  <mat-label>Einheit</mat-label>
-                  <mat-select [value]="size.unit"
-                              (click)="$any($event).stopPropagation()"
-                              (selectionChange)="changeGridSize('gridColumnSizes', i, true, $event.value)">
-                    <mat-option value="fr">{{'section-menu.fraction' | translate }}</mat-option>
-                    <mat-option value="px">{{'section-menu.pixel' | translate }}</mat-option>
-                  </mat-select>
-                </mat-form-field>
-              </div>
+              <ng-container *ngFor="let size of section.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>
             </ng-container>
           </fieldset>
 
@@ -141,27 +131,18 @@ import { SelectionService } from '../../services/selection.service';
               <mat-form-field appearance="outline">
                 <mat-label>{{'section-menu.rowCount' | translate }}</mat-label>
                 <input matInput type="number"
-                       [value]="$any(section.gridRowSizes.split(' ').length)"
+                       [value]="$any(section.gridRowSizes.length)"
                        (click)="$any($event).stopPropagation()"
                        (change)="modifySizeArray('gridRowSizes', $any($event).target.value)">
               </mat-form-field>
-              <div *ngFor="let size of rowSizes ; let i = index" class="size-inputs fx-row-start-stretch">
-                <mat-form-field class="size-item">
-                  <mat-label>{{'section-menu.height' | translate }} {{i + 1}}</mat-label>
-                  <input matInput type="number"
-                         [value]="size.value"
-                         (click)="$any($event).stopPropagation()"
-                         (change)="changeGridSize('gridRowSizes', i, false, $any($event).target.value)">
-                </mat-form-field>
-                <mat-form-field class="size-item-unit">
-                  <mat-select [value]="size.unit"
-                              (click)="$any($event).stopPropagation()"
-                              (selectionChange)="changeGridSize('gridRowSizes', i, true, $event.value)">
-                    <mat-option value="fr">{{'section-menu.fraction' | translate }}</mat-option>
-                    <mat-option value="px">{{'section-menu.pixel' | translate }}</mat-option>
-                  </mat-select>
-                </mat-form-field>
-              </div>
+              <ng-container *ngFor="let size of section.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>
             </ng-container>
           </fieldset>
         </div>
@@ -197,38 +178,15 @@ import { SelectionService } from '../../services/selection.service';
       <mat-icon>clear</mat-icon>
     </button>
   `,
-  styles: [`
-    ::ng-deep .layoutMenu {
-      padding: 0 15px; width: 250px;
-    }
-    ::ng-deep .layoutMenu fieldset {
-      margin: 10px 0; display: flex; flex-direction: column; align-items: flex-start;
-    }
-    ::ng-deep .layoutMenu .section-height-input {
-      margin-top: 10px;
-    }
-    ::ng-deep .layoutMenu .size-inputs .mat-form-field {
-      width: 100px;
-    }
-    ::ng-deep .layoutMenu .size-inputs {
-      display: flex; flex-direction: row; gap: 15px;
-    }
-    .menuItem {
-      margin-bottom: 5px;
-    }
-    ::ng-deep .activeAfterID-menu .mat-form-field {
-      width:90%; margin-left: 10px;
-    }
-    .fx-row-start-stretch {
-      box-sizing: border-box;
-      display: flex;
-      flex-direction: row;
-      justify-content: flex-start;
-      align-items: stretch;
-    }
-  `]
+  styles: [
+    '::ng-deep .layoutMenu {padding: 0 15px; width: 250px;}',
+    '::ng-deep .layoutMenu fieldset {margin: 10px 0; display: flex; flex-direction: column; align-items: start;}',
+    '::ng-deep .layoutMenu .section-height-input {margin-top: 10px;}',
+    '.menuItem {margin-bottom: 5px;}',
+    '::ng-deep .activeAfterID-menu .mat-form-field {width:90%; margin-left: 10px;}'
+  ]
 })
-export class SectionMenuComponent implements OnInit, OnDestroy {
+export class SectionMenuComponent implements OnDestroy {
   @Input() section!: Section;
   @Input() sectionIndex!: number;
   @Input() allowMoveUp!: boolean;
@@ -239,8 +197,6 @@ export class SectionMenuComponent implements OnInit, OnDestroy {
   @Output() selectElementComponent = new EventEmitter<UIElement>();
 
   @ViewChild('colorPicker') colorPicker!: ElementRef;
-  columnSizes: { value: string, unit: string }[] = [];
-  rowSizes: { value: string, unit: string }[] = [];
   private ngUnsubscribe = new Subject<void>();
 
   constructor(public unitService: UnitService,
@@ -250,11 +206,7 @@ export class SectionMenuComponent implements OnInit, OnDestroy {
               private idService: IDService,
               private clipboard: Clipboard) { }
 
-  ngOnInit(): void {
-    this.updateGridSizes();
-  }
-
-  updateModel(property: string, value: string | number | boolean): void {
+  updateModel(property: string, value: string | number | boolean | { value: number; unit: string }[]): void {
     this.unitService.updateSectionProperty(this.section, property, value);
   }
 
@@ -276,51 +228,28 @@ export class SectionMenuComponent implements OnInit, OnDestroy {
       });
   }
 
-  /* Initialize internal array of grid sizes. Where value and unit are put, split up, in an array. */
-  private updateGridSizes(): void {
-    this.columnSizes = [];
-    this.section.gridColumnSizes.split(' ').forEach((size: string) => {
-      this.columnSizes.push({ value: size.slice(0, -2), unit: size.slice(-2) });
-    });
-    this.rowSizes = [];
-    this.section.gridRowSizes.split(' ').forEach((size: string) => {
-      this.rowSizes.push({ value: size.slice(0, -2), unit: size.slice(-2) });
-    });
-  }
-
-  /* Add elements to size array. Default value 1fr. */
+  /* Add or remove elements to size array. Default value 1fr. */
   modifySizeArray(property: 'gridColumnSizes' | 'gridRowSizes', newLength: number): void {
-    const oldSizesAsArray = property === 'gridColumnSizes' ?
-      this.section.gridColumnSizes.split(' ') :
-      this.section.gridRowSizes.split(' ');
+    const sizeArray: { value: number; unit: string }[] = property === 'gridColumnSizes' ?
+      this.section.gridColumnSizes : this.section.gridRowSizes;
 
     let newArray = [];
-    if (newLength < oldSizesAsArray.length) {
-      newArray = oldSizesAsArray.slice(0, newLength);
+    if (newLength < sizeArray.length) {
+      newArray = sizeArray.slice(0, newLength);
     } else {
       newArray.push(
-        ...oldSizesAsArray,
-        ...Array(newLength - oldSizesAsArray.length).fill('1fr')
+        ...sizeArray,
+        ...Array(newLength - sizeArray.length).fill({ value: 1, unit: 'fr' })
       );
     }
-    this.updateModel(property, newArray.join(' '));
-    this.updateGridSizes();
+    this.updateModel(property, newArray);
   }
 
-  /* Replace number or unit is size string. '2fr 1fr' -> '2px 3fr' */
-  changeGridSize(property: string, index: number, unit: boolean = false, newValue: string): void {
-    const oldSizesAsArray = property === 'gridColumnSizes' ?
-      this.section.gridColumnSizes.split(' ') :
-      this.section.gridRowSizes.split(' ');
-
-    if (unit) {
-      oldSizesAsArray[index] = oldSizesAsArray[index].slice(0, -2) + newValue;
-    } else {
-      oldSizesAsArray[index] = newValue + oldSizesAsArray[index].slice(-2);
-    }
-
-    this.updateModel(property, oldSizesAsArray.join(' '));
-    this.updateGridSizes();
+  changeGridSize(property: string, index: number, newValue: { value: number; unit: string }): void {
+    const sizeArray: { value: number; unit: string }[] = property === 'gridColumnSizes' ?
+      this.section.gridColumnSizes : this.section.gridRowSizes;
+    sizeArray[index] = newValue;
+    this.updateModel(property, [...sizeArray]);
   }
 
   openColorPicker(): void {
diff --git a/projects/editor/src/app/components/properties-panel/position-properties-tab/input-groups/position-field-set.component.ts b/projects/editor/src/app/components/properties-panel/position-properties-tab/input-groups/position-field-set.component.ts
index 21fee4273..42aa1786e 100644
--- a/projects/editor/src/app/components/properties-panel/position-properties-tab/input-groups/position-field-set.component.ts
+++ b/projects/editor/src/app/components/properties-panel/position-properties-tab/input-groups/position-field-set.component.ts
@@ -60,44 +60,47 @@ import { PositionProperties } from 'common/models/elements/element';
         </div>
 
         {{'propertiesPanel.margin' | translate }}
-        <div class="margin-controls fx-column-start-stretch">
-          <mat-form-field class="fx-align-self-center">
-            <mat-label>{{'propertiesPanel.top' | translate }}</mat-label>
-            <input matInput type="number" #marginTop="ngModel"
-                   [ngModel]="positionProperties.marginTop"
-                   (ngModelChange)="updateModel.emit(
-                          { property: 'marginTop', value: $event, isInputValid: marginTop.valid && $event !== null })">
-          </mat-form-field>
-          <div class="fx-row-space-around-center">
-            <mat-form-field>
-              <mat-label>{{'propertiesPanel.left' | translate }}</mat-label>
-              <input matInput type="number" #marginLeft="ngModel"
-                     [ngModel]="positionProperties.marginLeft"
-                     (ngModelChange)="updateModel.emit({
-                       property: 'marginLeft',
-                       value: $event,
-                       isInputValid: marginLeft.valid && $event !== null
-                     })">
-            </mat-form-field>
-            <mat-form-field>
-              <mat-label>{{'propertiesPanel.right' | translate }}</mat-label>
-              <input matInput type="number" #marginRight="ngModel"
-                     [ngModel]="positionProperties.marginRight"
-                     (ngModelChange)="updateModel.emit(
-                            { property: 'marginRight',
-                              value: $event,
-                              isInputValid: marginRight .valid && $event !== null })">
-            </mat-form-field>
-          </div>
-          <mat-form-field class="fx-align-self-center">
-            <mat-label>{{'propertiesPanel.bottom' | translate }}</mat-label>
-            <input matInput type="number" #marginBottom="ngModel"
-                   [ngModel]="positionProperties.marginBottom"
-                   (ngModelChange)="updateModel.emit(
-                          { property: 'marginBottom',
-                            value: $event,
-                            isInputValid: marginBottom .valid && $event !== null })">
-          </mat-form-field>
+        <div>
+          <aspect-size-input-panel [label]="('propertiesPanel.top' | translate)"
+                                   [value]="positionProperties.marginTop.value"
+                                   [unit]="positionProperties.marginTop.unit"
+                                   [allowedUnits]="['px', '%']"
+                                   (valueUpdated)="updateModel.emit(
+                                                     {
+                                                       property: 'marginTop',
+                                                       value: $any($event)
+                                                     })">
+          </aspect-size-input-panel>
+          <aspect-size-input-panel [label]="('propertiesPanel.bottom' | translate)"
+                                   [value]="positionProperties.marginBottom.value"
+                                   [unit]="positionProperties.marginBottom.unit"
+                                   [allowedUnits]="['px', '%']"
+                                   (valueUpdated)="updateModel.emit(
+                                                     {
+                                                       property: 'marginBottom',
+                                                       value: $any($event)
+                                                     })">
+          </aspect-size-input-panel>
+          <aspect-size-input-panel [label]="('propertiesPanel.left' | translate)"
+                                   [value]="positionProperties.marginLeft.value"
+                                   [unit]="positionProperties.marginLeft.unit"
+                                   [allowedUnits]="['px', '%']"
+                                   (valueUpdated)="updateModel.emit(
+                                                     {
+                                                       property: 'marginLeft',
+                                                       value: $any($event)
+                                                     })">
+          </aspect-size-input-panel>
+          <aspect-size-input-panel [label]="('propertiesPanel.right' | translate)"
+                                   [value]="positionProperties.marginRight.value"
+                                   [unit]="positionProperties.marginRight.unit"
+                                   [allowedUnits]="['px', '%']"
+                                   (valueUpdated)="updateModel.emit(
+                                                     {
+                                                       property: 'marginRight',
+                                                       value: $any($event)
+                                                     })">
+          </aspect-size-input-panel>
         </div>
       </ng-template>
 
@@ -113,63 +116,16 @@ import { PositionProperties } from 'common/models/elements/element';
       </mat-form-field>
     </fieldset>
   `,
-  styles: [`
-    .margin-controls mat-form-field {
-      width: 100px;
-    }
-    .margin-controls {
-      margin-bottom: 10px;
-    }
-    mat-form-field {
-      width: 110px;
-    }
-
-    .fx-row-start-stretch {
-      box-sizing: border-box;
-      display: flex;
-      flex-direction: row;
-      justify-content: flex-start;
-      align-items: stretch;
-    }
-
-    .fx-row-space-around-center {
-      box-sizing: border-box;
-      display: flex;
-      flex-direction: row;
-      justify-content: space-around;
-      align-items: center;
-    }
-
-    .fx-column-start-stretch {
-      box-sizing: border-box;
-      display: flex;
-      flex-direction: column;
-      justify-content: flex-start;
-      align-items: stretch;
-    }
-
-    .fx-fix-gap-10 {
-      gap: 10px;
-    }
-
-    .fx-flex {
-      flex: 1 1 0;
-      box-sizing: border-box;
-    }
-
-    .fx-flex-40 {
-      flex: 1 1 100%;
-      box-sizing: border-box;
-      max-width: 40%;
-    }
-
-    .fx-align-self-center {
-      align-self: center;
-    }
-  `]
+  styles: [
+    'mat-form-field {width: 110px;}'
+  ]
 })
 export class PositionFieldSetComponent {
   @Input() positionProperties!: PositionProperties;
   @Output() updateModel =
-    new EventEmitter<{ property: string; value: string | number | boolean, isInputValid?: boolean | null }>();
+    new EventEmitter<{
+      property: string;
+      value: UIElementValue,
+      isInputValid?: boolean | null
+    }>();
 }
diff --git a/projects/editor/src/app/components/util/size-input-panel.component.ts b/projects/editor/src/app/components/util/size-input-panel.component.ts
new file mode 100644
index 000000000..9696b5fa8
--- /dev/null
+++ b/projects/editor/src/app/components/util/size-input-panel.component.ts
@@ -0,0 +1,45 @@
+import {
+  Component, EventEmitter, Input, Output
+} from '@angular/core';
+import { Measurement } from 'common/models/elements/element';
+
+@Component({
+  selector: 'aspect-size-input-panel',
+  template: `
+    <div class="panel">
+      <mat-form-field>
+        <mat-label>{{label}}</mat-label>
+        <input matInput type="number"
+               [(ngModel)]="value"
+               (change)="valueUpdated.emit(getCombinedString())">
+      </mat-form-field>
+      <mat-form-field>
+        <mat-label>Einheit</mat-label>
+        <mat-select [(ngModel)]="unit"
+                    (selectionChange)="valueUpdated.emit(getCombinedString())">
+          <mat-option *ngIf="allowedUnits.includes('fr')"
+                      value="fr">{{'section-menu.fraction' | translate }}</mat-option>
+          <mat-option *ngIf="allowedUnits.includes('px')"
+                      value="px">{{'section-menu.pixel' | translate }}</mat-option>
+          <mat-option *ngIf="allowedUnits.includes('%')"
+                      value="%">{{'section-menu.percent' | translate }}</mat-option>
+        </mat-select>
+      </mat-form-field>
+    </div>
+  `,
+  styles: [
+    '.panel {display: flex; flex-direction: row; gap: 15px;}',
+    '.panel .mat-form-field {width: 100px;}'
+  ]
+})
+export class SizeInputPanelComponent {
+  @Input() label!: string;
+  @Input() value!: number;
+  @Input() unit!: string;
+  @Input() allowedUnits!: string[];
+  @Output() valueUpdated = new EventEmitter<Measurement>();
+
+  getCombinedString(): { value: number; unit: string } {
+    return { value: this.value, unit: this.unit };
+  }
+}
diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts
index 50f0d68cd..6dbb1ca36 100644
--- a/projects/editor/src/app/services/unit.service.ts
+++ b/projects/editor/src/app/services/unit.service.ts
@@ -71,7 +71,7 @@ export class UnitService {
   }
 
   addSection(page: Page, newSection?: Partial<Section>): void {
-    page.sections.push(new Section(newSection));
+    page.sections.push(new Section(newSection as Record<string, UIElementValue>));
     this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit);
   }
 
@@ -86,7 +86,7 @@ export class UnitService {
   duplicateSection(section: Section, page: Page, sectionIndex: number): void {
     const newSection: Section = new Section({
       ...section,
-      elements: section.elements.map(element => this.duplicateElement(element) as PositionedUIElement)
+      elements: section.elements.map(element => this.duplicateElement(element))
     });
     page.sections.splice(sectionIndex + 1, 0, newSection);
     this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit);
@@ -221,7 +221,7 @@ export class UnitService {
     return newElement;
   }
 
-  updateSectionProperty(section: Section, property: string, value: string | number | boolean): void {
+  updateSectionProperty(section: Section, property: string, value: string | number | boolean | { value: number; unit: string }[]): void {
     if (property === 'dynamicPositioning') {
       section.dynamicPositioning = value as boolean;
       section.elements.forEach((element: UIElement) => {
-- 
GitLab