From 89befccda8f1a6fa83070d06d68400f5ce149731 Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Wed, 1 Jun 2022 20:45:07 +0200
Subject: [PATCH] Refactor cloze element's document handling

- Move methods from cloze parser helper library to cloze class.
- Cloze children are set up with a special placeholder which is then
recognized and replaced by a new ID. And the element is recreated (like
before) to have a proper element model class, which the TipTap editor
doesn't provide.
---
 .../elements/compound-elements/cloze/cloze.ts | 66 +++++++++++++++----
 .../editor/src/app/services/unit.service.ts   |  2 -
 .../drop-list-component-extension.ts          |  9 +--
 .../text-field-component-extension.ts         |  3 +-
 .../toggle-button-component-extension.ts      |  8 ++-
 projects/editor/src/app/util/cloze-parser.ts  | 62 -----------------
 6 files changed, 64 insertions(+), 86 deletions(-)
 delete mode 100644 projects/editor/src/app/util/cloze-parser.ts

diff --git a/projects/common/models/elements/compound-elements/cloze/cloze.ts b/projects/common/models/elements/compound-elements/cloze/cloze.ts
index b129ac832..4d13bf342 100644
--- a/projects/common/models/elements/compound-elements/cloze/cloze.ts
+++ b/projects/common/models/elements/compound-elements/cloze/cloze.ts
@@ -4,7 +4,7 @@ import {
   InputElement,
   PositionedUIElement,
   PositionProperties,
-  UIElement
+  UIElement, UIElementValue
 } from 'common/models/elements/element';
 import { Type } from '@angular/core';
 import { ElementComponent } from 'common/directives/element-component.directive';
@@ -17,6 +17,7 @@ import {
   DropListSimpleElement
 } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple';
 import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button';
+import { IDManager } from 'common/util/id-manager';
 
 export class ClozeElement extends CompoundElement implements PositionedUIElement {
   document: ClozeDocument = { type: 'doc', content: [] };
@@ -26,17 +27,53 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement
     lineHeight: number;
   };
 
