diff --git a/projects/common/app.module.ts b/projects/common/app.module.ts index 98d352674f57f0574d927b3d80098d25a10687c8..d85cc9b34cb77f06f40b786a9df12aeb45cd2c83 100644 --- a/projects/common/app.module.ts +++ b/projects/common/app.module.ts @@ -18,6 +18,7 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatTabsModule } from '@angular/material/tabs'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { ButtonComponent } from './element-components/button.component'; import { CorrectionComponent } from './element-components/compound-components/correction.component'; @@ -78,7 +79,8 @@ import { DropdownComponent } from './element-components/dropdown.component'; CheckboxComponent, DropdownComponent, CorrectionComponent, - MatSnackBarModule + MatSnackBarModule, + MatTooltipModule ] }) export class SharedModule { } diff --git a/projects/common/message.service.ts b/projects/common/message.service.ts index 6ff6669b800c89c7399a541a13abb71b940a30fe..b2db689927ccf12d0efd856c3dccb80de767d2d1 100644 --- a/projects/common/message.service.ts +++ b/projects/common/message.service.ts @@ -10,4 +10,8 @@ export class MessageService { showWarning(text: string): void { this._snackBar.open(text, undefined, { duration: 2000, panelClass: 'snackbar-warning' }); } + + showError(text: string): void { + this._snackBar.open(text, undefined, { duration: 2000, panelClass: 'snackbar-error' }); + } } diff --git a/projects/common/unit.ts b/projects/common/unit.ts index 22be11d30fd0183b882874d74f5b213400315f26..3ac0dee6e37005cb60da6dc63a454ac001e386d0 100644 --- a/projects/common/unit.ts +++ b/projects/common/unit.ts @@ -22,6 +22,7 @@ export interface UnitUIElement { id: string; xPosition: number; yPosition: number; + zIndex: number width: number; height: number; backgroundColor: string; diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas-drag-overlay.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas-drag-overlay.component.ts index 8eba494699794bfd1c8a007b0cd1be5e58006386..ea81fc4d84731c90b59e5d43267ccae1e5b2ccdb 100644 --- a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas-drag-overlay.component.ts +++ b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas-drag-overlay.component.ts @@ -49,6 +49,7 @@ export class CanvasDragOverlayComponent implements OnInit { border: this._selected ? '2px solid' : '', width: `${this.element.width}px`, height: `${this.element.height}px`, + 'z-index': `${this.element.zIndex}`, left: `${this.element.xPosition.toString()}px`, top: `${this.element.yPosition.toString()}px` }; diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.toolbar.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.toolbar.component.ts index 2af3ddab4d85a33ad43deebed255989cfb05c138..416ad4b4ccbfdfb15fbc02ca61fb55c3398f7f8b 100644 --- a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.toolbar.component.ts +++ b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.toolbar.component.ts @@ -1,20 +1,20 @@ -import { Component, EventEmitter, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ selector: 'app-canvas-toolbar', template: ` <div class="canvas-toolbar"> Ausrichtung - <button (click)="align('left')"> + <button [disabled]="disabled" (click)="align('left')"> <mat-icon>align_horizontal_left</mat-icon> </button> - <button (click)="align('right')"> + <button [disabled]="disabled" (click)="align('right')"> <mat-icon>align_horizontal_right</mat-icon> </button> - <button (click)="align('top')"> + <button [disabled]="disabled" (click)="align('top')"> <mat-icon>align_vertical_top</mat-icon> </button> - <button (click)="align('bottom')"> + <button [disabled]="disabled" (click)="align('bottom')"> <mat-icon>align_vertical_bottom</mat-icon> </button> </div> @@ -26,6 +26,7 @@ import { Component, EventEmitter, Output } from '@angular/core'; ] }) export class CanvasToolbarComponent { + @Input() disabled: boolean = false; @Output() alignElements = new EventEmitter<'left' | 'right' | 'top' | 'bottom'>(); align(direction: 'left' | 'right' | 'top' | 'bottom'): void { diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/page-canvas.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/page-canvas.component.ts index 7fc8220d5ab0375556edc94c240ccd2758e1d2a7..ac25f80196d428a9befa261c7bf248bf458c125a 100644 --- a/projects/editor/src/app/components/unit-view/page-view/canvas/page-canvas.component.ts +++ b/projects/editor/src/app/components/unit-view/page-view/canvas/page-canvas.component.ts @@ -19,8 +19,7 @@ import { CanvasDragOverlayComponent } from './canvas-drag-overlay.component'; (sectionEditMode)="sectionEditMode = $event"> </app-canvas-section-toolbar> - <app-canvas-toolbar [hidden]="selectedComponentElements.length < 2" - (alignElements)="alignElements($event)"> + <app-canvas-toolbar [disabled]="selectedComponentElements.length < 2" (alignElements)="alignElements($event)"> </app-canvas-toolbar> <div class="canvasFrame" fxLayout="row" [style.padding.px]="page.margin" @@ -38,7 +37,8 @@ import { CanvasDragOverlayComponent } from './canvas-drag-overlay.component'; *ngFor="let section of page.sections; let i = index" [section]="section" [childrenDraggable]="!sectionEditMode" (elementSelected)="elementSelected($event)" (click)="selectSection(i)" - cdkDropList (cdkDropListDropped)="elementDropped($event)" [cdkDropListData]="section" + cdkDropList cdkDropListSortingDisabled + (cdkDropListDropped)="elementDropped($event)" [cdkDropListData]="section" [ngStyle]="{ border: i === selectedSectionIndex ? '1px solid': '1px dotted', 'width.px': page.width, @@ -48,7 +48,7 @@ import { CanvasDragOverlayComponent } from './canvas-drag-overlay.component'; </div> `, styles: [ - '.canvasFrame {background-color: lightgrey; height: 65vh; overflow: auto; width: 100%}', + '.canvasFrame {background-color: lightgrey; height: 69vh; overflow: auto; width: 100%}', '.section {position: relative;}' ] }) diff --git a/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.html b/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.html index eede1afd6a15ec3c3446c9636931c5adb0ce3d46..1aa3afb675dfa585dadc78ee888bdda4e1420d30 100644 --- a/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.html +++ b/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.html @@ -1,12 +1,17 @@ -<h3>EIGENSCHAFTEN</h3> <div *ngIf="selectedElements.length === 0">Select an element</div> <div *ngIf="selectedElements.length > 0"> - <mat-tab-group mat-stretch-tabs> + <mat-tab-group mat-stretch-tabs dynamicHeight> <mat-tab> <ng-template mat-tab-label> <mat-icon class="example-tab-icon">build</mat-icon> </ng-template> <div fxLayout="column"> + <mat-form-field *ngIf="combinedProperties.hasOwnProperty('id')"> + <mat-label>ID</mat-label> + <input matInput type="text" *ngIf="selectedElements.length === 1" [value]="combinedProperties.id" + (input)="updateModel('id', $any($event.target).value, $event)"> + <input matInput type="text" disabled *ngIf="selectedElements.length > 1" [value]="'Muss eindeutig sein'"> + </mat-form-field> <mat-form-field *ngIf="combinedProperties.hasOwnProperty('label')"> <mat-label>Label</mat-label> <input matInput type="text" [value]="combinedProperties.label" @@ -64,10 +69,10 @@ </div> </mat-form-field> </div> - <button mat-raised-button (click)="deleteElement()"> + <button class="delete-element-button" mat-raised-button (click)="deleteElement()"> Element löschen </button> - <button mat-raised-button> + <button mat-raised-button class="duplicate-element-button"> Element duplizieren </button> </mat-tab> @@ -97,6 +102,12 @@ <input matInput type="number" [value]="combinedProperties.yPosition" (input)="updateModel('yPosition', $any($event.target).value)"> </mat-form-field> + <mat-form-field *ngIf="combinedProperties.hasOwnProperty('zIndex')"> + <mat-label>Z-Index</mat-label> + <input matInput type="number" [value]="combinedProperties.zIndex" + (input)="updateModel('zIndex', $any($event.target).value)" + matTooltip="Priorität beim Stapeln von Elementen. Der höhere Index erscheint vorne."> + </mat-form-field> </div> </mat-tab> diff --git a/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.ts b/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.ts index 54203640f9eb9454732abb85cfedf00af4052425..4f4f21dd879fc5f10d2b4cd52e4df30574b97850 100644 --- a/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.ts +++ b/projects/editor/src/app/components/unit-view/page-view/properties/properties.component.ts @@ -7,10 +7,13 @@ import { UnitUIElement } from '../../../../../../../common/unit'; selector: 'app-properties', templateUrl: './properties.component.html', styles: [ - ':host {text-align: center; font-size: larger}', + ':host {text-align: center; font-size: larger; margin-left: 1px}', ':host ::ng-deep .mat-tab-label {min-width: 0}', '.newOptionElement {margin-top: 20px}', - '.newOptionElement button {background-color: #696969; margin: 5px}' + '.newOptionElement button {background-color: #696969; margin: 5px}', + 'mat-tab-group {padding: 10px;}', + '.delete-element-button {margin-bottom: 5px; border: 1px solid red;}', + '.duplicate-element-button {margin-bottom: 5px}' ] }) export class PropertiesComponent { @@ -25,15 +28,15 @@ export class PropertiesComponent { this.selectedElementsSubscription = this.unitService.selectedElements.subscribe( (selectedElements: UnitUIElement[]) => { this.selectedElements = selectedElements; - this.calculateCombinedProperties(); + this.createCombinedProperties(); } ); this.elementUpdatedSubscription = this.unitService.elementUpdated.subscribe(() => { - this.calculateCombinedProperties(); + this.createCombinedProperties(); }); } - calculateCombinedProperties(): void { + createCombinedProperties(): void { if (this.selectedElements.length === 0) { this.combinedProperties = {}; } else { @@ -51,8 +54,14 @@ export class PropertiesComponent { } } - updateModel(property: string, value: string | number | boolean): void { - this.unitService.updateSelectedElementProperty(property, value); + // event as optional parameter in case the input is invalid and the old value needs + // to be restored. This is for now only relevant for IDs. Might need rework for other properties. + updateModel(property: string, value: string | number | boolean, event?: Event): void { + if (!this.unitService.updateSelectedElementProperty(property, value)) { + if (event) { + (event.target as HTMLInputElement).value = <string> this.combinedProperties[property]; + } + } } deleteElement(): void { diff --git a/projects/editor/src/app/components/unit-view/page-view/ui-element-toolbox/ui-element-toolbox.component.html b/projects/editor/src/app/components/unit-view/page-view/ui-element-toolbox/ui-element-toolbox.component.html index cf5ec34c7be4361f96d48cb83969ce94f7d3a269..3f36753917f5009cf754424f262c69685a2443b0 100644 --- a/projects/editor/src/app/components/unit-view/page-view/ui-element-toolbox/ui-element-toolbox.component.html +++ b/projects/editor/src/app/components/unit-view/page-view/ui-element-toolbox/ui-element-toolbox.component.html @@ -1,4 +1,3 @@ -<h3>ELEMENTE</h3> <mat-tab-group mat-stretch-tabs> <mat-tab> <ng-template mat-tab-label> diff --git a/projects/editor/src/app/components/unit-view/page-view/ui-element-toolbox/ui-element-toolbox.component.ts b/projects/editor/src/app/components/unit-view/page-view/ui-element-toolbox/ui-element-toolbox.component.ts index 817dad5ab2d7f466bae8965a7be59c22393e8e89..aec5241eeff9b0fd9c21ddee2a516a3cbf489128 100644 --- a/projects/editor/src/app/components/unit-view/page-view/ui-element-toolbox/ui-element-toolbox.component.ts +++ b/projects/editor/src/app/components/unit-view/page-view/ui-element-toolbox/ui-element-toolbox.component.ts @@ -5,10 +5,11 @@ import { UnitService } from '../../../../unit.service'; selector: 'app-ui-element-toolbox', templateUrl: './ui-element-toolbox.component.html', styles: [ - ':host {background-color: #F9FBFB;}', + ':host {background-color: #F9FBFB; margin-left: 1px}', ':host {text-align: center; font-size: larger}', ':host button {text-align: left; font-size: large; margin: 5px}', - ':host ::ng-deep .mat-tab-label {min-width: 0}' + ':host ::ng-deep .mat-tab-label {min-width: 0}', + 'mat-tab-group {padding: 10px;}' ] }) export class UiElementToolboxComponent { 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 5fee583d8d47af1630ec25ecb7b7090eb76e40c1..493f6232b967efe5f16be8f8cebd7aa3aaa50080 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 @@ -1,19 +1,17 @@ -<mat-drawer-container> +<div fxLayout="row" [style.height.%]="100"> + <button class="drawer-button show-elements-button" (click)="toolbox_drawer.toggle()"> + <span> + ELEMENTE + </span> + </button> - <mat-drawer #toolbox_drawer class="toolbox_drawer" mode="side" opened> - <app-ui-element-toolbox></app-ui-element-toolbox> - </mat-drawer> + <mat-drawer-container fxFlex> + <mat-drawer #toolbox_drawer class="toolbox_drawer" mode="side" opened> + <app-ui-element-toolbox></app-ui-element-toolbox> + </mat-drawer> - <mat-drawer-content> - <button mat-button (click)="toolbox_drawer.toggle()"> - Elementauswahl ein-/ausblenden - </button> - <button mat-button class="show_properties_button" (click)="properties_drawer.toggle()"> - Eigenschaften ein-/ausblenden - </button> - - <div> - <mat-tab-group mat-align-tabs="start" (selectedTabChange)="selectTab()"> + <mat-drawer-content> + <mat-tab-group mat-align-tabs="start" (selectedTabChange)="selectTab()" class="mat-tab-fill-height"> <mat-tab *ngFor="let page of unit.pages; let i = index" label="Seite {{i+1}}"> <ng-template mat-tab-label> Seite {{i+1}} @@ -31,11 +29,16 @@ </ng-template> </mat-tab> </mat-tab-group> - </div> - </mat-drawer-content> + </mat-drawer-content> - <mat-drawer #properties_drawer class="properties_drawer" position="end" mode="side" opened> - <app-properties></app-properties> - </mat-drawer> + <mat-drawer #properties_drawer class="properties_drawer" position="end" mode="side" opened> + <app-properties></app-properties> + </mat-drawer> + </mat-drawer-container> -</mat-drawer-container> + <button class="drawer-button show-properties-button" (click)="properties_drawer.toggle()"> + <span> + EIGENSCHAFTEN + </span> + </button> +</div> 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 95a6caa6ee8735a924da1accb9d24b202001717f..db225677ab8e502c06d62f6b991f9d606e1091ed 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 @@ -13,9 +13,12 @@ import { Unit } from '../../../../../common/unit'; templateUrl: './unit-view.component.html', styles: [ '.toolbox_drawer {width: 280px}', - '.properties_drawer {width: 400px}', - '.show_properties_button {float: right}', - '.delete-page-button {min-width: 0; padding: 0;position: absolute; left: 130px; bottom: 6px;}' + '.properties_drawer {width: 320px}', + '.delete-page-button {min-width: 0; padding: 0;position: absolute; left: 130px; bottom: 6px;}', + '.drawer-button {font-size: large;background-color: lightgray;min-width: 0;width: 2%;border: none;cursor: pointer}', + '.show-elements-button span {transform: rotate(-90deg); display: inherit}', + '.show-properties-button {padding-bottom: 140px}', + '.show-properties-button span {transform: rotate(90deg); display: inherit;}' ] }) export class UnitViewComponent implements OnInit, OnDestroy, AfterViewInit { diff --git a/projects/editor/src/app/id.service.ts b/projects/editor/src/app/id.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..da521b21de638491d42d0827076a1bce284af253 --- /dev/null +++ b/projects/editor/src/app/id.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@angular/core'; +import { + Unit, UnitPage, UnitPageSection, UnitUIElement +} from '../../../common/unit'; + +@Injectable({ + providedIn: 'root' +}) +export class IdService { + private givenIDs: string[] = []; + private idCounter: Record<string, number> = { + label: 0, + button: 0, + 'text-field': 0, + checkbox: 0, + dropdown: 0, + radio: 0, + image: 0, + audio: 0, + video: 0, + correction: 0 + }; + + getNewID(type: string): string { + do { + this.idCounter[type] += 1; + } + while (!this.isIdAvailable(`${type}_${this.idCounter[type]}`)); + this.givenIDs.push(`${type}_${this.idCounter[type]}`); + return `${type}_${this.idCounter[type]}`; + } + + readExistingIDs(unit: Unit): void { + unit.pages.forEach((page: UnitPage) => { + page.sections.forEach((section: UnitPageSection) => { + section.elements.forEach((element: UnitUIElement) => { + this.givenIDs.push(element.id); + }); + }); + }); + } + + isIdAvailable(value: string): boolean { + return !this.givenIDs.includes(value); + } + + addId(id: string): void { + this.givenIDs.push(id); + } + + removeId(id: string): void { + const index = this.givenIDs.indexOf(id, 0); + if (index > -1) { + this.givenIDs.splice(index, 1); + } + } +} diff --git a/projects/editor/src/app/model/UnitFactory.ts b/projects/editor/src/app/model/UnitFactory.ts index a3cfe1558c4116109737c93f292212e89ff40dea..9ce0342febac49c4e92db98b72d970ba7b955172 100644 --- a/projects/editor/src/app/model/UnitFactory.ts +++ b/projects/editor/src/app/model/UnitFactory.ts @@ -33,9 +33,10 @@ export function createUnitPageSection(): UnitPageSection { export function createUnitUIElement(type: string): UnitUIElement { return { type, - id: 'dummyID', + id: 'id_placeholder', xPosition: 0, yPosition: 0, + zIndex: 0, width: 180, height: 60, backgroundColor: 'lightgrey', diff --git a/projects/editor/src/app/unit.service.ts b/projects/editor/src/app/unit.service.ts index 42a851b945bcd820b2ad945348c6b74ad51fa9e4..e4f467504c69460c42b17941f1c9684adb83f88e 100644 --- a/projects/editor/src/app/unit.service.ts +++ b/projects/editor/src/app/unit.service.ts @@ -3,11 +3,12 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { - Unit, UnitPage, UnitUIElement + Unit, UnitPage, UnitPageSection, UnitUIElement } from '../../../common/unit'; import { FileService } from '../../../common/file.service'; import * as UnitFactory from './model/UnitFactory'; import { MessageService } from '../../../common/message.service'; +import { IdService } from './id.service'; @Injectable({ providedIn: 'root' @@ -22,7 +23,7 @@ export class UnitService { pageSwitch = new Subject(); elementUpdated = new Subject(); - constructor(private messageService: MessageService) { + constructor(private messageService: MessageService, private idService: IdService) { this._unit = new BehaviorSubject(UnitFactory.createUnit()); const initialPage = UnitFactory.createUnitPage(); const initialSection = UnitFactory.createUnitPageSection(); @@ -133,8 +134,10 @@ export class UnitService { case 'correction': newElement = UnitFactory.createCorrectionElement(); break; - // no default + default: + throw new Error(`ElementType ${elementType} not found!`); } + newElement.id = this.idService.getNewID(elementType); this._unit.value.pages[this._selectedPageIndex.value] .sections[this._selectedPageSectionIndex.value].elements.push(newElement!); @@ -159,15 +162,24 @@ export class UnitService { this.elementUpdated.next(); } - updateSelectedElementProperty(property: string, value: string | number | boolean): void { - this._selectedElements.value.forEach((element: UnitUIElement) => { + updateSelectedElementProperty(property: string, value: string | number | boolean): boolean { + for (const element of this._selectedElements.value) { if (['string', 'number', 'boolean'].indexOf(typeof element[property]) > -1) { + if (property === 'id') { + if (!this.idService.isIdAvailable((value as string))) { // prohibit existing IDs + this.messageService.showError('ID ist bereits vergeben'); + return false; + } + this.idService.removeId(element[property]); + this.idService.addId(<string>value); + } element[property] = value; } else if (Array.isArray(element[property])) { (element[property] as string[]).push(value as string); } - }); + } this.elementUpdated.next(); + return true; } deleteSelectedElements(): void { @@ -197,6 +209,8 @@ export class UnitService { this._unit.value.pages.forEach((page: UnitPage) => { this._pages.push(new BehaviorSubject(page)); }); + + this.idService.readExistingIDs(this._unit.value); } selectPageSection(index: number): void { diff --git a/projects/editor/src/styles.css b/projects/editor/src/styles.css index ed26e0b07d5dc3adbfb07017f56cab7954b98f7d..99d5117209a7ac8c7a48c463320a82c96539cc28 100644 --- a/projects/editor/src/styles.css +++ b/projects/editor/src/styles.css @@ -6,3 +6,4 @@ body { } .snackbar-warning {border: 3px double #ff4d4d} +.snackbar-error {background-color: #ff4d4d}