diff --git a/projects/editor/src/app/app.component.ts b/projects/editor/src/app/app.component.ts index ae09b257b27886800cdcb4cd118b55c8325d5c85..95ca2f6c011fc0c225146cb9812968be92b97375 100644 --- a/projects/editor/src/app/app.component.ts +++ b/projects/editor/src/app/app.component.ts @@ -3,7 +3,7 @@ import { TranslateService } from '@ngx-translate/core'; import { registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; import { VeronaAPIService, StartCommand } from './services/verona-api.service'; -import { UnitService } from './services/unit.service'; +import { UnitService } from './services/unit-services/unit.service'; @Component({ selector: 'aspect-editor', diff --git a/projects/editor/src/app/components/canvas/canvas.component.html b/projects/editor/src/app/components/canvas/canvas.component.html index 2db4c17e4210258f64c8caef7854b5403b2107f3..a3cc4691fa36d2105fc2334da974afe2e0349917 100644 --- a/projects/editor/src/app/components/canvas/canvas.component.html +++ b/projects/editor/src/app/components/canvas/canvas.component.html @@ -12,8 +12,8 @@ [allowMoveUp]="i != 0" [allowMoveDown]="i < page.sections.length - 1" [allowDelete]="page.sections.length > 1" - (moveSection)="unitService.moveSection(section, page, $event)" - (duplicateSection)="unitService.duplicateSection(section, page, i)" + (moveSection)="sectionService.moveSection(section, page, $event)" + (duplicateSection)="sectionService.duplicateSection(section, page, i)" (selectElementComponent)="selectElementOverlay($event)"> </aspect-section-menu> <aspect-section-static *ngIf="!section.dynamicPositioning" diff --git a/projects/editor/src/app/components/canvas/canvas.component.ts b/projects/editor/src/app/components/canvas/canvas.component.ts index b6ba5e7c2ff472c3f978a7abf8f834f33cb0571c..0a5e5e840ce64abd3780fe76dc4be8a572f75de3 100644 --- a/projects/editor/src/app/components/canvas/canvas.component.ts +++ b/projects/editor/src/app/components/canvas/canvas.component.ts @@ -5,11 +5,12 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { PositionedUIElement, UIElement } from 'common/models/elements/element'; import { Page } from 'common/models/page'; import { Section } from 'common/models/section'; -import { UnitService } from '../../services/unit.service'; +import { UnitService } from '../../services/unit-services/unit.service'; import { SelectionService } from '../../services/selection.service'; import { CanvasElementOverlay } from './overlays/canvas-element-overlay'; import { SectionStaticComponent } from './section-static.component'; import { SectionDynamicComponent } from './section-dynamic.component'; +import { SectionService } from 'editor/src/app/services/unit-services/section.service'; @Component({ selector: 'aspect-page-canvas', @@ -42,10 +43,12 @@ export class CanvasComponent { @ViewChildren('sectionComponent') sectionComponents!: QueryList<SectionStaticComponent | SectionDynamicComponent>; - constructor(public selectionService: SelectionService, public unitService: UnitService) { } + constructor(public selectionService: SelectionService, + public unitService: UnitService, + public sectionService: SectionService) { } moveElementsBetweenSections(elements: UIElement[], previousSectionIndex: number, newSectionIndex: number): void { - this.unitService.transferElement(elements, + this.sectionService.transferElement(elements, this.page.sections[previousSectionIndex], this.page.sections[newSectionIndex]); } @@ -86,7 +89,7 @@ export class CanvasComponent { } addSection(): void { - this.unitService.addSection(this.page); + this.sectionService.addSection(this.page); this.selectionService.selectedPageSectionIndex = this.page.sections.length - 1; } 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 e556a6e5d8fd3d97b7826f84fae06345af4d7f59..41df84a1d4a0cbe99a249832b99c80907a2cb80a 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 @@ -4,7 +4,8 @@ import { } from '@angular/core'; import { UIElement, UIElementType } from 'common/models/elements/element'; import { Section } from 'common/models/section'; -import { UnitService } from '../../services/unit.service'; +import { UnitService } from '../../services/unit-services/unit.service'; +import { ElementService } from 'editor/src/app/services/unit-services/element.service'; @Component({ selector: '[app-dynamic-section-helper-grid]', @@ -44,7 +45,7 @@ export class DynamicSectionHelperGridComponent implements OnInit, OnChanges { columnCountArray: unknown[] = []; rowCountArray: unknown[] = []; - constructor(public unitService: UnitService) {} + constructor(public unitService: UnitService, private elementService: ElementService) {} ngOnInit(): void { this.calculateColumnCount(); @@ -134,7 +135,7 @@ export class DynamicSectionHelperGridComponent implements OnInit, OnChanges { newElementDropped(event: DragEvent, gridX: number, gridY: number): void { event.preventDefault(); - this.unitService.addElementToSection( + this.elementService.addElementToSection( event.dataTransfer?.getData('elementType') as UIElementType, this.section, { x: gridX, y: gridY } diff --git a/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts b/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts index 4cf511684cbeacf2e519bc1f71f56c3cec496718..c3b19fc470919d1124696a664debc7d85fc27e47 100644 --- a/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts +++ b/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts @@ -12,8 +12,9 @@ import { UIElement } from 'common/models/elements/element'; import { GeometryComponent } from 'common/components/geometry/geometry.component'; import { FormElementComponent } from 'common/directives/form-element-component.directive'; import { MathTableComponent } from 'common/components/input-elements/math-table.component'; -import { UnitService } from '../../../services/unit.service'; +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'; @Directive() export abstract class CanvasElementOverlay implements OnInit, OnDestroy { @@ -30,6 +31,7 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy { constructor(public selectionService: SelectionService, protected unitService: UnitService, + protected elementService: ElementService, private changeDetectorRef: ChangeDetectorRef) { } ngOnInit(): void { @@ -106,7 +108,7 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy { } openEditDialog(): void { - this.unitService.showDefaultEditDialog(this.element); + this.elementService.showDefaultEditDialog(this.element); } setInteractionEnabled(isEnabled: boolean): void { diff --git a/projects/editor/src/app/components/canvas/overlays/static-canvas-overlay.component.ts b/projects/editor/src/app/components/canvas/overlays/static-canvas-overlay.component.ts index d3223609b7074437dc2ffa11ea67cb195cc88baa..006ba295e42b9044cf9368c96bc03ae7c1653599 100644 --- a/projects/editor/src/app/components/canvas/overlays/static-canvas-overlay.component.ts +++ b/projects/editor/src/app/components/canvas/overlays/static-canvas-overlay.component.ts @@ -66,12 +66,12 @@ export class StaticCanvasOverlayComponent extends CanvasElementOverlay { } updateModel(event: CdkDragEnd): void { - this.unitService.updateElementsProperty( + this.elementService.updateElementsProperty( this.selectionService.getSelectedElements(), 'width', Math.max(this.oldX + event.distance.x, 0) ); - this.unitService.updateElementsProperty( + this.elementService.updateElementsProperty( this.selectionService.getSelectedElements(), 'height', Math.max(this.oldY + event.distance.y, 0) @@ -82,7 +82,7 @@ export class StaticCanvasOverlayComponent extends CanvasElementOverlay { this.selectionService.selectedElements .pipe(take(1)) .subscribe((selectedElements: UIElement[]) => { - this.unitService.deleteElements(selectedElements); + this.elementService.deleteElements(selectedElements); this.selectionService.clearElementSelection(); }) .unsubscribe(); 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 3f155a59988df0dd44af66cd5c69d0da8737ae76..3f84e88649d2f512556ee0e9c0885052d945f872 100644 --- a/projects/editor/src/app/components/canvas/section-menu.component.ts +++ b/projects/editor/src/app/components/canvas/section-menu.component.ts @@ -11,9 +11,10 @@ import { DropListElement } from 'common/models/elements/input-elements/drop-list import { IDService } from 'editor/src/app/services/id.service'; import { VisibilityRule } from 'common/models/visibility-rule'; import { ReferenceManager } from 'editor/src/app/services/reference-manager'; -import { UnitService } from '../../services/unit.service'; +import { UnitService } from '../../services/unit-services/unit.service'; import { DialogService } from '../../services/dialog.service'; import { SelectionService } from '../../services/selection.service'; +import { SectionService } from 'editor/src/app/services/unit-services/section.service'; @Component({ selector: 'aspect-section-menu', @@ -184,6 +185,7 @@ export class SectionMenuComponent implements OnDestroy { private ngUnsubscribe = new Subject<void>(); constructor(public unitService: UnitService, + private sectionService: SectionService, private selectionService: SelectionService, private dialogService: DialogService, private messageService: MessageService, @@ -193,7 +195,7 @@ export class SectionMenuComponent implements OnDestroy { updateModel( property: string, value: string | number | boolean | VisibilityRule[] | { value: number; unit: string }[] ): void { - this.unitService.updateSectionProperty(this.section, property, value); + this.sectionService.updateSectionProperty(this.section, property, value); } selectElement(element: UIElement): void { @@ -209,7 +211,7 @@ export class SectionMenuComponent implements OnDestroy { .subscribe((result: boolean) => { if (result) { ReferenceManager.deleteReferences(refs); - this.unitService.deleteSection(this.selectionService.selectedPageIndex, this.sectionIndex); + this.sectionService.deleteSection(this.selectionService.selectedPageIndex, this.sectionIndex); this.selectionService.selectedPageSectionIndex = Math.max(0, this.selectionService.selectedPageSectionIndex - 1); } else { @@ -221,7 +223,7 @@ export class SectionMenuComponent implements OnDestroy { .pipe(takeUntil(this.ngUnsubscribe)) .subscribe((result: boolean) => { if (result) { - this.unitService.deleteSection(this.selectionService.selectedPageIndex, this.sectionIndex); + this.sectionService.deleteSection(this.selectionService.selectedPageIndex, this.sectionIndex); this.selectionService.selectedPageSectionIndex = Math.max(0, this.selectionService.selectedPageSectionIndex - 1); } @@ -284,7 +286,7 @@ export class SectionMenuComponent implements OnDestroy { }); } }); - this.unitService.replaceSection(this.selectionService.selectedPageIndex, this.sectionIndex, newSection); + this.sectionService.replaceSection(this.selectionService.selectedPageIndex, this.sectionIndex, newSection); } }); } diff --git a/projects/editor/src/app/components/canvas/section-static.component.ts b/projects/editor/src/app/components/canvas/section-static.component.ts index 79766c964a138d3ba56dc6fe6b0dca99481780f2..85e89eb3db886f26c0be8011afc168c59e1b6a02 100644 --- a/projects/editor/src/app/components/canvas/section-static.component.ts +++ b/projects/editor/src/app/components/canvas/section-static.component.ts @@ -3,8 +3,9 @@ import { } from '@angular/core'; import { Section } from 'common/models/section'; import { UIElementType } from 'common/models/elements/element'; -import { UnitService } from '../../services/unit.service'; +import { UnitService } from '../../services/unit-services/unit.service'; import { CanvasElementOverlay } from './overlays/canvas-element-overlay'; +import { ElementService } from 'editor/src/app/services/unit-services/element.service'; @Component({ selector: 'aspect-section-static', @@ -33,12 +34,12 @@ export class SectionStaticComponent { @ViewChild('sectionElement') sectionElement!: ElementRef; @ViewChildren('elementComponent') childElementComponents!: QueryList<CanvasElementOverlay>; - constructor(public unitService: UnitService) { } + constructor(public unitService: UnitService, private elementService: ElementService) { } newElementDropped(event: DragEvent): void { event.preventDefault(); const sectionRect = this.sectionElement.nativeElement.getBoundingClientRect(); - this.unitService.addElementToSection( + this.elementService.addElementToSection( event.dataTransfer?.getData('elementType') as UIElementType, this.section, { x: event.clientX - Math.round(sectionRect.left), y: event.clientY - Math.round(sectionRect.top) } diff --git a/projects/editor/src/app/components/dialogs/player-edit-dialog.component.ts b/projects/editor/src/app/components/dialogs/player-edit-dialog.component.ts index e8dca26463a276f19190451a15f869cefdff389c..005321174a09102049d5a4d808329268f41742cb 100644 --- a/projects/editor/src/app/components/dialogs/player-edit-dialog.component.ts +++ b/projects/editor/src/app/components/dialogs/player-edit-dialog.component.ts @@ -4,7 +4,7 @@ import { } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { PlayerProperties } from 'common/models/elements/property-group-interfaces'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; @Component({ selector: 'aspect-player-edit-dialog', diff --git a/projects/editor/src/app/components/new-ui-element-panel/show-state-variables-button.component.ts b/projects/editor/src/app/components/new-ui-element-panel/show-state-variables-button.component.ts index b65639baeade610bcb6dfa94d59f142d2285d176..c6e1eb14e3864c8248cc953319205f144cb30e1d 100644 --- a/projects/editor/src/app/components/new-ui-element-panel/show-state-variables-button.component.ts +++ b/projects/editor/src/app/components/new-ui-element-panel/show-state-variables-button.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { DialogService } from 'editor/src/app/services/dialog.service'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; @Component({ selector: 'aspect-show-state-variables-button', 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 829e135abecf66a880ef34a95a39717165e2b058..c4446d2b3e9b1e1398c9ce23a6bc67cbb3f0a951 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 @@ -1,7 +1,8 @@ import { Component } from '@angular/core'; import { UIElementType } from 'common/models/elements/element'; -import { UnitService } from '../../services/unit.service'; +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'; @Component({ selector: 'aspect-ui-element-toolbox', @@ -11,10 +12,13 @@ import { SelectionService } from '../../services/selection.service'; export class UiElementToolboxComponent { hoverRadioButton: boolean = false; hoverFormulaButton: boolean = false; - constructor(private selectionService: SelectionService, public unitService: UnitService) { } + + constructor(private selectionService: SelectionService, + public unitService: UnitService, + private elementService: ElementService) { } async addUIElement(elementType: UIElementType): Promise<void> { - this.unitService.addElementToSectionByIndex(elementType, + this.elementService.addElementToSectionByIndex(elementType, this.selectionService.selectedPageIndex, this.selectionService.selectedPageSectionIndex); } diff --git a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts index af88f64d8a5d8446a09b743051f03720fdd6e4dd..fb926868a0f389ce31b774839e84357fe02e4499 100644 --- a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts +++ b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts @@ -8,9 +8,11 @@ import { TranslateService } from '@ngx-translate/core'; import { MessageService } from 'common/services/message.service'; import { UIElement } from 'common/models/elements/element'; import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row'; -import { UnitService } from '../../services/unit.service'; +import { UnitService } from '../../services/unit-services/unit.service'; import { SelectionService } from '../../services/selection.service'; import { CanvasElementOverlay } from '../canvas/overlays/canvas-element-overlay'; +import { ElementService } from 'editor/src/app/services/unit-services/element.service'; +import { SectionService } from 'editor/src/app/services/unit-services/section.service'; export type CombinedProperties = UIElement & { idList?: string[] }; @@ -29,6 +31,8 @@ export class ElementPropertiesPanelComponent implements OnInit, OnDestroy { constructor(protected selectionService: SelectionService, public unitService: UnitService, + public sectionService: SectionService, + public elementService: ElementService, private messageService: MessageService, public sanitizer: DomSanitizer, private translateService: TranslateService) { } @@ -105,7 +109,7 @@ export class ElementPropertiesPanelComponent implements OnInit, OnDestroy { value: unknown, isInputValid: boolean | null = true): void { if (isInputValid) { - this.unitService.updateElementsProperty(this.selectedElements, property, value); + this.elementService.updateElementsProperty(this.selectedElements, property, value); } else { this.messageService.showWarning(this.translateService.instant('inputInvalid')); } @@ -118,11 +122,11 @@ export class ElementPropertiesPanelComponent implements OnInit, OnDestroy { } deleteElement(): void { - this.unitService.deleteElements(this.selectedElements); + this.elementService.deleteElements(this.selectedElements); } duplicateElement(): void { - this.unitService.duplicateSelectedElements(); + this.sectionService.duplicateSelectedElements(); } ngOnDestroy(): void { diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html index 2d10f975841c9494df9414b56c8be514813578f3..cbd13ef52d8d97be8f5b1528314c9beb9b38be68 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html @@ -53,7 +53,7 @@ <button *ngIf="combinedProperties.document" [style.align-self]="'center'" mat-raised-button - (click)="unitService.showDefaultEditDialog(selectedElements[0])"> + (click)="elementService.showDefaultEditDialog(selectedElements[0])"> Text und Elemente editieren </button> @@ -66,7 +66,7 @@ <button *ngIf="combinedProperties.player" [style.align-self]="'center'" mat-raised-button - (click)="unitService.showDefaultEditDialog(selectedElements[0])"> + (click)="elementService.showDefaultEditDialog(selectedElements[0])"> Medienoptionen anpassen </button> diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts index 4bfb98fae62b2122e3ad963d2b649599fcdedcdf..a38aa5542b4d7128cf0f89e6b33a4d39c877ea66 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts @@ -18,9 +18,10 @@ import { TextImageLabel, TextLabel } from 'common/models/elements/label-interfac import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; import { StateVariable } from 'common/models/state-variable'; import { GeometryComponent } from 'common/components/geometry/geometry.component'; -import { UnitService } from '../../../services/unit.service'; +import { UnitService } from '../../../services/unit-services/unit.service'; import { SelectionService } from '../../../services/selection.service'; import { DialogService } from '../../../services/dialog.service'; +import { ElementService } from 'editor/src/app/services/unit-services/element.service'; @Component({ selector: 'aspect-element-model-properties-component', @@ -40,6 +41,7 @@ export class ElementModelPropertiesComponent implements OnDestroy { private ngUnsubscribe = new Subject<void>(); constructor(public unitService: UnitService, + public elementService: ElementService, public selectionService: SelectionService, public dialogService: DialogService) { } diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/action-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/action-properties.component.ts index 97d71b492ccee1090f251c9f4e623ac08c0656b0..c5792be104a39216fccc7cd5b83d666893040fb7 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/action-properties.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/action-properties.component.ts @@ -5,7 +5,7 @@ import { import { UIElement } from 'common/models/elements/element'; import { StateVariable } from 'common/models/state-variable'; import { TextComponent } from 'common/components/text/text.component'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; import { SelectionService } from 'editor/src/app/services/selection.service'; import { Page } from 'common/models/page'; diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/drop-list-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/drop-list-properties.component.ts index 90dd5f064b4b6fdffb83030bd6a18bd474916c2e..48b73eaec40a77766bb644706be83e0a4787dd03 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/drop-list-properties.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/ele-specific/drop-list-properties.component.ts @@ -17,7 +17,7 @@ import { OptionListPanelComponent } from 'editor/src/app/components/properties-p import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; import { DialogService } from 'editor/src/app/services/dialog.service'; @Pipe({ diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts index 865e12ce51fa990b807ff6506ac7932ceed69862..a0037ed7c156ad2a389f3c3d17613c15be137f85 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts @@ -3,13 +3,14 @@ import { } from '@angular/core'; import { CombinedProperties } from 'editor/src/app/components/properties-panel/element-properties-panel.component'; import { LikertRowElement, LikertRowProperties } from 'common/models/elements/compound-elements/likert/likert-row'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; import { DialogService } from 'editor/src/app/services/dialog.service'; import { moveItemInArray } from '@angular/cdk/drag-drop'; import { SelectionService } from 'editor/src/app/services/selection.service'; import { IDService } from 'editor/src/app/services/id.service'; import { Label, TextImageLabel, TextLabel } from 'common/models/elements/label-interfaces'; import { OptionElement } from 'common/models/elements/element'; +import { ElementService } from 'editor/src/app/services/unit-services/element.service'; @Component({ selector: 'aspect-options-field-set', @@ -47,6 +48,7 @@ export class OptionsFieldSetComponent { }>(); constructor(private unitService: UnitService, + private elementService: ElementService, private selectionService: SelectionService, public dialogService: DialogService, private idService: IDService) { } @@ -56,7 +58,7 @@ export class OptionsFieldSetComponent { selectedElements.forEach(element => { const newValue = [...this.combinedProperties[property] as Label[], element.getNewOptionLabel(option)]; - this.unitService.updateElementsProperty([element], property, newValue); + this.elementService.updateElementsProperty([element], property, newValue); }); } @@ -112,31 +114,31 @@ export class OptionsFieldSetComponent { .subscribe((result: LikertRowElement) => { if (result) { if (result.id !== row.id) { - this.unitService.updateElementsProperty( + this.elementService.updateElementsProperty( [row], 'id', result.id ); } if (result.rowLabel !== row.rowLabel) { - this.unitService.updateElementsProperty([row], 'rowLabel', result.rowLabel); + this.elementService.updateElementsProperty([row], 'rowLabel', result.rowLabel); } if (result.value !== row.value) { - this.unitService.updateElementsProperty( + this.elementService.updateElementsProperty( [row], 'value', result.value ); } if (result.verticalButtonAlignment !== row.verticalButtonAlignment) { - this.unitService.updateElementsProperty( + this.elementService.updateElementsProperty( [row], 'verticalButtonAlignment', result.verticalButtonAlignment ); } if (result.readOnly !== row.readOnly) { - this.unitService.updateElementsProperty( + this.elementService.updateElementsProperty( [row], 'readOnly', result.readOnly diff --git a/projects/editor/src/app/components/properties-panel/position-properties-tab/element-position-properties.component.ts b/projects/editor/src/app/components/properties-panel/position-properties-tab/element-position-properties.component.ts index 3d9ca7b39306a25a156e1c6149cfac5f64a5f826..ed47e355b98cc5f09dca2cce41d8ea0bb7ae75a2 100644 --- a/projects/editor/src/app/components/properties-panel/position-properties-tab/element-position-properties.component.ts +++ b/projects/editor/src/app/components/properties-panel/position-properties-tab/element-position-properties.component.ts @@ -3,8 +3,9 @@ import { } from '@angular/core'; import { PositionedUIElement } from 'common/models/elements/element'; import { DimensionProperties, PositionProperties } from 'common/models/elements/property-group-interfaces'; -import { UnitService } from '../../../services/unit.service'; +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'; @Component({ selector: 'aspect-position-and-dimension-properties', @@ -48,9 +49,11 @@ export class ElementPositionPropertiesComponent { @Input() positionProperties: PositionProperties | undefined; @Input() isZIndexDisabled: boolean = false; - constructor(public unitService: UnitService, public selectionService: SelectionService) { } + constructor(public unitService: UnitService, + public selectionService: SelectionService, + private elementService: ElementService) { } alignElements(direction: 'left' | 'right' | 'top' | 'bottom'): void { - this.unitService.alignElements(this.selectionService.getSelectedElements() as PositionedUIElement[], direction); + this.elementService.alignElements(this.selectionService.getSelectedElements() as PositionedUIElement[], direction); } } diff --git a/projects/editor/src/app/components/properties-panel/position-properties-tab/input-groups/dimension-field-set.component.ts b/projects/editor/src/app/components/properties-panel/position-properties-tab/input-groups/dimension-field-set.component.ts index a85e483a07184e50f7f3a54df1451000713a4436..2964d6a4b8a82aec911a72f49313958178262e44 100644 --- a/projects/editor/src/app/components/properties-panel/position-properties-tab/input-groups/dimension-field-set.component.ts +++ b/projects/editor/src/app/components/properties-panel/position-properties-tab/input-groups/dimension-field-set.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; import { SelectionService } from 'editor/src/app/services/selection.service'; import { DimensionProperties, PositionProperties } from 'common/models/elements/property-group-interfaces'; 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 165423a9cf1912b60321325b58372f88bc77c020..40fc4153d33dcda81452710283e80679a1db40bc 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 @@ -4,7 +4,7 @@ import { import { UIElementValue } from 'common/models/elements/element'; import { PositionProperties } from 'common/models/elements/property-group-interfaces'; import { SelectionService } from 'editor/src/app/services/selection.service'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; @Component({ selector: 'aspect-position-field-set', diff --git a/projects/editor/src/app/components/properties-panel/style-properties-tab/element-style-properties.component.ts b/projects/editor/src/app/components/properties-panel/style-properties-tab/element-style-properties.component.ts index dbd688ebad86b11966b145d887c6409d325b0a2e..2b16f38c33b47fb06bc4750cf4d514d333f30dd1 100644 --- a/projects/editor/src/app/components/properties-panel/style-properties-tab/element-style-properties.component.ts +++ b/projects/editor/src/app/components/properties-panel/style-properties-tab/element-style-properties.component.ts @@ -1,6 +1,7 @@ import { Component, Input } from '@angular/core'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; import { Stylings } from 'common/models/elements/property-group-interfaces'; +import { ElementService } from 'editor/src/app/services/unit-services/element.service'; @Component({ selector: 'aspect-element-style-properties', @@ -10,7 +11,7 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; appearance="fill" class="mdInput textsingleline"> <mat-label>{{'propertiesPanel.helperRowColor' | translate }}</mat-label> <input matInput type="text" [value]="styles.helperRowColor" - (input)="unitService.updateSelectedElementsStyleProperty( + (input)="elementService.updateSelectedElementsStyleProperty( 'helperRowColor', $any($event.target).value)"> <button mat-icon-button matSuffix (click)="helperRowColorInput.click()"> <mat-icon>edit</mat-icon> @@ -18,11 +19,11 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; </mat-form-field> <input matInput type="color" hidden #helperRowColorInput [value]="styles.helperRowColor" - (input)="unitService.updateSelectedElementsStyleProperty('helperRowColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('helperRowColor', $any($event.target).value)"> <mat-checkbox *ngIf="styles.lineColoring !== undefined" [checked]="$any(styles.lineColoring)" - (change)="unitService.updateSelectedElementsStyleProperty('lineColoring', $event.checked)"> + (change)="elementService.updateSelectedElementsStyleProperty('lineColoring', $event.checked)"> {{'propertiesPanel.lineColoring' | translate }} </mat-checkbox> @@ -31,7 +32,7 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; <mat-label>{{'propertiesPanel.lineColoringColor' | translate }}</mat-label> <input matInput type="text" [value]="styles.lineColoringColor" [disabled]="!styles.lineColoring || styles.lineColoringColor === undefined" - (input)="unitService.updateSelectedElementsStyleProperty( + (input)="elementService.updateSelectedElementsStyleProperty( 'lineColoringColor', $any($event.target).value)"> <button mat-icon-button matSuffix (click)="lineColorInput.click()"> <mat-icon>edit</mat-icon> @@ -39,11 +40,11 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; </mat-form-field> <input matInput type="color" hidden #lineColorInput [value]="styles.lineColoringColor" - (input)="unitService.updateSelectedElementsStyleProperty('lineColoringColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('lineColoringColor', $any($event.target).value)"> <mat-checkbox *ngIf="styles.firstLineColoring !== undefined" [checked]="$any(styles.firstLineColoring)" - (change)="unitService.updateSelectedElementsStyleProperty('firstLineColoring', $event.checked)"> + (change)="elementService.updateSelectedElementsStyleProperty('firstLineColoring', $event.checked)"> {{'propertiesPanel.firstLineColoring' | translate }} </mat-checkbox> @@ -52,7 +53,7 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; <mat-label>{{'propertiesPanel.firstLineColoringColor' | translate }}</mat-label> <input matInput type="text" [value]="styles.firstLineColoringColor" [disabled]="!styles.firstLineColoring || styles.firstLineColoringColor === undefined" - (input)="unitService.updateSelectedElementsStyleProperty( + (input)="elementService.updateSelectedElementsStyleProperty( 'firstLineColoringColor', $any($event.target).value)"> <button mat-icon-button matSuffix (click)="firstLineColorInput.click()"> <mat-icon>edit</mat-icon> @@ -60,72 +61,72 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; </mat-form-field> <input matInput type="color" hidden #firstLineColorInput [value]="styles.firstLineColoringColor" - (input)="unitService.updateSelectedElementsStyleProperty( + (input)="elementService.updateSelectedElementsStyleProperty( 'firstLineColoringColor', $any($event.target).value)"> <mat-form-field *ngIf="styles.selectionColor !== undefined" appearance="fill"> <mat-label>{{'propertiesPanel.selectionColor' | translate }}</mat-label> <input matInput type="text" [value]="styles.selectionColor" - (input)="unitService.updateSelectedElementsStyleProperty('selectionColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('selectionColor', $any($event.target).value)"> <button mat-icon-button matSuffix (click)="selectionColorInput.click()"> <mat-icon>edit</mat-icon> </button> </mat-form-field> <input matInput type="color" hidden #selectionColorInput [value]="styles.selectionColor" - (input)="unitService.updateSelectedElementsStyleProperty('selectionColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('selectionColor', $any($event.target).value)"> <mat-form-field *ngIf="styles.itemBackgroundColor !== undefined" appearance="fill" class="mdInput textsingleline"> <mat-label>{{'propertiesPanel.itemBackgroundColor' | translate }}</mat-label> <input matInput type="text" [value]="styles.itemBackgroundColor" - (input)="unitService.updateSelectedElementsStyleProperty('itemBackgroundColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('itemBackgroundColor', $any($event.target).value)"> <button mat-icon-button matSuffix (click)="itembackgroundColorInput.click()"> <mat-icon>edit</mat-icon> </button> </mat-form-field> <input matInput type="color" hidden #itembackgroundColorInput [value]="styles.itemBackgroundColor" - (input)="unitService.updateSelectedElementsStyleProperty('itemBackgroundColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('itemBackgroundColor', $any($event.target).value)"> <mat-form-field *ngIf="styles.backgroundColor !== undefined" appearance="fill" class="mdInput textsingleline"> <mat-label>{{'propertiesPanel.backgroundColor' | translate }}</mat-label> <input matInput type="text" [value]="styles.backgroundColor" - (input)="unitService.updateSelectedElementsStyleProperty('backgroundColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('backgroundColor', $any($event.target).value)"> <button mat-icon-button matSuffix (click)="backgroundColorInput.click()"> <mat-icon>edit</mat-icon> </button> </mat-form-field> <input matInput type="color" hidden #backgroundColorInput [value]="styles.backgroundColor" - (input)="unitService.updateSelectedElementsStyleProperty('backgroundColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('backgroundColor', $any($event.target).value)"> <mat-form-field *ngIf="styles.fontColor !== undefined" appearance="fill" class="mdInput textsingleline"> <mat-label>{{'propertiesPanel.fontColor' | translate }}</mat-label> <input matInput type="text" [value]="styles.fontColor" - (input)="unitService.updateSelectedElementsStyleProperty('fontColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('fontColor', $any($event.target).value)"> <button mat-icon-button matSuffix (click)="fontColorInput.click()"> <mat-icon>edit</mat-icon> </button> </mat-form-field> <input matInput type="color" hidden #fontColorInput [value]="styles.fontColor" - (input)="unitService.updateSelectedElementsStyleProperty('fontColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('fontColor', $any($event.target).value)"> <!-- <mat-form-field *ngIf="styles.font !== undefined"--> <!-- appearance="fill" class="mdInput textsingleline">--> <!-- <mat-label>{{'propertiesPanel.font' | translate }}</mat-label>--> <!-- <input matInput type="text" [value]="styles.font" disabled--> -<!-- (input)="unitService.updateSelectedElementsStyleProperty('font', $any($event.target).value)">--> +<!-- (input)="elementService.updateSelectedElementsStyleProperty('font', $any($event.target).value)">--> <!-- </mat-form-field>--> <mat-form-field *ngIf="styles.fontSize !== undefined" appearance="fill" class="mdInput textsingleline"> <mat-label>{{'propertiesPanel.fontSize' | translate }}</mat-label> <input matInput type="number" #fontSize="ngModel" min="0" [ngModel]="styles.fontSize" - (ngModelChange)="unitService.updateSelectedElementsStyleProperty('fontSize', $event)" + (ngModelChange)="elementService.updateSelectedElementsStyleProperty('fontSize', $event)" (change)="styles.fontSize = styles.fontSize ? styles.fontSize : 0"> </mat-form-field> <mat-form-field *ngIf="styles.lineHeight !== undefined" @@ -133,23 +134,23 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; <mat-label>{{'propertiesPanel.lineHeight' | translate }}</mat-label> <input matInput type="number" #lineHeight="ngModel" min="0" [ngModel]="styles.lineHeight" - (ngModelChange)="unitService.updateSelectedElementsStyleProperty('lineHeight', $event)" + (ngModelChange)="elementService.updateSelectedElementsStyleProperty('lineHeight', $event)" (change)="styles.lineHeight = styles.lineHeight ? styles.lineHeight : 0"> </mat-form-field> <mat-checkbox *ngIf="styles.bold !== undefined" [checked]="$any(styles.bold)" - (change)="unitService.updateSelectedElementsStyleProperty('bold', $event.checked)"> + (change)="elementService.updateSelectedElementsStyleProperty('bold', $event.checked)"> {{'propertiesPanel.bold' | translate }} </mat-checkbox> <mat-checkbox *ngIf="styles.italic !== undefined" [checked]="$any(styles.italic)" - (change)="unitService.updateSelectedElementsStyleProperty('italic', $event.checked)"> + (change)="elementService.updateSelectedElementsStyleProperty('italic', $event.checked)"> {{'propertiesPanel.italic' | translate }} </mat-checkbox> <mat-checkbox *ngIf="styles.underline !== undefined" [checked]="$any(styles.underline)" - (change)="unitService.updateSelectedElementsStyleProperty('underline', $event.checked)"> + (change)="elementService.updateSelectedElementsStyleProperty('underline', $event.checked)"> {{'propertiesPanel.underline' | translate }} </mat-checkbox> @@ -161,7 +162,7 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; <mat-form-field *ngIf="styles.borderRadius !== undefined" appearance="fill"> <mat-label>{{'propertiesPanel.borderRadius' | translate }}</mat-label> <input matInput type="number" [ngModel]="styles.borderRadius" - (ngModelChange)="unitService.updateSelectedElementsStyleProperty('borderRadius', $event)" + (ngModelChange)="elementService.updateSelectedElementsStyleProperty('borderRadius', $event)" (change)="styles.borderRadius = styles.borderRadius ? styles.borderRadius : 0"> </mat-form-field> @@ -169,20 +170,20 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; appearance="fill" class="mdInput textsingleline"> <mat-label>{{'propertiesPanel.borderColor' | translate }}</mat-label> <input matInput type="text" [value]="styles.borderColor" - (input)="unitService.updateSelectedElementsStyleProperty('borderColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('borderColor', $any($event.target).value)"> <button mat-icon-button matSuffix (click)="borderColorInput.click()"> <mat-icon>edit</mat-icon> </button> </mat-form-field> <input matInput type="color" hidden #borderColorInput [value]="styles.borderColor" - (input)="unitService.updateSelectedElementsStyleProperty('borderColor', $any($event.target).value)"> + (input)="elementService.updateSelectedElementsStyleProperty('borderColor', $any($event.target).value)"> <mat-form-field *ngIf="styles.borderStyle !== undefined" appearance="fill"> <mat-label>{{'propertiesPanel.borderStyle' | translate }}</mat-label> <mat-select [value]="styles.borderStyle" - (selectionChange)="unitService.updateSelectedElementsStyleProperty('borderStyle', $event.value)"> + (selectionChange)="elementService.updateSelectedElementsStyleProperty('borderStyle', $event.value)"> <mat-option *ngFor="let option of ['solid', 'dotted', 'dashed', 'double', 'groove', 'ridge', 'inset', 'outset']" [value]="option"> @@ -195,7 +196,7 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; <mat-label>{{'propertiesPanel.borderWidth' | translate }}</mat-label> <input matInput type="number" #borderWidth="ngModel" [ngModel]="styles.borderWidth" - (ngModelChange)="unitService.updateSelectedElementsStyleProperty('borderWidth', $event)" + (ngModelChange)="elementService.updateSelectedElementsStyleProperty('borderWidth', $event)" (change)="styles.borderRadius = styles.borderRadius ? styles.borderRadius : 0"> </mat-form-field> </fieldset> @@ -204,5 +205,5 @@ import { Stylings } from 'common/models/elements/property-group-interfaces'; export class ElementStylePropertiesComponent { @Input() styles!: Stylings | undefined; - constructor(public unitService: UnitService) { } + constructor(public elementService: ElementService) { } } diff --git a/projects/editor/src/app/components/toolbar/toolbar.component.ts b/projects/editor/src/app/components/toolbar/toolbar.component.ts index 0aba1eaa2595a31a0a35aadd60a89bc0d4a30a6f..b08fab15059b5fe39de3d3d9963c22801dbd9cc8 100644 --- a/projects/editor/src/app/components/toolbar/toolbar.component.ts +++ b/projects/editor/src/app/components/toolbar/toolbar.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { UnitService } from '../../services/unit.service'; +import { UnitService } from '../../services/unit-services/unit.service'; @Component({ selector: 'aspect-toolbar', diff --git a/projects/editor/src/app/components/unit-view/page-menu.component.html b/projects/editor/src/app/components/unit-view/page-menu.component.html new file mode 100644 index 0000000000000000000000000000000000000000..0e87e011d4af2abb2cba7f77e7fb2d272580236a --- /dev/null +++ b/projects/editor/src/app/components/unit-view/page-menu.component.html @@ -0,0 +1,88 @@ +<div [style]="'display: flex;'"> + <button [disabled]="page.alwaysVisible || + pageIndex == 0 || + (pageIndex == 1 && unitService.unit.pages[0].alwaysVisible)" + [style]="'justify-content: center'" + [matTooltip]="'Seite nach vorn verschieben'" + mat-menu-item (click)="movePage('left')"> + <mat-icon>west</mat-icon> + </button> + <button [disabled]="page.alwaysVisible || + pageIndex == unitService.unit.pages.length - 1" + [style]="'justify-content: center;'" + [matTooltip]="'Seite nach hinten verschieben'" + mat-menu-item (click)="movePage('right')"> + <mat-icon>east</mat-icon> + </button> +</div> + +<button mat-menu-item class="delete-button" + [matTooltip]="'Seite löschen'" + (click)="deletePage()"> + <mat-icon>delete</mat-icon> +</button> + +<mat-divider></mat-divider> + +<fieldset class="fx-column-start-stretch"> + <legend>Seitenbreite</legend> + <mat-checkbox class="menuItem" + [matTooltip]="'Abgewählt wird die verfügbare Bildschirmbreite voll ausgenutzt.'" + [checked]="page.hasMaxWidth" + (click)="$event.stopPropagation()" + (change)="updateModel(page, 'hasMaxWidth', $event.source.checked)"> + Seitenbreite begrenzen + </mat-checkbox> + <p class="menuItem" [style.margin-top.px]="5" [style.margin-left.px]="10"> + effektive Seitenbreite: <br>{{page.hasMaxWidth ? page.maxWidth + 2 * page.margin + 'px' : '∞'}} + </p> + <mat-form-field class="menuItem" appearance="fill"> + <mat-label>Seitenbreite in px</mat-label> + <input matInput type="number" min="0" #maxWidth="ngModel" + [disabled]="!page.hasMaxWidth" + [ngModel]="page.hasMaxWidth ? page.maxWidth : null" + (click)="$event.stopPropagation()" + (ngModelChange)="updateModel(page,'maxWidth', $event || 0, maxWidth.valid)"> + </mat-form-field> + <mat-form-field class="menuItem" appearance="fill"> + <mat-label>Randbreite in px</mat-label> + <input matInput type="number" min="0" #margin="ngModel" + [ngModel]="page.margin" + (click)="$event.stopPropagation()" + (ngModelChange)="updateModel(page,'margin', $event || 0, margin.valid)"> + </mat-form-field> +</fieldset> + +<mat-form-field class="menuItem" appearance="fill" [style.margin-top.px]="16"> + <mat-label>{{'pageProperties.backgroundColor' | translate }}</mat-label> + <input matInput type="color" #backgroundColor="ngModel" + [ngModel]="page.backgroundColor" + (ngModelChange)="updateModel(page,'backgroundColor', $event, backgroundColor.valid)"> +</mat-form-field> +<mat-checkbox class="menuItem" + [disabled]="unitService.unit.pages.length < 2 || unitService.unit.pages[0].alwaysVisible && pageIndex != 0" + [ngModel]="page.alwaysVisible" + (click)="$event.stopPropagation()" + (change)="updateModel(page, 'alwaysVisible', $event.source.checked)"> + Seite dauerhaft sichtbar +</mat-checkbox> +<mat-form-field class="menuItem" appearance="fill"> + <mat-label>{{'pageProperties.position' | translate }}</mat-label> + <mat-select [disabled]="!page.alwaysVisible" + [value]="page.alwaysVisiblePagePosition" + (click)="$event.stopPropagation()" + (selectionChange)="updateModel(page, 'alwaysVisiblePagePosition', $event.value)"> + <mat-option *ngFor="let option of ['left', 'right', 'top', 'bottom']" + [value]="option"> + {{option | translate}} + </mat-option> + </mat-select> +</mat-form-field> +<mat-form-field class="menuItem" appearance="fill"> + <mat-label>{{'pageProperties.alwaysVisibleAspectRatio' | translate }}</mat-label> + <input matInput type="number" min="0" max="100" + [disabled]="!page.alwaysVisible" + [ngModel]="page.alwaysVisibleAspectRatio" + (click)="$event.stopPropagation()" + (ngModelChange)="updateModel(page, 'alwaysVisibleAspectRatio', $event || 0)"> +</mat-form-field> diff --git a/projects/editor/src/app/components/unit-view/page-menu.component.ts b/projects/editor/src/app/components/unit-view/page-menu.component.ts index 1b17001e8d156333832bbe22d6996a5642e0ebd1..6604a1c637f0632efe7065a48a27668c3ee5b446 100644 --- a/projects/editor/src/app/components/unit-view/page-menu.component.ts +++ b/projects/editor/src/app/components/unit-view/page-menu.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDividerModule } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -14,11 +14,12 @@ import { Page } from 'common/models/page'; import { takeUntil } from 'rxjs/operators'; import { ReferenceManager } from 'editor/src/app/services/reference-manager'; import { SelectionService } from 'editor/src/app/services/selection.service'; -import { UnitService } from 'editor/src/app/services/unit.service'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; import { DialogService } from 'editor/src/app/services/dialog.service'; import { MessageService } from 'common/services/message.service'; import { Subject } from 'rxjs'; import { MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltipDefaultOptions, MatTooltipModule } from '@angular/material/tooltip'; +import { PageService } from 'editor/src/app/services/unit-services/page.service'; /** Custom options the configure the tooltip's default show/hide delays. */ export const myCustomTooltipDefaults: MatTooltipDefaultOptions = { @@ -48,93 +49,7 @@ export const myCustomTooltipDefaults: MatTooltipDefaultOptions = { MatTooltipModule ], providers: [{provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: myCustomTooltipDefaults}], - template: ` - <div [style]="'display: flex;'"> - <button [disabled]="page.alwaysVisible" - [style]="'justify-content: center'" - [matTooltip]="'Seite nach vorn verschieben'" - mat-menu-item (click)="movePage(page,'left')"> - <mat-icon>west</mat-icon> - </button> - <button [disabled]="page.alwaysVisible" - [style]="'justify-content: center;'" - [matTooltip]="'Seite nach hinten verschieben'" - mat-menu-item (click)="movePage(page, 'right')"> - <mat-icon>east</mat-icon> - </button> - </div> - - <button mat-menu-item class="delete-button" - [matTooltip]="'Seite löschen'" - (click)="deletePage()"> - <mat-icon>delete</mat-icon> - </button> - - <mat-divider></mat-divider> - - <fieldset class="fx-column-start-stretch"> - <legend>Seitenbreite</legend> - <mat-checkbox class="menuItem" - [matTooltip]="'Abgewählt wird die verfügbare Bildschirmbreite voll ausgenutzt.'" - [checked]="page.hasMaxWidth" - (click)="$event.stopPropagation()" - (change)="updateModel(page, 'hasMaxWidth', $event.source.checked)"> - Seitenbreite begrenzen - </mat-checkbox> - <p class="menuItem" [style.margin-top.px]="5" [style.margin-left.px]="10"> - effektive Seitenbreite: <br>{{page.hasMaxWidth ? page.maxWidth + 2 * page.margin + 'px' : '∞'}} - </p> - <mat-form-field class="menuItem" appearance="fill"> - <mat-label>Seitenbreite in px</mat-label> - <input matInput type="number" min="0" #maxWidth="ngModel" - [disabled]="!page.hasMaxWidth" - [ngModel]="page.hasMaxWidth ? page.maxWidth : null" - (click)="$event.stopPropagation()" - (ngModelChange)="updateModel(page,'maxWidth', $event || 0, maxWidth.valid)"> - </mat-form-field> - <mat-form-field class="menuItem" appearance="fill"> - <mat-label>Randbreite in px</mat-label> - <input matInput type="number" min="0" #margin="ngModel" - [ngModel]="page.margin" - (click)="$event.stopPropagation()" - (ngModelChange)="updateModel(page,'margin', $event || 0, margin.valid)"> - </mat-form-field> - </fieldset> - - <mat-form-field class="menuItem" appearance="fill" [style.margin-top.px]="16"> - <mat-label>{{'pageProperties.backgroundColor' | translate }}</mat-label> - <input matInput type="color" #backgroundColor="ngModel" - [ngModel]="page.backgroundColor" - (ngModelChange)="updateModel(page,'backgroundColor', $event, backgroundColor.valid)"> - </mat-form-field> - <mat-checkbox class="menuItem" - [disabled]="unitService.unit.pages.length < 2 || unitService.unit.pages[0].alwaysVisible && pageIndex != 0" - [ngModel]="page.alwaysVisible" - (click)="$event.stopPropagation()" - (change)="updateModel(page, 'alwaysVisible', $event.source.checked)"> - Seite dauerhaft sichtbar - </mat-checkbox> - <mat-form-field class="menuItem" appearance="fill"> - <mat-label>{{'pageProperties.position' | translate }}</mat-label> - <mat-select [disabled]="!page.alwaysVisible" - [value]="page.alwaysVisiblePagePosition" - (click)="$event.stopPropagation()" - (selectionChange)="updateModel(page, 'alwaysVisiblePagePosition', $event.value)"> - <mat-option *ngFor="let option of ['left', 'right', 'top', 'bottom']" - [value]="option"> - {{option | translate}} - </mat-option> - </mat-select> - </mat-form-field> - <mat-form-field class="menuItem" appearance="fill"> - <mat-label>{{'pageProperties.alwaysVisibleAspectRatio' | translate }}</mat-label> - <input matInput type="number" min="0" max="100" - [disabled]="!page.alwaysVisible" - [ngModel]="page.alwaysVisibleAspectRatio" - (click)="$event.stopPropagation()" - (ngModelChange)="updateModel(page, 'alwaysVisibleAspectRatio', $event || 0)"> - </mat-form-field> - `, + templateUrl: 'page-menu.component.html', styles: ` :host { display: flex; @@ -161,16 +76,18 @@ export const myCustomTooltipDefaults: MatTooltipDefaultOptions = { export class PageMenu implements OnDestroy { @Input() page!: Page; @Input() pageIndex!: number; + @Output() pageOrderChanged = new EventEmitter<void>(); private ngUnsubscribe = new Subject<void>(); constructor(public unitService: UnitService, + public pageService: PageService, public selectionService: SelectionService, private dialogService: DialogService, private messageService: MessageService) {} - movePage(page: Page, direction: 'left' | 'right'): void { - this.unitService.moveSelectedPage(direction); - this.refreshTabs(); + movePage(direction: 'left' | 'right'): void { + this.pageService.moveSelectedPage(direction); + this.pageOrderChanged.emit(); } deletePage(): void { @@ -189,7 +106,7 @@ export class PageMenu implements OnDestroy { .subscribe((result: boolean) => { if (result) { ReferenceManager.deleteReferences(refs); - this.unitService.deletePage(this.selectionService.selectedPageIndex); + this.pageService.deletePage(this.selectionService.selectedPageIndex); this.selectionService.selectPreviousPage(); } else { this.messageService.showReferencePanel(refs); @@ -200,7 +117,7 @@ export class PageMenu implements OnDestroy { .pipe(takeUntil(this.ngUnsubscribe)) .subscribe((result: boolean) => { if (result) { - this.unitService.deletePage(this.selectionService.selectedPageIndex); + this.pageService.deletePage(this.selectionService.selectedPageIndex); this.selectionService.selectPreviousPage(); } }); @@ -213,7 +130,7 @@ export class PageMenu implements OnDestroy { this.movePageToFront(page); page.alwaysVisible = true; this.selectionService.selectedPageIndex = 0; - this.refreshTabs(); + this.pageOrderChanged.emit(); } page[property] = value; this.unitService.updateUnitDefinition(); // TODO @@ -230,15 +147,6 @@ export class PageMenu implements OnDestroy { } } - /* This is a hack. The tab element gets bugged when changing the underlying array. - With this we can temporarily remove it from the DOM and then add it again, re-initializing it. */ - private refreshTabs(): void { // TODO seems unnecessary (?); moving pages works fine - // this.pagesLoaded = false; - // setTimeout(() => { - // this.pagesLoaded = true; - // }); - } - ngOnDestroy(): void { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); diff --git a/projects/editor/src/app/components/unit-view/unit-view.component.html b/projects/editor/src/app/components/unit-view/unit-view.component.html index 9adaaff538c26131197086a50d65933ac17de4b2..06ea1ffaf18d25c58c0c92e09aa80f8232984477 100644 --- a/projects/editor/src/app/components/unit-view/unit-view.component.html +++ b/projects/editor/src/app/components/unit-view/unit-view.component.html @@ -40,7 +40,8 @@ </button> <mat-menu #pageMenu="matMenu"> <div (click)="$event.stopPropagation()"> - <aspect-unit-view-page-menu [page]="page" [pageIndex]="i"></aspect-unit-view-page-menu> + <aspect-unit-view-page-menu [page]="page" [pageIndex]="i" + (pageOrderChanged)="refreshTabs()"></aspect-unit-view-page-menu> </div> </mat-menu> </ng-template> diff --git a/projects/editor/src/app/components/unit-view/unit-view.component.ts b/projects/editor/src/app/components/unit-view/unit-view.component.ts index 9e33783beacef2146cf047d854036fae0b7a8504..71ee86a6431a9825656de7ad1d1c02de85bdf9bc 100644 --- a/projects/editor/src/app/components/unit-view/unit-view.component.ts +++ b/projects/editor/src/app/components/unit-view/unit-view.component.ts @@ -1,7 +1,9 @@ import { Component } from '@angular/core'; import { PageChangeService } from 'common/services/page-change.service'; -import { UnitService } from '../../services/unit.service'; +import { UnitService } from '../../services/unit-services/unit.service'; import { SelectionService } from '../../services/selection.service'; +import { HistoryService } from 'editor/src/app/services/history.service'; +import { PageService } from 'editor/src/app/services/unit-services/page.service'; @Component({ selector: 'aspect-unit-view', @@ -13,15 +15,24 @@ export class UnitViewComponent { constructor(public selectionService: SelectionService, public unitService: UnitService, + public pageService: PageService, public pageChangeService: PageChangeService, - private dialogService: DialogService, - private messageService: MessageService) { } + public historyService: HistoryService) { } selectPage(newIndex: number): void { this.selectionService.selectPage(newIndex); } addPage(): void { - this.unitService.addPage(); + this.pageService.addPage(); + } + + /* This is a hack. The tab element gets bugged when changing the underlying array. + With this we can temporarily remove it from the DOM and then add it again, re-initializing it. */ + refreshTabs(): void { + this.pagesLoaded = false; + setTimeout(() => { + this.pagesLoaded = true; + }); } } diff --git a/projects/editor/src/app/services/unit-services/element.service.ts b/projects/editor/src/app/services/unit-services/element.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7e25ef9ced89a75bcba0223c2d0a6643bf61589 --- /dev/null +++ b/projects/editor/src/app/services/unit-services/element.service.ts @@ -0,0 +1,325 @@ +import { Injectable } from '@angular/core'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; +import { SelectionService } from 'editor/src/app/services/selection.service'; +import { IDService } from 'editor/src/app/services/id.service'; +import { + InputElement, PlayerElement, + PositionedUIElement, + UIElement, + UIElementProperties, + UIElementType, UIElementValue +} from 'common/models/elements/element'; +import { Section } from 'common/models/section'; +import { GeometryProperties } from 'common/models/elements/geometry/geometry'; +import { firstValueFrom } from 'rxjs'; +import { FileService } from 'common/services/file.service'; +import { AudioProperties } from 'common/models/elements/media-elements/audio'; +import { VideoProperties } from 'common/models/elements/media-elements/video'; +import { ImageProperties } from 'common/models/elements/media-elements/image'; +import { + PlayerProperties, + PositionProperties, + PropertyGroupGenerators +} from 'common/models/elements/property-group-interfaces'; +import { ElementFactory } from 'common/util/element.factory'; +import { ReferenceManager } from 'editor/src/app/services/reference-manager'; +import { DialogService } from 'editor/src/app/services/dialog.service'; +import { MessageService } from 'common/services/message.service'; +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'; + +@Injectable({ + providedIn: 'root' +}) +export class ElementService { + unit = this.unitService.unit; + + constructor(private unitService: UnitService, + private selectionService: SelectionService, + private dialogService: DialogService, + private messageService: MessageService, + private idService: IDService, + private sanitizer: DomSanitizer) { } + + addElementToSectionByIndex(elementType: UIElementType, + pageIndex: number, + sectionIndex: number): void { + this.addElementToSection(elementType, this.unit.pages[pageIndex].sections[sectionIndex]); + } + + async addElementToSection(elementType: UIElementType, section: Section, + coordinates?: { x: number, y: number }): Promise<void> { + const newElementProperties: Partial<UIElementProperties> = {}; + if (['geometry'].includes(elementType)) { + (newElementProperties as GeometryProperties).appDefinition = + await firstValueFrom(this.dialogService.showGeogebraAppDefinitionDialog()); + if (!(newElementProperties as GeometryProperties).appDefinition) return; // dialog canceled + } + if (['audio', 'video', 'image', 'hotspot-image'].includes(elementType)) { + let mediaSrc = ''; + switch (elementType) { + case 'hotspot-image': + case 'image': + mediaSrc = await FileService.loadImage(); + break; + case 'audio': + mediaSrc = await FileService.loadAudio(); + break; + case 'video': + mediaSrc = await FileService.loadVideo(); + break; + // no default + } + (newElementProperties as AudioProperties | VideoProperties | ImageProperties).src = mediaSrc; + } + + // Coordinates are given if an element is dragged directly into a cell + if (coordinates) { + newElementProperties.position = { + ...(section.dynamicPositioning && { gridColumn: coordinates.x }), + ...(section.dynamicPositioning && { gridRow: coordinates.y }), + ...(!section.dynamicPositioning && { yPosition: coordinates.y }), + ...(!section.dynamicPositioning && { yPosition: coordinates.y }) + } as PositionProperties; + } + + // Use z-index -1 for frames + newElementProperties.position = { + zIndex: elementType === 'frame' ? -1 : 0, + ...newElementProperties.position + } as PositionProperties; + + section.addElement(ElementFactory.createElement({ + type: elementType, + position: PropertyGroupGenerators.generatePositionProps(newElementProperties.position), + ...newElementProperties, + id: this.idService.getAndRegisterNewID(elementType) + }) as PositionedUIElement); + this.unitService.updateUnitDefinition(); + } + + deleteElements(elements: UIElement[]): void { + const refs = + this.unitService.referenceManager.getElementsReferences(elements); + // console.log('element refs', refs); + if (refs.length > 0) { + this.dialogService.showDeleteReferenceDialog(refs) + .subscribe((result: boolean) => { + if (result) { + ReferenceManager.deleteReferences(refs); + this.unitService.unregisterIDs(elements); + this.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => { + section.elements = section.elements.filter(element => !elements.includes(element)); + }); + this.unitService.updateUnitDefinition(); + } else { + this.messageService.showReferencePanel(refs); + } + }); + } else { + this.dialogService.showConfirmDialog('Element(e) löschen?') + .subscribe((result: boolean) => { + if (result) { + this.unitService.unregisterIDs(elements); + this.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => { + section.elements = section.elements.filter(element => !elements.includes(element)); + }); + this.unitService.updateUnitDefinition(); + } + }); + } + } + + updateElementsProperty(elements: UIElement[], property: string, value: unknown): void { + console.log('updateElementProperty', elements, property, value); + elements.forEach(element => { + if (property === 'id') { + if (this.idService.validateAndAddNewID(value as string, element.id)) { + element.setProperty('id', value); + } + } else if (element.type === 'text' && property === 'text') { + this.handleTextElementChange(element as TextElement, value as string); + } else if (property === 'document') { + this.handleClozeDocumentChange(element as ClozeElement, value as ClozeDocument); + } else { + 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); + } + }); + this.unitService.elementPropertyUpdated.next(); + this.unitService.updateUnitDefinition(); + } + + private handleTextElementChange(element: TextElement, value: string): void { + const deletedAnchorIDs = UnitService.getRemovedTextAnchorIDs(element, value); + const refs = this.unitService.referenceManager.getTextAnchorReferences(deletedAnchorIDs); + if (refs.length > 0) { + this.dialogService.showDeleteReferenceDialog(refs) + .subscribe((result: boolean) => { + if (result) { + ReferenceManager.deleteReferences(refs); + element.setProperty('text', value); + } else { + this.messageService.showReferencePanel(refs); + } + }); + } else { + element.setProperty('text', value); + } + } + + private handleClozeDocumentChange(element: ClozeElement, newValue: ClozeDocument): void { + const deletedElements = UnitService.getRemovedClozeElements(element, newValue); + const refs = this.unitService.referenceManager.getElementsReferences(deletedElements); + if (refs.length > 0) { + this.dialogService.showDeleteReferenceDialog(refs) + .subscribe((result: boolean) => { + if (result) { + ReferenceManager.deleteReferences(refs); + this.applyClozeDocumentChange(element, newValue); + } else { + this.messageService.showReferencePanel(refs); + } + }); + } else { + this.applyClozeDocumentChange(element, newValue); + } + } + + private applyClozeDocumentChange(element: ClozeElement, value: ClozeDocument): void { + element.setProperty('document', value); + ClozeElement.getDocumentChildElements(value as ClozeDocument).forEach(clozeChild => { + if (clozeChild.id === 'cloze-child-id-placeholder') { + clozeChild.id = this.idService.getAndRegisterNewID(clozeChild.type); + delete clozeChild.position; + } + }); + } + + alignElements(elements: PositionedUIElement[], alignmentDirection: 'left' | 'right' | 'top' | 'bottom'): void { + switch (alignmentDirection) { + case 'left': + this.updateElementsProperty( + elements, + 'xPosition', + Math.min(...elements.map(element => element.position.xPosition)) + ); + break; + case 'right': + this.updateElementsProperty( + elements, + 'xPosition', + Math.max(...elements.map(element => element.position.xPosition)) + ); + break; + case 'top': + this.updateElementsProperty( + elements, + 'yPosition', + Math.min(...elements.map(element => element.position.yPosition)) + ); + break; + case 'bottom': + this.updateElementsProperty( + elements, + 'yPosition', + Math.max(...elements.map(element => element.position.yPosition)) + ); + break; + // no default + } + this.unitService.elementPropertyUpdated.next(); + this.unitService.updateUnitDefinition(); + } + + showDefaultEditDialog(element: UIElement): void { + switch (element.type) { + case 'button': + case 'dropdown': + case 'checkbox': + case 'radio': + this.dialogService.showTextEditDialog(element.label as string).subscribe((result: string) => { + if (result) { + this.updateElementsProperty([element], 'label', result); + } + }); + break; + case 'text': + this.dialogService.showRichTextEditDialog( + (element as TextElement).text, + (element as TextElement).styling.fontSize + ).subscribe((result: string) => { + if (result) { + // TODO add proper sanitization + this.updateElementsProperty( + [element], + 'text', + (this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string + ); + } + }); + break; + case 'cloze': + this.dialogService.showClozeTextEditDialog( + (element as ClozeElement).document!, + (element as ClozeElement).styling.fontSize + ).subscribe((result: string) => { + if (result) { + // TODO add proper sanitization + this.updateElementsProperty( + [element], + 'document', + (this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string + ); + } + }); + break; + case 'text-field': + this.dialogService.showTextEditDialog((element as InputElement).value as string) + .subscribe((result: string) => { + if (result) { + this.updateElementsProperty([element], 'value', result); + } + }); + break; + case 'text-area': + this.dialogService.showMultilineTextEditDialog((element as InputElement).value as string) + .subscribe((result: string) => { + if (result) { + this.updateElementsProperty([element], 'value', result); + } + }); + break; + case 'audio': + case 'video': + this.dialogService.showPlayerEditDialog(element.id, (element as PlayerElement).player) + .subscribe((result: PlayerProperties) => { + if (!result) return; + Object.keys(result).forEach( + key => this.updateElementsPlayerProperty([element], key, result[key] as UIElementValue) + ); + }); + break; + // no default + } + } + + updateSelectedElementsStyleProperty(property: string, value: UIElementValue): void { + const elements = this.selectionService.getSelectedElements(); + elements.forEach(element => { + element.setStyleProperty(property, value); + }); + this.unitService.elementPropertyUpdated.next(); + this.unitService.updateUnitDefinition(); + } + + updateElementsPlayerProperty(elements: UIElement[], property: string, value: UIElementValue): void { + elements.forEach(element => { + element.setPlayerProperty(property, value); + }); + this.unitService.elementPropertyUpdated.next(); + this.unitService.updateUnitDefinition(); + } +} diff --git a/projects/editor/src/app/services/unit-services/page.service.ts b/projects/editor/src/app/services/unit-services/page.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..28a7b82b8817bfa259cad42ee84e1830f18ee66c --- /dev/null +++ b/projects/editor/src/app/services/unit-services/page.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { Page } from 'common/models/page'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; +import { MessageService } from 'common/services/message.service'; +import { SelectionService } from 'editor/src/app/services/selection.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PageService { + unit = this.unitService.unit; + + constructor(private unitService: UnitService, + private messageService: MessageService, + private selectionService: SelectionService) { } + + addPage(): void { + this.unitService.updateUnitDefinition({ + title: 'Seite hinzugefügt', + command: () => { + this.unit.pages.push(new Page()); + this.selectionService.selectedPageIndex = this.unit.pages.length - 1; // TODO selection stuff here is not good + return {}; + }, + rollback: () => { + this.unit.pages.splice(this.unit.pages.length - 1, 1); + this.selectionService.selectPreviousPage(); + } + }); + } + + deletePage(pageIndex: number): void { + this.unitService.updateUnitDefinition({ + title: 'Seite gelöscht', + command: () => { + const deletedpage = this.unitService.unit.pages.splice(pageIndex, 1)[0]; + return { + pageIndex, + deletedpage + }; + }, + rollback: (deletedData: Record<string, unknown>) => { + this.unit.pages.splice(deletedData['pageIndex'] as number, 0, deletedData['deletedpage'] as Page); + } + }); + } + + moveSelectedPage(direction: 'left' | 'right') { + if (this.unit.canPageBeMoved(this.selectionService.selectedPageIndex, direction)) { + this.unit.movePage(this.selectionService.selectedPageIndex, direction); + this.unitService.updateUnitDefinition(); + } else { + this.messageService.showWarning('Seite kann nicht verschoben werden.'); + } + } +} diff --git a/projects/editor/src/app/services/unit-services/section.service.ts b/projects/editor/src/app/services/unit-services/section.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b559cf6315e9fc06ba310ba103236a682bef7038 --- /dev/null +++ b/projects/editor/src/app/services/unit-services/section.service.ts @@ -0,0 +1,99 @@ +import { Injectable } from '@angular/core'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; +import { SelectionService } from 'editor/src/app/services/selection.service'; +import { Page } from 'common/models/page'; +import { Section } from 'common/models/section'; +import { PositionedUIElement, UIElement } from 'common/models/elements/element'; +import { DropListElement } from 'common/models/elements/input-elements/drop-list'; +import { ArrayUtils } from 'common/util/array'; +import { IDService } from 'editor/src/app/services/id.service'; +import { VisibilityRule } from 'common/models/visibility-rule'; + +@Injectable({ + providedIn: 'root' +}) +export class SectionService { + unit = this.unitService.unit; + + constructor(private unitService: UnitService, + private selectionService: SelectionService, + private idService: IDService) { } + + updateSectionProperty(section: Section, property: string, value: string | number | boolean | VisibilityRule[] | { value: number; unit: string }[]): void { + section.setProperty(property, value); + this.unitService.elementPropertyUpdated.next(); + this.unitService.updateUnitDefinition(); + } + + addSection(page: Page, section?: Section): void { + // register section IDs + if (section) { + section.elements.forEach(element => { + if (['drop-list', 'drop-list-simple'].includes((element as UIElement).type as string)) { + (element as DropListElement).value.forEach(value => this.idService.addID(value.id)); + } + if (['likert', 'cloze'].includes((element as UIElement).type as string)) { + element.getChildElements().forEach(el => { + this.idService.addID(el.id); + if ((element as UIElement).type === 'drop-list') { + (element as DropListElement).value.forEach(value => this.idService.addID(value.id)); + } + }); + } + this.idService.addID(element.id); + }); + } + page.sections.push( + section || new Section() + ); + this.unitService.updateUnitDefinition(); + } + + deleteSection(pageIndex: number, sectionIndex: number): void { + this.unitService.unregisterIDs(this.unit.pages[pageIndex].sections[sectionIndex].getAllElements()); + this.unit.pages[pageIndex].sections.splice(sectionIndex, 1); + this.unitService.updateUnitDefinition(); + } + + duplicateSection(section: Section, page: Page, sectionIndex: number): void { + const newSection: Section = new Section({ + ...section, + elements: section.elements.map(element => this.unitService.duplicateElement(element) as PositionedUIElement) + }); + page.sections.splice(sectionIndex + 1, 0, newSection); + this.unitService.updateUnitDefinition(); + } + + moveSection(section: Section, page: Page, direction: 'up' | 'down'): void { + ArrayUtils.moveArrayItem(section, page.sections, direction); + if (direction === 'up' && this.selectionService.selectedPageSectionIndex > 0) { + this.selectionService.selectedPageSectionIndex -= 1; + } else if (direction === 'down') { + this.selectionService.selectedPageSectionIndex += 1; + } + this.unitService.updateUnitDefinition(); + } + + replaceSection(pageIndex: number, sectionIndex: number, newSection: Section): void { + this.deleteSection(pageIndex, sectionIndex); + this.addSection(this.unit.pages[pageIndex], newSection); + } + + /* Move element between sections */ + transferElement(elements: UIElement[], previousSection: Section, newSection: Section): void { + previousSection.elements = previousSection.elements.filter(element => !elements.includes(element)); + elements.forEach(element => { + newSection.elements.push(element as PositionedUIElement); + }); + this.unitService.updateUnitDefinition(); + } + + duplicateSelectedElements(): void { + const selectedSection = + this.unit.pages[this.selectionService.selectedPageIndex].sections[this.selectionService.selectedPageSectionIndex]; + this.selectionService.getSelectedElements().forEach((element: UIElement) => { + selectedSection.elements.push(this.unitService.duplicateElement(element, true) as PositionedUIElement); + }); + this.unitService.updateUnitDefinition(); + } +} diff --git a/projects/editor/src/app/services/unit-services/unit.service.ts b/projects/editor/src/app/services/unit-services/unit.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b62fc2252b02a7c2e0d69c4ea90b80759782de8c --- /dev/null +++ b/projects/editor/src/app/services/unit-services/unit.service.ts @@ -0,0 +1,253 @@ +import { Injectable } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { firstValueFrom, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { FileService } from 'common/services/file.service'; +import { MessageService } from 'common/services/message.service'; +import { Unit, UnitProperties } from 'common/models/unit'; +import { + PlayerProperties, + PositionProperties, + PropertyGroupGenerators +} from 'common/models/elements/property-group-interfaces'; +import { DragNDropValueObject } from 'common/models/elements/label-interfaces'; +import { + CompoundElement, InputElement, PlayerElement, PositionedUIElement, + UIElement, UIElementProperties, UIElementType, UIElementValue +} from 'common/models/elements/element'; +import { ClozeDocument, ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; +import { TextElement } from 'common/models/elements/text/text'; +import { DropListElement } from 'common/models/elements/input-elements/drop-list'; +import { Section } from 'common/models/section'; +import { ElementFactory } from 'common/util/element.factory'; +import { GeometryProperties } from 'common/models/elements/geometry/geometry'; +import { AudioProperties } from 'common/models/elements/media-elements/audio'; +import { VideoProperties } from 'common/models/elements/media-elements/video'; +import { ImageProperties } from 'common/models/elements/media-elements/image'; +import { StateVariable } from 'common/models/state-variable'; +import { VisibilityRule } from 'common/models/visibility-rule'; +import { VersionManager } from 'common/services/version-manager'; +import { ReferenceManager } from 'editor/src/app/services/reference-manager'; +import { DialogService } from '../dialog.service'; +import { VeronaAPIService } from '../verona-api.service'; +import { SelectionService } from '../selection.service'; +import { IDService } from '../id.service'; +import { UnitDefinitionSanitizer } from '../sanitizer'; +import { HistoryService, UnitUpdateCommand } from 'editor/src/app/services/history.service'; + +@Injectable({ + providedIn: 'root' +}) +export class UnitService { + unit: Unit; + elementPropertyUpdated: Subject<void> = new Subject<void>(); + geometryElementPropertyUpdated: Subject<string> = new Subject<string>(); + mathTableElementPropertyUpdated: Subject<string> = new Subject<string>(); + referenceManager: ReferenceManager; + private ngUnsubscribe = new Subject<void>(); + + constructor(private selectionService: SelectionService, + private veronaApiService: VeronaAPIService, + private messageService: MessageService, + private dialogService: DialogService, + private historyService: HistoryService, + + private idService: IDService) { + this.unit = new Unit(); + this.referenceManager = new ReferenceManager(this.unit); + } + + loadUnitDefinition(unitDefinition: string): void { + if (unitDefinition) { + try { + let unitDef = JSON.parse(unitDefinition); + if (!VersionManager.hasCompatibleVersion(unitDef)) { + if (VersionManager.isNewer(unitDef)) { + throw Error('Unit-Version ist neuer als dieser Editor. Bitte mit der neuesten Version öffnen.'); + } + if (!VersionManager.needsSanitization(unitDef)) { + throw Error('Unit-Version ist veraltet. Sie kann mit Version 1.38/1.39 aktualisiert werden.'); + } + this.dialogService.showSanitizationDialog().subscribe(() => { + unitDef = UnitDefinitionSanitizer.sanitizeUnit(unitDef); + this.loadUnit(unitDef); + this.updateUnitDefinition(); + }); + } else { + this.loadUnit(unitDef); + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + if (e instanceof Error) this.dialogService.showUnitDefErrorDialog(e.message); + } + } else { + this.idService.reset(); + this.unit = new Unit(); + this.referenceManager = new ReferenceManager(this.unit); + } + } + + private loadUnit(parsedUnitDefinition?: string): void { + this.idService.reset(); + this.unit = new Unit(parsedUnitDefinition as unknown as UnitProperties); + this.idService.registerUnitIds(this.unit); + this.referenceManager = new ReferenceManager(this.unit); + + const invalidRefs = this.referenceManager.getAllInvalidRefs(); + if (invalidRefs.length > 0) { + this.referenceManager.removeInvalidRefs(invalidRefs); + this.messageService.showFixedReferencePanel(invalidRefs); + this.updateUnitDefinition(); + } + } + + updateUnitDefinition(command?: UnitUpdateCommand): void { + if (command) { + const deletedData = command.command(); + this.historyService.addCommand(command, deletedData); + } + this.veronaApiService.sendChanged(this.unit); + } + + rollback(): void { + this.historyService.rollback(); + this.veronaApiService.sendChanged(this.unit); + } + + freeUpIds(elements: UIElement[]): void { + elements.forEach(element => { + if (element.type === 'drop-list') { + ((element as DropListElement).value as DragNDropValueObject[]).forEach((value: DragNDropValueObject) => { + this.idService.removeId(value.id); + }); + } + if (element instanceof CompoundElement) { + element.getChildElements().forEach((childElement: UIElement) => { + this.idService.removeId(childElement.id); + }); + } + this.idService.removeId(element.id); + }); + } + + /* - Also changes position of the element to not cover copied element. + - Also changes and registers all copied IDs. */ + duplicateElement(element: UIElement, adjustPosition: boolean = false): UIElement { + const newElement = element.getDuplicate(); + + if (newElement.position && adjustPosition) { + newElement.position.xPosition += 10; + newElement.position.yPosition += 10; + newElement.position.gridRow = null; + newElement.position.gridColumn = null; + } + + newElement.id = this.idService.getAndRegisterNewID(newElement.type); + if (newElement instanceof CompoundElement) { + newElement.getChildElements().forEach((child: UIElement) => { + child.id = this.idService.getAndRegisterNewID(child.type); + if (child.type === 'drop-list') { + (child.value as DragNDropValueObject[]).forEach(valueObject => { + valueObject.id = this.idService.getAndRegisterNewID('value'); + }); + } + }); + } + + // Special care with DropLists as they are no CompoundElement yet still have children with IDs + if (newElement.type === 'drop-list') { + (newElement.value as DragNDropValueObject[]).forEach(valueObject => { + valueObject.id = this.idService.getAndRegisterNewID('value'); + }); + } + return newElement; + } + + static getRemovedTextAnchorIDs(element: TextElement, newValue: string): string[] { + return TextElement.getAnchorIDs(element.text) + .filter(el => !TextElement.getAnchorIDs(newValue).includes(el)); + } + + static getRemovedClozeElements(cloze: ClozeElement, newClozeDoc: ClozeDocument): UIElement[] { + const newElements = ClozeElement.getDocumentChildElements(newClozeDoc); + return cloze.getChildElements() + .filter(element => !newElements.includes(element)); + } + + updateSelectedElementsPositionProperty(property: string, value: UIElementValue): void { + this.updateElementsPositionProperty(this.selectionService.getSelectedElements(), property, value); + } + + updateElementsPositionProperty(elements: UIElement[], property: string, value: UIElementValue): void { + elements.forEach(element => { + element.setPositionProperty(property, value); + }); + this.reorderElements(); + this.elementPropertyUpdated.next(); + this.updateUnitDefinition(); + } + + updateElementsDimensionsProperty(elements: UIElement[], property: string, value: number | null): void { + console.log('updateElementsDimensionsProperty', property, value); + elements.forEach(element => { + element.setDimensionsProperty(property, value); + }); + this.elementPropertyUpdated.next(); + this.updateUnitDefinition(); + } + + /* Reorder elements by their position properties, so the tab order is correct */ + reorderElements() { + const sectionElementList = this.unit.pages[this.selectionService.selectedPageIndex] + .sections[this.selectionService.selectedPageSectionIndex].elements; + const isDynamicPositioning = this.unit.pages[this.selectionService.selectedPageIndex] + .sections[this.selectionService.selectedPageSectionIndex].dynamicPositioning; + const sortDynamicPositioning = (a: PositionedUIElement, b: PositionedUIElement) => { + const rowSort = + (a.position.gridRow !== null ? a.position.gridRow : Infinity) - + (b.position.gridRow !== null ? b.position.gridRow : Infinity); + if (rowSort === 0) { + return a.position.gridColumn! - b.position.gridColumn!; + } + return rowSort; + }; + const sortStaticPositioning = (a: PositionedUIElement, b: PositionedUIElement) => { + const ySort = a.position.yPosition! - b.position.yPosition!; + if (ySort === 0) { + return a.position.xPosition! - b.position.xPosition!; + } + return ySort; + }; + if (isDynamicPositioning) { + sectionElementList.sort(sortDynamicPositioning); + } else { + sectionElementList.sort(sortStaticPositioning); + } + } + + saveUnit(): void { + FileService.saveUnitToFile(JSON.stringify(this.unit)); + } + + async loadUnitFromFile(): Promise<void> { + this.loadUnitDefinition(await FileService.loadFile(['.json', '.voud'])); + } + + getNewValueID(): string { + return this.idService.getAndRegisterNewID('value'); + } + + /* Used by props panel to show available dropLists to connect */ + getAllDropListElementIDs(): string[] { + const allDropLists = [ + ...this.unit.getAllElements('drop-list'), + ...this.unit.getAllElements('drop-list-simple')]; + return allDropLists.map(dropList => dropList.id); + } + + updateStateVariables(stateVariables: StateVariable[]): void { + this.unit.stateVariables = stateVariables; + this.updateUnitDefinition(); + } +} diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts deleted file mode 100644 index f589e9b6ed962358305ae94308fedc8097b788c9..0000000000000000000000000000000000000000 --- a/projects/editor/src/app/services/unit.service.ts +++ /dev/null @@ -1,629 +0,0 @@ -import { Injectable } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { firstValueFrom, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; -import { TranslateService } from '@ngx-translate/core'; -import { FileService } from 'common/services/file.service'; -import { MessageService } from 'common/services/message.service'; -import { ArrayUtils } from 'common/util/array'; -import { Unit, UnitProperties } from 'common/models/unit'; -import { - PlayerProperties, - PositionProperties, - PropertyGroupGenerators -} from 'common/models/elements/property-group-interfaces'; -import { DragNDropValueObject } from 'common/models/elements/label-interfaces'; -import { - CompoundElement, InputElement, PlayerElement, PositionedUIElement, - UIElement, UIElementProperties, UIElementType, UIElementValue -} from 'common/models/elements/element'; -import { ClozeDocument, ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; -import { TextElement } from 'common/models/elements/text/text'; -import { DropListElement } from 'common/models/elements/input-elements/drop-list'; -import { Page } from 'common/models/page'; -import { Section } from 'common/models/section'; -import { ElementFactory } from 'common/util/element.factory'; -import { GeometryProperties } from 'common/models/elements/geometry/geometry'; -import { AudioProperties } from 'common/models/elements/media-elements/audio'; -import { VideoProperties } from 'common/models/elements/media-elements/video'; -import { ImageProperties } from 'common/models/elements/media-elements/image'; -import { StateVariable } from 'common/models/state-variable'; -import { VisibilityRule } from 'common/models/visibility-rule'; -import { VersionManager } from 'common/services/version-manager'; -import { ReferenceManager } from 'editor/src/app/services/reference-manager'; -import { DialogService } from './dialog.service'; -import { VeronaAPIService } from './verona-api.service'; -import { SelectionService } from './selection.service'; -import { IDService } from './id.service'; -import { UnitDefinitionSanitizer } from './sanitizer'; - -@Injectable({ - providedIn: 'root' -}) -export class UnitService { - unit: Unit; - elementPropertyUpdated: Subject<void> = new Subject<void>(); - geometryElementPropertyUpdated: Subject<string> = new Subject<string>(); - mathTableElementPropertyUpdated: Subject<string> = new Subject<string>(); - referenceManager: ReferenceManager; - private ngUnsubscribe = new Subject<void>(); - - constructor(private selectionService: SelectionService, - private veronaApiService: VeronaAPIService, - private messageService: MessageService, - private dialogService: DialogService, - private sanitizer: DomSanitizer, - private translateService: TranslateService, - private idService: IDService) { - this.unit = new Unit(); - this.referenceManager = new ReferenceManager(this.unit); - } - - loadUnitDefinition(unitDefinition: string): void { - if (unitDefinition) { - try { - let unitDef = JSON.parse(unitDefinition); - if (!VersionManager.hasCompatibleVersion(unitDef)) { - if (VersionManager.isNewer(unitDef)) { - throw Error('Unit-Version ist neuer als dieser Editor. Bitte mit der neuesten Version öffnen.'); - } - if (!VersionManager.needsSanitization(unitDef)) { - throw Error('Unit-Version ist veraltet. Sie kann mit Version 1.38/1.39 aktualisiert werden.'); - } - this.dialogService.showSanitizationDialog().subscribe(() => { - unitDef = UnitDefinitionSanitizer.sanitizeUnit(unitDef); - this.loadUnit(unitDef); - this.unitUpdated(); - }); - } else { - this.loadUnit(unitDef); - } - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - if (e instanceof Error) this.dialogService.showUnitDefErrorDialog(e.message); - } - } else { - this.idService.reset(); - this.unit = new Unit(); - this.referenceManager = new ReferenceManager(this.unit); - } - } - - private loadUnit(parsedUnitDefinition?: string): void { - this.idService.reset(); - this.unit = new Unit(parsedUnitDefinition as unknown as UnitProperties); - this.idService.registerUnitIds(this.unit); - this.referenceManager = new ReferenceManager(this.unit); - - const invalidRefs = this.referenceManager.getAllInvalidRefs(); - if (invalidRefs.length > 0) { - this.referenceManager.removeInvalidRefs(invalidRefs); - this.messageService.showFixedReferencePanel(invalidRefs); - this.unitUpdated(); - } - } - - unitUpdated(): void { - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - addPage(): void { - this.unit.pages.push(new Page()); - this.selectionService.selectedPageIndex = this.unit.pages.length - 1; - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - deletePage(pageIndex: number): void { - this.unit.pages.splice(pageIndex, 1); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - moveSelectedPage(direction: 'left' | 'right') { - if (this.unit.canPageBeMoved(this.selectionService.selectedPageIndex, direction)) { - this.unit.movePage(this.selectionService.selectedPageIndex, direction); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } else { - this.messageService.showWarning('Seite kann nicht verschoben werden.'); - } - } - - addSection(page: Page, section?: Section): void { - // register section IDs - if (section) { - section.elements.forEach(element => { - if (['drop-list', 'drop-list-simple'].includes((element as UIElement).type as string)) { - (element as DropListElement).value.forEach(value => this.idService.addID(value.id)); - } - if (['likert', 'cloze'].includes((element as UIElement).type as string)) { - element.getChildElements().forEach(el => { - this.idService.addID(el.id); - if ((element as UIElement).type === 'drop-list') { - (element as DropListElement).value.forEach(value => this.idService.addID(value.id)); - } - }); - } - this.idService.addID(element.id); - }); - } - page.sections.push( - section || new Section() - ); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - deleteSection(pageIndex: number, sectionIndex: number): void { - this.freeUpIds(this.unit.pages[pageIndex].sections[sectionIndex].getAllElements()); - this.unit.pages[pageIndex].sections.splice(sectionIndex, 1); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - duplicateSection(section: Section, page: Page, sectionIndex: number): void { - const newSection: Section = new Section({ - ...section, - elements: section.elements.map(element => this.duplicateElement(element) as PositionedUIElement) - }); - page.sections.splice(sectionIndex + 1, 0, newSection); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - moveSection(section: Section, page: Page, direction: 'up' | 'down'): void { - ArrayUtils.moveArrayItem(section, page.sections, direction); - if (direction === 'up' && this.selectionService.selectedPageSectionIndex > 0) { - this.selectionService.selectedPageSectionIndex -= 1; - } else if (direction === 'down') { - this.selectionService.selectedPageSectionIndex += 1; - } - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - addElementToSectionByIndex(elementType: UIElementType, - pageIndex: number, - sectionIndex: number): void { - this.addElementToSection(elementType, this.unit.pages[pageIndex].sections[sectionIndex]); - } - - async addElementToSection(elementType: UIElementType, section: Section, - coordinates?: { x: number, y: number }): Promise<void> { - const newElementProperties: Partial<UIElementProperties> = {}; - if (['geometry'].includes(elementType)) { - (newElementProperties as GeometryProperties).appDefinition = - await firstValueFrom(this.dialogService.showGeogebraAppDefinitionDialog()); - if (!(newElementProperties as GeometryProperties).appDefinition) return; // dialog canceled - } - if (['audio', 'video', 'image', 'hotspot-image'].includes(elementType)) { - let mediaSrc = ''; - switch (elementType) { - case 'hotspot-image': - case 'image': - mediaSrc = await FileService.loadImage(); - break; - case 'audio': - mediaSrc = await FileService.loadAudio(); - break; - case 'video': - mediaSrc = await FileService.loadVideo(); - break; - // no default - } - (newElementProperties as AudioProperties | VideoProperties | ImageProperties).src = mediaSrc; - } - - // Coordinates are given if an element is dragged directly into a cell - if (coordinates) { - newElementProperties.position = { - ...(section.dynamicPositioning && { gridColumn: coordinates.x }), - ...(section.dynamicPositioning && { gridRow: coordinates.y }), - ...(!section.dynamicPositioning && { yPosition: coordinates.y }), - ...(!section.dynamicPositioning && { yPosition: coordinates.y }) - } as PositionProperties; - } - - // Use z-index -1 for frames - newElementProperties.position = { - zIndex: elementType === 'frame' ? -1 : 0, - ...newElementProperties.position - } as PositionProperties; - - section.addElement(ElementFactory.createElement({ - type: elementType, - position: PropertyGroupGenerators.generatePositionProps(newElementProperties.position), - ...newElementProperties, - id: this.idService.getAndRegisterNewID(elementType) - }) as PositionedUIElement); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - deleteElements(elements: UIElement[]): void { - const refs = - this.referenceManager.getElementsReferences(elements); - // console.log('element refs', refs); - if (refs.length > 0) { - this.dialogService.showDeleteReferenceDialog(refs) - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe((result: boolean) => { - if (result) { - ReferenceManager.deleteReferences(refs); - this.freeUpIds(elements); - this.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => { - section.elements = section.elements.filter(element => !elements.includes(element)); - }); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } else { - this.messageService.showReferencePanel(refs); - } - }); - } else { - this.dialogService.showConfirmDialog('Element(e) löschen?') - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe((result: boolean) => { - if (result) { - this.freeUpIds(elements); - this.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => { - section.elements = section.elements.filter(element => !elements.includes(element)); - }); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - }); - } - } - - private freeUpIds(elements: UIElement[]): void { - elements.forEach(element => { - if (element.type === 'drop-list') { - ((element as DropListElement).value as DragNDropValueObject[]).forEach((value: DragNDropValueObject) => { - this.idService.removeId(value.id); - }); - } - if (element instanceof CompoundElement) { - element.getChildElements().forEach((childElement: UIElement) => { - this.idService.removeId(childElement.id); - }); - } - this.idService.removeId(element.id); - }); - } - - /* Move element between sections */ - transferElement(elements: UIElement[], previousSection: Section, newSection: Section): void { - previousSection.elements = previousSection.elements.filter(element => !elements.includes(element)); - elements.forEach(element => { - newSection.elements.push(element as PositionedUIElement); - }); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - duplicateSelectedElements(): void { - const selectedSection = - this.unit.pages[this.selectionService.selectedPageIndex].sections[this.selectionService.selectedPageSectionIndex]; - this.selectionService.getSelectedElements().forEach((element: UIElement) => { - selectedSection.elements.push(this.duplicateElement(element, true) as PositionedUIElement); - }); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - /* - Also changes position of the element to not cover copied element. - - Also changes and registers all copied IDs. */ - private duplicateElement(element: UIElement, adjustPosition: boolean = false): UIElement { - const newElement = element.getDuplicate(); - - if (newElement.position && adjustPosition) { - newElement.position.xPosition += 10; - newElement.position.yPosition += 10; - newElement.position.gridRow = null; - newElement.position.gridColumn = null; - } - - newElement.id = this.idService.getAndRegisterNewID(newElement.type); - if (newElement instanceof CompoundElement) { - newElement.getChildElements().forEach((child: UIElement) => { - child.id = this.idService.getAndRegisterNewID(child.type); - if (child.type === 'drop-list') { - (child.value as DragNDropValueObject[]).forEach(valueObject => { - valueObject.id = this.idService.getAndRegisterNewID('value'); - }); - } - }); - } - - // Special care with DropLists as they are no CompoundElement yet still have children with IDs - if (newElement.type === 'drop-list') { - (newElement.value as DragNDropValueObject[]).forEach(valueObject => { - valueObject.id = this.idService.getAndRegisterNewID('value'); - }); - } - return newElement; - } - - updateSectionProperty(section: Section, property: string, value: string | number | boolean | VisibilityRule[] | { value: number; unit: string }[]): void { - section.setProperty(property, value); - this.elementPropertyUpdated.next(); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - updateElementsProperty(elements: UIElement[], property: string, value: unknown): void { - console.log('updateElementProperty', elements, property, value); - elements.forEach(element => { - if (property === 'id') { - if (this.idService.validateAndAddNewID(value as string, element.id)) { - element.setProperty('id', value); - } - } else if (element.type === 'text' && property === 'text') { - this.handleTextElementChange(element as TextElement, value as string); - } else if (property === 'document') { - this.handleClozeDocumentChange(element as ClozeElement, value as ClozeDocument); - } else { - element.setProperty(property, value); - if (element.type === 'geometry' && property !== 'trackedVariables') this.geometryElementPropertyUpdated.next(element.id); - if (element.type === 'math-table') this.mathTableElementPropertyUpdated.next(element.id); - } - }); - this.elementPropertyUpdated.next(); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - handleTextElementChange(element: TextElement, value: string): void { - const deletedAnchorIDs = UnitService.getRemovedTextAnchorIDs(element, value); - const refs = this.referenceManager.getTextAnchorReferences(deletedAnchorIDs); - if (refs.length > 0) { - this.dialogService.showDeleteReferenceDialog(refs) - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe((result: boolean) => { - if (result) { - ReferenceManager.deleteReferences(refs); - element.setProperty('text', value); - } else { - this.messageService.showReferencePanel(refs); - } - }); - } else { - element.setProperty('text', value); - } - } - - static getRemovedTextAnchorIDs(element: TextElement, newValue: string): string[] { - return TextElement.getAnchorIDs(element.text) - .filter(el => !TextElement.getAnchorIDs(newValue).includes(el)); - } - - handleClozeDocumentChange(element: ClozeElement, newValue: ClozeDocument): void { - const deletedElements = UnitService.getRemovedClozeElements(element, newValue); - const refs = this.referenceManager.getElementsReferences(deletedElements); - if (refs.length > 0) { - this.dialogService.showDeleteReferenceDialog(refs) - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe((result: boolean) => { - if (result) { - ReferenceManager.deleteReferences(refs); - this.applyClozeDocumentChange(element, newValue); - } else { - this.messageService.showReferencePanel(refs); - } - }); - } else { - this.applyClozeDocumentChange(element, newValue); - } - } - - applyClozeDocumentChange(element: ClozeElement, value: ClozeDocument): void { - element.setProperty('document', value); - ClozeElement.getDocumentChildElements(value as ClozeDocument).forEach(clozeChild => { - if (clozeChild.id === 'cloze-child-id-placeholder') { - clozeChild.id = this.idService.getAndRegisterNewID(clozeChild.type); - delete clozeChild.position; - } - }); - } - - static getRemovedClozeElements(cloze: ClozeElement, newClozeDoc: ClozeDocument): UIElement[] { - const newElements = ClozeElement.getDocumentChildElements(newClozeDoc); - return cloze.getChildElements() - .filter(element => !newElements.includes(element)); - } - - updateSelectedElementsPositionProperty(property: string, value: UIElementValue): void { - this.updateElementsPositionProperty(this.selectionService.getSelectedElements(), property, value); - } - - updateElementsPositionProperty(elements: UIElement[], property: string, value: UIElementValue): void { - elements.forEach(element => { - element.setPositionProperty(property, value); - }); - this.reorderElements(); - this.elementPropertyUpdated.next(); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - updateElementsDimensionsProperty(elements: UIElement[], property: string, value: number | null): void { - console.log('updateElementsDimensionsProperty', property, value); - elements.forEach(element => { - element.setDimensionsProperty(property, value); - }); - this.elementPropertyUpdated.next(); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - /* Reorder elements by their position properties, so the tab order is correct */ - reorderElements() { - const sectionElementList = this.unit.pages[this.selectionService.selectedPageIndex] - .sections[this.selectionService.selectedPageSectionIndex].elements; - const isDynamicPositioning = this.unit.pages[this.selectionService.selectedPageIndex] - .sections[this.selectionService.selectedPageSectionIndex].dynamicPositioning; - const sortDynamicPositioning = (a: PositionedUIElement, b: PositionedUIElement) => { - const rowSort = - (a.position.gridRow !== null ? a.position.gridRow : Infinity) - - (b.position.gridRow !== null ? b.position.gridRow : Infinity); - if (rowSort === 0) { - return a.position.gridColumn! - b.position.gridColumn!; - } - return rowSort; - }; - const sortStaticPositioning = (a: PositionedUIElement, b: PositionedUIElement) => { - const ySort = a.position.yPosition! - b.position.yPosition!; - if (ySort === 0) { - return a.position.xPosition! - b.position.xPosition!; - } - return ySort; - }; - if (isDynamicPositioning) { - sectionElementList.sort(sortDynamicPositioning); - } else { - sectionElementList.sort(sortStaticPositioning); - } - } - - updateSelectedElementsStyleProperty(property: string, value: UIElementValue): void { - const elements = this.selectionService.getSelectedElements(); - elements.forEach(element => { - element.setStyleProperty(property, value); - }); - this.elementPropertyUpdated.next(); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - updateElementsPlayerProperty(elements: UIElement[], property: string, value: UIElementValue): void { - elements.forEach(element => { - element.setPlayerProperty(property, value); - }); - this.elementPropertyUpdated.next(); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - alignElements(elements: PositionedUIElement[], alignmentDirection: 'left' | 'right' | 'top' | 'bottom'): void { - switch (alignmentDirection) { - case 'left': - this.updateElementsProperty( - elements, - 'xPosition', - Math.min(...elements.map(element => element.position.xPosition)) - ); - break; - case 'right': - this.updateElementsProperty( - elements, - 'xPosition', - Math.max(...elements.map(element => element.position.xPosition)) - ); - break; - case 'top': - this.updateElementsProperty( - elements, - 'yPosition', - Math.min(...elements.map(element => element.position.yPosition)) - ); - break; - case 'bottom': - this.updateElementsProperty( - elements, - 'yPosition', - Math.max(...elements.map(element => element.position.yPosition)) - ); - break; - // no default - } - this.elementPropertyUpdated.next(); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - saveUnit(): void { - FileService.saveUnitToFile(JSON.stringify(this.unit)); - } - - async loadUnitFromFile(): Promise<void> { - this.loadUnitDefinition(await FileService.loadFile(['.json', '.voud'])); - } - - showDefaultEditDialog(element: UIElement): void { - switch (element.type) { - case 'button': - case 'dropdown': - case 'checkbox': - case 'radio': - this.dialogService.showTextEditDialog(element.label as string).subscribe((result: string) => { - if (result) { - this.updateElementsProperty([element], 'label', result); - } - }); - break; - case 'text': - this.dialogService.showRichTextEditDialog( - (element as TextElement).text, - (element as TextElement).styling.fontSize - ).subscribe((result: string) => { - if (result) { - // TODO add proper sanitization - this.updateElementsProperty( - [element], - 'text', - (this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string - ); - } - }); - break; - case 'cloze': - this.dialogService.showClozeTextEditDialog( - (element as ClozeElement).document!, - (element as ClozeElement).styling.fontSize - ).subscribe((result: string) => { - if (result) { - // TODO add proper sanitization - this.updateElementsProperty( - [element], - 'document', - (this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string - ); - } - }); - break; - case 'text-field': - this.dialogService.showTextEditDialog((element as InputElement).value as string) - .subscribe((result: string) => { - if (result) { - this.updateElementsProperty([element], 'value', result); - } - }); - break; - case 'text-area': - this.dialogService.showMultilineTextEditDialog((element as InputElement).value as string) - .subscribe((result: string) => { - if (result) { - this.updateElementsProperty([element], 'value', result); - } - }); - break; - case 'audio': - case 'video': - this.dialogService.showPlayerEditDialog(element.id, (element as PlayerElement).player) - .subscribe((result: PlayerProperties) => { - if (!result) return; - Object.keys(result).forEach( - key => this.updateElementsPlayerProperty([element], key, result[key] as UIElementValue) - ); - }); - break; - // no default - } - } - - getNewValueID(): string { - return this.idService.getAndRegisterNewID('value'); - } - - /* Used by props panel to show available dropLists to connect */ - getAllDropListElementIDs(): string[] { - const allDropLists = [ - ...this.unit.getAllElements('drop-list'), - ...this.unit.getAllElements('drop-list-simple')]; - return allDropLists.map(dropList => dropList.id); - } - - replaceSection(pageIndex: number, sectionIndex: number, newSection: Section): void { - this.deleteSection(pageIndex, sectionIndex); - this.addSection(this.unit.pages[pageIndex], newSection); - } - - updateStateVariables(stateVariables: StateVariable[]): void { - this.unit.stateVariables = stateVariables; - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } -}