Skip to content
Snippets Groups Projects
Commit 03e8c00b authored by rhenck's avatar rhenck
Browse files

Move ID-Service to editor

Generally only the editor needs to generate new IDs. Before there was
need to have it in common because some elements (like compound elements)
needed to generte new Elements including IDs.
Now all ID stuff is handled by the UnitService and the elements classes
are handed the fully built elements. Unfortunately this means that some
logic has to move back from classes into the UnitService. To un-bloat
the UnitService a (and in the future some more) helper class is created
for this logic.

The most important thing is, that the UIElement no longer generates
an ID when created. The ID has to be created before and passed to the
constructor.
parent bfd5272c
No related branches found
No related tags found
No related merge requests found
import { Section } from './section'; import { Section } from './section';
import { moveArrayItem } from '../util/array'; import { moveArrayItem } from '../util/array';
import { UIElement } from './uI-element'; import { UIElement } from './uI-element';
import { IdService } from '../id.service'; import { IdService } from '../../editor/src/app/services/id.service';
export class Page { export class Page {
[index: string]: string | number | boolean | Section[] | undefined | ((...args: any) => any); [index: string]: string | number | boolean | Section[] | undefined | ((...args: any) => any);
...@@ -41,15 +41,6 @@ export class Page { ...@@ -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 { moveSection(section: Section, direction: 'up' | 'down'): void {
moveArrayItem(section, this.sections, direction); moveArrayItem(section, this.sections, direction);
} }
......
// eslint-disable-next-line max-classes-per-file // eslint-disable-next-line max-classes-per-file
import { IdService } from '../id.service';
export type UIElementType = 'text' | 'button' | 'text-field' | 'text-area' | 'checkbox' export type UIElementType = 'text' | 'button' | 'text-field' | 'text-area' | 'checkbox'
| 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert_row' | 'radio-group-images' | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert_row' | 'radio-group-images'
| 'drop-list' | 'cloze' | 'spell-correct' | 'slider' | 'frame' | 'toggle-button'; | 'drop-list' | 'cloze' | 'spell-correct' | 'slider' | 'frame' | 'toggle-button';
...@@ -27,10 +25,6 @@ export abstract class UIElement { ...@@ -27,10 +25,6 @@ export abstract class UIElement {
protected constructor(serializedElement: Partial<UIElement>) { protected constructor(serializedElement: Partial<UIElement>) {
Object.assign(this, serializedElement); 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 { getProperty(property: string): any {
...@@ -51,7 +45,8 @@ export abstract class UIElement { ...@@ -51,7 +45,8 @@ export abstract class UIElement {
// This can be overwritten by elements if they need to handle some property specifics. Likert does. // This can be overwritten by elements if they need to handle some property specifics. Likert does.
setProperty(property: string, setProperty(property: string,
value: InputElementValue | LikertColumn[] | LikertRow[] | DragNDropValueObject[]): void { value: InputElementValue | LikertColumn[] | LikertRow[] |
DragNDropValueObject[] | ClozePart[][]): void {
if (this.fontProps && property in this.fontProps) { if (this.fontProps && property in this.fontProps) {
this.fontProps[property] = value as string | number | boolean; this.fontProps[property] = value as string | number | boolean;
} else if (this.surfaceProps && property in this.surfaceProps) { } else if (this.surfaceProps && property in this.surfaceProps) {
...@@ -176,3 +171,9 @@ export interface LikertRow { ...@@ -176,3 +171,9 @@ export interface LikertRow {
text: string; text: string;
columnCount: number; columnCount: number;
} }
export type ClozePart = {
type: string;
value: string | UIElement;
style?: string;
};
import { import {
ClozePart,
CompoundElement, CompoundElement,
FontElement, FontElement,
FontProperties, FontProperties,
InputElement,
InputElementValue,
LikertColumn,
LikertRow,
PositionedElement, PositionProperties, PositionedElement, PositionProperties,
UIElement UIElement
} from '../../models/uI-element'; } 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 { 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 // 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 { 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[][] = []; parts: ClozePart[][] = [];
positionProps: PositionProperties; positionProps: PositionProperties;
...@@ -43,7 +27,7 @@ export class ClozeElement extends CompoundElement implements PositionedElement, ...@@ -43,7 +27,7 @@ export class ClozeElement extends CompoundElement implements PositionedElement,
serializedElement?.parts.forEach((subParts: ClozePart[]) => { serializedElement?.parts.forEach((subParts: ClozePart[]) => {
subParts.forEach((part: ClozePart) => { subParts.forEach((part: ClozePart) => {
if (!['p', 'h1', 'h2', 'h3', 'h4'].includes(part.type)) { 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, ...@@ -52,112 +36,4 @@ export class ClozeElement extends CompoundElement implements PositionedElement,
this.width = serializedElement.width || 450; this.width = serializedElement.width || 450;
this.height = serializedElement.height || 200; 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;
}
} }
import { import {
Component, EventEmitter, Output, QueryList, ViewChildren Component, EventEmitter, Output, QueryList, ViewChildren
} from '@angular/core'; } from '@angular/core';
import { ClozeElement, ClozePart } from './cloze-element'; import { ClozeElement } from './cloze-element';
import { CompoundElementComponent } from '../../directives/compound-element.directive'; 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'; import { FormElementComponent } from '../../directives/form-element-component.directive';
@Component({ @Component({
......
...@@ -9,7 +9,6 @@ import { ...@@ -9,7 +9,6 @@ import {
UIElement UIElement
} from '../../models/uI-element'; } from '../../models/uI-element';
import { initFontElement, initPositionedElement, initSurfaceElement } from '../../util/unit-interface-initializer'; import { initFontElement, initPositionedElement, initSurfaceElement } from '../../util/unit-interface-initializer';
import { IdService } from '../../id.service';
export class DropListElement extends InputElement implements PositionedElement, FontElement, SurfaceElement { export class DropListElement extends InputElement implements PositionedElement, FontElement, SurfaceElement {
onlyOneItem: boolean = false; onlyOneItem: boolean = false;
...@@ -54,9 +53,9 @@ export class DropListElement extends InputElement implements PositionedElement, ...@@ -54,9 +53,9 @@ export class DropListElement extends InputElement implements PositionedElement,
} }
if (oldValues.length > 0) { if (oldValues.length > 0) {
this.value = []; this.value = [];
oldValues.forEach((stringValue: string) => { oldValues.forEach((stringValue: string, i: number) => {
(this.value as DragNDropValueObject[]).push({ (this.value as DragNDropValueObject[]).push({
id: IdService.getInstance().getNewID('value'), id: `${this.id}_value_${i}`,
stringValue: stringValue stringValue: stringValue
}); });
}); });
......
...@@ -4,7 +4,7 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs'; ...@@ -4,7 +4,7 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { FileService } from './file.service'; import { FileService } from './file.service';
import { MessageService } from '../../../../common/services/message.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 { DialogService } from './dialog.service';
import { VeronaAPIService } from './verona-api.service'; import { VeronaAPIService } from './verona-api.service';
import { Unit } from '../../../../common/models/unit'; import { Unit } from '../../../../common/models/unit';
...@@ -24,6 +24,7 @@ import { LikertElement } from '../../../../common/ui-elements/likert/likert-elem ...@@ -24,6 +24,7 @@ import { LikertElement } from '../../../../common/ui-elements/likert/likert-elem
import { LikertElementRow } from '../../../../common/ui-elements/likert/likert-element-row'; import { LikertElementRow } from '../../../../common/ui-elements/likert/likert-element-row';
import { SelectionService } from './selection.service'; import { SelectionService } from './selection.service';
import * as ElementFactory from '../../../../common/util/element.factory'; import * as ElementFactory from '../../../../common/util/element.factory';
import { ClozeParser } from '../util/cloze-parser';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
...@@ -122,7 +123,12 @@ export class UnitService { ...@@ -122,7 +123,12 @@ export class UnitService {
} }
duplicateSection(section: Section, page: Page, sectionIndex: number): void { 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._unit.next(this._unit.value);
this.veronaApiService.sendVoeDefinitionChangedNotification(); this.veronaApiService.sendVoeDefinitionChangedNotification();
} }
...@@ -159,6 +165,7 @@ export class UnitService { ...@@ -159,6 +165,7 @@ export class UnitService {
} }
newElement = ElementFactory.createElement({ newElement = ElementFactory.createElement({
type: elementType, type: elementType,
id: IdService.getInstance().getNewID(elementType),
src: mediaSrc, src: mediaSrc,
positionProps: { positionProps: {
dynamicPositioning: section.dynamicPositioning dynamicPositioning: section.dynamicPositioning
...@@ -167,6 +174,7 @@ export class UnitService { ...@@ -167,6 +174,7 @@ export class UnitService {
} else { } else {
newElement = ElementFactory.createElement({ newElement = ElementFactory.createElement({
type: elementType, type: elementType,
id: IdService.getInstance().getNewID(elementType),
positionProps: { positionProps: {
dynamicPositioning: section.dynamicPositioning dynamicPositioning: section.dynamicPositioning
} }
...@@ -245,8 +253,11 @@ export class UnitService { ...@@ -245,8 +253,11 @@ export class UnitService {
} }
IdService.getInstance().removeId(element.id); IdService.getInstance().removeId(element.id);
IdService.getInstance().addId(<string>value); 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.elementPropertyUpdated.next();
this.veronaApiService.sendVoeDefinitionChangedNotification(); this.veronaApiService.sendVoeDefinitionChangedNotification();
...@@ -334,6 +345,7 @@ export class UnitService { ...@@ -334,6 +345,7 @@ export class UnitService {
return new LikertElementRow( return new LikertElementRow(
{ {
type: 'likert_row', type: 'likert_row',
id: IdService.getInstance().getNewID('likert_row'),
text: question, text: question,
columnCount: columnCount columnCount: columnCount
} as LikertElementRow } as LikertElementRow
......
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;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment