diff --git a/projects/common/services/sanitization.service.spec.ts b/projects/common/services/sanitization.service.spec.ts deleted file mode 100644 index 5b4c2f93986f1d008bb060490ef0520e3a632b7b..0000000000000000000000000000000000000000 --- a/projects/common/services/sanitization.service.spec.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import * as sampleUnit100 from 'test-data/unit-definitions/test100.json'; -import * as sampleUnit126 from 'test-data/unit-definitions/test126.json'; -import * as sampleUnit129 from 'test-data/unit-definitions/test129.json'; -import * as sampleUnit130 from 'test-data/unit-definitions/test130.json'; -import * as sampleUnit126Audio from 'test-data/unit-definitions/test126-audio.json'; -import * as sampleUnit130Audio from 'test-data/unit-definitions/test130-audio.json'; -import * as sampleUnit112Texts from 'test-data/unit-definitions/test112-texts.json'; -import * as sampleUnit112TextFields from 'test-data/unit-definitions/test112-Textfields.json'; -import * as sampleUnit130TextFields from 'test-data/unit-definitions/test130-textfields.json'; -import { Unit } from 'common/models/unit'; -import { ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; -import { APIService } from 'common/shared.module'; -import { SanitizationService } from './sanitization.service'; -import packageJSON from '../../../package.json'; - -describe('SanitizationService', () => { - let service: SanitizationService; - - class ApiStubService { - // eslint-disable-next-line class-methods-use-this - getResourceURL(): string { - return 'assets'; - } - } - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [{ provide: APIService, useClass: ApiStubService }] - }); - service = TestBed.inject(SanitizationService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('isUnitDefinitionOutdated should return false on current version', () => { - const basicUnitDefinition: Partial<Unit> = { - type: 'aspect-unit-definition', - version: packageJSON.config.unit_definition_version, - pages: [] - }; - expect(SanitizationService.isUnitDefinitionOutdated(basicUnitDefinition)).toBe(false); - }); - - it('isUnitDefinitionOutdated should return true on older version', () => { - const unitDefinition: Partial<Unit> = { - type: 'aspect-unit-definition', - version: '3.3.0', - pages: [] - }; - expect(SanitizationService.isUnitDefinitionOutdated(unitDefinition)).toBe(true); - }); - - it('sanitizeUnitDefinition should return basic unit definition', () => { - const basicUnitDefinition: Partial<Unit> = { - type: 'aspect-unit-definition', - version: packageJSON.config.unit_definition_version, - pages: [] - }; - expect(service.sanitizeUnitDefinition(basicUnitDefinition)).toEqual(basicUnitDefinition); - }); - - it('sanitizeUnitDefinition should return position', () => { - const testData130 = JSON.parse(JSON.stringify(sampleUnit130)); - const expectedPositionProps = { - position: { - fixedSize: false, - dynamicPositioning: true, - xPosition: 0, - yPosition: 0, - useMinHeight: false, - gridColumn: 1, - gridColumnRange: 1, - gridRow: 1, - gridRowRange: 1, - marginLeft: 0, - marginRight: 0, - marginTop: 0, - marginBottom: 0, - zIndex: 0 - } - }; - expect(service.sanitizeUnitDefinition(testData130).pages![0].sections[0].elements[0]) - .toEqual(jasmine.objectContaining(expectedPositionProps)); - }); - - it('sanitizeUnitDefinition should repair positionProps', () => { - const expectedPositionProps = { - xPosition: 0, - yPosition: 0, - gridColumn: 1, - gridColumnRange: 1, - gridRow: 1, - gridRowRange: 1 - }; - const sanitizedUnit126 = service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit126))); - expect(Object.keys(sanitizedUnit126.pages![0].sections[0].elements[0])) - .toContain('position'); - expect(sanitizedUnit126.pages![0].sections[0].elements[0].position) - .toEqual(jasmine.objectContaining(expectedPositionProps)); - }); - - // very old stuff - it('sanitizeUnitDefinition should repair position properties outside sub-elements', () => { - const expectedPositionProps = { - xPosition: 420, - yPosition: 69, - gridColumn: 2, - gridColumnRange: 2, - gridRow: 3, - gridRowRange: 1 - }; - const sanitizedUnit100 = service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit100))); - expect(Object.keys(sanitizedUnit100.pages![0].sections[0].elements[0])) - .toContain('position'); - expect(sanitizedUnit100.pages![0].sections[0].elements[0].position) - .toEqual(jasmine.objectContaining(expectedPositionProps)); - }); - - it('sanitizeUnitDefinition should fix dynamicPositioning according to parent section', () => { - // manipulate section prop first, so element needs to be corrected - const unit = sampleUnit126; - unit.pages[0].sections[0].dynamicPositioning = true; - const sanitizedUnit = service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(unit))); - expect(sanitizedUnit.pages![0].sections[0].elements[0].position) - .toEqual(jasmine.objectContaining({ - dynamicPositioning: true - })); - - // no change necessary - const sanitizedUnit129 = service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit129))); - expect(sanitizedUnit129.pages![0].sections[0].elements[0].position) - .toEqual(jasmine.objectContaining({ - dynamicPositioning: false - })); - }); - - it('sanitizeUnitDefinition should return styling', () => { - const expectedStylingProps = { - fontColor: '#000000', - font: 'Roboto', - fontSize: 20, - bold: false, - italic: false, - underline: false, - backgroundColor: 'transparent', - lineHeight: 135 - }; - const sanitizedUnit130 = service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit130))); - expect(Object.keys(sanitizedUnit130.pages![0].sections[0].elements[0])) - .toContain('styling'); - expect(sanitizedUnit130.pages![0].sections[0].elements[0].styling) - .toEqual(jasmine.objectContaining(expectedStylingProps)); - }); - - it('sanitizeUnitDefinition should repair fontProps and surfaceProps', () => { - const expectedStylingProps = { - fontColor: '#000000', - font: 'Roboto', - fontSize: 20, - lineHeight: 135, - bold: false, - italic: false, - underline: false, - backgroundColor: 'red' - }; - const sanitizedUnit126 = service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit126))); - expect(Object.keys(sanitizedUnit126.pages![0].sections[0].elements[0])) - .toContain('styling'); - expect(sanitizedUnit126.pages![0].sections[0].elements[0].styling) - .toEqual(jasmine.objectContaining(expectedStylingProps)); - }); - - it('sanitizeUnitDefinition should return player properties', () => { - const sanitizedUnit130Audio = - service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit130Audio))); - const expectedPlayerProps = { - autostart: false, - autostartDelay: 0, - loop: false, - startControl: true, - pauseControl: false, - progressBar: true, - interactiveProgressbar: false, - volumeControl: true - }; - expect(Object.keys(sanitizedUnit130Audio.pages![0].sections[0].elements[0])) - .toContain('player'); - expect(sanitizedUnit130Audio.pages![0].sections[0].elements[0].player) - .toEqual(jasmine.objectContaining(expectedPlayerProps)); - }); - - it('sanitizeUnitDefinition should repair playerProps', () => { - const sanitizedUnit126Audio = - service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit126Audio))); - const expectedPlayerProps = { - autostart: false, - autostartDelay: 0, - loop: false, - startControl: true, - pauseControl: false, - progressBar: true, - interactiveProgressbar: false, - volumeControl: true, - defaultVolume: 0.8, - minVolume: 0, - muteControl: true - }; - expect(Object.keys(sanitizedUnit126Audio.pages![0].sections[0].elements[0])) - .toContain('player'); - expect(sanitizedUnit126Audio.pages![0].sections[0].elements[0].player) - .toEqual(jasmine.objectContaining(expectedPlayerProps)); - }); - - it('sanitizeUnitDefinition should repair text element - interaction: "none"', () => { - const sanitizedUnit112Texts = - service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit112Texts))); - const expectedTextProps = { - highlightableYellow: false, - highlightableTurquoise: false, - highlightableOrange: false - }; - expect(sanitizedUnit112Texts.pages![0].sections[0].elements[1]) - .not.toEqual(jasmine.objectContaining(expectedTextProps)); - }); - - it('sanitizeUnitDefinition should repair text element - interaction: "highlightable"', () => { - const sanitizedUnit112Texts = - service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit112Texts))); - const expectedTextProps = { - highlightableYellow: true, - highlightableTurquoise: true, - highlightableOrange: true - }; - expect(sanitizedUnit112Texts.pages![0].sections[0].elements[0]) - .toEqual(jasmine.objectContaining(expectedTextProps)); - }); - - it('sanitizeUnitDefinition should repair text element - interaction: "strikable"', () => { - const sanitizedUnit112Texts = - service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit112Texts))); - const expectedTextProps = { - highlightableYellow: true, - highlightableTurquoise: false, - highlightableOrange: false - }; - expect(sanitizedUnit112Texts.pages![0].sections[0].elements[2]) - .not.toEqual(jasmine.objectContaining(expectedTextProps)); - }); - - it('sanitizeUnitDefinition should return text field element', () => { - const sanitizedUnit130TextFields = - service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit130TextFields))); - expect(sanitizedUnit130TextFields.pages![0].sections[0].elements[0]) - .toEqual(jasmine.objectContaining({ - inputAssistancePreset: null, - inputAssistancePosition: 'floating', - restrictedToInputAssistanceChars: false - })); - expect(sanitizedUnit130TextFields.pages![0].sections[0].elements[1]) - .toEqual(jasmine.objectContaining({ - inputAssistancePreset: 'french', - inputAssistancePosition: 'floating', - restrictedToInputAssistanceChars: true - })); - expect(sanitizedUnit130TextFields.pages![0].sections[0].elements[2]) - .toEqual(jasmine.objectContaining({ - inputAssistancePreset: 'numbers', - inputAssistancePosition: 'floating', - restrictedToInputAssistanceChars: false - })); - }); - - // only french inputAssistance is corrected to false, since the default is true - it('sanitizeUnitDefinition should repair text field and area elements', () => { - const sanitizedUnit112TextFields = - service.sanitizeUnitDefinition(JSON.parse(JSON.stringify(sampleUnit112TextFields))); - expect(sanitizedUnit112TextFields.pages![0].sections[0].elements[0]) - .toEqual(jasmine.objectContaining({ - inputAssistancePreset: null, - inputAssistancePosition: 'floating' - })); - expect(sanitizedUnit112TextFields.pages![0].sections[0].elements[0]) - .not.toEqual(jasmine.objectContaining({ - restrictedToInputAssistanceChars: false - })); - expect(sanitizedUnit112TextFields.pages![0].sections[0].elements[1]) - .toEqual(jasmine.objectContaining({ - inputAssistancePreset: 'french', - inputAssistancePosition: 'right', - restrictedToInputAssistanceChars: false - })); - expect(sanitizedUnit112TextFields.pages![0].sections[0].elements[2]) - .toEqual(jasmine.objectContaining({ - inputAssistancePreset: 'numbers', - inputAssistancePosition: 'floating' - })); - expect(sanitizedUnit112TextFields.pages![0].sections[0].elements[2]) - .not.toEqual(jasmine.objectContaining({ - restrictedToInputAssistanceChars: false - })); - // text areas - expect(sanitizedUnit112TextFields.pages![0].sections[0].elements[3]) - .toEqual(jasmine.objectContaining({ - inputAssistancePreset: null, - inputAssistancePosition: 'floating' - })); - expect(sanitizedUnit112TextFields.pages![0].sections[0].elements[3]) - .not.toEqual(jasmine.objectContaining({ - restrictedToInputAssistanceChars: false - })); - expect(sanitizedUnit112TextFields.pages![0].sections[0].elements[4]) - .toEqual(jasmine.objectContaining({ - inputAssistancePreset: 'french', - inputAssistancePosition: 'floating', - restrictedToInputAssistanceChars: false - })); - }); - - it('should rename cloze children with "simple" affix', () => { - const unitDefStub: Record<string, any> = { - type: 'aspect-unit-definition', - version: '3.4.0', - pages: [{ - sections: [ - { - elements: [] - }] - }] - }; - - const unitDef = { - ...unitDefStub, - pages: [{ - sections: [ - { - elements: [{ - id: 'cloze_1', - type: 'cloze', - document: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'TextField', - attrs: { - model: { - id: 'text-field_1', - type: 'text-field' - } - } - }, - { - type: 'DropList', - attrs: { - model: { - id: 'drop-list_1', - type: 'drop-list' - } - } - }, - { - type: 'ToggleButton', - attrs: { - model: { - id: 'toggle_button_1', - type: 'toggle-button' - } - } - }] - }] - } - } - ] - }] - }] - }; - const sanitizedUnitDefinition = service.sanitizeUnitDefinition(unitDef as unknown as Unit); - - expect(sanitizedUnitDefinition.pages![0].sections[0].elements[0]) - .toEqual(jasmine.objectContaining({ - id: 'cloze_1', - type: 'cloze' - })); - - const sanatizedClozeChild1 = (sanitizedUnitDefinition.pages![0].sections[0].elements[0] as ClozeElement) - .document.content[0].content[0]?.attrs?.model; - expect(sanatizedClozeChild1) - .toEqual(jasmine.objectContaining({ - id: 'text-field_1', - type: 'text-field-simple' - })); - - const sanatizedClozeChild2 = (sanitizedUnitDefinition.pages![0].sections[0].elements[0] as ClozeElement) - .document.content[0].content[1]?.attrs?.model; - expect(sanatizedClozeChild2) - .toEqual(jasmine.objectContaining({ - id: 'drop-list_1', - type: 'drop-list' - })); - - const sanatizedClozeChild3 = (sanitizedUnitDefinition.pages![0].sections[0].elements[0] as ClozeElement) - .document.content[0].content[2]?.attrs?.model; - expect(sanatizedClozeChild3) - .toEqual(jasmine.objectContaining({ - id: 'toggle_button_1', - type: 'toggle-button' - })); - }); -}); diff --git a/projects/common/services/sanitization.service.ts b/projects/common/services/sanitization.service.ts deleted file mode 100644 index 153862d326221f13da5cff6c6a96bd826595cb12..0000000000000000000000000000000000000000 --- a/projects/common/services/sanitization.service.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Editor } from '@tiptap/core'; -import ToggleButtonExtension from - 'common/models/elements/compound-elements/cloze/tiptap-editor-extensions/toggle-button'; -import DropListExtension from 'common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list'; -import TextFieldExtension from 'common/models/elements/compound-elements/cloze/tiptap-editor-extensions/text-field'; -import { Unit } from 'common/models/unit'; -import { - InputElement, PositionedUIElement, UIElement, UIElementValue -} from 'common/models/elements/element'; -import { LikertElement } from 'common/models/elements/compound-elements/likert/likert'; -import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; -import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row'; -import { TextElement } from 'common/models/elements/text/text'; -import { - ClozeDocument, - ClozeDocumentWrapperNode, - ClozeDocumentContentNode, - ClozeElement -} from 'common/models/elements/compound-elements/cloze/cloze'; -import { DropListElement } from 'common/models/elements/input-elements/drop-list'; -import { Page } from 'common/models/page'; -import { Section } from 'common/models/section'; -import { RadioButtonGroupComplexElement } from 'common/models/elements/input-elements/radio-button-group-complex'; -import { RadioButtonGroupElement } from 'common/models/elements/input-elements/radio-button-group'; -import { MessageService } from 'editor/src/app/services/message.service'; -import { IDService } from 'editor/src/app/services/id.service'; -import { - BasicStyles, - PlayerProperties, - PositionProperties -} from 'common/models/elements/property-group-interfaces'; -import { DragNDropValueObject, TextImageLabel } from 'common/models/elements/label-interfaces'; -import packageJSON from '../../../package.json'; - -@Injectable({ - providedIn: 'root' -}) -export class SanitizationService { - private static expectedUnitVersion: [number, number, number] = - packageJSON.config.unit_definition_version.split('.') as unknown as [number, number, number]; - - private static unitDefinitionVersion: [number, number, number] | undefined; - - repairLog: string[] = []; - - // TODO: isUnitDefinitionOutdated must not set the unitDefinitionVersion - static isUnitDefinitionOutdated(unitDefinition: Partial<Unit>): boolean { - SanitizationService.unitDefinitionVersion = - SanitizationService.readUnitDefinitionVersion(unitDefinition as unknown as Record<string, string>); - return SanitizationService.isVersionOlderThanCurrent(SanitizationService.unitDefinitionVersion); - } - - sanitizeUnitDefinition(unitDefinition: Partial<Unit>): Partial<Unit> { - return { - ...unitDefinition, - pages: unitDefinition.pages?.map((page: Page) => this.sanitizePage(page)) as Page[] - }; - } - - checkAndRepairIDs(unitDefinition: Partial<Unit>, idManager: IDService, messageService: MessageService): Partial<Unit> { - this.repairLog = []; - unitDefinition.pages?.forEach(page => { - page.sections.forEach(section => { - section.elements.forEach(element => { - this.checkIDList(element, idManager, messageService); - if (element.type === 'likert') { - (element as LikertElement).rows.forEach(row => this.checkIDList(row, idManager, messageService)); - } - if (element.type === 'cloze') { - ClozeElement.getDocumentChildElements((element as ClozeElement).document) - .forEach(clozeChild => this.checkIDList(clozeChild, idManager, messageService)); - } - }); - }); - }); - if (this.repairLog.length > 0) { - messageService.showPrompt( - `Doppelte IDs gefunden: \n${this.repairLog.join('\n')}\n\n Es wurden neue IDs generiert.`); - } - idManager.reset(); - return unitDefinition; - } - - private checkIDList(element: UIElement | DragNDropValueObject, - idManager: IDService, - messageService: MessageService): void { - if (!idManager.isIdAvailable(element.id)) { - console.warn(`Id already in: ${element.id}! Generating a new one...`); - this.repairLog.push(element.id); - element.id = idManager.getAndRegisterNewID((element as UIElement).type || 'value'); - } - idManager.addID(element.id); - - if (['drop-list', 'drop-list-simple'].includes((element as UIElement).type as string)) { - (element as DropListElement).value.forEach(value => { - this.checkIDList(value, idManager, messageService); - }); - } - } - - private static readUnitDefinitionVersion(unitDefinition: Record<string, string>): [number, number, number] { - return ( - unitDefinition.version || - (unitDefinition.unitDefinitionType && unitDefinition.unitDefinitionType.split('@')[1]) || - (unitDefinition.veronaModuleVersion && unitDefinition.veronaModuleVersion.split('@')[1])) - .split('.') as unknown as [number, number, number]; - } - - private static isVersionOlderThanCurrent(version: [number, number, number]): boolean { - if (!version) return true; - if (Number(version[0]) < Number(SanitizationService.expectedUnitVersion[0])) { - return true; - } - if (Number(version[1]) < Number(SanitizationService.expectedUnitVersion[1])) { - return true; - } - return Number(version[2]) < Number(SanitizationService.expectedUnitVersion[2]); - } - - private sanitizePage(page: Page): Partial<Page> { - return { - ...page, - sections: page.sections.map((section: Section) => this.sanitizeSection(section)) - }; - } - - /* Transform grid sizes from string to array with value and unit. */ - private sanitizeSection(section: any): Section { - return { - ...section, - gridColumnSizes: typeof section.gridColumnSizes === 'string' ? - (section.gridColumnSizes as string) - .split(' ') - .map(size => ({ value: size.slice(0, -2), unit: size.slice(-2) })) : - section.gridColumnSizes, - gridRowSizes: typeof section.gridRowSizes === 'string' ? - (section.gridRowSizes as string) - .split(' ') - .map(size => ({ value: size.slice(0, -2), unit: size.slice(-2) })) : - section.gridRowSizes, - elements: section.elements.map((element: UIElement) => ( - this.sanitizeElement( - element as Record<string, UIElementValue>, - section.dynamicPositioning - ))) as PositionedUIElement[] - } as Section; - } - - private sanitizeElement(element: Record<string, UIElementValue>, - sectionDynamicPositioning?: boolean): UIElement { - let newElement: Partial<UIElement> = { - ...element, - position: SanitizationService.getPositionProps(element, sectionDynamicPositioning), - styling: SanitizationService.getStyleProps(element) as unknown as BasicStyles, - player: SanitizationService.getPlayerProps(element) - }; - if (newElement.type === 'text') { - newElement = SanitizationService.handleTextElement(newElement as Record<string, UIElementValue>); - } - if (['text-field', 'text-area', 'text-field-simple', 'spell-correct'] - .includes(newElement.type as string)) { - newElement = SanitizationService.sanitizeTextInputElement(newElement as Record<string, UIElementValue>); - } - if (newElement.type === 'cloze') { - newElement = this.handleClozeElement(newElement as Record<string, UIElementValue>); - } - if (newElement.type === 'toggle-button') { - newElement = SanitizationService.handleToggleButtonElement(newElement as Record<string, UIElementValue>); - } - if (['drop-list', 'drop-list-simple'].includes(newElement.type as string)) { - newElement = SanitizationService.handleDropListElement(newElement as Record<string, UIElementValue>); - } - if (['radio'].includes(newElement.type as string)) { - newElement = SanitizationService.handleRadioGroup(newElement as RadioButtonGroupElement); - } - if (['radio-group-images'].includes(newElement.type as string)) { - newElement = SanitizationService.fixImageLabel(newElement as RadioButtonGroupComplexElement); - } - if (['likert'].includes(newElement.type as string)) { - newElement = this.handleLikertElement(newElement as LikertElement); - } - if (['likert-row', 'likert_row'].includes(newElement.type as string)) { - newElement = SanitizationService.handleLikertRowElement(newElement as Record<string, UIElementValue>); - } - - return newElement as unknown as UIElement; - } - - private static getPositionProps(element: Record<string, any>, - sectionDynamicPositioning?: boolean): PositionProperties { - if (element.position && element.position.gridColumnEnd) { - return { - ...element.position, - dynamicPositioning: sectionDynamicPositioning, - gridColumn: element.position.gridColumn !== undefined ? - element.position.gridColumn : element.position.gridColumnStart, - gridColumnRange: element.position.gridColumnEnd - element.position.gridColumnStart, - gridRow: element.position.gridRow !== undefined ? - element.position.gridRow : element.position.gridRowStart, - gridRowRange: element.position.gridRowEnd - element.position.gridRowStart - }; - } - if (element.position) { - return { - ...element.position, - dynamicPositioning: sectionDynamicPositioning - }; - } - if (element.positionProps) { - return { - ...element.positionProps, - dynamicPositioning: sectionDynamicPositioning, - gridColumn: element.positionProps.gridColumn !== undefined ? - element.positionProps.gridColumn : element.positionProps.gridColumnStart, - gridColumnRange: element.positionProps.gridColumnEnd - element.positionProps.gridColumnStart, - gridRow: element.positionProps.gridRow !== undefined ? - element.positionProps.gridRow : element.positionProps.gridRowStart, - gridRowRange: element.positionProps.gridRowEnd - element.positionProps.gridRowStart - }; - } - return { - ...element, - gridColumn: element.gridColumn !== undefined ? - element.gridColumn : element.gridColumnStart, - gridColumnRange: element.gridColumnEnd - element.gridColumnStart, - gridRow: element.gridRow !== undefined ? - element.gridRow : element.gridRowStart, - gridRowRange: element.gridRowEnd - element.gridRowStart - } as PositionProperties; - } - - /* Style properties are expected to be in 'stylings'. If not they may be in fontProps and/or - * surfaceProps. Even older versions had them in the root of the object, which is uses as last resort. - * The styles object then has all other properties of the element, but that is not a problem - * since the factory methods only use the values they care for and all others are discarded. */ - private static getStyleProps(element: Record<string, UIElementValue>): Record<string, UIElementValue> { - if (element.styling !== undefined) { - return element.styling as Record<string, UIElementValue>; - } - if (element.fontProps !== undefined) { - return { - ...(element.fontProps as Record<string, any>), - // additional props that were neither fontProp nor surfaceProp before - backgroundColor: (element.surfaceProps as Record<string, any>)?.backgroundColor, - borderRadius: element.borderRadius as number | undefined, - itemBackgroundColor: element.itemBackgroundColor as string | undefined, - borderWidth: element.borderWidth as number | undefined, - borderColor: element.borderColor as string | undefined, - borderStyle: element.borderStyle as - 'solid' | 'dotted' | 'dashed' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | undefined, - lineColoring: element.lineColoring as boolean | undefined, - lineColoringColor: element.lineColoringColor as string | undefined - }; - } - return element; - } - - private static getPlayerProps(element: Record<string, UIElementValue>): PlayerProperties { - if (element.playerProps !== undefined) { - return element.playerProps as PlayerProperties; - } - if (element.player !== undefined) { - return element.player as PlayerProperties; - } - return element as unknown as PlayerProperties; - } - - private static handleTextElement(element: Record<string, UIElementValue>): TextElement { - const newElement = { ...element }; - if (newElement.highlightable || newElement.interaction === 'highlightable') { - newElement.highlightableYellow = true; - newElement.highlightableTurquoise = true; - newElement.highlightableOrange = true; - } - if (newElement.interaction === 'underlinable') { - newElement.highlightableYellow = true; - } - return newElement as TextElement; - } - - private static sanitizeTextInputElement(element: Record<string, UIElementValue>): InputElement { - const newElement = { ...element }; - if (newElement.restrictedToInputAssistanceChars === undefined && newElement.inputAssistancePreset === 'french') { - newElement.restrictedToInputAssistanceChars = false; - } - if (newElement.inputAssistancePreset === 'none') { - newElement.inputAssistancePreset = null; - } - return newElement as InputElement; - } - - /* - Replace raw text with backslash-markers with HTML tags. - The TipTap editor module can create JSOM from the HTML. It needs plugins though to be able - to create ui-elements. - Afterwards element models are added to the JSON. - */ - private handleClozeElement(element: Record<string, UIElementValue>): ClozeElement { - if (!element.document && (!element.parts || !element.text)) throw Error('Can\'t read Cloze Element'); - - let childElements: UIElement[]; - let doc: ClozeDocument; - - // TODO: create a sub method - if (element.document) { - doc = element.document as ClozeDocument; - childElements = ClozeElement.getDocumentChildElements(doc); - } else { - childElements = (element.parts as any[]) - .map((el: any) => el - .filter((el2: { type: string; }) => [ - 'text-field', 'text-field-simple', 'drop-list', 'drop-list-simple', 'toggle-button' - ].includes(el2.type)).value) - .flat(); - doc = SanitizationService.createClozeDocument(element); - } - - // TODO: and create a sub method - // repair child element types - childElements.forEach(childElement => { - childElement.type = childElement.type === 'text-field' ? 'text-field-simple' : childElement.type; - }); - - return { - ...element, - // document: { - // ...doc, - // content: doc.content - // .map((paragraph: ClozeDocumentWrapperNode) => ({ - // ...paragraph, - // content: paragraph.content ? paragraph.content - // .map((paraPart: ClozeDocumentContentNode) => ( - // ['TextField', 'DropList', 'ToggleButton', 'Button'].includes(paraPart.type) ? - // { - // ...paraPart, - // attrs: { - // ...paraPart.attrs, - // model: this.sanitizeElement(childElements.shift() as Record<string, UIElementValue>) - // } - // } : - // { - // ...paraPart - // } - // )) : undefined - // })) - // } as ClozeDocument - } as ClozeElement; - } - - private static createClozeDocument(element: Record<string, UIElementValue>): ClozeDocument { - const replacedText = (element.text as string).replace(/\\i|\\z|\\r/g, (match: string) => { - switch (match) { - case '\\i': - return '<aspect-nodeview-text-field></aspect-nodeview-text-field>'; - case '\\z': - return '<aspect-nodeview-drop-list></aspect-nodeview-drop-list>'; - case '\\r': - return '<aspect-nodeview-toggle-button></aspect-nodeview-toggle-button>'; - default: - throw Error('error in match'); - } - }); - - const editor = new Editor({ - extensions: [ToggleButtonExtension, DropListExtension, TextFieldExtension], - content: replacedText - }); - return editor.getJSON() as ClozeDocument; - } - - /* before: simple string[]; after: DragNDropValueObject with ID and value. - * Needs to be done to selectable options and the possibly set preset (value). */ - private static handleDropListElement(element: Record<string, UIElementValue>): DropListElement { - const newElement = { ...element }; - if (newElement.options) { - console.warn('New dropList value IDs have been generated'); - newElement.value = []; - (newElement.options as string[]).forEach((option, index) => { - (newElement.value as DragNDropValueObject[]).push({ - id: 'id_placeholder', - text: option, - imgSrc: null, - audioSrc: null, - imgPosition: 'above', - originListID: newElement.id as string, - originListIndex: index - }); - }); - } - if (newElement.value && - (newElement.value as []).length > 0 && - !((newElement.value as DragNDropValueObject[])[0] instanceof Object)) { - const newValues: DragNDropValueObject[] = []; - (newElement.value as string[]).forEach((value, index) => { - newValues.push({ - id: 'id_placeholder', - text: value, - imgSrc: null, - audioSrc: null, - imgPosition: 'above', - originListID: newElement.id as string, - originListIndex: index - }); - }); - // fix DragNDropValueObject stringValue -> text - // imgSrcValue -> imgSrc - (newElement as DropListElement).value.forEach((valueObject: any) => { - valueObject.text = valueObject.text || valueObject.stringValue; - valueObject.imgSrc = valueObject.text || valueObject.imgSrcValue; - valueObject.imgPosition = valueObject.imgPosition || valueObject.position; - }); - newElement.value = newValues; - } - - if (newElement.value && (newElement.value as any)[0]?.stringValue !== undefined) { - type OldDragNDropValueObject = { - id: string; - stringValue?: string; - imgSrcValue?: string; - }; - newElement.value = (newElement.value as OldDragNDropValueObject[]) - .map((value: OldDragNDropValueObject, index) => ({ - text: value.stringValue, - id: value.id, - imgSrc: value.imgSrcValue, - imgPosition: 'above' - } as DragNDropValueObject)); - } - // originListID and originListIndex are mandatory and need to be added to all values - if (newElement.value && - (newElement.value as DragNDropValueObject[]).length && - !(newElement.value as DragNDropValueObject[])[0].originListID) { - newElement.value = (newElement.value as DragNDropValueObject[]) - .map((value: DragNDropValueObject, index) => ({ - ...value, - originListID: newElement.id as string, - originListIndex: index - } as DragNDropValueObject)); - } - return newElement as DropListElement; - } - - private handleLikertElement(element: Partial<LikertElement>): LikertElement { - element.options = element.options || element.columns as any; - return { - ...element, - options: element.options?.map(option => ({ - text: option.text, - imgSrc: option.imgSrc, - imgPosition: option.imgPosition || (option as any).position - })), - rows: element.rows - ?.map((row: LikertRowElement) => ( - this.sanitizeElement(row as Record<string, UIElementValue>) as LikertRowElement)) - } as LikertElement; - } - - private static handleLikertRowElement(element: Record<string, UIElementValue>): Partial<LikertRowElement> { - return { - ...element, - rowLabel: { - text: (element.rowLabel as TextImageLabel).text, - imgSrc: (element.rowLabel as TextImageLabel).imgSrc, - imgPosition: (element.rowLabel as TextImageLabel).imgPosition || element.position || 'above' - } as TextImageLabel - }; - } - - private static handleToggleButtonElement(element: Record<string, UIElementValue>): ToggleButtonElement { - // if (element.richTextOptions) { - // return new ToggleButtonElement({ - // ...element, - // options: (element.richTextOptions as string[]) - // .map(richTextOption => ({ text: richTextOption })) - // }); - // } - // if (element.options && (element.options as unknown[]).length) { - // if (typeof (element.options as unknown[])[0] === 'string') { - // return new ToggleButtonElement({ - // ...element, - // options: (element.options as string[]) - // .map(options => ({ text: options })) - // }); - // } - // } - return element as ToggleButtonElement; - } - - private static fixImageLabel(element: Partial<RadioButtonGroupComplexElement>): RadioButtonGroupComplexElement { - return { - ...element, - options: element.options ? - element.options : - (element.columns as TextImageLabel[]) - .map((column: TextImageLabel) => ({ - ...column, imgPosition: column.imgPosition || (column as any).position || 'above' - })) - } as RadioButtonGroupComplexElement; - } - - private static handleRadioGroup(element: Partial<RadioButtonGroupElement>): RadioButtonGroupElement { - return { - ...element, - options: element.options ? - element.options : - (element.richTextOptions as string[]) - .map((option: string) => ({ - text: option - })) - } as RadioButtonGroupElement; - } -}