diff --git a/projects/common/models/page.ts b/projects/common/models/page.ts index a4321c6f82129896741eb66843f417937401746c..b7c3345fa07fddce5d2db1c8e7f19339c20c8b23 100644 --- a/projects/common/models/page.ts +++ b/projects/common/models/page.ts @@ -1,7 +1,7 @@ import { Section } from './section'; import { moveArrayItem } from '../util/array'; import { UIElement } from './uI-element'; -import { IdService } from '../id.service'; +import { IdService } from '../../editor/src/app/services/id.service'; export class Page { [index: string]: string | number | boolean | Section[] | undefined | ((...args: any) => any); @@ -41,15 +41,6 @@ export class Page { ); } - /** Create new section with old section elements are overwrite the ids. */ - duplicateSection(section: Section, sectionIndex: number): void { - const newSection = new Section(section); - newSection.elements.forEach((element: UIElement) => { - element.id = IdService.getInstance().getNewID(element.type); - }); - this.sections.splice(sectionIndex + 1, 0, newSection); - } - moveSection(section: Section, direction: 'up' | 'down'): void { moveArrayItem(section, this.sections, direction); } diff --git a/projects/common/models/uI-element.ts b/projects/common/models/uI-element.ts index 302257af0a299f430a29d97fe33c223871c77cd3..5a05bfbf6d95a3d349c05894aa322360c79ef23a 100644 --- a/projects/common/models/uI-element.ts +++ b/projects/common/models/uI-element.ts @@ -1,6 +1,4 @@ // eslint-disable-next-line max-classes-per-file -import { IdService } from '../id.service'; - export type UIElementType = 'text' | 'button' | 'text-field' | 'text-area' | 'checkbox' | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert_row' | 'radio-group-images' | 'drop-list' | 'cloze' | 'spell-correct' | 'slider' | 'frame' | 'toggle-button'; @@ -27,10 +25,6 @@ export abstract class UIElement { protected constructor(serializedElement: Partial<UIElement>) { Object.assign(this, serializedElement); - if (!serializedElement.id) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.id = IdService.getInstance().getNewID(serializedElement.type!); - } } getProperty(property: string): any { @@ -51,7 +45,8 @@ 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[]): void { + value: InputElementValue | LikertColumn[] | LikertRow[] | + DragNDropValueObject[] | ClozePart[][]): void { if (this.fontProps && property in this.fontProps) { this.fontProps[property] = value as string | number | boolean; } else if (this.surfaceProps && property in this.surfaceProps) { @@ -176,3 +171,9 @@ export interface LikertRow { text: string; columnCount: number; } + +export type ClozePart = { + type: string; + value: string | UIElement; + style?: string; +}; diff --git a/projects/common/ui-elements/cloze/cloze-element.ts b/projects/common/ui-elements/cloze/cloze-element.ts index c005d215555e790545fc27e288bfc4a6fec8ccf2..a60430429769e2feaeeac56ee046cb2ed2ed84c1 100644 --- a/projects/common/ui-elements/cloze/cloze-element.ts +++ b/projects/common/ui-elements/cloze/cloze-element.ts @@ -1,33 +1,17 @@ import { + ClozePart, CompoundElement, FontElement, FontProperties, - InputElement, - InputElementValue, - LikertColumn, - LikertRow, PositionedElement, PositionProperties, UIElement } from '../../models/uI-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 { initFontElement, initPositionedElement } from '../../util/unit-interface-initializer'; -import { TextFieldSimpleElement } from '../textfield-simple/text-field-simple-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 type ClozePart = { - type: string; - value: string | UIElement; - style?: string; -}; - export class ClozeElement extends CompoundElement implements PositionedElement, FontElement { - text: string = '<p>Lorem ipsum dolor \\r sdfsdf \\i sdfsdf</p>'; + text: string = 'Lorem ipsum dolor \\r sdfsdf \\i sdfsdf'; parts: ClozePart[][] = []; positionProps: PositionProperties; @@ -43,7 +27,7 @@ export class ClozeElement extends CompoundElement implements PositionedElement, 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); + part.value = this.createElement(part.value as UIElement); } }); }); @@ -52,112 +36,4 @@ export class ClozeElement extends CompoundElement implements PositionedElement, this.width = serializedElement.width || 450; this.height = serializedElement.height || 200; } - - setProperty(property: string, value: InputElementValue | string[] | LikertColumn[] | LikertRow[]): void { - super.setProperty(property, value); - - if (property === 'text') { - this.createParts(value as string); - } - } - - private createParts(htmlText: string): void { - const elementList = ClozeElement.readElementArray(htmlText); - - this.parts = []; - elementList.forEach((element: HTMLParagraphElement | HTMLHeadingElement, i: number) => { - this.parseParagraphs(element, i); - }); - // console.log('PARTS:', this.parts); - } - - private static readElementArray(htmlText: string): (HTMLParagraphElement | HTMLHeadingElement)[] { - const el = document.createElement('html'); - el.innerHTML = htmlText; - return Array.from(el.children[1].children) as (HTMLParagraphElement | HTMLHeadingElement)[]; - } - - private parseParagraphs(element: HTMLParagraphElement | HTMLHeadingElement, partIndex: number): void { - this.parts[partIndex] = []; // init array to be able to push - let [nextSpecialElementIndex, nextElementType] = ClozeElement.getNextSpecialElement(element.innerHTML); - let indexOffset = 0; - - while (nextSpecialElementIndex !== -1) { - nextSpecialElementIndex += indexOffset; - this.parts[partIndex].push({ - type: element.localName, - value: element.innerHTML.substring(indexOffset, nextSpecialElementIndex), - style: element.style.cssText - }); - - const newElement = ClozeElement.createElement({ type: nextElementType } as UIElement); - this.parts[partIndex].push({ type: nextElementType, value: newElement }); - - indexOffset = nextSpecialElementIndex + 2; // + 2 to get rid of the marker, i.e. '\b' - [nextSpecialElementIndex, nextElementType] = - ClozeElement.getNextSpecialElement(element.innerHTML.substring(indexOffset)); - } - this.parts[partIndex].push({ - type: element.localName, - value: element.innerHTML.substring(indexOffset), - style: element.style.cssText - }); - } - - private static getNextSpecialElement(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: 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; - newElement.width = 100; - break; - case 'toggle-button': - newElement = new ToggleButtonElement(elementModel); - break; - default: - throw new Error(`ElementType ${elementModel.type} not found!`); - } - return newElement; - } } diff --git a/projects/common/ui-elements/cloze/cloze.component.ts b/projects/common/ui-elements/cloze/cloze.component.ts index 6268dbdd0aeb55717886aa498314555534ccff3f..427cc4d94326fe87d3778aac7abdc6477e837023 100644 --- a/projects/common/ui-elements/cloze/cloze.component.ts +++ b/projects/common/ui-elements/cloze/cloze.component.ts @@ -1,9 +1,9 @@ import { Component, EventEmitter, Output, QueryList, ViewChildren } from '@angular/core'; -import { ClozeElement, ClozePart } from './cloze-element'; +import { ClozeElement } from './cloze-element'; import { CompoundElementComponent } from '../../directives/compound-element.directive'; -import { InputElement } from '../../models/uI-element'; +import { InputElement, ClozePart } from '../../models/uI-element'; import { FormElementComponent } from '../../directives/form-element-component.directive'; @Component({ diff --git a/projects/common/ui-elements/drop-list/drop-list.ts b/projects/common/ui-elements/drop-list/drop-list.ts index 02ced635fe4124d2136ef4e76ef9523c06203c77..8096efeeb35958f49a719cd18b651be295b6bb8e 100644 --- a/projects/common/ui-elements/drop-list/drop-list.ts +++ b/projects/common/ui-elements/drop-list/drop-list.ts @@ -9,7 +9,6 @@ import { UIElement } from '../../models/uI-element'; import { initFontElement, initPositionedElement, initSurfaceElement } from '../../util/unit-interface-initializer'; -import { IdService } from '../../id.service'; export class DropListElement extends InputElement implements PositionedElement, FontElement, SurfaceElement { onlyOneItem: boolean = false; @@ -54,9 +53,9 @@ export class DropListElement extends InputElement implements PositionedElement, } if (oldValues.length > 0) { this.value = []; - oldValues.forEach((stringValue: string) => { + oldValues.forEach((stringValue: string, i: number) => { (this.value as DragNDropValueObject[]).push({ - id: IdService.getInstance().getNewID('value'), + id: `${this.id}_value_${i}`, stringValue: stringValue }); }); diff --git a/projects/common/id.service.ts b/projects/editor/src/app/services/id.service.ts similarity index 100% rename from projects/common/id.service.ts rename to projects/editor/src/app/services/id.service.ts diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts index 334a599a435454a24d3efe684f16fe14e575942e..2cc4b24323e549b9c1e9add10efd9785d3b2e8cf 100644 --- a/projects/editor/src/app/services/unit.service.ts +++ b/projects/editor/src/app/services/unit.service.ts @@ -4,7 +4,7 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; import { FileService } from './file.service'; import { MessageService } from '../../../../common/services/message.service'; -import { IdService } from '../../../../common/id.service'; +import { IdService } from './id.service'; import { DialogService } from './dialog.service'; import { VeronaAPIService } from './verona-api.service'; import { Unit } from '../../../../common/models/unit'; @@ -24,6 +24,7 @@ import { LikertElement } from '../../../../common/ui-elements/likert/likert-elem import { LikertElementRow } from '../../../../common/ui-elements/likert/likert-element-row'; import { SelectionService } from './selection.service'; import * as ElementFactory from '../../../../common/util/element.factory'; +import { ClozeParser } from '../util/cloze-parser'; @Injectable({ providedIn: 'root' @@ -122,7 +123,12 @@ export class UnitService { } duplicateSection(section: Section, page: Page, sectionIndex: number): void { - page.duplicateSection(section, sectionIndex); + const newSection = new Section(section); + newSection.elements.forEach((element: UIElement) => { + element.id = IdService.getInstance().getNewID(element.type); + }); + page.sections.splice(sectionIndex + 1, 0, newSection); + this._unit.next(this._unit.value); this.veronaApiService.sendVoeDefinitionChangedNotification(); } @@ -159,6 +165,7 @@ export class UnitService { } newElement = ElementFactory.createElement({ type: elementType, + id: IdService.getInstance().getNewID(elementType), src: mediaSrc, positionProps: { dynamicPositioning: section.dynamicPositioning @@ -167,6 +174,7 @@ export class UnitService { } else { newElement = ElementFactory.createElement({ type: elementType, + id: IdService.getInstance().getNewID(elementType), positionProps: { dynamicPositioning: section.dynamicPositioning } @@ -245,8 +253,11 @@ export class UnitService { } IdService.getInstance().removeId(element.id); IdService.getInstance().addId(<string>value); + } else if (property === 'text' && element.type === 'cloze') { + element.setProperty('parts', ClozeParser.parse(value as string, IdService.getInstance())); + } else { + element.setProperty(property, value); } - element.setProperty(property, value); } this.elementPropertyUpdated.next(); this.veronaApiService.sendVoeDefinitionChangedNotification(); @@ -334,6 +345,7 @@ export class UnitService { return new LikertElementRow( { type: 'likert_row', + id: IdService.getInstance().getNewID('likert_row'), text: question, columnCount: columnCount } as LikertElementRow diff --git a/projects/editor/src/app/util/cloze-parser.ts b/projects/editor/src/app/util/cloze-parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..3899137fdaf765a7931a90079de0ece3823d0df1 --- /dev/null +++ b/projects/editor/src/app/util/cloze-parser.ts @@ -0,0 +1,120 @@ +import { InputElement, UIElement, ClozePart } from '../../../../common/models/uI-element'; +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 parse(text: string, idService: IdService): ClozePart[][] { + return ClozeParser.createParts(text, idService); + } + + private static createParts(htmlText: string, idService: IdService): ClozePart[][] { + const elementList = ClozeParser.readElementArray(htmlText); + + const parts: ClozePart[][] = []; + elementList.forEach((element: HTMLParagraphElement | HTMLHeadingElement, i: number) => { + ClozeParser.parseParagraphs(element, i, parts, idService); + }); + return parts; + } + + private static readElementArray(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.getNextSpecialElement(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 + }); + + 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.getNextSpecialElement(element.innerHTML.substring(indexOffset)); + } + parts[partIndex].push({ + type: element.localName, + value: element.innerHTML.substring(indexOffset), + style: element.style.cssText + }); + return parts; + } + + private static getNextSpecialElement(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 { + 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; + newElement.width = 100; + break; + case 'toggle-button': + newElement = new ToggleButtonElement(elementModel); + break; + default: + throw new Error(`ElementType ${elementModel.type} not found!`); + } + return newElement; + } +}