-  constructor(element: Partial<ClozeElement>, ...args: unknown[]) {
-    super({ height: 200, ...element }, ...args);
+  constructor(element: Partial<ClozeElement>, idManager?: IDManager) {
+    super({ height: 200, ...element }, idManager);
     if (element.columnCount) this.columnCount = element.columnCount;
-    this.document = this.initDocument(element);
+    this.document = this.initDocument(element, idManager);
     this.position = ElementFactory.initPositionProps(element.position);
     this.styling = {
       ...ElementFactory.initStylingProps({ lineHeight: 150, ...element.styling })
     };
   }
 
-  private initDocument(element: Partial<ClozeElement>): ClozeDocument {
+  setProperty(property: string, value: UIElementValue): void {
+    if (property === 'document') {
+      this.document = value as ClozeDocument;
+
+      this.document.content.forEach((node: any) => {
+        if (node.type === 'paragraph' || node.type === 'heading') {
+          ClozeElement.createSubNodeElements(node);
+        } else if (node.type === 'bulletList' || node.type === 'orderedList') {
+          node.content.forEach((listItem: any) => {
+            listItem.content.forEach((listItemParagraph: any) => {
+              ClozeElement.createSubNodeElements(listItemParagraph);
+            });
+          });
+        } else if (node.type === 'blockquote') {
+          node.content.forEach((blockQuoteItem: any) => {
+            ClozeElement.createSubNodeElements(blockQuoteItem);
+          });
+        }
+      });
+
+    } else {
+      super.setProperty(property, value);
+    }
+  }
+
+  private static createSubNodeElements(node: any) {
+    node.content?.forEach((subNode: any) => {
+      if (['ToggleButton', 'DropList', 'TextField'].includes(subNode.type) &&
+        subNode.attrs.model.id === 'cloze-child-id-placeholder') {
+        const newID = IDManager.getInstance().getNewID(subNode.attrs.model.type);
+        subNode.attrs.model =
+          ClozeElement.createChildElement({ ...subNode.attrs.model, id: newID }, IDManager.getInstance());
+      }
+    });
+  }
+
+  private initDocument(element: Partial<ClozeElement>, idManager?: IDManager): ClozeDocument {
     return {
       ...element.document,
       content: element.document?.content ? element.document.content
@@ -49,7 +86,7 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement
                   ...paraPart,
                   attrs: {
                     ...paraPart.attrs,
-                    model: ClozeElement.createChildElement(paraPart.attrs?.model as InputElement)
+                    model: ClozeElement.createChildElement(paraPart.attrs?.model as InputElement, idManager)
                   }
                 } :
                 {
@@ -65,8 +102,12 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement
   }
 
   getChildElements(): UIElement[] {
-    if (!this.document) return [];
-    const clozeDocument: ClozeDocument = this.document;
+    return ClozeElement.getDocumentChildElements(this.documnent);
+  }
+
+  static getDocumentChildElements(document: ClozeDocument): UIElement[] {
+    if (!document) return [];
+    const clozeDocument: ClozeDocument = document;
     const elementList: InputElement[] = [];
     clozeDocument.content.forEach((documentPart: ClozeDocumentParagraph) => {
       if (documentPart.type === 'paragraph' || documentPart.type === 'heading') {
@@ -86,21 +127,22 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement
     return elementList;
   }
 
-  private static createChildElement(elementModel: Partial<UIElement>): InputElement {
+  private static createChildElement(elementModel: Partial<UIElement>, idManager?: IDManager): InputElement {
     let newElement: InputElement;
     switch (elementModel.type) {
       case 'text-field-simple':
-        newElement = new TextFieldSimpleElement({ type: elementModel.type, elementModel }) as InputElement;
+        newElement = new TextFieldSimpleElement(elementModel as TextFieldSimpleElement, idManager);
         break;
       case 'drop-list-simple':
-        newElement = new DropListSimpleElement({ type: elementModel.type, elementModel }) as InputElement;
+        newElement = new DropListSimpleElement(elementModel as DropListSimpleElement, idManager);
         break;
       case 'toggle-button':
-        newElement = new ToggleButtonElement({ type: elementModel.type, elementModel }) as InputElement;
+        newElement = new ToggleButtonElement(elementModel as ToggleButtonElement, idManager);
         break;
       default:
         throw new Error(`ElementType ${elementModel.type} not found!`);
     }
+    // console.log('newElement', newElement);
     return newElement;
   }
 
diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts
index 8aa11ba95..7d68ba2c9 100644
--- a/projects/editor/src/app/services/unit.service.ts
+++ b/projects/editor/src/app/services/unit.service.ts
@@ -231,8 +231,6 @@ export class UnitService {
           IDManager.getInstance().addID(value as string);
           element.id = value as string;
         }
-      } else if (property === 'document') {
-        element[property] = ClozeParser.setMissingIDs(value as ClozeDocument);
       } else if (element.type === 'likert' && property === 'columns') {
         (element as LikertElement).rows.forEach(row => {
           row.columnCount = (element as LikertElement).columns.length;
diff --git a/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts
index b9ae078e1..cf7df19a0 100644
--- a/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts
+++ b/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts
@@ -2,11 +2,8 @@ import { Injector } from '@angular/core';
 import { Node, mergeAttributes } from '@tiptap/core';
 import { AngularNodeViewRenderer } from 'ngx-tiptap';
 import { DropListNodeviewComponent } from './drop-list-nodeview.component';
-
-import { ElementFactory } from 'common/util/element.factory';
-import {
-  DropListSimpleElement
-} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple';
+import { DropListSimpleElement }
+  from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple';
 
 const DropListComponentExtension = (injector: Injector): Node => {
   return Node.create({
@@ -17,7 +14,7 @@ const DropListComponentExtension = (injector: Injector): Node => {
     addAttributes() {
       return {
         model: {
-          default: new DropListSimpleElement({ type: 'drop-list-simple' })
+          default: new DropListSimpleElement({ type: 'drop-list-simple', id: 'cloze-child-id-placeholder' })
         }
       };
     },
diff --git a/projects/editor/src/app/text-editor/angular-node-views/text-field-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/text-field-component-extension.ts
index 44a7bc45d..9e57277b8 100644
--- a/projects/editor/src/app/text-editor/angular-node-views/text-field-component-extension.ts
+++ b/projects/editor/src/app/text-editor/angular-node-views/text-field-component-extension.ts
@@ -2,7 +2,6 @@ import { Injector } from '@angular/core';
 import { Node, mergeAttributes } from '@tiptap/core';
 import { AngularNodeViewRenderer } from 'ngx-tiptap';
 import { TextFieldNodeviewComponent } from './text-field-nodeview.component';
-import { ElementFactory } from 'common/util/element.factory';
 import {
   TextFieldSimpleElement
 } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple';
@@ -16,7 +15,7 @@ const TextFieldComponentExtension = (injector: Injector): Node => {
     addAttributes() {
       return {
         model: {
-          default: new TextFieldSimpleElement({ type: 'text-field-simple' })
+          default: new TextFieldSimpleElement({ type: 'text-field-simple', id: 'cloze-child-id-placeholder' })
         }
       };
     },
diff --git a/projects/editor/src/app/text-editor/angular-node-views/toggle-button-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/toggle-button-component-extension.ts
index 52b4b381e..ca5dc0d9e 100644
--- a/projects/editor/src/app/text-editor/angular-node-views/toggle-button-component-extension.ts
+++ b/projects/editor/src/app/text-editor/angular-node-views/toggle-button-component-extension.ts
@@ -3,7 +3,6 @@ import { Node, mergeAttributes } from '@tiptap/core';
 import { AngularNodeViewRenderer } from 'ngx-tiptap';
 
 import { ToggleButtonNodeviewComponent } from './toggle-button-nodeview.component';
-import { ElementFactory } from 'common/util/element.factory';
 import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button';
 
 const ToggleButtonComponentExtension = (injector: Injector): Node => {
@@ -15,7 +14,12 @@ const ToggleButtonComponentExtension = (injector: Injector): Node => {
     addAttributes() {
       return {
         model: {
-          default: new ToggleButtonElement({ type: 'toggle-button', height: 25, width: 100 })
+          default: new ToggleButtonElement({
+            type: 'toggle-button',
+            height: 25,
+            width: 100,
+            id: 'cloze-child-id-placeholder'
+          })
         }
       };
     },
diff --git a/projects/editor/src/app/util/cloze-parser.ts b/projects/editor/src/app/util/cloze-parser.ts
deleted file mode 100644
index 0fd1c876b..000000000
--- a/projects/editor/src/app/util/cloze-parser.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { InputElement, UIElement } from 'common/models/elements/element';
-import { ClozeDocument } from 'common/models/elements/compound-elements/cloze/cloze';
-import { ElementFactory } from 'common/util/element.factory';
-import { IDManager } from 'common/util/id-manager';
-import {
-  TextFieldSimpleElement
-} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple';
-import {
-  DropListSimpleElement
-} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple';
-import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button';
-
-export abstract class ClozeParser {
-  static setMissingIDs(clozeJSON: ClozeDocument): ClozeDocument {
-    clozeJSON.content.forEach((node: any) => {
-      if (node.type === 'paragraph' || node.type === 'heading') {
-        ClozeParser.createSubNodeElements(node);
-      } else if (node.type === 'bulletList' || node.type === 'orderedList') {
-        node.content.forEach((listItem: any) => {
-          listItem.content.forEach((listItemParagraph: any) => {
-            ClozeParser.createSubNodeElements(listItemParagraph);
-          });
-        });
-      } else if (node.type === 'blockquote') {
-        node.content.forEach((blockQuoteItem: any) => {
-          ClozeParser.createSubNodeElements(blockQuoteItem);
-        });
-      }
-    });
-    return clozeJSON;
-  }
-
-  // create element anew because the TextEditor can't create multiple element instances
-  private static createSubNodeElements(node: any) {
-    const idService = IDManager.getInstance();
-    node.content?.forEach((subNode: any) => {
-      if (['ToggleButton', 'DropList', 'TextField'].includes(subNode.type) &&
-        subNode.attrs.model.id === 'id_placeholder') {
-        subNode.attrs.model = ClozeParser.createElement(subNode.attrs.model);
-        subNode.attrs.model.id = idService.getNewID(subNode.attrs.model.type);
-      }
-    });
-  }
-
-  private static createElement(elementModel: Partial<UIElement>): InputElement {
-    let newElement: InputElement;
-    switch (elementModel.type) {
-      case 'text-field-simple':
-        newElement = new TextFieldSimpleElement({ type: elementModel.type, elementModel }) as InputElement;
-        break;
-      case 'drop-list-simple':
-        newElement = new DropListSimpleElement({ type: elementModel.type, elementModel }) as InputElement;
-        break;
-      case 'toggle-button':
-        newElement = new ToggleButtonElement({ type: elementModel.type, elementModel }) as InputElement;
-        break;
-      default:
-        throw new Error(`ElementType ${elementModel.type} not found!`);
-    }
-    return newElement;
-  }
-}
-- 
GitLab