From 15cdca5ea99ba03e7aef73f8e38ce6f815946d73 Mon Sep 17 00:00:00 2001 From: rhenck <richard.henck@iqb.hu-berlin.de> Date: Wed, 12 Jan 2022 21:19:03 +0100 Subject: [PATCH] Rework cloze element data - Now only has a variable 'document'. This holds the HTML representation in an object. This object is enriched with element models. - Because the the TextEditor extension can neither create multiple element instances nor use the IDService to generate their IDs, this has to be done afterwards. See ClozeParser. - The cloze element has rather extensive compatibility handling because cloze elements used to save an actual HTML representation. This has to be transformed to JSON/object. Therefore we replace the old backslash- markers with custom HTML tags. The editor object does this transformation. It needs some custom extensions to recognize (and don't remove) the HTML tags though. - Cloze now shows a placeholder text when empty - The cloze component needs a small pipe to extract text formatting options from the paragraph parts. - For getting the child elements for the player the models have to be extracted from the somewhat complex (JSON)document. - Added some rudimentary interfaces for the TextEditor document format. - Removed the old ClozePart interface. This is quasi part of the new interfaces. --- projects/common/models/uI-element.ts | 26 ++- projects/common/shared.module.ts | 10 +- .../common/ui-elements/cloze/cloze-element.ts | 109 ++++++++---- .../ui-elements/cloze/cloze.component.ts | 163 ++++++++---------- .../common/ui-elements/cloze/mark.pipe.ts | 15 ++ .../tiptap-editor-extensions/drop-list.ts | 25 +++ .../tiptap-editor-extensions/text-field.ts | 25 +++ .../tiptap-editor-extensions/toggle-button.ts | 25 +++ .../editor/src/app/services/unit.service.ts | 16 +- projects/editor/src/app/util/cloze-parser.ts | 103 ++--------- 10 files changed, 295 insertions(+), 222 deletions(-) create mode 100644 projects/common/ui-elements/cloze/mark.pipe.ts create mode 100644 projects/common/ui-elements/cloze/tiptap-editor-extensions/drop-list.ts create mode 100644 projects/common/ui-elements/cloze/tiptap-editor-extensions/text-field.ts create mode 100644 projects/common/ui-elements/cloze/tiptap-editor-extensions/toggle-button.ts diff --git a/projects/common/models/uI-element.ts b/projects/common/models/uI-element.ts index 0914e857f..97d423067 100644 --- a/projects/common/models/uI-element.ts +++ b/projects/common/models/uI-element.ts @@ -46,7 +46,7 @@ export abstract class UIElement { // This can be overwritten by elements if they need to handle some property specifics. Likert does. setProperty(property: string, value: InputElementValue | LikertColumn[] | LikertRow[] | - DragNDropValueObject[] | ClozePart[][]): void { + DragNDropValueObject[] | ClozeDocument): void { if (this.fontProps && property in this.fontProps) { this.fontProps[property] = value as string | number | boolean; } else if (this.surfaceProps && property in this.surfaceProps) { @@ -81,6 +81,24 @@ export abstract class InputElement extends UIElement { export abstract class CompoundElement extends UIElement {} +export interface ClozeDocument { + type: string; + content: ClozeDocumentParagraph[] +} + +export interface ClozeDocumentParagraph { + type: string; + attrs: Record<string, string | number | boolean>; + content: ClozeDocumentPart[]; +} + +export interface ClozeDocumentPart { + type: string; + text?: string; + marks?: any; + attrs?: Record<string, string | number | boolean | InputElement>; +} + export interface ValueChangeElement { id: string; values: [InputElementValue, InputElementValue]; @@ -172,9 +190,3 @@ export interface LikertRow { text: string; columnCount: number; } - -export type ClozePart = { - type: string; - value: string | UIElement; - style?: string; -}; diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts index 0c0b9b991..993815e51 100644 --- a/projects/common/shared.module.ts +++ b/projects/common/shared.module.ts @@ -49,6 +49,7 @@ import { SpellCorrectComponent } from './ui-elements/spell-correct/spell-correct import { DropListSimpleComponent } from './ui-elements/drop-list-simple/drop-list-simple.component'; import { FrameComponent } from './ui-elements/frame/frame.component'; import { ToggleButtonComponent } from './ui-elements/toggle-button/toggle-button.component'; +import { MarkPipe } from './ui-elements/cloze/mark.pipe'; @NgModule({ imports: [ @@ -99,7 +100,8 @@ import { ToggleButtonComponent } from './ui-elements/toggle-button/toggle-button SpellCorrectComponent, TextFieldSimpleComponent, FrameComponent, - ToggleButtonComponent + ToggleButtonComponent, + MarkPipe ], exports: [ CommonModule, @@ -115,7 +117,11 @@ import { ToggleButtonComponent } from './ui-elements/toggle-button/toggle-button MatTooltipModule, MatDialogModule, TranslateModule, - SafeResourceHTMLPipe + SafeResourceHTMLPipe, + ToggleButtonComponent, + TextFieldComponent, + DropListSimpleComponent, + TextFieldSimpleComponent ] }) export class SharedModule { diff --git a/projects/common/ui-elements/cloze/cloze-element.ts b/projects/common/ui-elements/cloze/cloze-element.ts index 2f4901d8d..426ccdf9c 100644 --- a/projects/common/ui-elements/cloze/cloze-element.ts +++ b/projects/common/ui-elements/cloze/cloze-element.ts @@ -1,25 +1,21 @@ +import { Editor } from '@tiptap/core'; +import StarterKit from '@tiptap/starter-kit'; +import ToggleButtonExtension from './tiptap-editor-extensions/toggle-button'; +import DropListExtension from './tiptap-editor-extensions/drop-list'; +import TextFieldExtension from './tiptap-editor-extensions/text-field'; import { - ClozePart, - CompoundElement, - FontElement, - FontProperties, InputElement, + UIElement, InputElement, CompoundElement, + ClozeDocument, PositionedElement, PositionProperties, - UIElement + FontElement, FontProperties } from '../../models/uI-element'; import { initFontElement, initPositionedElement } from '../../util/unit-interface-initializer'; import { TextFieldSimpleElement } from '../textfield-simple/text-field-simple-element'; -import { TextFieldElement } from '../text-field/text-field-element'; -import { TextAreaElement } from '../text-area/text-area-element'; -import { CheckboxElement } from '../checkbox/checkbox-element'; -import { DropdownElement } from '../dropdown/dropdown-element'; import { DropListSimpleElement } from '../drop-list-simple/drop-list-simple'; import { ToggleButtonElement } from '../toggle-button/toggle-button'; -// TODO styles like em dont continue after inserted components - export class ClozeElement extends CompoundElement implements PositionedElement, FontElement { - text: string = 'Lorem ipsum dolor \\r sdfsdf \\i sdfsdf'; - parts: ClozePart[][] = []; + document: ClozeDocument = { type: 'doc', content: [] }; positionProps: PositionProperties; fontProps: FontProperties; @@ -30,40 +26,91 @@ export class ClozeElement extends CompoundElement implements PositionedElement, this.positionProps = initPositionedElement(serializedElement); this.fontProps = initFontElement(serializedElement); - if (serializedElement?.parts) { - serializedElement?.parts.forEach((subParts: ClozePart[]) => { - subParts.forEach((part: ClozePart) => { - if (!['p', 'h1', 'h2', 'h3', 'h4'].includes(part.type)) { - part.value = ClozeElement.createElement(part.value as UIElement); + if (serializedElement.document) { + serializedElement.document.content.forEach((paragraph: any) => { + paragraph.content?.forEach((node: any) => { + if (['ToggleButton', 'DropList', 'TextField'].includes(node.type)) { + node.attrs.model = ClozeElement.createElement(node.attrs.model); } }); }); } + // text property indicates old unit definition + if (serializedElement.text) { + this.handleBackwardsCompatibility(serializedElement); + } + this.width = serializedElement.width || 450; this.height = serializedElement.height || 200; } - static createElement(elementModel: Partial<UIElement>): InputElement { + private handleBackwardsCompatibility(serializedElement: Partial<UIElement>): void { + const childModels = ClozeElement.parseElementList(serializedElement.parts); + + const textFieldElementList = Object.values(childModels).filter((el: any) => el.type === 'text-field'); + const dropListElementList = Object.values(childModels).filter((el: any) => el.type === 'drop-list'); + const radioElementList = Object.values(childModels).filter((el: any) => el.type === 'toggle-button'); + + const replacedText = (serializedElement.text as string).replace(/\\i|\\z|\\r/g, match => { + switch (match) { + case '\\i': + return `<app-nodeview-text-field id="${textFieldElementList.shift()?.id}"></app-nodeview-text-field>`; + break; + case '\\z': + return `<app-nodeview-drop-list id="${dropListElementList.shift()?.id}"></app-nodeview-drop-list>`; + break; + case '\\r': + return `<app-nodeview-toggle-button id="${radioElementList.shift()?.id}"></app-nodeview-toggle-button>`; + break; + default: + throw Error('error in match'); + } + return match; + }); + + if (textFieldElementList.length === 0 || + dropListElementList.length === 0 || + radioElementList.length === 0) { + throw Error('Error while reading cloze element!'); + } + + const editor = new Editor({ + extensions: [ + StarterKit, + ToggleButtonExtension, + DropListExtension, + TextFieldExtension + ], + content: replacedText + }); + this.document = editor.getJSON() as ClozeDocument; + } + + private static parseElementList( + serializedParts: { type: string; value: string | UIElement; style?: string; }[][] + ): InputElement[] { + const knownElementTypes = ['text-field', 'drop-list', 'toggle-button']; + const newElementList: InputElement[] = []; + + serializedParts.forEach((part: any) => { + for (const subPart of part) { + if (knownElementTypes.includes(subPart.type)) { + newElementList.push(subPart.value); + } + } + }); + return newElementList; + } + + private static createElement(elementModel: Partial<UIElement>): InputElement { let newElement: InputElement; switch (elementModel.type) { case 'text-field': newElement = new TextFieldSimpleElement(elementModel); - (newElement as TextFieldElement).label = ''; - break; - case 'text-area': - newElement = new TextAreaElement(elementModel); - break; - case 'checkbox': - newElement = new CheckboxElement(elementModel); - break; - case 'dropdown': - newElement = new DropdownElement(elementModel); break; case 'drop-list': newElement = new DropListSimpleElement(elementModel); - newElement.height = 25; // TODO weg? - newElement.width = 100; break; case 'toggle-button': newElement = new ToggleButtonElement(elementModel); diff --git a/projects/common/ui-elements/cloze/cloze.component.ts b/projects/common/ui-elements/cloze/cloze.component.ts index 1348395a1..436d1c99f 100644 --- a/projects/common/ui-elements/cloze/cloze.component.ts +++ b/projects/common/ui-elements/cloze/cloze.component.ts @@ -1,93 +1,83 @@ import { - Component, EventEmitter, Output, QueryList, ViewChildren + Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core'; import { ClozeElement } from './cloze-element'; import { CompoundElementComponent } from '../../directives/compound-element.directive'; -import { InputElement, ClozePart } from '../../models/uI-element'; +import { ClozeDocumentParagraph, ClozeDocumentPart, InputElement } from '../../models/uI-element'; import { FormElementComponent } from '../../directives/form-element-component.directive'; @Component({ selector: 'app-cloze', template: ` + <ng-container *ngIf="elementModel.document.content.length == 0"> + Kein Dokument vorhanden + </ng-container> <div [class.center-content]="elementModel.positionProps.dynamicPositioning && elementModel.positionProps.fixedSize" [style.width]="elementModel.positionProps.fixedSize ? elementModel.width + 'px' : '100%'" [style.height]="elementModel.positionProps.fixedSize ? elementModel.height + 'px' : 'auto'"> - <p *ngFor="let paragraph of elementModel.parts; let i = index" - [style.line-height.%]="elementModel.fontProps.lineHeight" - [style.color]="elementModel.fontProps.fontColor" - [style.font-family]="elementModel.fontProps.font" - [style.font-size.px]="elementModel.fontProps.fontSize" - [style.font-weight]="elementModel.fontProps.bold ? 'bold' : ''" - [style.font-style]="elementModel.fontProps.italic ? 'italic' : ''" - [style.text-decoration]="elementModel.fontProps.underline ? 'underline' : ''"> - <ng-container *ngFor="let part of paragraph; let j = index"> - - <span *ngIf="part.type === 'p'" - [innerHTML]="part.value" - [style]="part.style"> - </span> - - <h1 *ngIf="part.type === 'h1'" - [innerHTML]="part.value" - [style.display]="'inline'" - [style]="part.style"> - </h1> - <h2 *ngIf="part.type === 'h2'" - [innerHTML]="part.value" - [style.display]="'inline'" - [style]="part.style"> - </h2> - <h3 *ngIf="part.type === 'h3'" - [innerHTML]="part.value" - [style.display]="'inline'" - [style]="part.style"> - </h3> - <h4 *ngIf="part.type === 'h4'" - [innerHTML]="part.value" - [style.display]="'inline'" - [style]="part.style"> - </h4> - - <span (click)="allowClickThrough || selectElement($any(part.value), $event)"> - <app-dropdown *ngIf="part.type === 'dropdown'" #drowdownComponent - [parentForm]="parentForm" - [style.display]="'inline-block'" - [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'" - [elementModel]="$any(part.value)" - (elementValueChanged)="elementValueChanged.emit($event)"> - </app-dropdown> - <app-text-field-simple *ngIf="part.type === 'text-field'" #textfieldComponent - [parentForm]="parentForm" - [style.display]="'inline-block'" - [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'" - [elementModel]="$any(part.value)" - (elementValueChanged)="elementValueChanged.emit($event)"> - </app-text-field-simple> - - <app-toggle-button *ngIf="part.type === 'toggle-button'" #radioComponent - [parentForm]="parentForm" - [style.display]="'inline-block'" - [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'" - [elementModel]="$any(part.value)" - (elementValueChanged)="elementValueChanged.emit($event)"> - </app-toggle-button> - - <div *ngIf="part.type === 'drop-list'" - [style.display]="'inline-block'" - [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'" - [style.vertical-align]="'middle'" - [style.width.px]="$any(part.value).width" - [style.height.px]="$any(part.value).height"> - <app-drop-list-simple #droplistComponent - [parentForm]="parentForm" - (elementValueChanged)="elementValueChanged.emit($event)" - [elementModel]="$any(part.value)"> - </app-drop-list-simple> - </div> - </span> - </ng-container> - </p> + <ng-container *ngFor="let part of elementModel.document.content"> + <p *ngIf="part.type === 'paragraph'" + [style.line-height.%]="elementModel.fontProps.lineHeight" + [style.color]="elementModel.fontProps.fontColor" + [style.font-family]="elementModel.fontProps.font" + [style.font-size.px]="elementModel.fontProps.fontSize" + [style.font-weight]="elementModel.fontProps.bold ? 'bold' : ''" + [style.font-style]="elementModel.fontProps.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.fontProps.underline ? 'underline' : ''"> + <ng-container *ngFor="let subPart of part.content"> + <ng-container *ngIf="subPart.type === 'text'"> + <span [style.font-weight]="$any((subPart.marks | mark)?.includes('bold')) ? 'bold' : ''" + [style.font-style]="$any((subPart.marks | mark)?.includes('italic')) ? 'italic' : ''" + [style.text-decoration]="$any((subPart.marks | mark)?.includes('underline')) ? 'underline' : ''"> + {{subPart.text}} + </span> + </ng-container> + <span *ngIf="['ToggleButton', 'DropList', 'TextField'].includes(subPart.type)" + (click)="selectElement($any(subPart.attrs).model, $event)"> + <app-toggle-button *ngIf="subPart.type === 'ToggleButton'" #radioComponent + [parentForm]="parentForm" + [style.display]="'inline-block'" + [style.vertical-align]="'middle'" + [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'" + [elementModel]="$any(subPart.attrs).model" + (elementValueChanged)="elementValueChanged.emit($event)"> + </app-toggle-button> + <app-text-field-simple *ngIf="subPart.type === 'TextField'" #textfieldComponent + [parentForm]="parentForm" + [style.display]="'inline-block'" + [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'" + [elementModel]="$any(subPart.attrs).model" + (elementValueChanged)="elementValueChanged.emit($event)"> + </app-text-field-simple> + <app-drop-list-simple *ngIf="subPart.type === 'DropList'" #droplistComponent + [parentForm]="parentForm" + [style.display]="'inline-block'" + [style.vertical-align]="'middle'" + [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'" + [elementModel]="$any(subPart.attrs).model" + (elementValueChanged)="elementValueChanged.emit($event)"> + </app-drop-list-simple> + </span> + </ng-container> + </p> + <h1 *ngIf="part.type === 'heading' && part.attrs.level === 1" + [style.display]="'inline'"> + {{part.content[0].text}} + </h1> + <h2 *ngIf="part.type === 'heading' && part.attrs.level === 2" + [style.display]="'inline'"> + {{part.content[0].text}} + </h2> + <h3 *ngIf="part.type === 'heading' && part.attrs.level === 3" + [style.display]="'inline'"> + {{part.content[0].text}} + </h3> + <h4 *ngIf="part.type === 'heading' && part.attrs.level === 4" + [style.display]="'inline'"> + {{part.content[0].text}} + </h4> + </ng-container> </div> `, styles: [ @@ -96,25 +86,24 @@ import { FormElementComponent } from '../../directives/form-element-component.di ':host ::ng-deep app-text-field .mat-form-field {height: 100%}', ':host ::ng-deep app-text-field .mat-form-field-flex {height: 100%}', 'p {margin: 0}', + ':host ::ng-deep p strong {letter-spacing: 0.04em; font-weight: 600;}', // bold less bold + ':host ::ng-deep p:empty::after {content: "\\00A0"}', // render empty p 'p span {font-size: inherit}' ] }) export class ClozeComponent extends CompoundElementComponent { - elementModel!: ClozeElement; + @Input() elementModel!: ClozeElement; @Output() elementSelected = new EventEmitter<{ element: ClozeElement, event: MouseEvent }>(); @ViewChildren('drowdownComponent, textfieldComponent, droplistComponent, radioComponent') compoundChildren!: QueryList<FormElementComponent>; getFormElementModelChildren(): InputElement[] { - const uiElements: InputElement[] = []; - this.elementModel.parts.forEach((subParts: ClozePart[]) => { - subParts.forEach((part: ClozePart) => { - if (part.value instanceof InputElement) { - uiElements.push(part.value); - } - }); - }); - return uiElements; + return this.elementModel.document.content + .filter((paragraph: ClozeDocumentParagraph) => paragraph.content) // filter empty paragraphs + .map((paragraph: ClozeDocumentParagraph) => paragraph.content // get custom paragraph parts + .filter((word: ClozeDocumentPart) => ['TextField', 'DropList', 'ToggleButton'].includes(word.type))) + .reduce((accumulator: any[], currentValue: any) => accumulator // put all collected paragraph parts into one list + .concat(currentValue.map((node: ClozeDocumentPart) => node.attrs?.model)), []); // model is in node.attrs.model } selectElement(element: ClozeElement, event: MouseEvent): void { diff --git a/projects/common/ui-elements/cloze/mark.pipe.ts b/projects/common/ui-elements/cloze/mark.pipe.ts new file mode 100644 index 000000000..d9b24d9e0 --- /dev/null +++ b/projects/common/ui-elements/cloze/mark.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'mark' +}) +/* This extracts marks from an item and puts them is a list to be searchable. + Only used in cloze component */ +export class MarkPipe implements PipeTransform { + transform(items: any[]): any[] { + if (!items) { + return items; + } + return items.map(item => item.type); + } +} diff --git a/projects/common/ui-elements/cloze/tiptap-editor-extensions/drop-list.ts b/projects/common/ui-elements/cloze/tiptap-editor-extensions/drop-list.ts new file mode 100644 index 000000000..9aea0b7cd --- /dev/null +++ b/projects/common/ui-elements/cloze/tiptap-editor-extensions/drop-list.ts @@ -0,0 +1,25 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +const DropListExtension = + Node.create({ + group: 'inline', + inline: true, + name: 'DropList', + + addAttributes() { + return { + id: { + default: 'will be generated' + } + }; + }, + + parseHTML() { + return [{ tag: 'app-nodeview-drop-list' }]; + }, + renderHTML({ HTMLAttributes }) { + return ['app-nodeview-drop-list', mergeAttributes(HTMLAttributes)]; + } + }); + +export default DropListExtension; diff --git a/projects/common/ui-elements/cloze/tiptap-editor-extensions/text-field.ts b/projects/common/ui-elements/cloze/tiptap-editor-extensions/text-field.ts new file mode 100644 index 000000000..251cc0385 --- /dev/null +++ b/projects/common/ui-elements/cloze/tiptap-editor-extensions/text-field.ts @@ -0,0 +1,25 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +const TextFieldExtension = + Node.create({ + group: 'inline', + inline: true, + name: 'TextField', + + addAttributes() { + return { + id: { + default: 'will be generated' + } + }; + }, + + parseHTML() { + return [{ tag: 'app-nodeview-text-field' }]; + }, + renderHTML({ HTMLAttributes }) { + return ['app-nodeview-text-field', mergeAttributes(HTMLAttributes)]; + } + }); + +export default TextFieldExtension; diff --git a/projects/common/ui-elements/cloze/tiptap-editor-extensions/toggle-button.ts b/projects/common/ui-elements/cloze/tiptap-editor-extensions/toggle-button.ts new file mode 100644 index 000000000..1cf4d2e8f --- /dev/null +++ b/projects/common/ui-elements/cloze/tiptap-editor-extensions/toggle-button.ts @@ -0,0 +1,25 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +const ToggleButtonExtension = + Node.create({ + group: 'inline', + inline: true, + name: 'ToggleButton', + + addAttributes() { + return { + id: { + default: 'will be generated' + } + }; + }, + + parseHTML() { + return [{ tag: 'app-nodeview-toggle-button' }]; + }, + renderHTML({ HTMLAttributes }) { + return ['app-nodeview-toggle-button', mergeAttributes(HTMLAttributes)]; + } + }); + +export default ToggleButtonExtension; diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts index 545bbd872..3af112e55 100644 --- a/projects/editor/src/app/services/unit.service.ts +++ b/projects/editor/src/app/services/unit.service.ts @@ -245,7 +245,7 @@ export class UnitService { } updateElementProperty(elements: UIElement[], property: string, - value: InputElementValue | LikertColumn[] | LikertRow[] | + value: InputElementValue | LikertColumn[] | LikertRow[] | ClozeDocument | DragNDropValueObject[] | null): boolean { // console.log('updateElementProperty', elements, property, value); for (const element of elements) { @@ -257,8 +257,11 @@ export class UnitService { this.idService.removeId(element.id); this.idService.addID(value as string); element.setProperty('id', value); - } else if (property === 'text' && element.type === 'cloze') { - element.setProperty('parts', ClozeParser.createClozeParts(value as string, this.idService)); + } else if (property === 'document') { + element.setProperty('document', ClozeParser.setMissingIDs( + value as ClozeDocument, + this.idService + )); } else { element.setProperty(property, value); } @@ -427,12 +430,15 @@ export class UnitService { }); break; case 'cloze': - this.dialogService.showClozeTextEditDialog((element as TextElement).text).subscribe((result: string) => { + this.dialogService.showClozeTextEditDialog( + element.document, + (element as ClozeElement).fontProps.fontSize as number + ).subscribe((result: string) => { if (result) { // TODO add proper sanitization this.updateElementProperty( [element], - 'text', + 'document', (this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string ); } diff --git a/projects/editor/src/app/util/cloze-parser.ts b/projects/editor/src/app/util/cloze-parser.ts index 9a7d75cf5..7805dcd3e 100644 --- a/projects/editor/src/app/util/cloze-parser.ts +++ b/projects/editor/src/app/util/cloze-parser.ts @@ -1,109 +1,32 @@ -import { InputElement, UIElement, ClozePart } from '../../../../common/models/uI-element'; +import { ClozeDocument, InputElement, UIElement } from '../../../../common/models/uI-element'; +import { IdService } from '../services/id.service'; import { TextFieldSimpleElement } from '../../../../common/ui-elements/textfield-simple/text-field-simple-element'; -import { TextFieldElement } from '../../../../common/ui-elements/text-field/text-field-element'; -import { TextAreaElement } from '../../../../common/ui-elements/text-area/text-area-element'; -import { CheckboxElement } from '../../../../common/ui-elements/checkbox/checkbox-element'; -import { DropdownElement } from '../../../../common/ui-elements/dropdown/dropdown-element'; import { DropListSimpleElement } from '../../../../common/ui-elements/drop-list-simple/drop-list-simple'; import { ToggleButtonElement } from '../../../../common/ui-elements/toggle-button/toggle-button'; -import { IdService } from '../services/id.service'; export abstract class ClozeParser { - static createClozeParts(htmlText: string, idService: IdService): ClozePart[][] { - const elementList = ClozeParser.createElementList(htmlText); - - const parts: ClozePart[][] = []; - elementList.forEach((element: HTMLParagraphElement | HTMLHeadingElement, i: number) => { - ClozeParser.parseParagraphs(element, i, parts, idService); - }); - return parts; - } - - private static createElementList(htmlText: string): (HTMLParagraphElement | HTMLHeadingElement)[] { - const el = document.createElement('html'); - el.innerHTML = htmlText; - return Array.from(el.children[1].children) as (HTMLParagraphElement | HTMLHeadingElement)[]; - } - - // TODO refactor passed parts, so the Part is returned instead if manipulating the param array - private static parseParagraphs( - element: HTMLParagraphElement | HTMLHeadingElement, partIndex: number, parts: ClozePart[][], idService: IdService - ): ClozePart[][] { - parts[partIndex] = []; - let [nextSpecialElementIndex, nextElementType] = ClozeParser.getNextElementMarker(element.innerHTML); - let indexOffset = 0; - - while (nextSpecialElementIndex !== -1) { - nextSpecialElementIndex += indexOffset; - parts[partIndex].push({ - type: element.localName, - value: element.innerHTML.substring(indexOffset, nextSpecialElementIndex), - style: element.style.cssText + static setMissingIDs(clozeJSON: ClozeDocument, idService: IdService): ClozeDocument { + clozeJSON.content.forEach((paragraph: any) => { + paragraph.content?.forEach((node: any) => { + if (['ToggleButton', 'DropList', 'TextField'].includes(node.type) && + node.attrs.model.id === 'id_placeholder') { + // create element anew because the TextEditor can't create multiple element instances + node.attrs.model = ClozeParser.createElement(node.attrs.model); + node.attrs.model.id = idService.getNewID(node.attrs.model.type); + } }); - - const newElement = ClozeParser.createElement({ type: nextElementType } as UIElement, idService); - parts[partIndex].push({ type: nextElementType, value: newElement }); - - indexOffset = nextSpecialElementIndex + 2; // + 2 to get rid of the marker, i.e. '\b' - [nextSpecialElementIndex, nextElementType] = - ClozeParser.getNextElementMarker(element.innerHTML.substring(indexOffset)); - } - parts[partIndex].push({ - type: element.localName, - value: element.innerHTML.substring(indexOffset), - style: element.style.cssText }); - return parts; + return clozeJSON; } - private static getNextElementMarker(p: string): [number, string] { - const x = []; - if (p.indexOf('\\d') > 0) { - x.push(p.indexOf('\\d')); - } - if (p.indexOf('\\i') > 0) { - x.push(p.indexOf('\\i')); - } - if (p.indexOf('\\z') > 0) { - x.push(p.indexOf('\\z')); - } - if (p.indexOf('\\r') > 0) { - x.push(p.indexOf('\\r')); - } - - const y = Math.min(...x); - let nextElementType = ''; - switch (p[y + 1]) { - case 'd': nextElementType = 'dropdown'; break; - case 'i': nextElementType = 'text-field'; break; - case 'z': nextElementType = 'drop-list'; break; - case 'r': nextElementType = 'toggle-button'; break; - default: return [-1, 'unknown']; - } - return [y, nextElementType]; - } - - private static createElement(elementModel: Partial<UIElement>, idService: IdService): InputElement { + private static createElement(elementModel: Partial<UIElement>): InputElement { let newElement: InputElement; - elementModel.id = idService.getNewID(elementModel.type as string); switch (elementModel.type) { case 'text-field': newElement = new TextFieldSimpleElement(elementModel); - (newElement as TextFieldElement).label = ''; - break; - case 'text-area': - newElement = new TextAreaElement(elementModel); - break; - case 'checkbox': - newElement = new CheckboxElement(elementModel); - break; - case 'dropdown': - newElement = new DropdownElement(elementModel); break; case 'drop-list': newElement = new DropListSimpleElement(elementModel); - newElement.height = 25; // TODO weg? - newElement.width = 100; break; case 'toggle-button': newElement = new ToggleButtonElement(elementModel); -- GitLab