From 6481d53ac491f6204448f221af5f461036bf93bf Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Tue, 10 Aug 2021 12:22:16 +0200
Subject: [PATCH] [editor] Improve dynamic positioning

- Add (editable) rows to the grid
- Add drag and drop functionality to grid elements. The element is in
'absolute' positioning mode while being dragged and snaps back to the
grid when droppped.
---
 projects/common/unit.ts                       |  1 +
 .../canvas/canvas-section.component.ts        | 57 +++++++++++++++++-
 .../dynamic-canvas-overlay.component.ts       | 59 ++++++++++++++++---
 .../section-properties.component.ts           |  4 ++
 projects/editor/src/app/model/UnitFactory.ts  | 11 ++--
 5 files changed, 117 insertions(+), 15 deletions(-)

diff --git a/projects/common/unit.ts b/projects/common/unit.ts
index cbe01124e..f8e07a0a7 100644
--- a/projects/common/unit.ts
+++ b/projects/common/unit.ts
@@ -21,6 +21,7 @@ export interface UnitPageSection {
   backgroundColor: string;
   dynamicPositioning: boolean;
   gridColumnSizes: string;
+  gridRowSizes: string;
 }
 
 export interface UnitUIElement {
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas-section.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas-section.component.ts
index a6816a424..09635dba6 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas-section.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas-section.component.ts
@@ -1,4 +1,5 @@
 import { Component, Input, OnInit } from '@angular/core';
+import { CdkDragDrop } from '@angular/cdk/drag-drop/drag-events';
 import { UnitPageSection } from '../../../../../../../common/unit';
 import { UnitService } from '../../../../unit.service';
 
@@ -21,13 +22,37 @@ import { UnitService } from '../../../../unit.service';
       <div *ngIf="section.dynamicPositioning"
            [style.display]="'grid'"
            [style.grid-template-columns]="section.gridColumnSizes"
-           [style.height.%]="100">
+           [style.grid-template-rows]="section.gridRowSizes"
+           [style.height.%]="100"
+           cdkDropListGroup>
+
+        <ng-container *ngFor="let column of this.section.gridColumnSizes.split(' '); let x = index">
+          <ng-container *ngFor="let row of this.section.gridRowSizes.split(' '); let y = index">
+            <div [style.grid-column-start]="x + 1"
+                 [style.grid-column-end]="x + 1"
+                 [style.grid-row-start]="y + 1"
+                 [style.grid-row-end]="y + 1"
+                 [style.border]="'25px inset'"
+                 cdkDropList [cdkDropListData]="[x + 1, y + 1]" (cdkDropListDropped)="drop($event)">
+              {{x + 1}} / {{y + 1}}
+            </div>
+          </ng-container>
+        </ng-container>
+
         <app-dynamic-canvas-overlay *ngFor="let element of section.elements"
                                     [element]="$any(element)"
                                     [style.min-width.px]="element.width"
                                     [style.min-height.px]="element.height"
                                     [style.grid-column-start]="element.gridColumnStart"
-                                    [style.grid-column-end]="element.gridColumnEnd">
+                                    [style.grid-column-end]="element.gridColumnEnd"
+                                    [style.grid-row-start]="element.gridRowStart"
+                                    [style.grid-row-end]="element.gridRowEnd"
+                                    cdkDropList cdkDropListSortingDisabled
+                                    (resize)="resizeOverlay($event)"
+                                    [style.pointer-events]="dragging ? 'none' : 'auto'"
+                                    [style.position]="dragging ? 'absolute' : null"
+                                    [style.width.px]="dragging ? draggingElementWidth : null"
+                                    [style.height.px]="dragging ? draggingElementHeight : null">
         </app-dynamic-canvas-overlay>
       </div>
 
@@ -46,7 +71,7 @@ import { UnitService } from '../../../../unit.service';
   styles: [
     '.sectionMenu {visibility: hidden; transition: 0.2s 0.7s;}',
     '.section-wrapper:hover .sectionMenu {visibility: visible; transition-delay: 0s;}',
-    '.sectionMenu {position: absolute; left: -29px}',
+    '.sectionMenu {position: absolute; left: -29px; top: 0}',
     '.sectionMenu button {width: 28px; height: 28px}',
     '.sectionMenu .mat-mini-fab {line-height: 0;}',
     '::ng-deep .sectionMenu .mat-mini-fab .mat-button-wrapper {line-height: 0;}'
@@ -56,6 +81,9 @@ export class CanvasSectionComponent implements OnInit {
   @Input() section!: UnitPageSection;
   @Input() sectionIndex!: number;
   selected = true;
+  dragging = false;
+  draggingElementWidth: number | undefined = 0;
+  draggingElementHeight: number | undefined = 0;
 
   constructor(public unitService: UnitService) {
     this.unitService.selectSection(this);
@@ -64,4 +92,27 @@ export class CanvasSectionComponent implements OnInit {
   ngOnInit(): void {
     this.unitService.selectSection(this);
   }
+
+  drop(event: CdkDragDrop<number[]>): void {
+    if (event.item.data.dragType === 'move') {
+      this.unitService.updateSelectedElementProperty('gridColumnStart', event.container.data[0]);
+      // Ensure the end value is at least the same as the start, otherwise the grid breaks
+      this.unitService.updateSelectedElementProperty(
+        'gridColumnEnd', Math.max(event.item.data.element.gridColumnEnd, event.container.data[0])
+      );
+      this.unitService.updateSelectedElementProperty('gridRowStart', event.container.data[1]);
+      this.unitService.updateSelectedElementProperty(
+        'gridRowEnd', Math.max(event.item.data.element.gridRowEnd, event.container.data[1])
+      );
+    } else { // resize
+      this.unitService.updateSelectedElementProperty('gridColumnEnd', event.container.data[0] + 1);
+      this.unitService.updateSelectedElementProperty('gridRowEnd', event.container.data[1] + 1);
+    }
+  }
+
+  resizeOverlay(event: { dragging: boolean, elementWidth?: number, elementHeight?: number }): void {
+    this.dragging = event.dragging;
+    this.draggingElementWidth = event.elementWidth;
+    this.draggingElementHeight = event.elementHeight;
+  }
 }
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/dynamic-canvas-overlay.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/dynamic-canvas-overlay.component.ts
index 931772141..a70f4309b 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/dynamic-canvas-overlay.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/dynamic-canvas-overlay.component.ts
@@ -1,18 +1,63 @@
-import { Component, Input } from '@angular/core';
+import {
+  Component, Input, Output, EventEmitter, ViewChild, ElementRef
+} from '@angular/core';
+import { CdkDragMove } from '@angular/cdk/drag-drop';
 import { CanvasElementOverlay } from './canvas-element-overlay';
+import { UnitUIElement } from '../../../../../../../common/unit';
 
 @Component({
   selector: 'app-dynamic-canvas-overlay',
   template: `
-    <div class="draggable-element" [class.draggable-element-selected]="selected"
-         cdkDrag [cdkDragData]="this.element" [cdkDragDisabled]="!selected"
-         (click)="click($event)"
-         (dblclick)="openEditDialog()"
-         [style.height.%]="100">
+    <div #draggableElement class="draggable-element" [class.draggable-element-selected]="selected"
+         cdkDrag [cdkDragData]="{dragType: 'move', element: this.element}" [cdkDragDisabled]="!selected"
+         (click)="click($event)" (dblclick)="openEditDialog()"
+         [style.height.%]="100"
+         [style.border]="selected ? '1px solid' : ''">
+        <div *ngIf="selected" class="resizeHandle"
+             cdkDrag [cdkDragData]="{dragType: 'resize', element: this.element}"
+             (cdkDragStarted)="dragStart()" (cdkDragEnded)="dragEnd()" (cdkDragMoved)="resizeElement($event)"
+             [style.right.px]="2" [style.bottom.px]="-3">
+          <mat-icon>south_east</mat-icon>
+          <div *cdkDragPlaceholder></div>
+      </div>
       <ng-template #elementContainer></ng-template>
     </div>
-  `
+  `,
+  styles: [
+    '.draggable-element {position: relative}',
+    '.resizeHandle {position: absolute}'
+  ]
 })
 export class DynamicCanvasOverlayComponent extends CanvasElementOverlay {
   @Input() dynamicPositioning!: boolean;
+  @Output() resize = new EventEmitter<{ dragging: boolean, elementWidth?: number, elementHeight?: number }>();
+
+  @ViewChild('draggableElement') dragElement!: ElementRef;
+  private gridElementWidth: number = 0;
+  private gridElementHeight: number = 0;
+
+  dragStart(): void {
+    this.gridElementWidth = this.dragElement.nativeElement.offsetWidth - 2;
+    this.gridElementHeight = this.dragElement.nativeElement.offsetHeight - 2;
+
+    this.resize.emit({
+      dragging: true,
+      elementWidth: this.dragElement.nativeElement.offsetWidth - 2,
+      elementHeight: this.dragElement.nativeElement.offsetHeight - 2
+    });
+  }
+
+  resizeElement(event: CdkDragMove<{ dragType: string; element: UnitUIElement }>): void {
+    this.resize.emit({
+      dragging: true,
+      elementWidth: this.gridElementWidth + event.distance.x,
+      elementHeight: this.gridElementHeight + event.distance.y
+    });
+  }
+
+  dragEnd(): void {
+    this.resize.emit({
+      dragging: false
+    });
+  }
 }
diff --git a/projects/editor/src/app/components/unit-view/page-view/properties/section-properties.component.ts b/projects/editor/src/app/components/unit-view/page-view/properties/section-properties.component.ts
index 34e3d16a9..e2df59abc 100644
--- a/projects/editor/src/app/components/unit-view/page-view/properties/section-properties.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/properties/section-properties.component.ts
@@ -30,6 +30,10 @@ import { UnitService } from '../../../../unit.service';
         <mat-label>Grid-Spalten</mat-label>
         <input matInput [(ngModel)]="selectedPageSection.gridColumnSizes">
       </mat-form-field>
+      <mat-form-field>
+        <mat-label>Grid-Zeilen</mat-label>
+        <input matInput [(ngModel)]="selectedPageSection.gridRowSizes">
+      </mat-form-field>
     </div>
     <button mat-raised-button
             (click)="unitService.deleteSelectedSection()">
diff --git a/projects/editor/src/app/model/UnitFactory.ts b/projects/editor/src/app/model/UnitFactory.ts
index 94efdf565..a4b5c9a10 100644
--- a/projects/editor/src/app/model/UnitFactory.ts
+++ b/projects/editor/src/app/model/UnitFactory.ts
@@ -30,8 +30,9 @@ export function createUnitPageSection(): UnitPageSection {
     width: 1100,
     height: 200,
     backgroundColor: '#FFFAF0',
-    dynamicPositioning: false,
-    gridColumnSizes: '1fr 1fr 1fr'
+    dynamicPositioning: true,
+    gridColumnSizes: '1fr 1fr',
+    gridRowSizes: '1fr'
   };
 }
 
@@ -42,13 +43,13 @@ function createUnitUIElement(type: string): UnitUIElement {
     zIndex: 0,
     width: 180,
     height: 60,
-    dynamicPositioning: false,
+    dynamicPositioning: true,
     xPosition: 0,
     yPosition: 0,
     gridColumnStart: 1,
-    gridColumnEnd: 1,
+    gridColumnEnd: 2,
     gridRowStart: 1,
-    gridRowEnd: 1
+    gridRowEnd: 2
   };
 }
 
-- 
GitLab