From af1b9ad7d0d508cbbc5e8123d39f511437ad5307 Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Tue, 7 Dec 2021 14:33:24 +0100
Subject: [PATCH] [editor] Add section element list

This allows to select elements without clicking them on the canvas, as
elements may be obstructed or not visible for some reason.

The selection logic is a bit complicated because usually only the
overlay components are usd for selection which are also the place to
apply any highlighting on. Here we have to find the component element
reference from the section. Therefore the double looping
'getElementComponent' method.

Also when an elements is selected it briefly gets z-index 100 so it is
moved to the front and visible and manipulatable.
---
 projects/editor/src/app/app.module.ts         |  4 ++-
 .../page-view/canvas/canvas.component.html    |  3 +++
 .../page-view/canvas/canvas.component.ts      | 27 ++++++++++++++++++-
 .../canvas/overlays/canvas-element-overlay.ts |  6 +++++
 .../dynamic-canvas-overlay.component.ts       |  6 +++--
 .../static-canvas-overlay.component.ts        |  6 +++--
 .../canvas/section-dynamic.component.ts       |  6 ++++-
 .../canvas/section-menu.component.ts          | 17 ++++++++++++
 .../canvas/section-static.component.ts        |  7 +++--
 9 files changed, 73 insertions(+), 9 deletions(-)

diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts
index 7ea87272d..83d8b11aa 100644
--- a/projects/editor/src/app/app.module.ts
+++ b/projects/editor/src/app/app.module.ts
@@ -44,6 +44,7 @@ import { LikertRowEditDialogComponent } from './components/dialogs/likert-row-ed
 import { RichTextEditDialogComponent } from './components/dialogs/rich-text-edit-dialog.component';
 import { ElementModelPropertiesComponent } from './components/unit-view/page-view/properties-panel/element-model-properties.component';
 import { DropListOptionEditDialogComponent } from './components/dialogs/drop-list-option-edit-dialog.component';
+import { MatListModule } from '@angular/material/list';
 
 @NgModule({
   declarations: [
@@ -93,7 +94,8 @@ import { DropListOptionEditDialogComponent } from './components/dialogs/drop-lis
         provide: TranslateLoader,
         useClass: EditorTranslateLoader
       }
-    })
+    }),
+    MatListModule
   ],
   providers: []
 })
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.component.html b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.component.html
index 3ac6ffafa..ddf998ee5 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.component.html
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.component.html
@@ -14,10 +14,12 @@
                           [allowDelete]="page.sections.length > 1"
                           (moveSection)="unitService.moveSection(section, page, $event)"
                           (duplicateSection)="unitService.duplicateSection(section, page, i)"
+                          (selectElementComponent)="selectElementComponent($event)"
                           (mouseover)="hoveredSection = i" (mouseleave)="hoveredSection = -1"> <!-- keep menu open-->
         </app-section-menu>
         <!-- TODO split section into static and dynamic-->
         <app-section-static *ngIf="!section.dynamicPositioning"
+             #sectionComponent
              class="section drop-list" id="section-{{i}}"
              [section]="section"
              [isSelected]="selectionService.selectedPageSectionIndex === i"
@@ -29,6 +31,7 @@
              (click)="selectionService.selectedPageSectionIndex = i">
         </app-section-static>
         <app-section-dynamic *ngIf="section.dynamicPositioning"
+             #sectionComponent
              class="section drop-list"
              [section]="section" [sectionIndex]="i"
              [isSelected]="selectionService.selectedPageSectionIndex === i"
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.component.ts
index 77157747b..582c60a13 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/canvas.component.ts
@@ -1,5 +1,5 @@
 import {
-  Component, Input, OnDestroy, OnInit
+  Component, Input, OnDestroy, OnInit, QueryList, ViewChildren
 } from '@angular/core';
 import { CdkDragDrop } from '@angular/cdk/drag-drop';
 import { Subject } from 'rxjs';
@@ -9,6 +9,9 @@ import { SelectionService } from '../../../../services/selection.service';
 import { Page } from '../../../../../../../common/models/page';
 import { PositionedElement, UIElement } from '../../../../../../../common/models/uI-element';
 import { Section } from '../../../../../../../common/models/section';
