Skip to content
Snippets Groups Projects
unit.service.ts 17 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { Injectable } from '@angular/core';
    
    rhenck's avatar
    rhenck committed
    import { DomSanitizer } from '@angular/platform-browser';
    
    rhenck's avatar
    rhenck committed
    import { Subject } from 'rxjs';
    
    import { TranslateService } from '@ngx-translate/core';
    
    rhenck's avatar
    rhenck committed
    import { FileService } from './file.service';
    
    import { MessageService } from '../../../../common/services/message.service';
    
    rhenck's avatar
    rhenck committed
    import { IdService } from './id.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';
    
    rhenck's avatar
    rhenck committed
      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';
    
    rhenck's avatar
    rhenck committed
    import { ClozeParser } from '../util/cloze-parser';
    
    import { Copy } from '../../../../common/util/copy';
    
    import { ClozeElement } from '../../../../common/ui-elements/cloze/cloze-element';
    
    
    @Injectable({
      providedIn: 'root'
    })
    export class UnitService {
    
    rhenck's avatar
    rhenck committed
      unit: Unit;
    
      elementPropertyUpdated: Subject<void> = new Subject<void>();
    
      pageMoved: Subject<void> = new Subject<void>();
    
      constructor(private selectionService: SelectionService,
    
                  private idService: IdService,
    
                  private veronaApiService: VeronaAPIService,
    
                  private messageService: MessageService,
    
                  private dialogService: DialogService,
    
                  private sanitizer: DomSanitizer,
                  private translateService: TranslateService) {
    
    rhenck's avatar
    rhenck committed
        this.unit = new Unit();
    
      loadUnitDefinition(unitDefinition: string): void {
        if (unitDefinition) {
    
    rhenck's avatar
    rhenck committed
          this.unit = new Unit(JSON.parse(unitDefinition));
    
    rhenck's avatar
    rhenck committed
        this.unit.addPage();
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
    
      deletePage(page: Page): void {
    
    rhenck's avatar
    rhenck committed
        this.unit.deletePage(page);
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
    
      movePage(selectedPage: Page, direction: 'up' | 'down'): void {
    
    rhenck's avatar
    rhenck committed
        this.unit.movePage(selectedPage, direction);
    
        this.pageMoved.next();
        this.veronaApiService.sendVoeDefinitionChangedNotification();
    
      updatePageProperty(page: Page, property: string, value: number | boolean): void {
    
        if (property === 'alwaysVisible' && value === true) {
          this.handlePageAlwaysVisiblePropertyChange(page);
    
        page[property] = value;
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
      }
    
    
      private handlePageAlwaysVisiblePropertyChange(page: Page): void {
    
    rhenck's avatar
    rhenck committed
        const pageIndex = this.unit.pages.indexOf(page);
    
        if (pageIndex !== 0) {
    
    rhenck's avatar
    rhenck committed
          this.unit.movePageToTop(pageIndex, page);
    
          this.pageMoved.next();
    
        page.alwaysVisible = true;
    
    rhenck's avatar
    rhenck committed
      addSection(page: Page): void {
        page.addSection();
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
    
      deleteSection(section: Section): void {
    
    rhenck's avatar
    rhenck committed
        this.unit.pages[this.selectionService.selectedPageIndex].deleteSection(section);
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
      }
    
    
      duplicateSection(section: Section, page: Page, sectionIndex: number): void {
    
    rhenck's avatar
    rhenck committed
        const newSection = new Section(section);
        newSection.elements.forEach((element: UIElement) => {
    
          element.id = this.idService.getNewID(element.type);
    
    rhenck's avatar
    rhenck committed
        });
        page.sections.splice(sectionIndex + 1, 0, newSection);
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
      }
    
    
      moveSection(section: Section, page: Page, direction: 'up' | 'down'): void {
    
    rhenck's avatar
    rhenck committed
        page.moveSection(section, direction);
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
    
    rhenck's avatar
    rhenck committed
      addElementToSectionByIndex(elementType: UIElementType,
                                 pageIndex: number,
                                 sectionIndex: number): void {
    
    rhenck's avatar
    rhenck committed
        this.addElementToSection(elementType, this.unit.pages[pageIndex].sections[sectionIndex]);
    
    rhenck's avatar
    rhenck committed
      async addElementToSection(elementType: UIElementType,
    
                                section: Section,
    
                                coordinates?: { x: number, y: number }): Promise<void> {
    
        let newElement: PositionedElement;
    
    rhenck's avatar
    rhenck committed
        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;
    
    rhenck's avatar
    rhenck committed
        } else {
    
          newElement = ElementFactory.createElement({
            type: elementType,
    
            id: this.idService.getNewID(elementType),
    
            positionProps: {
              dynamicPositioning: section.dynamicPositioning
            }
          } as unknown as Partial<UIElement>) as PositionedElement;
    
    rhenck's avatar
    rhenck committed
        }
        if (coordinates && section.dynamicPositioning) {
    
          newElement.positionProps.gridColumnStart = coordinates.x;
          newElement.positionProps.gridColumnEnd = coordinates.x + 1;
          newElement.positionProps.gridRowStart = coordinates.y;
          newElement.positionProps.gridRowEnd = coordinates.y + 1;
    
    rhenck's avatar
    rhenck committed
        } else if (coordinates && !section.dynamicPositioning) {
    
          newElement.positionProps.xPosition = coordinates.x;
          newElement.positionProps.yPosition = coordinates.y;
    
    rhenck's avatar
    rhenck committed
        }
    
        section.addElement(newElement as PositionedElement);
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
    
      deleteElements(elements: UIElement[]): void {
    
        this.freeUpIds(elements);
    
    rhenck's avatar
    rhenck committed
        this.unit.pages[this.selectionService.selectedPageIndex].sections.forEach(section => {
    
          section.deleteElements(elements);
        });
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
    
      private freeUpIds(elements: UIElement[]): void {
    
    rhenck's avatar
    rhenck committed
        elements.forEach(element => {
          if (element.type === 'drop-list') {
    
    rhenck's avatar
    rhenck committed
            element.value.forEach((value: DragNDropValueObject) => {
    
              this.idService.removeId(value.id);
    
          this.idService.removeId(element.id);
    
      /* 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 {
    
    rhenck's avatar
    rhenck committed
        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');
            });
          }
    
    rhenck's avatar
    rhenck committed
          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,
    
    rhenck's avatar
    rhenck committed
                            value: InputElementValue | LikertColumn[] | LikertRow[] | ClozeDocument |
    
                            DragNDropValueObject[] | null): boolean {
    
    rhenck's avatar
    rhenck committed
        // console.log('updateElementProperty', elements, property, value);
    
        for (const element of elements) {
    
          if (property === 'id') {
    
            if (!this.idService.isIdAvailable((value as string))) { // prohibit existing IDs
    
              this.messageService.showError(this.translateService.instant('idTaken'));
    
            this.idService.removeId(element.id);
    
    rhenck's avatar
    rhenck committed
            this.idService.addID(value as string);
            element.setProperty('id', value);
    
    rhenck's avatar
    rhenck committed
          } else if (property === 'document') {
            element.setProperty('document', ClozeParser.setMissingIDs(
              value as ClozeDocument,
              this.idService
            ));
    
    rhenck's avatar
    rhenck committed
          } else {
    
            element.setProperty(property, Copy.getCopy(value));
    
        this.elementPropertyUpdated.next();
    
        this.veronaApiService.sendVoeDefinitionChangedNotification();
    
    rhenck's avatar
    rhenck committed
      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) {
    
              if (result.id !== row.id) {
    
                this.updateElementProperty(
    
              if (result.text !== row.text) {
    
                this.updateElementProperty(
    
              if (result.value !== row.value) {
                this.updateElementProperty(
                  [row],
                  'value',
                  result.value
                );
              }
    
    rhenck's avatar
    rhenck committed
      async editLikertColumn(likertElements: LikertElement[], columnIndex: number): Promise<void> {
        await this.dialogService.showLikertColumnEditDialog(likertElements[0].columns[columnIndex])
    
          .subscribe((result: LikertColumn) => {
    
    rhenck's avatar
    rhenck committed
              likertElements[0].columns[columnIndex] = result;
    
              this.updateElementProperty(
                likertElements,
    
    rhenck's avatar
    rhenck committed
                'columns',
    
                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
        );
    
    rhenck's avatar
    rhenck committed
      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();
    
    rhenck's avatar
    rhenck committed
        FileService.saveUnitToFile(JSON.stringify(this.unit));
    
      async loadUnitFromFile(): Promise<void> {
        this.loadUnitDefinition(await FileService.loadFile(['.json']));
    
      showDefaultEditDialog(element: UIElement): void {
    
        switch (element.type) {
          case 'button':
          case 'dropdown':
    
          case 'checkbox':
          case 'radio':
    
            this.dialogService.showTextEditDialog(element.label).subscribe((result: string) => {
    
              if (result) {
    
                this.updateElementProperty([element], 'label', result);
    
              }
            });
            break;
          case 'text':
    
            this.dialogService.showRichTextEditDialog(
    
              (element as TextElement).text,
    
              (element as TextElement).fontProps.fontSize as number
    
            ).subscribe((result: string) => {
    
              if (result) {
    
                // TODO add proper sanitization
                this.updateElementProperty(
                  [element],
                  'text',
                  (this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string
                );
    
    rhenck's avatar
    rhenck committed
          case 'cloze':
    
    rhenck's avatar
    rhenck committed
            this.dialogService.showClozeTextEditDialog(
              element.document,
    
              (element as ClozeElement).fontProps.fontSize as number
    
    rhenck's avatar
    rhenck committed
            ).subscribe((result: string) => {
    
    rhenck's avatar
    rhenck committed
              if (result) {
                // TODO add proper sanitization
                this.updateElementProperty(
                  [element],
    
    rhenck's avatar
    rhenck committed
                  'document',
    
    rhenck's avatar
    rhenck committed
                  (this.sanitizer.bypassSecurityTrustHtml(result) as any).changingThisBreaksApplicationSecurity as string
                );
              }
            });
            break;
    
          case 'text-field':
    
            this.dialogService.showTextEditDialog((element as InputElement).value as string).subscribe((result: string) => {
    
              if (result) {
    
                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);
                }
              });
    
            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;
    
          // no default
    
    
      getNewValueID(): string {
    
        return this.idService.getNewID('value');