Newer
Older
import { Injectable } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { MessageService } from '../../../../common/services/message.service';
import { DialogService } from './dialog.service';
import { VeronaAPIService } from './verona-api.service';
import { Unit } from '../../../../common/models/unit';
import { Page } from '../../../../common/models/page';
import { Section } from '../../../../common/models/section';
DragNDropValueObject,
InputElement, InputElementValue,
LikertColumn,
LikertRow, PlayerElement,
PlayerProperties, PositionedElement, ClozeDocument,
UIElement,
UIElementType
} from '../../../../common/models/uI-element';
import { TextElement } from '../../../../common/ui-elements/text/text-element';
import { LikertElement } from '../../../../common/ui-elements/likert/likert-element';
import { LikertElementRow } from '../../../../common/ui-elements/likert/likert-element-row';
import { SelectionService } from './selection.service';
import { ElementFactory } from '../../../../common/util/element.factory';
import { Copy } from '../../../../common/util/copy';
import { ClozeElement } from '../../../../common/ui-elements/cloze/cloze-element';
@Injectable({
providedIn: 'root'
})
export class UnitService {
elementPropertyUpdated: Subject<void> = new Subject<void>();
pageMoved: Subject<void> = new Subject<void>();
constructor(private selectionService: SelectionService,
private veronaApiService: VeronaAPIService,
private dialogService: DialogService,
private sanitizer: DomSanitizer,
private translateService: TranslateService) {
loadUnitDefinition(unitDefinition: string): void {
if (unitDefinition) {
this.idService.reset();
this.unit = new Unit(JSON.parse(unitDefinition));
this.veronaApiService.sendVoeDefinitionChangedNotification();
deletePage(page: Page): void {
this.veronaApiService.sendVoeDefinitionChangedNotification();
movePage(selectedPage: Page, direction: 'up' | 'down'): void {
this.pageMoved.next();
this.veronaApiService.sendVoeDefinitionChangedNotification();
updatePageProperty(page: Page, property: string, value: number | boolean): void {
if (property === 'alwaysVisible' && value === true) {
this.handlePageAlwaysVisiblePropertyChange(page);
this.veronaApiService.sendVoeDefinitionChangedNotification();
}
private handlePageAlwaysVisiblePropertyChange(page: Page): void {
page.alwaysVisible = true;
this.veronaApiService.sendVoeDefinitionChangedNotification();
deleteSection(section: Section): void {
this.unit.pages[this.selectionService.selectedPageIndex].deleteSection(section);
this.veronaApiService.sendVoeDefinitionChangedNotification();
}
duplicateSection(section: Section, page: Page, sectionIndex: number): void {
const newSection = new Section(section);
newSection.elements.forEach((element: UIElement) => {
element.id = this.idService.getNewID(element.type);
});
page.sections.splice(sectionIndex + 1, 0, newSection);
this.veronaApiService.sendVoeDefinitionChangedNotification();
}
moveSection(section: Section, page: Page, direction: 'up' | 'down'): void {
this.veronaApiService.sendVoeDefinitionChangedNotification();
addElementToSectionByIndex(elementType: UIElementType,
pageIndex: number,
sectionIndex: number): void {
this.addElementToSection(elementType, this.unit.pages[pageIndex].sections[sectionIndex]);
coordinates?: { x: number, y: number }): Promise<void> {
if (['audio', 'video', 'image'].includes(elementType)) {
let mediaSrc = '';
switch (elementType) {
case 'image':
mediaSrc = await FileService.loadImage();
break;
case 'audio':
mediaSrc = await FileService.loadAudio();
break;
case 'video':
mediaSrc = await FileService.loadVideo();
break;
// no default
}
newElement = ElementFactory.createElement({
type: elementType,
id: this.idService.getNewID(elementType),
src: mediaSrc,
positionProps: {
dynamicPositioning: section.dynamicPositioning
}
} as unknown as Partial<UIElement>) as PositionedElement;
newElement = ElementFactory.createElement({
type: elementType,
id: this.idService.getNewID(elementType),
positionProps: {
dynamicPositioning: section.dynamicPositioning
}
} as unknown as Partial<UIElement>) as PositionedElement;
newElement.positionProps.gridColumnStart = coordinates.x;
newElement.positionProps.gridColumnEnd = coordinates.x + 1;
newElement.positionProps.gridRowStart = coordinates.y;
newElement.positionProps.gridRowEnd = coordinates.y + 1;
newElement.positionProps.xPosition = coordinates.x;
newElement.positionProps.yPosition = coordinates.y;
section.addElement(newElement as PositionedElement);
this.veronaApiService.sendVoeDefinitionChangedNotification();
deleteElements(elements: UIElement[]): void {
this.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => {
section.deleteElements(elements);
});
this.veronaApiService.sendVoeDefinitionChangedNotification();
private freeUpIds(elements: UIElement[]): void {
elements.forEach(element => {
if (element.type === 'drop-list') {
element.value.forEach((value: DragNDropValueObject) => {
/* Move element between sections */
transferElement(elements: UIElement[], previousSection: Section, newSection: Section): void {
previousSection.elements = previousSection.elements.filter(element => !elements.includes(element));
elements.forEach(element => {
newSection.elements.push(element as PositionedElement);
(element as PositionedElement).positionProps.dynamicPositioning = newSection.dynamicPositioning;
this.veronaApiService.sendVoeDefinitionChangedNotification();
}
duplicateElementsInSection(elements: UIElement[],
pageIndex: number,
sectionIndex: number): void {
const section = this.unit.pages[pageIndex].sections[sectionIndex];
(elements as PositionedElement[]).forEach((element: PositionedElement) => {
const newElement = ElementFactory.createElement({
...JSON.parse(JSON.stringify(element)),
id: this.idService.getNewID(element.type),
positionProps: {
...element.positionProps,
xPosition: element.positionProps.xPosition + 10,
yPosition: element.positionProps.yPosition + 10
}
} as unknown as Partial<PositionedElement>);
if (newElement.value instanceof Object) { // replace value Ids with fresh ones (dropList)
newElement.value.forEach((valueObject: { id: string }) => {
valueObject.id = this.idService.getNewID('value');
});
}
if (newElement.rows instanceof Object) { // replace row Ids with fresh ones (likert)
newElement.rows.forEach((rowObject: { id: string }) => {
rowObject.id = this.idService.getNewID('likert_row');
});
}
if (newElement.type === 'cloze') {
(newElement as ClozeElement).getChildElements().forEach((childElement: InputElement) => {
childElement.id = this.idService.getNewID(childElement.type);
if ((childElement as UIElement).value instanceof Object) { // replace value Ids with fresh ones (dropList)
(childElement as UIElement).value.forEach((valueObject: { id: string }) => {
valueObject.id = this.idService.getNewID('value');
});
}
section.elements.push(newElement as PositionedElement);
});
this.veronaApiService.sendVoeDefinitionChangedNotification();
updateSectionProperty(section: Section, property: string, value: string | number | boolean): void {
section.updateProperty(property, value);
this.elementPropertyUpdated.next();
this.veronaApiService.sendVoeDefinitionChangedNotification();
}
updateElementProperty(elements: UIElement[], property: string,
value: InputElementValue | LikertColumn[] | LikertRow[] | ClozeDocument |
DragNDropValueObject[] | null): boolean {
// console.log('updateElementProperty', elements, property, value);
for (const element of elements) {
if (!this.idService.isIdAvailable((value as string))) { // prohibit existing IDs
this.messageService.showError(this.translateService.instant('idTaken'));
this.idService.addID(value as string);
element.setProperty('id', value);
} else if (property === 'document') {
element.setProperty('document', ClozeParser.setMissingIDs(
value as ClozeDocument,
this.idService
));
element.setProperty(property, Copy.getCopy(value));
this.veronaApiService.sendVoeDefinitionChangedNotification();
return true;
}
async editTextOption(property: string, optionIndex: number): Promise<void> {
const oldOptions = this.selectionService.getSelectedElements()[0][property] as string[];
await this.dialogService.showTextEditDialog(oldOptions[optionIndex])
.subscribe((result: string) => {
if (result) {
oldOptions[optionIndex] = result;
this.updateElementProperty(this.selectionService.getSelectedElements(), property, oldOptions);
async editDropListOption(optionIndex: number): Promise<void> {
const oldOptions = this.selectionService.getSelectedElements()[0].value as DragNDropValueObject[];
await this.dialogService.showDropListOptionEditDialog(oldOptions[optionIndex])
.subscribe((result: DragNDropValueObject) => {
if (result) {
if (result.id !== oldOptions[optionIndex].id && !this.idService.isIdAvailable(result.id)) {
this.messageService.showError(this.translateService.instant('idTaken'));
return;
}
oldOptions[optionIndex] = result;
this.updateElementProperty(this.selectionService.getSelectedElements(), 'value', oldOptions);
}
});
}
async editLikertRow(row: LikertElementRow, columns: LikertColumn[]): Promise<void> {
await this.dialogService.showLikertRowEditDialog(row, columns)
.subscribe((result: LikertElementRow) => {
if (result) {
'id',
result.id
);
}
'text',
result.text
);
}
if (result.value !== row.value) {
this.updateElementProperty(
[row],
'value',
result.value
);
}
async editLikertColumn(likertElements: LikertElement[], columnIndex: number): Promise<void> {
await this.dialogService.showLikertColumnEditDialog(likertElements[0].columns[columnIndex])
.subscribe((result: LikertColumn) => {
this.updateElementProperty(
likertElements,
likertElements[0].columns
static createLikertColumn(value: string): LikertColumn {
return {
text: value,
imgSrc: null,
position: 'above'
};
}
createLikertRow(question: string, columnCount: number): LikertElementRow {
return new LikertElementRow(
{
type: 'likert_row',
id: this.idService.getNewID('likert_row'),
text: question,
columnCount: columnCount
} as LikertElementRow
);
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
alignElements(elements: PositionedElement[], alignmentDirection: 'left' | 'right' | 'top' | 'bottom'): void {
switch (alignmentDirection) {
case 'left':
this.updateElementProperty(
elements,
'xPosition',
Math.min(...elements.map(element => element.positionProps.xPosition))
);
break;
case 'right':
this.updateElementProperty(
elements,
'xPosition',
Math.max(...elements.map(element => element.positionProps.xPosition))
);
break;
case 'top':
this.updateElementProperty(
elements,
'yPosition',
Math.min(...elements.map(element => element.positionProps.yPosition))
);
break;
case 'bottom':
this.updateElementProperty(
elements,
'yPosition',
Math.max(...elements.map(element => element.positionProps.yPosition))
);
break;
// no default
}
this.elementPropertyUpdated.next();
this.veronaApiService.sendVoeDefinitionChangedNotification();
async loadUnitFromFile(): Promise<void> {
this.loadUnitDefinition(await FileService.loadFile(['.json']));
showDefaultEditDialog(element: UIElement): void {
switch (element.type) {
case 'button':
case 'dropdown':
this.dialogService.showTextEditDialog(element.label).subscribe((result: string) => {
this.updateElementProperty([element], 'label', result);
this.dialogService.showRichTextEditDialog(
(element as TextElement).fontProps.fontSize as number
// TODO add proper sanitization
this.updateElementProperty(
[element],
'text',
(this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string
);
this.dialogService.showClozeTextEditDialog(
element.document,
(element as ClozeElement).fontProps.fontSize as number
if (result) {
// TODO add proper sanitization
this.updateElementProperty(
[element],
(this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string
);
}
});
break;
this.dialogService.showTextEditDialog((element as InputElement).value as string).subscribe((result: string) => {
this.updateElementProperty([element], 'value', result);
}
});
break;
case 'text-area':
this.dialogService.showMultilineTextEditDialog((element as InputElement).value as string)
.subscribe((result: string) => {
if (result) {
this.updateElementProperty([element], 'value', result);
}
});
case 'audio':
case 'video':
this.dialogService.showPlayerEditDialog((element as PlayerElement).playerProps)
.subscribe((result: PlayerProperties) => {
if (result) {
for (const key in result) {
// @ts-ignore
this.updateElementProperty([element], key, result[key]);
}
}
});
break;