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