+import { CanvasElementOverlay } from './overlays/canvas-element-overlay';
+import { SectionStaticComponent } from './section-static.component';
+import { SectionDynamicComponent } from './section-dynamic.component';
 
 @Component({
   selector: 'app-page-canvas',
@@ -28,6 +31,8 @@ export class CanvasComponent implements OnInit, OnDestroy {
   hoveredSection: number = -1;
   private ngUnsubscribe = new Subject<void>();
 
+  @ViewChildren('sectionComponent') childSectionComponents!: QueryList<SectionStaticComponent | SectionDynamicComponent>;
+
   constructor(public selectionService: SelectionService, public unitService: UnitService) { }
 
   ngOnInit(): void {
@@ -113,6 +118,26 @@ export class CanvasComponent implements OnInit, OnDestroy {
     this.selectionService.selectedPageSectionIndex = this.page.sections.length - 1;
   }
 
+  selectElementComponent(element: UIElement): void {
+    const elementComponent = this.getElementComponent(element);
+    if (elementComponent) {
+      this.selectionService.selectElement({ componentElement: elementComponent, multiSelect: false });
+    } else {
+      throw Error('Element not found. This is a bug!');
+    }
+  }
+
+  private getElementComponent(element: UIElement): CanvasElementOverlay | null {
+    for (const sectionComponent of this.childSectionComponents.toArray()) {
+      for (const elementComponent of sectionComponent.childElementComponents.toArray()) {
+        if (elementComponent.element.id === element.id) {
+          return elementComponent;
+        }
+      }
+    }
+    return null;
+  }
+
   ngOnDestroy(): void {
     this.ngUnsubscribe.next();
     this.ngUnsubscribe.complete();
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/canvas-element-overlay.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/canvas-element-overlay.ts
index f22a93c94..d7487eddf 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/canvas-element-overlay.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/canvas-element-overlay.ts
@@ -24,6 +24,8 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy {
   protected childComponent!: ComponentRef<ElementComponent | CompoundElementComponent>;
   private ngUnsubscribe = new Subject<void>();
 
+  temporaryHighlight: boolean = false;
+
   constructor(public selectionService: SelectionService,
               protected unitService: UnitService,
               private componentFactoryResolver: ComponentFactoryResolver,
@@ -54,6 +56,10 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy {
   }
 
   setSelected(newValue: boolean): void {
+    this.temporaryHighlight = true;
+    setTimeout(() => {
+      this.temporaryHighlight = false;
+    }, 2000);
     this.isSelected = newValue;
     // This avoids: "NG0100: Expression has changed after it was checked"
     // The selection service may change the "selected" variable after onInit has run.
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/dynamic-canvas-overlay.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/dynamic-canvas-overlay.component.ts
index d5f17a38c..4849c6476 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/dynamic-canvas-overlay.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/dynamic-canvas-overlay.component.ts
@@ -10,7 +10,8 @@ import { UIElement } from '../../../../../../../../common/models/uI-element';
   template: `
     <!-- TabIndex is needed to make the div selectable and catch keyboard events (delete). -->
     <!-- DragStart and DragEnd are part of a cursor hack to style the body. See global styling file. -->
-    <div #draggableElement class="draggable-element" [class.draggable-element-selected]="isSelected"
+    <div #draggableElement class="draggable-element"
+         [class.temporaryHighlight]="temporaryHighlight"
          [style.display]="dragging ? 'none' : ''"
          tabindex="-1"
          cdkDrag [cdkDragData]="{dragType: 'move', element: element}"
@@ -45,7 +46,8 @@ import { UIElement } from '../../../../../../../../common/models/uI-element';
     '.draggable-element:active {cursor: grabbing}',
     '.resizeHandle {position: absolute; right: 3px; bottom: 3px; z-index: 1; height: 25px}',
     '.resizeHandle {cursor: nwse-resize}',
-    '.cdk-drag {position: absolute; bottom: 0; right: 0}'
+    '.cdk-drag {position: absolute; bottom: 0; right: 0}',
+    '.temporaryHighlight {z-index: 100}'
   ]
 })
 export class DynamicCanvasOverlayComponent extends CanvasElementOverlay {
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/static-canvas-overlay.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/static-canvas-overlay.component.ts
index bd4c8c7c8..9663b25e9 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/static-canvas-overlay.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/static-canvas-overlay.component.ts
@@ -9,7 +9,8 @@ import { UIElement } from '../../../../../../../../common/models/uI-element';
   template: `
     <!-- Is also a droplist to catch the resize drop and not let it bubble up to the canvas drop handler. -->
     <!-- TabIndex is needed to make the div selectable and catch keyboard events (delete). -->
-    <div class="draggable-element" [class.draggable-element-selected]="isSelected"
+    <div class="draggable-element"
+         [class.temporaryHighlight]="temporaryHighlight"
          cdkDrag [cdkDragData]="{dragType: 'move', element: element}"
          (click)="selectElement($event.shiftKey); $event.stopPropagation()"
          (cdkDragStarted)="!isSelected && selectElement()"
@@ -45,7 +46,8 @@ import { UIElement } from '../../../../../../../../common/models/uI-element';
     '.draggable-element {position: absolute}',
     '.draggable-element:active {cursor: grabbing}',
     '.resizeHandle {position: absolute; cursor: nwse-resize}',
-    '.resize-droplist {position: absolute}'
+    '.resize-droplist {position: absolute}',
+    '.temporaryHighlight {z-index: 100}'
   ]
 })
 export class StaticCanvasOverlayComponent extends CanvasElementOverlay {
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/section-dynamic.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/section-dynamic.component.ts
index e08521c3d..8977537ae 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/section-dynamic.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/section-dynamic.component.ts
@@ -1,11 +1,12 @@
 import {
-  Component, Input, Output, EventEmitter
+  Component, Input, Output, EventEmitter, ViewChildren, QueryList
 } from '@angular/core';
 import { CdkDragDrop } from '@angular/cdk/drag-drop/drag-events';
 import { DragItemData, DropListData } from './canvas.component';
 import { UnitService } from '../../../../services/unit.service';
 import { Section } from '../../../../../../../common/models/section';
 import { UIElementType } from '../../../../../../../common/models/uI-element';
+import { CanvasElementOverlay } from './overlays/canvas-element-overlay';
 
 @Component({
   selector: 'app-section-dynamic',
@@ -38,6 +39,7 @@ import { UIElementType } from '../../../../../../../common/models/uI-element';
       </ng-container>
 
       <app-dynamic-canvas-overlay *ngFor="let element of section.elements"
+                                  #elementComponent
                                   [element]="$any(element)"
                                   [style.min-width.px]="element.width"
                                   [style.min-height.px]="element.height"
@@ -69,6 +71,8 @@ export class SectionDynamicComponent {
   @Input() isSelected!: boolean;
   @Output() transferElement = new EventEmitter<{ previousSectionIndex: number, newSectionIndex: number }>();
 
+  @ViewChildren('elementComponent') childElementComponents!: QueryList<CanvasElementOverlay>;
+
   dragging = false;
 
   constructor(public unitService: UnitService) { }
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/section-menu.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/section-menu.component.ts
index e173e81db..942ff09f1 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/section-menu.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/section-menu.component.ts
@@ -8,10 +8,22 @@ import { UnitService } from '../../../../services/unit.service';
 import { DialogService } from '../../../../services/dialog.service';
 import { SelectionService } from '../../../../services/selection.service';
 import { Section } from '../../../../../../../common/models/section';
+import { UIElement } from '../../../../../../../common/models/uI-element';
 
 @Component({
   selector: 'app-section-menu',
   template: `
+    <button mat-mini-fab [matMenuTriggerFor]="elementListMenu">
+      <mat-icon>list</mat-icon>
+    </button>
+    <mat-menu #elementListMenu="matMenu" class="layoutMenu" xPosition="before">
+      <mat-action-list>
+        <mat-list-item *ngFor="let element of section.elements"
+                         (click)="selectElement(element)">
+          {{element.id}}
+        </mat-list-item>
+      </mat-action-list>
+    </mat-menu>
     <button mat-mini-fab [matMenuTriggerFor]="heightMenu">
       <mat-icon>height</mat-icon>
     </button>
@@ -141,6 +153,7 @@ export class SectionMenuComponent implements OnInit, OnDestroy {
   @Input() allowDelete!: boolean;
   @Output() moveSection = new EventEmitter<'up' | 'down'>();
   @Output() duplicateSection = new EventEmitter();
+  @Output() selectElementComponent = new EventEmitter<UIElement>();
 
   @ViewChild('colorPicker') colorPicker!: ElementRef;
   columnSizes: { value: string, unit: string }[] = [];
@@ -159,6 +172,10 @@ export class SectionMenuComponent implements OnInit, OnDestroy {
     this.unitService.updateSectionProperty(this.section, property, value);
   }
 
+  selectElement(element: UIElement): void {
+    this.selectElementComponent.emit(element);
+  }
+
   deleteSection(): void {
     this.dialogService.showConfirmDialog('Abschnitt löschen?')
       .pipe(takeUntil(this.ngUnsubscribe))
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/section-static.component.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/section-static.component.ts
index 56ba5d797..f75d0d0cd 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/section-static.component.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/section-static.component.ts
@@ -1,9 +1,11 @@
 import {
-  Component, ElementRef, Input, ViewChild
+  Component, ElementRef, Input, QueryList, ViewChild, ViewChildren
 } from '@angular/core';
 import { UnitService } from '../../../../services/unit.service';
 import { Section } from '../../../../../../../common/models/section';
 import { UIElementType } from '../../../../../../../common/models/uI-element';
+import { SectionDynamicComponent } from './section-dynamic.component';
+import { CanvasElementOverlay } from './overlays/canvas-element-overlay';
 
 @Component({
   selector: 'app-section-static',
@@ -14,7 +16,7 @@ import { UIElementType } from '../../../../../../../common/models/uI-element';
          [style.height.px]="section.height"
          [style.background-color]="section.backgroundColor"
          (dragover)="$event.preventDefault()" (drop)="newElementDropped($event)">
-      <app-static-canvas-overlay
+      <app-static-canvas-overlay #elementComponent
         *ngFor="let element of section.elements"
         [element]="$any(element)">
       </app-static-canvas-overlay>
@@ -28,6 +30,7 @@ export class SectionStaticComponent {
   @Input() section!: Section;
   @Input() isSelected!: boolean;
   @ViewChild('sectionElement') sectionElement!: ElementRef;
+  @ViewChildren('elementComponent') childElementComponents!: QueryList<CanvasElementOverlay>;
 
   constructor(public unitService: UnitService) { }
 
-- 
GitLab