diff --git a/angular.json b/angular.json index 33fee26da5fa2ed1306fc5725a1b05e7a9a945e6..3ef8f9824a92ba6f592c29f2d8b1a460f139f611 100644 --- a/angular.json +++ b/angular.json @@ -61,6 +61,10 @@ { "replace": "projects/editor/src/environments/environment.ts", "with": "projects/editor/src/environments/environment.prod.ts" + }, + { + "replace": "projects/common/environment.ts", + "with": "projects/editor/src/environments/environment.ts" } ], "outputHashing": "all" @@ -71,7 +75,13 @@ "vendorChunk": true, "extractLicenses": false, "sourceMap": true, - "namedChunks": true + "namedChunks": true, + "fileReplacements": [ + { + "replace": "projects/common/environment.ts", + "with": "projects/editor/src/environments/environment.ts" + } + ] } }, "defaultConfiguration": "production" @@ -114,7 +124,13 @@ "projects/editor/src/styles.css", "projects/common/assets/customTheme.scss" ], - "scripts": [] + "scripts": [], + "fileReplacements": [ + { + "replace": "projects/common/environment.ts", + "with": "projects/editor/src/environments/environment.ts" + } + ] } } } @@ -229,7 +245,13 @@ "styles": [ "projects/player/src/styles.css" ], - "scripts": [] + "scripts": [], + "fileReplacements": [ + { + "replace": "projects/common/environment.ts", + "with": "projects/editor/src/environments/environment.ts" + } + ] } } } diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt index 691f250225f35e5e95046a65e4ef38ddccafa2da..d3056abb4752e82d88e2bde5f4b397438aeb954f 100644 --- a/docs/unit_definition_changelog.txt +++ b/docs/unit_definition_changelog.txt @@ -73,7 +73,19 @@ iqb-aspect-definition@1.0.0 - DropList: +allowReplacement - DragAndDropValueObject: -returnToOriginOnReplacement; originListID and originListIndex are now mandatory -3.12.0 +4.0.0 +- UIElement: + - remove width + - remove height + - add DimensionProperties: + width: number; + height: number; + isWidthFixed: boolean; + isHeightFixed: boolean; + minWidth: number | null; + maxWidth: number | null; + minHeight: number | null; + maxHeight: number | null; - PositionProperties: - remove "dynamicPositioning" - remove "fixedSize" diff --git a/package.json b/package.json index f529aa01a63052edffe500261c9f0fc2fe036671..d42ff4f77a1c097bd5b85850204f984c907fb51d 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "verona-modules-aspect", "config": { - "player_version": "1.32.0", - "editor_version": "1.39.0", - "unit_definition_version": "3.11.0" + "player_version": "2.0.0", + "editor_version": "2.0.0", + "unit_definition_version": "4.0.0" }, "scripts": { "ng": "ng", @@ -119,4 +119,4 @@ "karma-spec-reporter": "^0.0.36", "typescript": "~4.9.5" } -} \ No newline at end of file +} diff --git a/projects/common/components/unit-def-error-dialog.component.ts b/projects/common/components/unit-def-error-dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e97cc0302fd192f1e2752ace0f17326fe3283732 --- /dev/null +++ b/projects/common/components/unit-def-error-dialog.component.ts @@ -0,0 +1,13 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +@Component({ + selector: 'aspect-unit-def-error-dialog', + template: ` + <h1 mat-dialog-title>Unit-Definition kann nicht geladen werden</h1> + <p mat-dialog-content>{{data.text}}</p> + ` +}) +export class UnitDefErrorDialogComponent { + constructor(@Inject(MAT_DIALOG_DATA) public data: { text: string }) { } +} diff --git a/projects/common/environment.ts b/projects/common/environment.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2695c9d96453a1256623ea702052f071413f6d8 --- /dev/null +++ b/projects/common/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + strictInstantiation: true +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/projects/common/models/elements/button/button.ts b/projects/common/models/elements/button/button.ts index be53892c09f29a2313571fcf80feeff4c55a3d1a..fee060264aef9c63be5bd6063d28a5b974b57ae3 100644 --- a/projects/common/models/elements/button/button.ts +++ b/projects/common/models/elements/button/button.ts @@ -2,25 +2,48 @@ import { Type } from '@angular/core'; import { UIElement, UIElementProperties, UIElementType } from 'common/models/elements/element'; import { ButtonComponent } from 'common/components/button/button.component'; import { ElementComponent } from 'common/directives/element-component.directive'; -import { BasicStyles, BorderStyles } from 'common/models/elements/property-group-interfaces'; +import { + BasicStyles, BorderStyles, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { StateVariable } from 'common/models/state-variable'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class ButtonElement extends UIElement implements ButtonProperties { type: UIElementType = 'button'; - label: string; - imageSrc: string | null; - asLink: boolean; - action: null | ButtonAction; - actionParam: null | UnitNavParam | number | string; + label: string = 'Knopf'; + imageSrc: string | null = null; + asLink: boolean = false; + action: null | ButtonAction = null; + actionParam: null | UnitNavParam | number | string = null; styling: BasicStyles & BorderStyles; - constructor(element: ButtonProperties) { + constructor(element?: ButtonProperties) { super(element); - this.label = element.label; - this.imageSrc = element.imageSrc; - this.asLink = element.asLink; - this.action = element.action; - this.actionParam = element.actionParam; - this.styling = element.styling; + if (element && isValid(element)) { + this.label = element.label; + this.imageSrc = element.imageSrc; + this.asLink = element.asLink; + this.action = element.action; + this.actionParam = element.actionParam; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Button instantiation', element); + } + if (element?.label !== undefined) this.label = element.label; + if (element?.imageSrc !== undefined) this.imageSrc = element.imageSrc; + if (element?.asLink !== undefined) this.asLink = element.asLink; + if (element?.action !== undefined) this.action = element.action; + if (element?.actionParam !== undefined) this.actionParam = element.actionParam; + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = element?.styling !== undefined ? + element.styling : + { + ...PropertyGroupGenerators.generateBasicStyleProps(), + ...PropertyGroupGenerators.generateBorderStylingProps() + }; + } } getElementComponent(): Type<ElementComponent> { @@ -37,10 +60,21 @@ export interface ButtonProperties extends UIElementProperties { styling: BasicStyles & BorderStyles; } +function isValid(blueprint?: ButtonProperties): boolean { + if (!blueprint) return false; + return blueprint.label !== undefined && + blueprint.imageSrc !== undefined && + blueprint.asLink !== undefined && + blueprint.action !== undefined && + blueprint.actionParam !== undefined && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + PropertyGroupValidators.isValidBorderStyles(blueprint.styling); +} + export interface ButtonEvent { action: ButtonAction; - param: UnitNavParam | number | string; + param: UnitNavParam | number | string | StateVariable; } -export type ButtonAction = 'unitNav' | 'pageNav' | 'highlightText'; +export type ButtonAction = 'unitNav' | 'pageNav' | 'highlightText' | 'stateVariableChange'; export type UnitNavParam = 'previous' | 'next' | 'first' | 'last' | 'end'; diff --git a/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple.ts b/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple.ts index 500f6ffa0751324b0fa6109519be2339fe464ba3..abf491992ad3959d63b0b59e602591d5e20871d4 100644 --- a/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple.ts +++ b/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple.ts @@ -6,34 +6,62 @@ import { ElementComponent } from 'common/directives/element-component.directive' import { TextFieldSimpleComponent } from 'common/components/compound-elements/cloze/cloze-child-elements/text-field-simple.component'; -import { BasicStyles } from 'common/models/elements/property-group-interfaces'; +import { + BasicStyles, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class TextFieldSimpleElement extends TextInputElement implements TextFieldSimpleProperties { type: UIElementType = 'text-field-simple'; - minLength: number | null; - minLengthWarnMessage: string; - maxLength: number | null; - maxLengthWarnMessage: string; - isLimitedToMaxLength: boolean; - pattern: string | null; - patternWarnMessage: string; + minLength: number | null = null; + minLengthWarnMessage: string = 'Eingabe zu kurz'; + maxLength: number | null = null; + maxLengthWarnMessage: string = 'Eingabe zu lang'; + isLimitedToMaxLength: boolean = false; + pattern: string | null = null; + patternWarnMessage: string = 'Eingabe entspricht nicht der Vorgabe'; clearable: boolean = false; styling: BasicStyles & { lineHeight: number; }; - constructor(element: TextFieldSimpleProperties) { + constructor(element?: TextFieldSimpleProperties) { super(element); - this.minLength = element.minLength; - this.minLengthWarnMessage = element.minLengthWarnMessage; - this.maxLength = element.maxLength; - this.maxLengthWarnMessage = element.maxLengthWarnMessage; - this.isLimitedToMaxLength = element.isLimitedToMaxLength; - this.pattern = element.pattern; - this.patternWarnMessage = element.patternWarnMessage; - this.clearable = element.clearable; - this.styling = element.styling; + if (element && isValid(element)) { + this.minLength = element.minLength; + this.minLengthWarnMessage = element.minLengthWarnMessage; + this.maxLength = element.maxLength; + this.maxLengthWarnMessage = element.maxLengthWarnMessage; + this.isLimitedToMaxLength = element.isLimitedToMaxLength; + this.pattern = element.pattern; + this.patternWarnMessage = element.patternWarnMessage; + this.clearable = element.clearable; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at TextFieldSimple instantiation', element); + } + if (element?.minLength !== undefined) this.minLength = element.minLength; + if (element?.minLengthWarnMessage !== undefined) this.minLengthWarnMessage = element.minLengthWarnMessage; + if (element?.maxLength !== undefined) this.maxLength = element.maxLength; + if (element?.maxLengthWarnMessage !== undefined) this.maxLengthWarnMessage = element.maxLengthWarnMessage; + if (element?.isLimitedToMaxLength !== undefined) this.isLimitedToMaxLength = element.isLimitedToMaxLength; + if (element?.pattern !== undefined) this.pattern = element.pattern; + if (element?.patternWarnMessage !== undefined) this.patternWarnMessage = element.patternWarnMessage; + if (element?.clearable !== undefined) this.clearable = element.clearable; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 150, + height: 30, + isWidthFixed: true, + ...element?.dimensions + }); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 100 + }; + } } hasAnswerScheme(): boolean { @@ -70,3 +98,17 @@ export interface TextFieldSimpleProperties extends TextInputElementProperties { lineHeight: number; }; } + +function isValid(blueprint?: TextFieldSimpleProperties): boolean { + if (!blueprint) return false; + return blueprint.minLength !== undefined && + blueprint.minLengthWarnMessage !== undefined && + blueprint.maxLength !== undefined && + blueprint.maxLengthWarnMessage !== undefined && + blueprint.isLimitedToMaxLength !== undefined && + blueprint.pattern !== undefined && + blueprint.patternWarnMessage !== undefined && + blueprint.clearable !== undefined && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined; +} diff --git a/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button.ts b/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button.ts index df82da81634b5ad8d1e3293e0ffa41eda2db55f6..ebee223addff6c12028ad2a43aa1baf085ec7400 100644 --- a/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button.ts +++ b/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button.ts @@ -6,28 +6,51 @@ import { ElementComponent } from 'common/directives/element-component.directive' import { ToggleButtonComponent } from 'common/components/compound-elements/cloze/cloze-child-elements/toggle-button.component'; -import { BasicStyles } from 'common/models/elements/property-group-interfaces'; +import { + BasicStyles, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; import { TextLabel } from 'common/models/elements/label-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class ToggleButtonElement extends InputElement implements ToggleButtonProperties { type: UIElementType = 'toggle-button'; - options: TextLabel[]; - strikeOtherOptions: boolean; - strikeSelectedOption: boolean; - verticalOrientation: boolean; + options: TextLabel[] = []; + strikeOtherOptions: boolean = false; + strikeSelectedOption: boolean = false; + verticalOrientation: boolean = false; styling: BasicStyles & { lineHeight: number; selectionColor: string; }; - constructor(element: ToggleButtonProperties) { + constructor(element?: ToggleButtonProperties) { super(element); - this.options = element.options; - this.strikeOtherOptions = element.strikeOtherOptions; - this.strikeSelectedOption = element.strikeSelectedOption; - this.verticalOrientation = element.verticalOrientation; - this.styling = element.styling; + if (element && isValid(element)) { + this.options = [...element.options]; + this.strikeOtherOptions = element.strikeOtherOptions; + this.strikeSelectedOption = element.strikeSelectedOption; + this.verticalOrientation = element.verticalOrientation; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at ToggleButton instantiation', element); + } + if (element?.options !== undefined) this.options = [...element.options]; + if (element?.strikeOtherOptions !== undefined) this.strikeOtherOptions = element.strikeOtherOptions; + if (element?.strikeSelectedOption !== undefined) this.strikeSelectedOption = element.strikeSelectedOption; + if (element?.verticalOrientation !== undefined) this.verticalOrientation = element.verticalOrientation; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + height: 30, + ...element?.dimensions + }); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 100, + selectionColor: element?.styling?.selectionColor || '#c7f3d0' + }; + } } hasAnswerScheme(): boolean { @@ -73,3 +96,14 @@ export interface ToggleButtonProperties extends InputElementProperties { selectionColor: string; }; } + +function isValid(blueprint?: ToggleButtonProperties): boolean { + if (!blueprint) return false; + return blueprint.options !== undefined && + blueprint.strikeOtherOptions !== undefined && + blueprint.strikeSelectedOption !== undefined && + blueprint.verticalOrientation !== undefined && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined && + blueprint.styling.selectionColor !== undefined; +} diff --git a/projects/common/models/elements/compound-elements/cloze/cloze.ts b/projects/common/models/elements/compound-elements/cloze/cloze.ts index af3d01421ea79842bbb5211778415319b07690fe..a12ab20defc8db6ffc71f1a03a9552eb5986b482 100644 --- a/projects/common/models/elements/compound-elements/cloze/cloze.ts +++ b/projects/common/models/elements/compound-elements/cloze/cloze.ts @@ -8,31 +8,52 @@ import { Type } from '@angular/core'; import { ElementComponent } from 'common/directives/element-component.directive'; import { ClozeComponent } from 'common/components/compound-elements/cloze/cloze.component'; import { - TextFieldSimpleElement + TextFieldSimpleElement, TextFieldSimpleProperties } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple'; -import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; -import { ButtonElement } from 'common/models/elements/button/button'; -import { DropListElement } from 'common/models/elements/input-elements/drop-list'; import { - BasicStyles, - PositionProperties + ToggleButtonElement, + ToggleButtonProperties +} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; +import { ButtonElement, ButtonProperties } from 'common/models/elements/button/button'; +import { DropListElement, DropListProperties } from 'common/models/elements/input-elements/drop-list'; +import { + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators } from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class ClozeElement extends CompoundElement implements PositionedUIElement, ClozeProperties { type: UIElementType = 'cloze'; document: ClozeDocument = { type: 'doc', content: [] }; - columnCount: number; + columnCount: number = 1; position: PositionProperties; styling: BasicStyles & { lineHeight: number; }; - constructor(element: ClozeProperties) { + constructor(element?: ClozeProperties) { super(element); - this.columnCount = element.columnCount; - this.document = ClozeElement.initDocument(element.document); - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.columnCount = element.columnCount; + this.document = ClozeElement.initDocument(element.document); + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Cloze instantiation', element); + } + if (element?.columnCount !== undefined) this.columnCount = element.columnCount; + this.document = ClozeElement.initDocument(element?.document); + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + height: 200, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 150 + }; + } } setProperty(property: string, value: UIElementValue): void { @@ -63,7 +84,7 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement if (['ToggleButton', 'DropList', 'TextField', 'Button'].includes(subNode.type) && subNode.attrs.model.id === 'cloze-child-id-placeholder') { subNode.attrs.model = - ClozeElement.createChildElement({ ...subNode.attrs.model }); + ClozeElement.createChildElement(subNode.attrs.model); } }); } @@ -142,21 +163,21 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement let newElement: InputElement | ButtonElement; switch (elementModel.type) { case 'text-field-simple': - newElement = new TextFieldSimpleElement(elementModel as TextFieldSimpleElement); + newElement = new TextFieldSimpleElement(elementModel as unknown as TextFieldSimpleProperties); break; case 'drop-list': - case 'drop-list-simple' as UIElementType: // keep here for compatibility - newElement = new DropListElement(elementModel as DropListElement); + newElement = new DropListElement(elementModel as unknown as DropListProperties); break; case 'toggle-button': - newElement = new ToggleButtonElement(elementModel as ToggleButtonElement); + newElement = new ToggleButtonElement(elementModel as unknown as ToggleButtonProperties); break; case 'button': - newElement = new ButtonElement(elementModel as ButtonElement); + newElement = new ButtonElement(elementModel as unknown as ButtonProperties); break; default: throw new Error(`ElementType ${elementModel.type} not found!`); } + delete newElement.position; return newElement; } @@ -179,6 +200,15 @@ export interface ClozeProperties extends UIElementProperties { }; } +function isValid(blueprint?: ClozeProperties): boolean { + if (!blueprint) return false; + return blueprint.document !== undefined && + blueprint.columnCount !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined; +} + export interface ClozeDocument { type: string; content: ClozeDocumentParagraph[] diff --git a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/button.ts b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/button.ts index 7c2de06eac94a0dbe44619ca0ea6ead16d9d8eb0..de730ae495cf076b4af1e7e25324245554fb2c9d 100644 --- a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/button.ts +++ b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/button.ts @@ -1,6 +1,5 @@ import { Node, mergeAttributes } from '@tiptap/core'; import { ButtonElement } from 'common/models/elements/button/button'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element'; const ButtonExtension = Node.create({ @@ -11,7 +10,7 @@ const ButtonExtension = addAttributes() { return { model: { - default: new ButtonElement(ElementPropertyGenerator.getButton()) + default: new ButtonElement() } }; }, diff --git a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list.ts b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list.ts index a1f0c53b34dca39310812dfa1d3c54cf8ee46836..def64fa48e2a485c52ac386fb0e6b34b1425458f 100644 --- a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list.ts +++ b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list.ts @@ -1,6 +1,5 @@ import { Node, mergeAttributes } from '@tiptap/core'; import { DropListElement } from 'common/models/elements/input-elements/drop-list'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element-properties'; const DropListExtension = Node.create({ @@ -11,7 +10,7 @@ const DropListExtension = addAttributes() { return { model: { - default: new DropListElement(ElementPropertyGenerator.getDropList()) + default: new DropListElement() } }; }, diff --git a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/text-field.ts b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/text-field.ts index 84a29fa2787ebed3f341a0d9876a6ef4288ce696..6d51377b369f02450a80598f1445485585ca39a3 100644 --- a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/text-field.ts +++ b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/text-field.ts @@ -2,7 +2,6 @@ import { Node, mergeAttributes } from '@tiptap/core'; import { TextFieldSimpleElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element-properties'; const TextFieldExtension = Node.create({ @@ -13,9 +12,7 @@ const TextFieldExtension = addAttributes() { return { model: { - default: new TextFieldSimpleElement({ - ...ElementPropertyGenerator.getTextFieldSimple() - }) + default: new TextFieldSimpleElement() } }; }, diff --git a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/toggle-button.ts b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/toggle-button.ts index edd27369cf8ce3bf1efa5ec2adb22074026273eb..1ca84473259d58243387f81900909dfe1efacef2 100644 --- a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/toggle-button.ts +++ b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/toggle-button.ts @@ -1,6 +1,5 @@ import { Node, mergeAttributes } from '@tiptap/core'; import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element-properties'; const ToggleButtonExtension = Node.create({ @@ -11,7 +10,7 @@ const ToggleButtonExtension = addAttributes() { return { model: { - default: new ToggleButtonElement(ElementPropertyGenerator.getToggleButton()) + default: new ToggleButtonElement() } }; }, diff --git a/projects/common/models/elements/compound-elements/likert/likert-row.ts b/projects/common/models/elements/compound-elements/likert/likert-row.ts index 27d7eed96cbfcbf50da88d404ac845d2ba1f6814..be27500134de2c1511a0ac745f4f6d132ff6da38 100644 --- a/projects/common/models/elements/compound-elements/likert/likert-row.ts +++ b/projects/common/models/elements/compound-elements/likert/likert-row.ts @@ -8,20 +8,34 @@ import { } from 'common/components/compound-elements/likert/likert-radio-button-group.component'; import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; import { TextImageLabel } from 'common/models/elements/label-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class LikertRowElement extends InputElement implements LikertRowProperties { type: UIElementType = 'likert-row'; - rowLabel: TextImageLabel; - columnCount: number; - firstColumnSizeRatio: number; - verticalButtonAlignment: 'auto' | 'center'; + rowLabel: TextImageLabel = { text: '', imgSrc: null, imgPosition: 'above' }; + columnCount: number = 0; + firstColumnSizeRatio: number = 5; + verticalButtonAlignment: 'auto' | 'center' = 'center'; - constructor(element: LikertRowProperties) { + constructor(element?: LikertRowProperties) { super(element); - this.rowLabel = element.rowLabel; - this.columnCount = element.columnCount; - this.firstColumnSizeRatio = element.firstColumnSizeRatio; - this.verticalButtonAlignment = element.verticalButtonAlignment; + if (element && isValid(element)) { + this.rowLabel = element.rowLabel; + this.columnCount = element.columnCount; + this.firstColumnSizeRatio = element.firstColumnSizeRatio; + this.verticalButtonAlignment = element.verticalButtonAlignment; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Likert-Row instantiation', element); + } + if (element?.rowLabel !== undefined) this.rowLabel = element.rowLabel; + if (element?.columnCount !== undefined) this.columnCount = element.columnCount; + if (element?.firstColumnSizeRatio !== undefined) this.firstColumnSizeRatio = element.firstColumnSizeRatio; + if (element?.verticalButtonAlignment !== undefined) { + this.verticalButtonAlignment = element.verticalButtonAlignment; + } + } } hasAnswerScheme(): boolean { @@ -60,3 +74,11 @@ export interface LikertRowProperties extends InputElementProperties { firstColumnSizeRatio: number; verticalButtonAlignment: 'auto' | 'center'; } + +function isValid(blueprint?: LikertRowProperties): boolean { + if (!blueprint) return false; + return blueprint.rowLabel !== undefined && + blueprint.columnCount !== undefined && + blueprint.firstColumnSizeRatio !== undefined && + blueprint.verticalButtonAlignment !== undefined; +} diff --git a/projects/common/models/elements/compound-elements/likert/likert.ts b/projects/common/models/elements/compound-elements/likert/likert.ts index 5baa09b648200772da654cbfb96745fa56bca442..e27b6d088c4067ded81f195a743711d11e4ff4c3 100644 --- a/projects/common/models/elements/compound-elements/likert/likert.ts +++ b/projects/common/models/elements/compound-elements/likert/likert.ts @@ -7,18 +7,20 @@ import { LikertRowElement } from 'common/models/elements/compound-elements/liker import { ElementComponent } from 'common/directives/element-component.directive'; import { LikertComponent } from 'common/components/compound-elements/likert/likert.component'; import { - BasicStyles, PositionProperties + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators } from 'common/models/elements/property-group-interfaces'; import { TextImageLabel } from 'common/models/elements/label-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class LikertElement extends CompoundElement implements PositionedUIElement, OptionElement, LikertProperties { type: UIElementType = 'likert'; - rows: LikertRowElement[]; - options: TextImageLabel[]; - firstColumnSizeRatio: number; - label: string; - label2: string; - stickyHeader: boolean; + rows: LikertRowElement[] = []; + options: TextImageLabel[] = []; + firstColumnSizeRatio: number = 5; + label: string = 'Optionentabelle Beschriftung'; + label2: string = 'Beschriftung Erste Spalte'; + stickyHeader: boolean = false; position: PositionProperties; styling: BasicStyles & { lineHeight: number; @@ -26,16 +28,46 @@ export class LikertElement extends CompoundElement implements PositionedUIElemen lineColoringColor: string; }; - constructor(element: LikertProperties) { + constructor(element?: LikertProperties) { super(element); - this.options = element.options; - this.firstColumnSizeRatio = element.firstColumnSizeRatio; - this.rows = element.rows.map(row => new LikertRowElement(row)); - this.label = element.label; - this.label2 = element.label2; - this.stickyHeader = element.stickyHeader; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.options = element.options; + this.firstColumnSizeRatio = element.firstColumnSizeRatio; + this.rows = element.rows.map(row => new LikertRowElement(row)); + this.label = element.label; + this.label2 = element.label2; + this.stickyHeader = element.stickyHeader; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Likert instantiation', element); + } + if (element?.options !== undefined) this.options = element.options; + if (element?.firstColumnSizeRatio !== undefined) this.firstColumnSizeRatio = element.firstColumnSizeRatio; + if (element?.rows !== undefined) this.rows = element.rows.map(row => new LikertRowElement(row)); + if (element?.label !== undefined) this.label = element.label; + if (element?.label2 !== undefined) this.label2 = element.label2; + if (element?.stickyHeader !== undefined) this.stickyHeader = element.stickyHeader; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 250, + height: 200, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps({ + marginBottom: { value: 35, unit: 'px' }, + ...element?.position + }); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps({ + backgroundColor: 'white', + ...element?.styling + }), + lineHeight: element?.styling?.lineHeight || 135, + lineColoring: element?.styling?.lineColoring !== undefined ? element?.styling.lineColoring : true, + lineColoringColor: element?.styling?.lineColoringColor || '#c9e0e0' + }; + } } getNewOptionLabel(optionText: string): TextImageLabel { @@ -78,3 +110,18 @@ export interface LikertProperties extends UIElementProperties { lineColoringColor: string; }; } + +function isValid(blueprint?: LikertProperties): boolean { + if (!blueprint) return false; + return blueprint.rows !== undefined && + blueprint.options !== undefined && + blueprint.firstColumnSizeRatio !== undefined && + blueprint.label !== undefined && + blueprint.label2 !== undefined && + blueprint.stickyHeader !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined && + blueprint.styling.lineColoring !== undefined && + blueprint.styling.lineColoringColor !== undefined; +} diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts index a20cdcd12c7e13d72b2f502e392c467d74459c64..887536be0a7006ae579549f8bc4012667a8e5fec 100644 --- a/projects/common/models/elements/element.ts +++ b/projects/common/models/elements/element.ts @@ -7,10 +7,12 @@ import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { Label, TextLabel } from 'common/models/elements/label-interfaces'; import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; import { - DimensionProperties, PlayerProperties, PositionProperties, Stylings + DimensionProperties, PlayerProperties, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators, Stylings } from 'common/models/elements/property-group-interfaces'; import { VisibilityRule } from 'common/models/visibility-rule'; import { StateVariable } from 'common/models/state-variable'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export type UIElementType = 'text' | 'button' | 'text-field' | 'text-field-simple' | 'text-area' | 'checkbox' | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert-row' | 'radio-group-images' | 'hotspot-image' @@ -46,21 +48,36 @@ export interface UIElementProperties { player?: PlayerProperties; } +function isValidUIElementProperties(blueprint?: UIElementProperties): boolean { + if (!blueprint) return false; + return blueprint.id !== undefined && + PropertyGroupValidators.isValidDimensionProps(blueprint.dimensions); +} + export abstract class UIElement implements UIElementProperties { [index: string]: unknown; - id: string; + id: string = 'id-placeholder'; abstract type: UIElementType; position?: PositionProperties; dimensions: DimensionProperties; styling?: Stylings; player?: PlayerProperties; - isRelevantForPresentationComplete?: boolean; - - constructor(element: UIElementProperties) { - this.id = element.id; - this.dimensions = element.dimensions; - this.position = element.position; - this.styling = element.styling; + isRelevantForPresentationComplete?: boolean = true; + + constructor(element?: UIElementProperties) { + if (element && isValidUIElementProperties(element)) { + this.id = element.id; + this.dimensions = element.dimensions; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at UIElement instantiation', element); + } + if (element?.id) this.id = element.id; + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.dimensions = PropertyGroupGenerators.generateDimensionProps(element?.dimensions); + } } setProperty(property: string, value: UIElementValue): void { @@ -118,20 +135,40 @@ export interface InputElementProperties extends UIElementProperties { readOnly: boolean; } +function isValidInputElementProperties(blueprint?: InputElementProperties): boolean { + if (!blueprint) return false; + return blueprint?.label !== undefined && + blueprint?.value !== undefined && + blueprint?.required !== undefined && + blueprint?.requiredWarnMessage !== undefined && + blueprint?.readOnly !== undefined; +} + export abstract class InputElement extends UIElement implements InputElementProperties { - label: string; - value: InputElementValue; - required: boolean; - requiredWarnMessage: string; - readOnly: boolean; + label: string = 'Beschriftung'; + value: InputElementValue = null; + required: boolean = false; + requiredWarnMessage: string = 'Eingabe erforderlich'; + readOnly: boolean = false; - protected constructor(element: InputElementProperties) { + protected constructor(element?: InputElementProperties) { super(element); - this.label = element.label; - this.value = element.value; - this.required = element.required; - this.requiredWarnMessage = element.requiredWarnMessage; - this.readOnly = element.readOnly; + if (element && isValidInputElementProperties(element)) { + this.label = element.label; + this.value = element.value; + this.required = element.required; + this.requiredWarnMessage = element.requiredWarnMessage; + this.readOnly = element.readOnly; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at InputElement instantiation', element); + } + if (element?.label) this.label = element.label; + if (element?.value) this.value = element.value; + if (element?.required) this.required = element.required; + if (element?.requiredWarnMessage) this.requiredWarnMessage = element.requiredWarnMessage; + if (element?.readOnly) this.readOnly = element.readOnly; + } } abstract getAnswerScheme(options?: unknown): AnswerScheme; @@ -155,28 +192,56 @@ export interface TextInputElementProperties extends InputElementProperties { softwareKeyboardShowFrench: boolean; } -export abstract class TextInputElement extends InputElement implements TextInputElementProperties { - inputAssistancePreset: InputAssistancePreset; - inputAssistanceCustomKeys: string; - inputAssistancePosition: 'floating' | 'right'; - inputAssistanceFloatingStartPosition: 'startBottom' | 'endCenter'; - restrictedToInputAssistanceChars: boolean; - hasArrowKeys: boolean; - hasBackspaceKey: boolean; - showSoftwareKeyboard: boolean; - softwareKeyboardShowFrench: boolean; +function isValidTextInputElementProperties(blueprint?: TextInputElementProperties): boolean { + if (!blueprint) return false; + return blueprint.inputAssistancePreset !== undefined && + blueprint.inputAssistanceCustomKeys !== undefined && + blueprint.inputAssistancePosition !== undefined && + blueprint.inputAssistanceFloatingStartPosition !== undefined && + blueprint.restrictedToInputAssistanceChars !== undefined && + blueprint.hasArrowKeys !== undefined && + blueprint.hasBackspaceKey !== undefined && + blueprint.showSoftwareKeyboard !== undefined && + blueprint.softwareKeyboardShowFrench !== undefined; +} - protected constructor(element: TextInputElementProperties) { +export abstract class TextInputElement extends InputElement implements TextInputElementProperties { + inputAssistancePreset: InputAssistancePreset = null; + inputAssistanceCustomKeys: string = ''; + inputAssistancePosition: 'floating' | 'right' = 'floating'; + inputAssistanceFloatingStartPosition: 'startBottom' | 'endCenter' = 'startBottom'; + restrictedToInputAssistanceChars: boolean = true; + hasArrowKeys: boolean = false; + hasBackspaceKey: boolean = false; + showSoftwareKeyboard: boolean = false; + softwareKeyboardShowFrench: boolean = false; + + protected constructor(element?: TextInputElementProperties) { super(element); - this.inputAssistancePreset = element.inputAssistancePreset; - this.inputAssistanceCustomKeys = element.inputAssistanceCustomKeys; - this.inputAssistancePosition = element.inputAssistancePosition; - this.inputAssistanceFloatingStartPosition = element.inputAssistanceFloatingStartPosition; - this.restrictedToInputAssistanceChars = element.restrictedToInputAssistanceChars; - this.hasArrowKeys = element.hasArrowKeys; - this.hasBackspaceKey = element.hasBackspaceKey; - this.showSoftwareKeyboard = element.showSoftwareKeyboard; - this.softwareKeyboardShowFrench = element.softwareKeyboardShowFrench; + if (element && isValidTextInputElementProperties(element)) { + this.inputAssistancePreset = element.inputAssistancePreset; + this.inputAssistanceCustomKeys = element.inputAssistanceCustomKeys; + this.inputAssistancePosition = element.inputAssistancePosition; + this.inputAssistanceFloatingStartPosition = element.inputAssistanceFloatingStartPosition; + this.restrictedToInputAssistanceChars = element.restrictedToInputAssistanceChars; + this.hasArrowKeys = element.hasArrowKeys; + this.hasBackspaceKey = element.hasBackspaceKey; + this.showSoftwareKeyboard = element.showSoftwareKeyboard; + this.softwareKeyboardShowFrench = element.softwareKeyboardShowFrench; + } else { + if (environment.strictInstantiation) { + throw Error('Error at TextInputElement instantiation'); + } + if (element?.inputAssistancePreset) this.inputAssistancePreset = element.inputAssistancePreset; + if (element?.inputAssistanceCustomKeys) this.inputAssistanceCustomKeys = element.inputAssistanceCustomKeys; + if (element?.inputAssistancePosition) this.inputAssistancePosition = element.inputAssistancePosition; + if (element?.inputAssistanceFloatingStartPosition) this.inputAssistanceFloatingStartPosition = element.inputAssistanceFloatingStartPosition; + if (element?.restrictedToInputAssistanceChars) this.restrictedToInputAssistanceChars = element.restrictedToInputAssistanceChars; + if (element?.hasArrowKeys) this.hasArrowKeys = element.hasArrowKeys; + if (element?.hasBackspaceKey) this.hasBackspaceKey = element.hasBackspaceKey; + if (element?.showSoftwareKeyboard) this.showSoftwareKeyboard = element.showSoftwareKeyboard; + if (element?.softwareKeyboardShowFrench) this.softwareKeyboardShowFrench = element.softwareKeyboardShowFrench; + } } } @@ -188,12 +253,24 @@ export interface PlayerElementBlueprint extends UIElementProperties { player: PlayerProperties; } +function isValidPlayerElementBlueprint(blueprint?: PlayerElementBlueprint): boolean { + if (!blueprint) return false; + return blueprint.player !== undefined; +} + export abstract class PlayerElement extends UIElement implements PlayerElementBlueprint { player: PlayerProperties; - protected constructor(element: PlayerElementBlueprint) { + protected constructor(element?: PlayerElementBlueprint) { super(element); - this.player = element.player; + if (element && isValidPlayerElementBlueprint(element)) { + this.player = element.player; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at PlayerElement instantiation', element); + } + this.player = PropertyGroupGenerators.generatePlayerProps(element?.player); + } } hasAnswerScheme(): boolean { diff --git a/projects/common/models/elements/frame/frame.ts b/projects/common/models/elements/frame/frame.ts index bb264df4f44baf9583e472b3c0176b5e6913cc4a..8284d7743a0f0c7f78e8134b7230062376a52560 100644 --- a/projects/common/models/elements/frame/frame.ts +++ b/projects/common/models/elements/frame/frame.ts @@ -4,32 +4,50 @@ import { } from 'common/models/elements/element'; import { FrameComponent } from 'common/components/frame/frame.component'; import { ElementComponent } from 'common/directives/element-component.directive'; -import { BorderStyles, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + BorderStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class FrameElement extends UIElement implements PositionedUIElement, FrameProperties { type: UIElementType = 'frame'; - hasBorderTop: boolean; - hasBorderBottom: boolean; - hasBorderLeft: boolean; - hasBorderRight: boolean; + hasBorderTop: boolean = true; + hasBorderBottom: boolean = true; + hasBorderLeft: boolean = true; + hasBorderRight: boolean = true; position: PositionProperties; styling: BorderStyles & { backgroundColor: string; }; - constructor(element: FrameProperties) { + constructor(element?: FrameProperties) { super(element); - this.hasBorderTop = element.hasBorderTop; - this.hasBorderBottom = element.hasBorderBottom; - this.hasBorderLeft = element.hasBorderLeft; - this.hasBorderRight = element.hasBorderRight; - this.position = element.position; - this.styling = { - ...element.styling, - backgroundColor: element.styling.backgroundColor, - borderWidth: element.styling.borderWidth, - borderColor: element.styling.borderColor, - borderStyle: element.styling.borderStyle, - borderRadius: element.styling.borderRadius - }; + if (element && isValid(element)) { + this.hasBorderTop = element.hasBorderTop; + this.hasBorderBottom = element.hasBorderBottom; + this.hasBorderLeft = element.hasBorderLeft; + this.hasBorderRight = element.hasBorderRight; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Frame instantiation', element); + } + if (element?.hasBorderTop !== undefined) this.hasBorderTop = element.hasBorderTop; + if (element?.hasBorderBottom !== undefined) this.hasBorderBottom = element.hasBorderBottom; + if (element?.hasBorderLeft !== undefined) this.hasBorderLeft = element.hasBorderLeft; + if (element?.hasBorderRight !== undefined) this.hasBorderRight = element.hasBorderRight; + this.position = PropertyGroupGenerators.generatePositionProps({ + zIndex: -1, + ...element?.position + }); + this.styling = { + ...PropertyGroupGenerators.generateBorderStylingProps({ + borderWidth: 1, + ...element?.styling + }), + backgroundColor: element?.styling?.backgroundColor || 'transparent' + }; + } } getElementComponent(): Type<ElementComponent> { @@ -45,3 +63,14 @@ export interface FrameProperties extends UIElementProperties { position: PositionProperties; styling: BorderStyles & { backgroundColor: string; }; } + +function isValid(blueprint?: FrameProperties): boolean { + if (!blueprint) return false; + return blueprint.hasBorderTop !== undefined && + blueprint.hasBorderBottom !== undefined && + blueprint.hasBorderLeft !== undefined && + blueprint.hasBorderRight !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBorderStyles(blueprint.styling) && + blueprint.styling.backgroundColor !== undefined; +} diff --git a/projects/common/models/elements/geometry/geometry.ts b/projects/common/models/elements/geometry/geometry.ts index f17150f5477330fec8785c89d11619a8fe652f2e..22093c0a0575d43a20bfa15255a6f686adc42b1d 100644 --- a/projects/common/models/elements/geometry/geometry.ts +++ b/projects/common/models/elements/geometry/geometry.ts @@ -4,33 +4,57 @@ import { } from 'common/models/elements/element'; import { ElementComponent } from 'common/directives/element-component.directive'; import { GeometryComponent } from 'common/components/geometry/geometry.component'; -import { PositionProperties } from 'common/models/elements/property-group-interfaces'; - +import { + PositionProperties, + PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class GeometryElement extends UIElement implements PositionedUIElement, GeometryProperties { type: UIElementType = 'geometry'; - appDefinition: string; - showResetIcon: boolean; - enableUndoRedo: boolean; - showToolbar: boolean; - enableShiftDragZoom: boolean; - showZoomButtons: boolean; - showFullscreenButton: boolean; - customToolbar: string; + appDefinition: string = ''; + showResetIcon: boolean = true; + enableUndoRedo: boolean = true; + showToolbar: boolean = true; + enableShiftDragZoom: boolean = true; + showZoomButtons: boolean = true; + showFullscreenButton: boolean = true; + customToolbar: string = ''; position: PositionProperties; - constructor(element: GeometryProperties) { + constructor(element?: GeometryProperties) { super(element); - this.appDefinition = element.appDefinition; - this.showResetIcon = element.showResetIcon; - this.enableUndoRedo = element.enableUndoRedo; - this.showToolbar = element.showToolbar; - this.enableShiftDragZoom = element.enableShiftDragZoom; - this.showZoomButtons = element.showZoomButtons; - this.showFullscreenButton = element.showFullscreenButton; - this.customToolbar = element.customToolbar; - this.position = element.position; + if (element && isValid(element)) { + this.appDefinition = element.appDefinition; + this.showResetIcon = element.showResetIcon; + this.enableUndoRedo = element.enableUndoRedo; + this.showToolbar = element.showToolbar; + this.enableShiftDragZoom = element.enableShiftDragZoom; + this.showZoomButtons = element.showZoomButtons; + this.showFullscreenButton = element.showFullscreenButton; + this.customToolbar = element.customToolbar; + this.position = element.position; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Geometry instantiation', element); + } + if (element?.appDefinition !== undefined) this.appDefinition = element.appDefinition; + if (element?.showResetIcon !== undefined) this.showResetIcon = element.showResetIcon; + if (element?.enableUndoRedo !== undefined) this.enableUndoRedo = element.enableUndoRedo; + if (element?.showToolbar !== undefined) this.showToolbar = element.showToolbar; + if (element?.enableShiftDragZoom !== undefined) this.enableShiftDragZoom = element.enableShiftDragZoom; + if (element?.showZoomButtons !== undefined) this.showZoomButtons = element.showZoomButtons; + if (element?.showFullscreenButton !== undefined) this.showFullscreenButton = element.showFullscreenButton; + if (element?.customToolbar !== undefined) this.customToolbar = element.customToolbar; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 600, + height: 400, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + } } hasAnswerScheme(): boolean { @@ -65,3 +89,16 @@ export interface GeometryProperties extends UIElementProperties { customToolbar: string; position: PositionProperties; } + +function isValid(blueprint?: GeometryProperties): boolean { + if (!blueprint) return false; + return blueprint.appDefinition !== undefined && + blueprint.showResetIcon !== undefined && + blueprint.enableUndoRedo !== undefined && + blueprint.showToolbar !== undefined && + blueprint.enableShiftDragZoom !== undefined && + blueprint.showZoomButtons !== undefined && + blueprint.showFullscreenButton !== undefined && + blueprint.customToolbar !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position); +} diff --git a/projects/common/models/elements/input-elements/checkbox.ts b/projects/common/models/elements/input-elements/checkbox.ts index 3ff2cf1277ec22919f4a514ef70fdae1fc9637fc..7b62f738a4eb98823995408794c47f850d367d9f 100644 --- a/projects/common/models/elements/input-elements/checkbox.ts +++ b/projects/common/models/elements/input-elements/checkbox.ts @@ -6,19 +6,32 @@ import { ElementComponent } from 'common/directives/element-component.directive' import { CheckboxComponent } from 'common/components/input-elements/checkbox.component'; import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; import { - BasicStyles, - PositionProperties + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators } from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class CheckboxElement extends InputElement implements PositionedUIElement, CheckboxProperties { type: UIElementType = 'checkbox'; position: PositionProperties; styling: BasicStyles; - constructor(element: CheckboxProperties) { + constructor(element?: CheckboxProperties) { super(element); - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Checkbox instantiation', element); + } + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 215, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = PropertyGroupGenerators.generateBasicStyleProps(element?.styling); + } } hasAnswerScheme(): boolean { @@ -53,3 +66,9 @@ export interface CheckboxProperties extends InputElementProperties { position: PositionProperties; styling: BasicStyles; } + +function isValid(blueprint?: CheckboxProperties): boolean { + if (!blueprint) return false; + return PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling); +} diff --git a/projects/common/models/elements/input-elements/drop-list.ts b/projects/common/models/elements/input-elements/drop-list.ts index 3530ecf20ae3de06689405ab913b12ec7272a30f..d56bc2320cbe4b47c9157288ef2aec608055817a 100644 --- a/projects/common/models/elements/input-elements/drop-list.ts +++ b/projects/common/models/elements/input-elements/drop-list.ts @@ -8,8 +8,10 @@ import { DropListComponent } from 'common/components/input-elements/drop-list.co import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; import { DragNDropValueObject } from 'common/models/elements/label-interfaces'; import { - BasicStyles + BasicStyles, PropertyGroupGenerators } from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class DropListElement extends InputElement implements DropListProperties { type: UIElementType = 'drop-list'; @@ -25,23 +27,48 @@ export class DropListElement extends InputElement implements DropListProperties itemBackgroundColor: string; }; - constructor(element: DropListProperties) { + constructor(element?: DropListProperties) { super(element); - this.value = element.value !== undefined ? - element.value.map(val => ({ ...val })) : - []; - this.onlyOneItem = element.onlyOneItem; - this.connectedTo = [...element.connectedTo]; - this.copyOnDrop = element.copyOnDrop; - this.allowReplacement = element.allowReplacement; - this.orientation = element.orientation; - this.highlightReceivingDropList = element.highlightReceivingDropList; - this.highlightReceivingDropListColor = element.highlightReceivingDropListColor; - - this.styling = { - ...element.styling, - itemBackgroundColor: element.styling.itemBackgroundColor - }; + if (element && isValid(element)) { + this.value = element.value.map(val => ({ ...val })); + this.onlyOneItem = element.onlyOneItem; + this.connectedTo = [...element.connectedTo]; + this.copyOnDrop = element.copyOnDrop; + this.allowReplacement = element.allowReplacement; + this.orientation = element.orientation; + this.highlightReceivingDropList = element.highlightReceivingDropList; + this.highlightReceivingDropListColor = element.highlightReceivingDropListColor; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at DropList instantiation', element); + } + this.value = element?.value !== undefined ? + element.value.map(val => ({ ...val })) : + []; + if (element?.onlyOneItem) this.onlyOneItem = element.onlyOneItem; + if (element?.connectedTo) this.connectedTo = [...element.connectedTo]; + if (element?.copyOnDrop) this.copyOnDrop = element.copyOnDrop; + if (element?.allowReplacement) this.allowReplacement = element.allowReplacement; + if (element?.orientation) this.orientation = element.orientation; + if (element?.highlightReceivingDropList) this.highlightReceivingDropList = element.highlightReceivingDropList; + if (element?.highlightReceivingDropListColor) { + this.highlightReceivingDropListColor = element.highlightReceivingDropListColor; + } + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + height: 100, + minHeight: 100, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps({ + backgroundColor: '#ededed', + ...element?.styling + }), + itemBackgroundColor: element?.styling?.itemBackgroundColor || '#c9e0e0' + }; + } } /* Set originListID and originListIndex if applicable. */ @@ -107,3 +134,17 @@ export interface DropListProperties extends InputElementProperties { itemBackgroundColor: string; }; } + +function isValid(blueprint?: DropListProperties): boolean { + if (!blueprint) return false; + return blueprint.value !== undefined && + blueprint.onlyOneItem !== undefined && + blueprint.connectedTo !== undefined && + blueprint.copyOnDrop !== undefined && + blueprint.allowReplacement !== undefined && + blueprint.orientation !== undefined && + blueprint.highlightReceivingDropList !== undefined && + blueprint.highlightReceivingDropListColor !== undefined && + blueprint.styling !== undefined && + blueprint.styling.itemBackgroundColor !== undefined; +} diff --git a/projects/common/models/elements/input-elements/dropdown.ts b/projects/common/models/elements/input-elements/dropdown.ts index 37598f527aca812491548cd27edc3e74913d6983..341f0e3978de1910387dceb927c16b7e9be0d853 100644 --- a/projects/common/models/elements/input-elements/dropdown.ts +++ b/projects/common/models/elements/input-elements/dropdown.ts @@ -6,21 +6,40 @@ import { ElementComponent } from 'common/directives/element-component.directive' import { DropdownComponent } from 'common/components/input-elements/dropdown.component'; import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; import { TextLabel } from 'common/models/elements/label-interfaces'; -import { BasicStyles, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class DropdownElement extends InputElement implements PositionedUIElement, OptionElement, DropdownProperties { type: UIElementType = 'dropdown'; - options: TextLabel[]; - allowUnset: boolean; + options: TextLabel[] = []; + allowUnset: boolean = false; position: PositionProperties; styling: BasicStyles; - constructor(element: DropdownProperties) { + constructor(element?: DropdownProperties) { super(element); - this.options = element.options; - this.allowUnset = element.allowUnset; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.options = element.options; + this.allowUnset = element.allowUnset; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Dropdown instantiation', element); + } + if (element?.options) this.options = element.options; + if (element?.allowUnset) this.allowUnset = element.allowUnset; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 240, + height: 83, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = PropertyGroupGenerators.generateBasicStyleProps(element?.styling); + } } hasAnswerScheme(): boolean { @@ -62,3 +81,11 @@ export interface DropdownProperties extends InputElementProperties { position: PositionProperties; styling: BasicStyles; } + +function isValid(blueprint?: DropdownProperties): boolean { + if (!blueprint) return false; + return blueprint.options !== undefined && + blueprint.allowUnset !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling); +} diff --git a/projects/common/models/elements/input-elements/hotspot-image.ts b/projects/common/models/elements/input-elements/hotspot-image.ts index c32873131fe80a1f537b206aef7baf5336ad498b..4a18bf4b38ebfae75d0b7b933cced7d18d3babf4 100644 --- a/projects/common/models/elements/input-elements/hotspot-image.ts +++ b/projects/common/models/elements/input-elements/hotspot-image.ts @@ -8,7 +8,11 @@ import { import { ElementComponent } from 'common/directives/element-component.directive'; import { HotspotImageComponent } from 'common/components/input-elements/hotspot-image.component'; import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; -import { PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export interface Hotspot { top: number; @@ -26,15 +30,28 @@ export interface Hotspot { export class HotspotImageElement extends InputElement implements PositionedUIElement, HotspotImageProperties { type: UIElementType = 'hotspot-image'; - value: Hotspot[]; + value: Hotspot[] = []; src: string | null = null; position: PositionProperties; - constructor(element: HotspotImageProperties) { + constructor(element?: HotspotImageProperties) { super(element); - this.value = element.value; - this.src = element.src; - this.position = element.position; + if (element && isValid(element)) { + this.value = element.value; + this.src = element.src; + this.position = element.position; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at HotspotImage instantiation', element); + } + if (element?.value) this.value = element.value; + if (element?.src) this.src = element.src; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + height: 100, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + } } hasAnswerScheme(): boolean { @@ -78,3 +95,10 @@ export interface HotspotImageProperties extends InputElementProperties { src: string | null; position: PositionProperties; } + +function isValid(blueprint?: HotspotImageProperties): boolean { + if (!blueprint) return false; + return blueprint.value !== undefined && + blueprint.src !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position); +} diff --git a/projects/common/models/elements/input-elements/math-field.ts b/projects/common/models/elements/input-elements/math-field.ts index a3b56f53f9d55ad9f2a1922d2cfc9f50ae794cbb..4713b930da45ab09ad8d9b1d4fcadae1be6a23d2 100644 --- a/projects/common/models/elements/input-elements/math-field.ts +++ b/projects/common/models/elements/input-elements/math-field.ts @@ -5,21 +5,37 @@ import { Type } from '@angular/core'; import { ElementComponent } from 'common/directives/element-component.directive'; import { MathFieldComponent } from 'common/components/input-elements/math-field.component'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; -import { BasicStyles, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class MathFieldElement extends InputElement implements MathFieldProperties { type: UIElementType = 'math-field'; enableModeSwitch: boolean = false; - position: PositionProperties | undefined; + position: PositionProperties; styling: BasicStyles & { lineHeight: number; }; - constructor(element: MathFieldProperties) { + constructor(element?: MathFieldProperties) { super(element); - this.enableModeSwitch = element.enableModeSwitch; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.enableModeSwitch = element.enableModeSwitch; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Mathfield instantiation', element); + } + if (element?.enableModeSwitch !== undefined) this.enableModeSwitch = element.enableModeSwitch; + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 135 + }; + } } hasAnswerScheme(): boolean { @@ -45,8 +61,16 @@ export class MathFieldElement extends InputElement implements MathFieldPropertie export interface MathFieldProperties extends InputElementProperties { enableModeSwitch: boolean; - position?: PositionProperties; + position: PositionProperties; styling: BasicStyles & { lineHeight: number; }; } + +function isValid(blueprint?: MathFieldProperties): boolean { + if (!blueprint) return false; + return blueprint.enableModeSwitch !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined; +} diff --git a/projects/common/models/elements/input-elements/radio-button-group-complex.ts b/projects/common/models/elements/input-elements/radio-button-group-complex.ts index 6355c5467dc2f7f8ecc075985bb9efe4e77ba86d..de1d886fc71d57fef20e3c996268b3505a6ac657 100644 --- a/projects/common/models/elements/input-elements/radio-button-group-complex.ts +++ b/projects/common/models/elements/input-elements/radio-button-group-complex.ts @@ -6,22 +6,43 @@ import { ElementComponent } from 'common/directives/element-component.directive' import { RadioGroupImagesComponent } from 'common/components/input-elements/radio-group-images.component'; import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; import { TextImageLabel } from 'common/models/elements/label-interfaces'; -import { BasicStyles, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class RadioButtonGroupComplexElement extends InputElement implements PositionedUIElement, OptionElement, RadioButtonGroupComplexProperties { type: UIElementType = 'radio-group-images'; - options: TextImageLabel[]; - itemsPerRow: number | null; + options: TextImageLabel[] = []; + itemsPerRow: number | null = null; position: PositionProperties; styling: BasicStyles; - constructor(element: RadioButtonGroupComplexProperties) { + constructor(element?: RadioButtonGroupComplexProperties) { super(element); - this.options = element.options; - this.itemsPerRow = element.itemsPerRow; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.options = element.options; + this.itemsPerRow = element.itemsPerRow; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at RadioButtonGroupComplex instantiation', element); + } + if (element?.options) this.options = element.options; + if (element?.itemsPerRow) this.itemsPerRow = element.itemsPerRow; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + height: 100, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps({ + marginBottom: { value: 40, unit: 'px' }, + ...element?.position + }); + this.styling = PropertyGroupGenerators.generateBasicStyleProps(element?.styling); + } } hasAnswerScheme(): boolean { @@ -63,3 +84,11 @@ export interface RadioButtonGroupComplexProperties extends InputElementPropertie position: PositionProperties; styling: BasicStyles; } + +function isValid(blueprint?: RadioButtonGroupComplexProperties): boolean { + if (!blueprint) return false; + return blueprint.options !== undefined && + blueprint.itemsPerRow !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling); +} diff --git a/projects/common/models/elements/input-elements/radio-button-group.ts b/projects/common/models/elements/input-elements/radio-button-group.ts index 8cff4a22b1e94f89b6dc241ff48b56090e8274e0..baddbf3e2032bea096d24185ec04b778ca348dab 100644 --- a/projects/common/models/elements/input-elements/radio-button-group.ts +++ b/projects/common/models/elements/input-elements/radio-button-group.ts @@ -6,7 +6,11 @@ import { ElementComponent } from 'common/directives/element-component.directive' import { RadioButtonGroupComponent } from 'common/components/input-elements/radio-button-group.component'; import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; import { TextLabel } from 'common/models/elements/label-interfaces'; -import { BasicStyles, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class RadioButtonGroupElement extends InputElement implements PositionedUIElement, OptionElement, RadioButtonGroupProperties { @@ -19,13 +23,31 @@ export class RadioButtonGroupElement extends InputElement lineHeight: number; }; - constructor(element: RadioButtonGroupProperties) { + constructor(element?: RadioButtonGroupProperties) { super(element); - this.options = element.options; - this.alignment = element.alignment; - this.strikeOtherOptions = element.strikeOtherOptions; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.options = element.options; + this.alignment = element.alignment; + this.strikeOtherOptions = element.strikeOtherOptions; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at RadioButtonGroupElement instantiation', element); + } + if (element?.options) this.options = element.options; + if (element?.alignment) this.alignment = element.alignment; + if (element?.strikeOtherOptions) this.strikeOtherOptions = element.strikeOtherOptions; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + height: 100, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 135 + }; + } } hasAnswerScheme(): boolean { @@ -70,3 +92,13 @@ export interface RadioButtonGroupProperties extends InputElementProperties { lineHeight: number; }; } + +function isValid(blueprint?: RadioButtonGroupProperties): boolean { + if (!blueprint) return false; + return blueprint.options !== undefined && + blueprint.alignment !== undefined && + blueprint.strikeOtherOptions !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined; +} diff --git a/projects/common/models/elements/input-elements/slider.ts b/projects/common/models/elements/input-elements/slider.ts index 33259d70f9ed845386dff06750a5c780c2e10566..7c4333c38857e5523aa8acf5d3a7ae838d6bf868 100644 --- a/projects/common/models/elements/input-elements/slider.ts +++ b/projects/common/models/elements/input-elements/slider.ts @@ -5,29 +5,49 @@ import { import { ElementComponent } from 'common/directives/element-component.directive'; import { SliderComponent } from 'common/components/input-elements/slider.component'; import { AnswerScheme, AnswerSchemeValue } from 'common/models/elements/answer-scheme-interfaces'; -import { BasicStyles, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class SliderElement extends InputElement implements PositionedUIElement, SliderProperties { type: UIElementType = 'slider'; - minValue: number; - maxValue: number; - showValues: boolean; - barStyle: boolean; - thumbLabel: boolean; + minValue: number = 0; + maxValue: number = 100; + showValues: boolean = true; + barStyle: boolean = false; + thumbLabel: boolean = false; position: PositionProperties; styling: BasicStyles & { lineHeight: number; }; - constructor(element: SliderProperties) { + constructor(element?: SliderProperties) { super(element); - this.minValue = element.minValue; - this.maxValue = element.maxValue; - this.showValues = element.showValues; - this.barStyle = element.barStyle; - this.thumbLabel = element.thumbLabel; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.minValue = element.minValue; + this.maxValue = element.maxValue; + this.showValues = element.showValues; + this.barStyle = element.barStyle; + this.thumbLabel = element.thumbLabel; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Slider instantiation', element); + } + if (element?.minValue) this.minValue = element.minValue; + if (element?.maxValue) this.maxValue = element.maxValue; + if (element?.showValues) this.showValues = element.showValues; + if (element?.barStyle) this.barStyle = element.barStyle; + if (element?.thumbLabel) this.thumbLabel = element.thumbLabel; + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 135 + }; + } } hasAnswerScheme(): boolean { @@ -68,3 +88,15 @@ export interface SliderProperties extends InputElementProperties { lineHeight: number; }; } + +function isValid(blueprint?: SliderProperties): boolean { + if (!blueprint) return false; + return blueprint.minValue !== undefined && + blueprint.maxValue !== undefined && + blueprint.showValues !== undefined && + blueprint.barStyle !== undefined && + blueprint.thumbLabel !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined; +} diff --git a/projects/common/models/elements/input-elements/spell-correct.ts b/projects/common/models/elements/input-elements/spell-correct.ts index ede9473ee0b987fa3dbaa2dd2f6e9a509097ab4c..3b2d337a2a3fdf8add417789cfb83a51b97e480d 100644 --- a/projects/common/models/elements/input-elements/spell-correct.ts +++ b/projects/common/models/elements/input-elements/spell-correct.ts @@ -1,24 +1,38 @@ import { Type } from '@angular/core'; import { - PositionedUIElement, UIElement, TextInputElement, TextInputElementProperties, UIElementType + PositionedUIElement, TextInputElement, TextInputElementProperties, UIElementType } from 'common/models/elements/element'; import { ElementComponent } from 'common/directives/element-component.directive'; import { SpellCorrectComponent } from 'common/components/input-elements/spell-correct.component'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { - BasicStyles, - PositionProperties + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators } from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class SpellCorrectElement extends TextInputElement implements PositionedUIElement, SpellCorrectProperties { type: UIElementType = 'spell-correct'; position: PositionProperties; styling: BasicStyles; - constructor(element: SpellCorrectProperties) { + constructor(element?: SpellCorrectProperties) { super(element); - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at SpellCorrect instantiation', element); + } + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 230, + height: 80, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = PropertyGroupGenerators.generateBasicStyleProps(element?.styling); + } } hasAnswerScheme(): boolean { @@ -46,3 +60,9 @@ export interface SpellCorrectProperties extends TextInputElementProperties { position: PositionProperties; styling: BasicStyles; } + +function isValid(blueprint?: SpellCorrectProperties): boolean { + if (!blueprint) return false; + return PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling); +} diff --git a/projects/common/models/elements/input-elements/text-area.ts b/projects/common/models/elements/input-elements/text-area.ts index f5feeacd6e0ce629e60a0c0d7d4c3afcbcaa683c..34ebc0e7febf466bf54094687a7908e4d67d9e82 100644 --- a/projects/common/models/elements/input-elements/text-area.ts +++ b/projects/common/models/elements/input-elements/text-area.ts @@ -1,40 +1,64 @@ import { Type } from '@angular/core'; import { - PositionedUIElement, UIElement, TextInputElement, TextInputElementProperties, UIElementType + PositionedUIElement, TextInputElement, TextInputElementProperties, UIElementType } from 'common/models/elements/element'; import { ElementComponent } from 'common/directives/element-component.directive'; import { TextAreaComponent } from 'common/components/input-elements/text-area.component'; - import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { - BasicStyles, PositionProperties + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators } from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class TextAreaElement extends TextInputElement implements PositionedUIElement, TextAreaProperties { type: UIElementType = 'text-area'; - appearance: 'fill' | 'outline'; - resizeEnabled: boolean; - hasDynamicRowCount: boolean; - rowCount: number; - expectedCharactersCount: number; - hasReturnKey: boolean; - hasKeyboardIcon: boolean; + appearance: 'fill' | 'outline' = 'outline'; + resizeEnabled: boolean = false; + hasDynamicRowCount: boolean = false; + rowCount: number = 3; + expectedCharactersCount: number = 300; + hasReturnKey: boolean = false; + hasKeyboardIcon: boolean = false; position: PositionProperties; styling: BasicStyles & { lineHeight: number; }; - constructor(element: TextAreaProperties) { + constructor(element?: TextAreaProperties) { super(element); - this.appearance = element.appearance; - this.resizeEnabled = element.resizeEnabled; - this.rowCount = element.rowCount; - this.hasDynamicRowCount = element.hasDynamicRowCount; - this.expectedCharactersCount = element.expectedCharactersCount; - this.hasReturnKey = element.hasReturnKey; - this.hasKeyboardIcon = element.hasKeyboardIcon; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.appearance = element.appearance; + this.resizeEnabled = element.resizeEnabled; + this.rowCount = element.rowCount; + this.hasDynamicRowCount = element.hasDynamicRowCount; + this.expectedCharactersCount = element.expectedCharactersCount; + this.hasReturnKey = element.hasReturnKey; + this.hasKeyboardIcon = element.hasKeyboardIcon; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at TextArea instantiation', element); + } + if (element?.appearance) this.appearance = element.appearance; + if (element?.resizeEnabled) this.resizeEnabled = element.resizeEnabled; + if (element?.rowCount) this.rowCount = element.rowCount; + if (element?.hasDynamicRowCount) this.hasDynamicRowCount = element.hasDynamicRowCount; + if (element?.expectedCharactersCount) this.expectedCharactersCount = element.expectedCharactersCount; + if (element?.hasReturnKey) this.hasReturnKey = element.hasReturnKey; + if (element?.hasKeyboardIcon) this.hasKeyboardIcon = element.hasKeyboardIcon; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 230, + height: 132, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 135 + }; + } } hasAnswerScheme(): boolean { @@ -71,3 +95,17 @@ export interface TextAreaProperties extends TextInputElementProperties { lineHeight: number; }; } + +function isValid(blueprint?: TextAreaProperties): boolean { + if (!blueprint) return false; + return blueprint.appearance !== undefined && + blueprint.resizeEnabled !== undefined && + blueprint.hasDynamicRowCount !== undefined && + blueprint.rowCount !== undefined && + blueprint.expectedCharactersCount !== undefined && + blueprint.hasReturnKey !== undefined && + blueprint.hasKeyboardIcon !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined; +} diff --git a/projects/common/models/elements/input-elements/text-field.ts b/projects/common/models/elements/input-elements/text-field.ts index 02af6f3542756aaaa44f5443ee35b0c4235ee959..ccd15aadc83c5ab66fa770b922b08418afc7f15b 100644 --- a/projects/common/models/elements/input-elements/text-field.ts +++ b/projects/common/models/elements/input-elements/text-field.ts @@ -6,41 +6,68 @@ import { ElementComponent } from 'common/directives/element-component.directive' import { TextFieldComponent } from 'common/components/input-elements/text-field.component'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { - BasicStyles, - PositionProperties + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators } from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class TextFieldElement extends TextInputElement implements PositionedUIElement, TextFieldProperties { type: UIElementType = 'text-field'; - appearance: 'fill' | 'outline'; - minLength: number | null; - minLengthWarnMessage: string; - maxLength: number | null; - maxLengthWarnMessage: string; - isLimitedToMaxLength: boolean; - pattern: string | null; - patternWarnMessage: string; - hasKeyboardIcon: boolean; - clearable: boolean; + appearance: 'fill' | 'outline' = 'outline'; + minLength: number | null = null; + minLengthWarnMessage: string = 'Eingabe zu kurz'; + maxLength: number | null = null; + maxLengthWarnMessage: string = 'Eingabe zu lang'; + isLimitedToMaxLength: boolean = false; + pattern: string | null = null; + patternWarnMessage: string = 'Eingabe entspricht nicht der Vorgabe'; + hasKeyboardIcon: boolean = false; + clearable: boolean = false; position: PositionProperties; styling: BasicStyles & { lineHeight: number; }; - constructor(element: TextFieldProperties) { + constructor(element?: TextFieldProperties) { super(element); - this.appearance = element.appearance; - this.minLength = element.minLength; - this.minLengthWarnMessage = element.minLengthWarnMessage; - this.maxLength = element.maxLength; - this.maxLengthWarnMessage = element.maxLengthWarnMessage; - this.isLimitedToMaxLength = element.isLimitedToMaxLength; - this.pattern = element.pattern; - this.patternWarnMessage = element.patternWarnMessage; - this.clearable = element.clearable; - this.hasKeyboardIcon = element.hasKeyboardIcon; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.appearance = element.appearance; + this.minLength = element.minLength; + this.minLengthWarnMessage = element.minLengthWarnMessage; + this.maxLength = element.maxLength; + this.maxLengthWarnMessage = element.maxLengthWarnMessage; + this.isLimitedToMaxLength = element.isLimitedToMaxLength; + this.pattern = element.pattern; + this.patternWarnMessage = element.patternWarnMessage; + this.clearable = element.clearable; + this.hasKeyboardIcon = element.hasKeyboardIcon; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at TextField instantiation', element); + } + if (element?.appearance) this.appearance = element.appearance; + if (element?.minLength) this.minLength = element.minLength; + if (element?.minLengthWarnMessage) this.minLengthWarnMessage = element.minLengthWarnMessage; + if (element?.maxLength) this.maxLength = element.maxLength; + if (element?.maxLengthWarnMessage) this.maxLengthWarnMessage = element.maxLengthWarnMessage; + if (element?.isLimitedToMaxLength) this.isLimitedToMaxLength = element.isLimitedToMaxLength; + if (element?.pattern) this.pattern = element.pattern; + if (element?.patternWarnMessage) this.patternWarnMessage = element.patternWarnMessage; + if (element?.clearable) this.clearable = element.clearable; + if (element?.hasKeyboardIcon) this.hasKeyboardIcon = element.hasKeyboardIcon; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 180, + height: 120, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 135 + }; + } } hasAnswerScheme(): boolean { @@ -80,3 +107,20 @@ export interface TextFieldProperties extends TextInputElementProperties { lineHeight: number; }; } + +function isValid(blueprint?: TextFieldProperties): boolean { + if (!blueprint) return false; + return blueprint.appearance !== undefined && + blueprint.minLength !== undefined && + blueprint.minLengthWarnMessage !== undefined && + blueprint.maxLength !== undefined && + blueprint.maxLengthWarnMessage !== undefined && + blueprint.isLimitedToMaxLength !== undefined && + blueprint.pattern !== undefined && + blueprint.patternWarnMessage !== undefined && + blueprint.hasKeyboardIcon !== undefined && + blueprint.clearable !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined; +} diff --git a/projects/common/models/elements/media-elements/audio.ts b/projects/common/models/elements/media-elements/audio.ts index 67d02db8fa1c70b93d34929cf69f9877b15dfc32..c93d72581c5dcfb44a2f3db21bd5dc4433d8bbce 100644 --- a/projects/common/models/elements/media-elements/audio.ts +++ b/projects/common/models/elements/media-elements/audio.ts @@ -1,24 +1,37 @@ import { Type } from '@angular/core'; import { - PlayerElement, - PlayerElementBlueprint, - PositionedUIElement, - UIElement, - UIElementType + PlayerElement, PlayerElementBlueprint, PositionedUIElement, UIElementType } from 'common/models/elements/element'; import { ElementComponent } from 'common/directives/element-component.directive'; import { AudioComponent } from 'common/components/media-elements/audio.component'; -import { PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class AudioElement extends PlayerElement implements PositionedUIElement, AudioProperties { type: UIElementType = 'audio'; - src: string | null; + src: string | null = null; position: PositionProperties; - constructor(element: AudioProperties) { + constructor(element?: AudioProperties) { super(element); - this.src = element.src; - this.position = element.position; + if (element && isValid(element)) { + this.src = element.src; + this.position = element.position; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Audio instantiation', element); + } + if (element?.src !== undefined) this.src = element.src; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 250, + height: 90, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + } } getElementComponent(): Type<ElementComponent> { @@ -30,3 +43,9 @@ export interface AudioProperties extends PlayerElementBlueprint { src: string | null; position: PositionProperties; } + +function isValid(blueprint?: AudioProperties): boolean { + if (!blueprint) return false; + return blueprint.src !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position); +} diff --git a/projects/common/models/elements/media-elements/image.ts b/projects/common/models/elements/media-elements/image.ts index 8344481dfcd72d6f7f1dd3cdd20c00f8565fe25f..99e36f825df6b02a5acdf8d7a516d1a8476caee2 100644 --- a/projects/common/models/elements/media-elements/image.ts +++ b/projects/common/models/elements/media-elements/image.ts @@ -4,31 +4,53 @@ import { } from 'common/models/elements/element'; import { ElementComponent } from 'common/directives/element-component.directive'; import { ImageComponent } from 'common/components/media-elements/image.component'; -import { DimensionProperties, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class ImageElement extends UIElement implements PositionedUIElement, ImageProperties { type: UIElementType = 'image'; - src: string | null; - alt: string; - scale: boolean; - magnifier: boolean; - magnifierSize: number; - magnifierZoom: number; - magnifierUsed: boolean; + src: string | null = null; + alt: string = 'Bild nicht gefunden'; + scale: boolean = false; + magnifier: boolean = false; + magnifierSize: number = 100; + magnifierZoom: number = 1.5; + magnifierUsed: boolean = false; position: PositionProperties; - constructor(element: ImageProperties) { + constructor(element?: ImageProperties) { super(element); - this.src = element.src; - this.alt = element.alt; - this.scale = element.scale; - this.magnifier = element.magnifier; - this.magnifierSize = element.magnifierSize; - this.magnifierZoom = element.magnifierZoom; - this.magnifierUsed = element.magnifierUsed; - this.position = element.position; + if (element && isValid(element)) { + this.src = element.src; + this.alt = element.alt; + this.scale = element.scale; + this.magnifier = element.magnifier; + this.magnifierSize = element.magnifierSize; + this.magnifierZoom = element.magnifierZoom; + this.magnifierUsed = element.magnifierUsed; + this.position = element.position; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Image instantiation', element); + } + if (element?.src !== undefined) this.src = element.src; + if (element?.alt !== undefined) this.alt = element.alt; + if (element?.scale !== undefined) this.scale = element.scale; + if (element?.magnifier !== undefined) this.magnifier = element.magnifier; + if (element?.magnifierSize !== undefined) this.magnifierSize = element.magnifierSize; + if (element?.magnifierZoom !== undefined) this.magnifierZoom = element.magnifierZoom; + if (element?.magnifierUsed !== undefined) this.magnifierUsed = element.magnifierUsed; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + height: 100, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + } } getElementComponent(): Type<ElementComponent> { @@ -62,3 +84,15 @@ export interface ImageProperties extends UIElementProperties { magnifierUsed: boolean; position: PositionProperties; } + +function isValid(blueprint?: ImageProperties): boolean { + if (!blueprint) return false; + return blueprint.src !== undefined && + blueprint.alt !== undefined && + blueprint.scale !== undefined && + blueprint.magnifier !== undefined && + blueprint.magnifierSize !== undefined && + blueprint.magnifierZoom !== undefined && + blueprint.magnifierUsed !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position); +} diff --git a/projects/common/models/elements/media-elements/video.ts b/projects/common/models/elements/media-elements/video.ts index a3a07b4fd1c96ca6c7db832d61c777f77f5f3a3b..6a6e5848eebd50377b09bb2f0c1d23fcf71f31a0 100644 --- a/projects/common/models/elements/media-elements/video.ts +++ b/projects/common/models/elements/media-elements/video.ts @@ -1,22 +1,40 @@ import { Type } from '@angular/core'; import { - PlayerElement, PlayerElementBlueprint, PositionedUIElement, UIElement, UIElementType + PlayerElement, PlayerElementBlueprint, PositionedUIElement, UIElementType } from 'common/models/elements/element'; import { ElementComponent } from 'common/directives/element-component.directive'; import { VideoComponent } from 'common/components/media-elements/video.component'; -import { PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { + PositionProperties, PropertyGroupGenerators, PropertyGroupValidators +} from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class VideoElement extends PlayerElement implements PositionedUIElement, VideoProperties { type: UIElementType = 'video'; - src: string | null; - scale: boolean; + src: string | null = null; + scale: boolean = false; position: PositionProperties; - constructor(element: VideoProperties) { + constructor(element?: VideoProperties) { super(element); - this.src = element.src; - this.scale = element.scale; - this.position = element.position; + if (element && isValid(element)) { + this.src = element.src; + this.scale = element.scale; + this.position = element.position; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Video instantiation', element); + } + if (element?.src !== undefined) this.src = element.src; + if (element?.scale !== undefined) this.scale = element.scale; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + width: 280, + height: 230, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + } } getElementComponent(): Type<ElementComponent> { @@ -29,3 +47,10 @@ export interface VideoProperties extends PlayerElementBlueprint { scale: boolean; position: PositionProperties; } + +function isValid(blueprint?: VideoProperties): boolean { + if (!blueprint) return false; + return blueprint.src !== undefined && + blueprint.scale !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position); +} diff --git a/projects/common/models/elements/property-group-interfaces.ts b/projects/common/models/elements/property-group-interfaces.ts index f22776143909b67efdff49479b26d744318de5c5..aebd98e14c5b62c932179a294fc38cf78919ee60 100644 --- a/projects/common/models/elements/property-group-interfaces.ts +++ b/projects/common/models/elements/property-group-interfaces.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line max-classes-per-file import { Measurement } from 'common/models/elements/element'; export interface PositionProperties { @@ -30,16 +31,6 @@ export interface DimensionProperties { export type Stylings = Partial<FontStyles & BorderStyles & OtherStyles>; export type BasicStyles = FontStyles & { backgroundColor: string }; -export interface OtherStyles { - [index: string]: unknown; - backgroundColor?: string; - lineHeight?: number; - itemBackgroundColor?: string; - lineColoring?: boolean; - lineColoringColor?: string; - selectionColor?: string; -} - export interface FontStyles { [index: string]: unknown; fontColor: string; @@ -58,6 +49,16 @@ export interface BorderStyles { borderRadius: number; } +export interface OtherStyles { + [index: string]: unknown; + backgroundColor?: string; + lineHeight?: number; + itemBackgroundColor?: string; + lineColoring?: boolean; + lineColoringColor?: string; + selectionColor?: string; +} + export interface PlayerProperties { [index: string]: unknown; autostart: boolean; @@ -81,3 +82,139 @@ export interface PlayerProperties { showRestTime: boolean; playbackTime: number; } + +export abstract class PropertyGroupValidators { + static isValidDimensionProps(blueprint?: DimensionProperties): boolean { + if (!blueprint) return false; + return blueprint.width !== undefined && + blueprint.height !== undefined && + blueprint.isWidthFixed !== undefined && + blueprint.isHeightFixed !== undefined && + blueprint.minWidth !== undefined && + blueprint.maxWidth !== undefined && + blueprint.minHeight !== undefined && + blueprint.maxHeight !== undefined; + } + + static isValidPosition(blueprint: PositionProperties): boolean { + return blueprint.xPosition !== undefined && + blueprint.yPosition !== undefined && + blueprint.gridColumn !== undefined && + blueprint.gridColumnRange !== undefined && + blueprint.gridRow !== undefined && + blueprint.gridRowRange !== undefined && + blueprint.marginLeft !== undefined && + blueprint.marginRight !== undefined && + blueprint.marginTop !== undefined && + blueprint.marginBottom !== undefined && + blueprint.zIndex !== undefined; + } + + static isValidBasicStyles(blueprint: BasicStyles): boolean { + return blueprint.backgroundColor !== undefined && + PropertyGroupValidators.isValidFontStyles(blueprint); + } + + static isValidFontStyles(blueprint: FontStyles): boolean { + return blueprint.fontColor !== undefined && + blueprint.font !== undefined && + blueprint.fontSize !== undefined && + blueprint.bold !== undefined && + blueprint.italic !== undefined && + blueprint.underline !== undefined; + } + + static isValidBorderStyles(blueprint: BorderStyles): boolean { + return blueprint.borderWidth !== undefined && + blueprint.borderColor !== undefined && + blueprint.borderStyle !== undefined && + blueprint.borderRadius !== undefined; + } +} + +export abstract class PropertyGroupGenerators { + static generatePositionProps(defaults: Partial<PositionProperties> = {}): PositionProperties { + return { + xPosition: defaults.xPosition !== undefined ? defaults.xPosition : 0, + yPosition: defaults.yPosition !== undefined ? defaults.yPosition : 0, + gridColumn: defaults.gridColumn !== undefined ? defaults.gridColumn : null, + gridColumnRange: defaults.gridColumnRange !== undefined ? defaults.gridColumnRange : 1, + gridRow: defaults.gridRow !== undefined ? defaults.gridRow : null, + gridRowRange: defaults.gridRowRange !== undefined ? defaults.gridRowRange : 1, + marginLeft: defaults.marginLeft !== undefined ? defaults.marginLeft : { value: 0, unit: 'px' }, + marginRight: defaults.marginRight !== undefined ? defaults.marginRight : { value: 0, unit: 'px' }, + marginTop: defaults.marginTop !== undefined ? defaults.marginTop : { value: 0, unit: 'px' }, + marginBottom: defaults.marginBottom !== undefined ? defaults.marginBottom : { value: 0, unit: 'px' }, + zIndex: defaults.zIndex !== undefined ? defaults.zIndex : 0 + }; + } + + static generateDimensionProps(defaults: Partial<DimensionProperties> = {}): DimensionProperties { + return { + width: defaults.width !== undefined ? defaults.width : 180, + height: defaults.height !== undefined ? defaults.height : 60, + isWidthFixed: defaults.isWidthFixed !== undefined ? defaults.isWidthFixed : false, + isHeightFixed: defaults.isHeightFixed !== undefined ? defaults.isHeightFixed : false, + minWidth: defaults.minWidth !== undefined ? defaults.minWidth : null, + maxWidth: defaults.maxWidth !== undefined ? defaults.maxWidth : null, + minHeight: defaults.minHeight !== undefined ? defaults.minHeight : null, + maxHeight: defaults.maxHeight !== undefined ? defaults.maxHeight : null + }; + } + + static generateBasicStyleProps(defaults: Partial<BasicStyles> = {}): BasicStyles { + return { + backgroundColor: defaults.backgroundColor !== undefined ? defaults.backgroundColor : 'transparent', + ...PropertyGroupGenerators.generateFontStylingProps(defaults) + }; + } + + static generateFontStylingProps(defaults: Partial<FontStyles> = {}): FontStyles { + return { + fontColor: defaults.fontColor !== undefined ? defaults.fontColor as string : '#000000', + font: defaults?.font !== undefined ? defaults.font as string : 'Roboto', + fontSize: defaults?.fontSize !== undefined ? defaults.fontSize as number : 20, + bold: defaults?.bold !== undefined ? defaults.bold as boolean : false, + italic: defaults?.italic !== undefined ? defaults.italic as boolean : false, + underline: defaults?.underline !== undefined ? defaults.underline as boolean : false + }; + } + + static generateBorderStylingProps(defaults: Partial<Stylings> = {}): BorderStyles { + return { + borderWidth: defaults.borderWidth !== undefined ? defaults.borderWidth : 0, + borderColor: defaults.borderColor !== undefined ? defaults.borderColor : 'black', + borderStyle: defaults.borderStyle !== undefined ? defaults.borderStyle : 'solid', + borderRadius: defaults.borderRadius !== undefined ? defaults.borderRadius : 0 + }; + } + + static generatePlayerProps(properties: Partial<PlayerProperties> = {}): PlayerProperties { + return { + autostart: properties.autostart !== undefined ? properties.autostart as boolean : false, + autostartDelay: properties.autostartDelay !== undefined ? properties.autostartDelay as number : 0, + loop: properties.loop !== undefined ? properties.loop as boolean : false, + startControl: properties.startControl !== undefined ? properties.startControl as boolean : true, + pauseControl: properties.pauseControl !== undefined ? properties.pauseControl as boolean : false, + progressBar: properties.progressBar !== undefined ? properties.progressBar as boolean : true, + interactiveProgressbar: properties.interactiveProgressbar !== undefined ? + properties.interactiveProgressbar as boolean : + false, + volumeControl: properties.volumeControl !== undefined ? properties.volumeControl as boolean : true, + defaultVolume: properties.defaultVolume !== undefined ? properties.defaultVolume as number : 0.8, + minVolume: properties.minVolume !== undefined ? properties.minVolume as number : 0, + muteControl: properties.muteControl !== undefined ? properties.muteControl as boolean : true, + interactiveMuteControl: properties.interactiveMuteControl !== undefined ? + properties.interactiveMuteControl as boolean : + false, + hintLabel: properties.hintLabel !== undefined ? properties.hintLabel as string : '', + hintLabelDelay: properties.hintLabelDelay !== undefined ? properties.hintLabelDelay as number : 0, + activeAfterID: properties.activeAfterID !== undefined ? properties.activeAfterID as string : '', + minRuns: properties.minRuns !== undefined ? properties.minRuns as number : 1, + maxRuns: properties.maxRuns !== undefined ? properties.maxRuns as number | null : null, + showRestRuns: properties.showRestRuns !== undefined ? properties.showRestRuns as boolean : false, + showRestTime: properties.showRestTime !== undefined ? properties.showRestTime as boolean : true, + playbackTime: properties.playbackTime !== undefined ? properties.playbackTime as number : 0 + }; + } +} diff --git a/projects/common/models/elements/text/text.ts b/projects/common/models/elements/text/text.ts index 28bd99b2ec5dab08f5dfc863cbbd968261f21d06..32970bba888bab0cf84f74a240bb45caf37af4da 100644 --- a/projects/common/models/elements/text/text.ts +++ b/projects/common/models/elements/text/text.ts @@ -8,33 +8,55 @@ import { TextComponent } from 'common/components/text/text.component'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { - BasicStyles, - PositionProperties + BasicStyles, PositionProperties, PropertyGroupGenerators, PropertyGroupValidators } from 'common/models/elements/property-group-interfaces'; +import { environment } from 'common/environment'; +import { InstantiationEror } from 'common/util/errors'; export class TextElement extends UIElement implements PositionedUIElement, TextProperties { type: UIElementType = 'text'; - text: string; - highlightableOrange: boolean; - highlightableTurquoise: boolean; - highlightableYellow: boolean; - hasSelectionPopup: boolean; - columnCount: number; + text: string = 'Lorem ipsum dolor sit amet'; + highlightableOrange: boolean = false; + highlightableTurquoise: boolean = false; + highlightableYellow: boolean = false; + hasSelectionPopup: boolean = true; + columnCount: number = 1; position: PositionProperties; styling: BasicStyles & { lineHeight: number; }; - constructor(element: TextProperties) { + constructor(element?: TextProperties) { super(element); - this.text = element.text; - this.highlightableOrange = element.highlightableOrange; - this.highlightableTurquoise = element.highlightableTurquoise; - this.highlightableYellow = element.highlightableYellow; - this.hasSelectionPopup = element.hasSelectionPopup; - this.columnCount = element.columnCount; - this.position = element.position; - this.styling = element.styling; + if (element && isValid(element)) { + this.text = element.text; + this.highlightableOrange = element.highlightableOrange; + this.highlightableTurquoise = element.highlightableTurquoise; + this.highlightableYellow = element.highlightableYellow; + this.hasSelectionPopup = element.hasSelectionPopup; + this.columnCount = element.columnCount; + this.position = element.position; + this.styling = element.styling; + } else { + if (environment.strictInstantiation) { + throw new InstantiationEror('Error at Text instantiation', element); + } + if (element?.text !== undefined) this.text = element.text; + if (element?.highlightableOrange !== undefined) this.highlightableOrange = element.highlightableOrange; + if (element?.highlightableTurquoise !== undefined) this.highlightableTurquoise = element.highlightableTurquoise; + if (element?.highlightableYellow !== undefined) this.highlightableYellow = element.highlightableYellow; + if (element?.hasSelectionPopup !== undefined) this.hasSelectionPopup = element.hasSelectionPopup; + if (element?.columnCount !== undefined) this.columnCount = element.columnCount; + this.dimensions = PropertyGroupGenerators.generateDimensionProps({ + height: 98, + ...element?.dimensions + }); + this.position = PropertyGroupGenerators.generatePositionProps(element?.position); + this.styling = { + ...PropertyGroupGenerators.generateBasicStyleProps(element?.styling), + lineHeight: element?.styling?.lineHeight || 135 + }; + } } private isHighlightable(): boolean { @@ -87,3 +109,16 @@ export interface TextProperties extends UIElementProperties { lineHeight: number; }; } + +function isValid(blueprint?: TextProperties): boolean { + if (!blueprint) return false; + return blueprint.text !== undefined && + blueprint.highlightableOrange !== undefined && + blueprint.highlightableTurquoise !== undefined && + blueprint.highlightableYellow !== undefined && + blueprint.hasSelectionPopup !== undefined && + blueprint.columnCount !== undefined && + PropertyGroupValidators.isValidPosition(blueprint.position) && + PropertyGroupValidators.isValidBasicStyles(blueprint.styling) && + blueprint.styling.lineHeight !== undefined; +} diff --git a/projects/common/models/page.ts b/projects/common/models/page.ts index 1f9b4b64f80b439678de7aab332c068c86e4688a..161ac8e00c5790ccf7321dfad8850decbdfcbf55 100644 --- a/projects/common/models/page.ts +++ b/projects/common/models/page.ts @@ -1,6 +1,7 @@ import { Section } from 'common/models/section'; import { UIElement } from 'common/models/elements/element'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; +import { environment } from 'common/environment'; export class Page { [index: string]: unknown; @@ -13,15 +14,29 @@ export class Page { alwaysVisiblePagePosition: 'left' | 'right' | 'top' | 'bottom' = 'left'; alwaysVisibleAspectRatio: number = 50; - constructor(page: PageProperties) { - this.hasMaxWidth = page.hasMaxWidth; - this.maxWidth = page.maxWidth; - this.margin = page.margin; - this.backgroundColor = page.backgroundColor; - this.alwaysVisible = page.alwaysVisible; - this.alwaysVisiblePagePosition = page.alwaysVisiblePagePosition; - this.alwaysVisibleAspectRatio = page.alwaysVisibleAspectRatio; - this.sections = page.sections.map(section => new Section(section)); + constructor(page?: PageProperties) { + if (page && isValid(page)) { + this.hasMaxWidth = page.hasMaxWidth; + this.maxWidth = page.maxWidth; + this.margin = page.margin; + this.backgroundColor = page.backgroundColor; + this.alwaysVisible = page.alwaysVisible; + this.alwaysVisiblePagePosition = page.alwaysVisiblePagePosition; + this.alwaysVisibleAspectRatio = page.alwaysVisibleAspectRatio; + this.sections = page.sections.map(section => new Section(section)); + } else { + if (environment.strictInstantiation) { + throw Error('Error at Page instantiation'); + } + if (page?.hasMaxWidth !== undefined) this.hasMaxWidth = page.hasMaxWidth; + if (page?.maxWidth !== undefined) this.maxWidth = page.maxWidth; + if (page?.margin !== undefined) this.margin = page.margin; + if (page?.backgroundColor !== undefined) this.backgroundColor = page.backgroundColor; + if (page?.alwaysVisible !== undefined) this.alwaysVisible = page.alwaysVisible; + if (page?.alwaysVisiblePagePosition !== undefined) this.alwaysVisiblePagePosition = page.alwaysVisiblePagePosition; + if (page?.alwaysVisibleAspectRatio !== undefined) this.alwaysVisibleAspectRatio = page.alwaysVisibleAspectRatio; + this.sections = page?.sections.map(section => new Section(section)) || [new Section()]; + } } getAllElements(elementType?: string): UIElement[] { @@ -43,3 +58,15 @@ export interface PageProperties { alwaysVisiblePagePosition: 'left' | 'right' | 'top' | 'bottom'; alwaysVisibleAspectRatio: number; } + +function isValid(blueprint?: PageProperties): boolean { + if (!blueprint) return false; + return blueprint.sections !== undefined && + blueprint.hasMaxWidth !== undefined && + blueprint.maxWidth !== undefined && + blueprint.margin !== undefined && + blueprint.backgroundColor !== undefined && + blueprint.alwaysVisible !== undefined && + blueprint.alwaysVisiblePagePosition !== undefined && + blueprint.alwaysVisibleAspectRatio !== undefined; +} diff --git a/projects/common/models/section.ts b/projects/common/models/section.ts index 3b0026465a5ca1308e180511091495a5ea616cfd..d21be97a0683f33fecf6ea6ab5b56da068b70188 100644 --- a/projects/common/models/section.ts +++ b/projects/common/models/section.ts @@ -11,6 +11,7 @@ import { ImageElement } from 'common/models/elements/media-elements/image'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { VisibilityRule } from 'common/models/visibility-rule'; import { ElementFactory } from 'common/util/element.factory'; +import { environment } from 'common/environment'; export class Section { [index: string]: unknown; @@ -27,20 +28,40 @@ export class Section { enableReHide: boolean = false; visibilityRules: VisibilityRule[] = []; - constructor(blueprint: SectionProperties) { - this.height = blueprint.height; - this.backgroundColor = blueprint.backgroundColor; - this.dynamicPositioning = blueprint.dynamicPositioning; - this.autoColumnSize = blueprint.autoColumnSize; - this.autoRowSize = blueprint.autoRowSize; - this.gridColumnSizes = blueprint.gridColumnSizes; - this.gridRowSizes = blueprint.gridRowSizes; - this.visibilityDelay = blueprint.visibilityDelay; - this.animatedVisibility = blueprint.animatedVisibility; - this.enableReHide = blueprint.enableReHide; - this.visibilityRules = blueprint.visibilityRules; - this.elements = blueprint.elements - .map(element => ElementFactory.createElement(element)) as PositionedUIElement[]; + constructor(section?: SectionProperties) { + if (section && isValid(section)) { + this.height = section.height; + this.backgroundColor = section.backgroundColor; + this.dynamicPositioning = section.dynamicPositioning; + this.autoColumnSize = section.autoColumnSize; + this.autoRowSize = section.autoRowSize; + this.gridColumnSizes = section.gridColumnSizes; + this.gridRowSizes = section.gridRowSizes; + this.visibilityDelay = section.visibilityDelay; + this.animatedVisibility = section.animatedVisibility; + this.enableReHide = section.enableReHide; + this.visibilityRules = section.visibilityRules; + this.elements = section.elements + .map(element => ElementFactory.createElement(element)) as PositionedUIElement[]; + } else { + if (environment.strictInstantiation) { + throw Error('Error at Section instantiation'); + } + if (section?.height !== undefined) this.height = section.height; + if (section?.backgroundColor !== undefined) this.backgroundColor = section.backgroundColor; + if (section?.dynamicPositioning !== undefined) this.dynamicPositioning = section.dynamicPositioning; + if (section?.autoColumnSize !== undefined) this.autoColumnSize = section.autoColumnSize; + if (section?.autoRowSize !== undefined) this.autoRowSize = section.autoRowSize; + if (section?.gridColumnSizes !== undefined) this.gridColumnSizes = section.gridColumnSizes; + if (section?.gridRowSizes !== undefined) this.gridRowSizes = section.gridRowSizes; + if (section?.visibilityDelay !== undefined) this.visibilityDelay = section.visibilityDelay; + if (section?.animatedVisibility !== undefined) this.animatedVisibility = section.animatedVisibility; + if (section?.enableReHide !== undefined) this.enableReHide = section.enableReHide; + if (section?.visibilityRules !== undefined) this.visibilityRules = section.visibilityRules; + this.elements = section?.elements !== undefined ? + section.elements.map(element => ElementFactory.createElement(element)) as PositionedUIElement[] : + []; + } } setProperty(property: string, value: UIElementValue): void { @@ -69,24 +90,6 @@ export class Section { (element as InputElement).getAnswerScheme(dropLists) : (element as InputElement | PlayerElement | TextElement | ImageElement).getAnswerScheme())); } - - static sanitizeBlueprint(blueprint?: Record<string, UIElementValue>): Partial<Section> { - if (!blueprint) return {}; - - return { - ...blueprint, - gridColumnSizes: typeof blueprint.gridColumnSizes === 'string' ? - (blueprint.gridColumnSizes as string) - .split(' ') - .map(size => ({ value: Number(size.slice(0, -2)), unit: size.slice(-2) })) : - blueprint.gridColumnSizes as Measurement[], - gridRowSizes: typeof blueprint.gridRowSizes === 'string' ? - (blueprint.gridRowSizes as string) - .split(' ') - .map(size => ({ value: Number(size.slice(0, -2)), unit: size.slice(-2) })) : - blueprint.gridRowSizes as Measurement[] - }; - } } export interface SectionProperties { @@ -103,3 +106,19 @@ export interface SectionProperties { enableReHide: boolean; visibilityRules: VisibilityRule[]; } + +function isValid(blueprint?: SectionProperties): boolean { + if (!blueprint) return false; + return blueprint.elements !== undefined && + blueprint.height !== undefined && + blueprint.backgroundColor !== undefined && + blueprint.dynamicPositioning !== undefined && + blueprint.autoColumnSize !== undefined && + blueprint.autoRowSize !== undefined && + blueprint.gridColumnSizes !== undefined && + blueprint.gridRowSizes !== undefined && + blueprint.visibilityDelay !== undefined && + blueprint.animatedVisibility !== undefined && + blueprint.enableReHide !== undefined && + blueprint.visibilityRules !== undefined; +} diff --git a/projects/common/models/unit.ts b/projects/common/models/unit.ts index 2a8f46b4451a9f8d620bfe9883e2979641a3a0cf..58da915c4794bb8b7929c5e5939dbc8ed74e8bbc 100644 --- a/projects/common/models/unit.ts +++ b/projects/common/models/unit.ts @@ -2,17 +2,28 @@ import { Page } from 'common/models/page'; import { UIElement } from 'common/models/elements/element'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; import { StateVariable } from 'common/models/state-variable'; +import { environment } from 'common/environment'; +import { VersionManager } from 'common/services/version-manager'; export class Unit implements UnitProperties { type = 'aspect-unit-definition'; version: string; - stateVariables: StateVariable[]; + stateVariables: StateVariable[] = []; pages: Page[]; - constructor(unit: UnitProperties) { - this.version = unit.version; - this.stateVariables = unit.stateVariables; - this.pages = unit.pages.map(page => new Page(page)); + constructor(unit?: UnitProperties) { + if (unit && isValid(unit)) { + this.version = unit.version; + this.stateVariables = unit.stateVariables; + this.pages = unit.pages.map(page => new Page(page)); + } else { + if (environment.strictInstantiation) { + throw Error('Error at unit instantiation'); + } + this.version = VersionManager.getCurrentVersion(); + if (unit?.stateVariables !== undefined) this.stateVariables = unit.stateVariables; + this.pages = unit?.pages.map(page => new Page(page)) || [new Page()]; + } } getAllElements(elementType?: string): UIElement[] { @@ -27,6 +38,14 @@ export class Unit implements UnitProperties { } } +function isValid(blueprint?: UnitProperties): boolean { + if (!blueprint) return false; + return blueprint.version !== undefined && + blueprint.stateVariables !== undefined && + blueprint.type !== undefined && + blueprint.pages !== undefined; +} + export interface UnitProperties { type: string; version: string; diff --git a/projects/common/services/message.service.ts b/projects/common/services/message.service.ts index e2006fd7eaec8643018368ef6dd201ab0fd94cf4..c843602c618178b691bbed883cca7064481c6194 100644 --- a/projects/common/services/message.service.ts +++ b/projects/common/services/message.service.ts @@ -1,8 +1,10 @@ +// eslint-disable-next-line max-classes-per-file import { Component, Inject, Injectable, Input, Optional } from '@angular/core'; import { MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar'; import { ReferenceList } from 'editor/src/app/services/reference-manager'; +import { UIElement } from 'common/models/elements/element'; @Injectable({ providedIn: 'root' @@ -10,32 +12,39 @@ import { ReferenceList } from 'editor/src/app/services/reference-manager'; export class MessageService { constructor(private _snackBar: MatSnackBar) {} - showMessage(text: string): void { - this._snackBar.open(text, undefined, { duration: 3000 }); + showMessage(text: string, duration: number = 3000): void { + this._snackBar.open(text, undefined, { duration: duration }); } - showSuccess(text: string): void { - this._snackBar.open(text, undefined, { duration: 3000, panelClass: 'snackbar-success' }); + showSuccess(text: string, duration: number = 3000): void { + this._snackBar.open(text, undefined, { duration: duration, panelClass: 'snackbar-success' }); } - showWarning(text: string): void { - this._snackBar.open(text, undefined, { duration: 3000, panelClass: 'snackbar-warning' }); + showWarning(text: string, duration: number = 3000): void { + this._snackBar.open(text, undefined, { duration: duration, panelClass: 'snackbar-warning' }); } - showError(text: string): void { - this._snackBar.open(text, undefined, { duration: 3000, panelClass: 'snackbar-error' }); + showError(text: string, duration: number = 3000): void { + this._snackBar.open(text, undefined, { duration: duration, panelClass: 'snackbar-error' }); } showPrompt(text: string): void { this._snackBar.open(text, 'OK', { panelClass: 'snackbar-error' }); } - showReferencePanel(refs: any[]): void { + showReferencePanel(refs: ReferenceList[]): void { this._snackBar.openFromComponent(ReferenceListSnackbarComponent, { data: refs, horizontalPosition: 'left' }); } + + showFixedReferencePanel(refs: UIElement[]): void { + this._snackBar.openFromComponent(FixedReferencesSnackbarComponent, { + data: refs, + horizontalPosition: 'left' + }); + } } @Component({ @@ -62,3 +71,51 @@ export class ReferenceListSnackbarComponent { constructor(public snackBarRef: MatSnackBarRef<ReferenceListSnackbarComponent>, @Optional()@Inject(MAT_SNACK_BAR_DATA) public data?: ReferenceList[]) { } } + +@Component({ + selector: 'aspect-invalid-reference-elements-list-snackbar', + template: ` + Invalide Referenzen bei folgenden <br> Elementen wurden entfernt: + <mat-list> + <mat-list-item *ngFor="let element of data"> + <mat-icon *ngIf="element.type == 'drop-list'" matListItemIcon> + drag_indicator + </mat-icon> + <div *ngIf="element.type == 'drop-list'" matListItemTitle> + Ablegeliste: {{element.id}} + </div> + <mat-icon *ngIf="element.type == 'button'" matListItemIcon> + smart_button + </mat-icon> + <div *ngIf="element.type == 'button'" matListItemTitle> + Knopf: {{element.id}} + </div> + <mat-icon *ngIf="element.type == 'audio'" matListItemIcon> + volume_up + </mat-icon> + <div *ngIf="element.type == 'audio'" matListItemTitle> + Audio: {{element.id}} + </div> + </mat-list-item> + </mat-list> + <span matSnackBarActions> + <button mat-stroked-button matSnackBarAction (click)="snackBarRef.dismiss()"> + Schließen + </button> + </span> + `, + styles: [` + :host {font-size: large;} + button { + color: var(--mat-snack-bar-button-color) !important; + --mat-mdc-button-persistent-ripple-color: currentColor !important; + } + mat-icon {color: inherit !important;} + .mat-mdc-list-item-title {color: inherit !important;} + ` + ] +}) +export class FixedReferencesSnackbarComponent { + constructor(public snackBarRef: MatSnackBarRef<FixedReferencesSnackbarComponent>, + @Optional()@Inject(MAT_SNACK_BAR_DATA) public data: UIElement[]) { } +} diff --git a/projects/common/services/version-manager.spec.ts b/projects/common/services/version-manager.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b0f0defa0d421d3ef2378c2a59f25abbdbd1da1 --- /dev/null +++ b/projects/common/services/version-manager.spec.ts @@ -0,0 +1,9 @@ +import { VersionManager } from 'common/services/version-manager'; + +describe('VersionManager', () => { + it('should be able to read version from package.json', () => { + const result = VersionManager.getCurrentVersion(); + expect(result).toBeDefined(); + expect(result.split('.').length).toEqual(3); + }); +}); diff --git a/projects/common/services/version-manager.ts b/projects/common/services/version-manager.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ef369e966f36c2d117832d36690863135fcee7f --- /dev/null +++ b/projects/common/services/version-manager.ts @@ -0,0 +1,58 @@ +import packageJSON from '../../../package.json'; + +/* General version strategy: + Player + Editor: + - isNewer -> abgelehnt + - isOlder -> wunderbar + + - isLesserMajor is accepeted by Editor and sanitized + */ +export class VersionManager { + private static acceptedLesserMajor = [3, 10, 0]; + private static currentVersion: [number, number, number] = + packageJSON.config.unit_definition_version.split('.').map(Number) as [number, number, number]; + + static getCurrentVersion(): string { + return VersionManager.currentVersion.join('.'); + } + + static hasCompatibleVersion(unitDefinition: Record<string, unknown>): boolean { + const unitDefinitionVersion = VersionManager.getUnitDefinitionVersion(unitDefinition); + return !VersionManager.isNewer(unitDefinition) && + VersionManager.isSameMajor(unitDefinitionVersion); + } + + static isNewer(unitDefinition: Record<string, unknown>): boolean { + return VersionManager.compare(VersionManager.getUnitDefinitionVersion(unitDefinition)) === 1; + } + + static needsSanitization(unitDefinition: Record<string, unknown>): boolean { + const unitDefinitionVersion = VersionManager.getUnitDefinitionVersion(unitDefinition); + return !VersionManager.isSameMajor(unitDefinitionVersion) && + unitDefinitionVersion.join() === VersionManager.acceptedLesserMajor.join(); + } + + private static getUnitDefinitionVersion(unitDefinition: Record<string, any>): [number, number, number] { + return unitDefinition.version.split('.').map(Number); + } + + private static compare(unitDefinitionVersion: [number, number, number]): number { + let i = 0; + let result = 0; + while (result === 0 && i < 3) { + result = VersionManager.compareVersionDigit(unitDefinitionVersion[i], VersionManager.currentVersion[i]); + i += 1; + } + return result; + } + + /* -1 for older */ + private static compareVersionDigit(a: number, b: number): number { + if (a === b) return 0; + return a < b ? -1 : 1; + } + + private static isSameMajor(unitDefinitionVersion: [number, number, number]): boolean { + return VersionManager.compareVersionDigit(unitDefinitionVersion[0], VersionManager.currentVersion[0]) === 0; + } +} diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts index 8da83915b50d7e93e0dd0252c8186fe41b2d0e83..4ab919b66cfa355530f71b401a4a85df05918287 100644 --- a/projects/common/shared.module.ts +++ b/projects/common/shared.module.ts @@ -78,7 +78,11 @@ import { GetValuePipe, MathFieldComponent } from './components/input-elements/ma import { MeasurePipe } from './pipes/measure.pipe'; import { TextImagePanelComponent } from './components/text-image-panel.component'; import { ReferenceListComponent } from './components/reference-list.component'; -import { ReferenceListSnackbarComponent } from './services/message.service'; +import { + FixedReferencesSnackbarComponent, + ReferenceListSnackbarComponent +} from './services/message.service'; +import { UnitDefErrorDialogComponent } from './components/unit-def-error-dialog.component'; @NgModule({ imports: [ @@ -150,7 +154,9 @@ import { ReferenceListSnackbarComponent } from './services/message.service'; MeasurePipe, TextImagePanelComponent, ReferenceListComponent, - ReferenceListSnackbarComponent + ReferenceListSnackbarComponent, + FixedReferencesSnackbarComponent, + UnitDefErrorDialogComponent ], exports: [ CommonModule, diff --git a/projects/common/util/errors.ts b/projects/common/util/errors.ts new file mode 100644 index 0000000000000000000000000000000000000000..562d0a021a36796418287bc7b0e7c5549708230c --- /dev/null +++ b/projects/common/util/errors.ts @@ -0,0 +1,11 @@ +import { UIElementProperties } from 'common/models/elements/element'; + +/* Custom Error to show the element blueprint that failed validation. */ +export class InstantiationEror extends Error { + faultyBlueprint: UIElementProperties | undefined; + + constructor(message: string, faultyBlueprint?: UIElementProperties) { + super(message); + this.faultyBlueprint = faultyBlueprint; + } +} diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts index c93063804b0924caeb4ada28367b317462fc6803..edef07ffb5b21c1c3bef3a226a90969b3362091e 100644 --- a/projects/editor/src/app/app.module.ts +++ b/projects/editor/src/app/app.module.ts @@ -123,6 +123,7 @@ import { DeleteReferenceDialogComponent } from './components/dialogs/delete-refe import { GetStateVariableIdsPipe } from './components/properties-panel/model-properties-tab/input-groups/button-properties/get-state-variable-ids.pipe'; +import { SanitizationDialogComponent } from './components/dialogs/sanitization-dialog.component'; @NgModule({ declarations: [ @@ -189,7 +190,8 @@ import { StateVariableEditorComponent, ButtonActionParamStateVariableComponent, GetStateVariableIdsPipe, - VisibilityRulesDialogComponent + VisibilityRulesDialogComponent, + SanitizationDialogComponent ], imports: [ BrowserModule, diff --git a/projects/editor/src/app/components/dialogs/sanitization-dialog.component.ts b/projects/editor/src/app/components/dialogs/sanitization-dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..22ae1ad5327c99e718d82c61a834182d32219998 --- /dev/null +++ b/projects/editor/src/app/components/dialogs/sanitization-dialog.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'aspect-sanitization-dialog', + template: ` + <h1 mat-dialog-title>Unit-Definition wird aktualisiert</h1> + <p mat-dialog-content> + Eine veraltete Unit-Definition wurde geladen und muss angepasst werden.<br> + Sobald gespeichert wird, ist sie nicht mehr mit alten Versionen kompatibel. + </p> + <div mat-dialog-actions> + <button mat-button mat-dialog-close>Weiter</button> + </div> + ` +}) +export class SanitizationDialogComponent { } diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts index 258172791a2639e4898ef61b678fd8adcc7df272..dc5a68f2b5c8929ea5d125f9a6537a293abb0e28 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/options-field-set.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { CombinedProperties } from 'editor/src/app/components/properties-panel/element-properties-panel.component'; -import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row'; +import { LikertRowElement, LikertRowProperties } from 'common/models/elements/compound-elements/likert/likert-row'; import { UnitService } from 'editor/src/app/services/unit.service'; import { DialogService } from 'editor/src/app/services/dialog.service'; import { moveItemInArray } from '@angular/cdk/drag-drop'; @@ -10,7 +10,6 @@ import { SelectionService } from 'editor/src/app/services/selection.service'; import { IDService } from 'editor/src/app/services/id.service'; import { Label, TextImageLabel, TextLabel } from 'common/models/elements/label-interfaces'; import { OptionElement } from 'common/models/elements/element'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element-properties'; @Component({ selector: 'aspect-options-field-set', @@ -94,7 +93,6 @@ export class OptionsFieldSetComponent { addLikertRow(rowLabelText: string): void { const newRow = new LikertRowElement({ - ...ElementPropertyGenerator.getLikertRow(), id: this.idService.getAndRegisterNewID('likert-row'), rowLabel: { text: rowLabelText, @@ -102,7 +100,7 @@ export class OptionsFieldSetComponent { imgPosition: 'above' }, columnCount: (this.combinedProperties.options as unknown[]).length - }); + } as LikertRowProperties); (this.combinedProperties.rows as LikertRowElement[]).push(newRow); this.updateModel.emit({ property: 'rows', value: this.combinedProperties.rows as LikertRowElement[] }); } diff --git a/projects/editor/src/app/components/unit-view/unit-view.component.ts b/projects/editor/src/app/components/unit-view/unit-view.component.ts index 87ee76d684dae487f033badaa805fbe1a64402e2..6a2bdde4aa923cb745eadaea8280f3ae1a7817b9 100644 --- a/projects/editor/src/app/components/unit-view/unit-view.component.ts +++ b/projects/editor/src/app/components/unit-view/unit-view.component.ts @@ -40,18 +40,10 @@ export class UnitViewComponent implements OnDestroy { this.unitService.unit.pages[this.selectionService.selectedPageIndex] ); - const pageNavButtonRefs = this.unitService.referenceManager.getPageButtonReferences( + const pageNavButtonRefs = this.unitService.referenceManager.getButtonReferencesForPage( this.selectionService.selectedPageIndex ); - if (pageNavButtonRefs.length > 0) { - refs = refs.concat([{ - element: { - id: `Seite ${this.selectionService.selectedPageIndex + 1}`, - type: 'page' - }, - refs: pageNavButtonRefs - }]); - } + refs = refs.concat(pageNavButtonRefs); if (refs.length > 0) { this.dialogService.showDeleteReferenceDialog(refs) diff --git a/projects/editor/src/app/services/default-property-generators/element-properties.ts b/projects/editor/src/app/services/default-property-generators/element-properties.ts deleted file mode 100644 index 007f6ab36290d2efa5d91fd1fa897c6f722c4c9a..0000000000000000000000000000000000000000 --- a/projects/editor/src/app/services/default-property-generators/element-properties.ts +++ /dev/null @@ -1,474 +0,0 @@ -import { UIElementProperties, UIElementType } from 'common/models/elements/element'; -import { ButtonProperties } from 'common/models/elements/button/button'; -import { TextFieldProperties } from 'common/models/elements/input-elements/text-field'; -import { TextAreaProperties } from 'common/models/elements/input-elements/text-area'; -import { CheckboxProperties } from 'common/models/elements/input-elements/checkbox'; -import { DropdownProperties } from 'common/models/elements/input-elements/dropdown'; -import { RadioButtonGroupProperties } from 'common/models/elements/input-elements/radio-button-group'; -import { ImageProperties } from 'common/models/elements/media-elements/image'; -import { AudioProperties } from 'common/models/elements/media-elements/audio'; -import { VideoProperties } from 'common/models/elements/media-elements/video'; -import { LikertProperties } from 'common/models/elements/compound-elements/likert/likert'; -import { RadioButtonGroupComplexProperties } from 'common/models/elements/input-elements/radio-button-group-complex'; -import { DropListProperties } from 'common/models/elements/input-elements/drop-list'; -import { SliderProperties } from 'common/models/elements/input-elements/slider'; -import { SpellCorrectProperties } from 'common/models/elements/input-elements/spell-correct'; -import { FrameProperties } from 'common/models/elements/frame/frame'; -import { GeometryProperties } from 'common/models/elements/geometry/geometry'; -import { HotspotImageProperties } from 'common/models/elements/input-elements/hotspot-image'; -import { LikertRowProperties } from 'common/models/elements/compound-elements/likert/likert-row'; -import { TextProperties } from 'common/models/elements/text/text'; -import { ClozeElement, ClozeProperties } from 'common/models/elements/compound-elements/cloze/cloze'; -import { TextFieldSimpleProperties } - from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple'; -import { ToggleButtonProperties } - from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; -import { MathFieldProperties } from 'common/models/elements/input-elements/math-field'; -import { ElementPropertyGroupGenerator } - from 'editor/src/app/services/default-property-generators/element-property-groups'; - -export class ElementPropertyGenerator { - static getButton(): ButtonProperties { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - label: 'Knopf', - imageSrc: null, - asLink: false, - action: null, - actionParam: null, - styling: { - ...ElementPropertyGroupGenerator.generateFontStylingProps(), - ...ElementPropertyGroupGenerator.generateBorderStylingProps(), - backgroundColor: 'transparent' - } - }; - } - - static getFrame(): FrameProperties { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - hasBorderTop: true, - hasBorderBottom: true, - hasBorderLeft: true, - hasBorderRight: true, - position: { - ...ElementPropertyGroupGenerator.generatePositionProps(), - zIndex: -1 - }, - styling: { - ...ElementPropertyGroupGenerator.generateBorderStylingProps(), - borderWidth: 1, - backgroundColor: 'transparent' - } - }; - } - - static getCheckbox(): CheckboxProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 215 - }, - position: ElementPropertyGroupGenerator.generatePositionProps(), - styling: ElementPropertyGroupGenerator.generateBasicStyleProps() - }; - } - - static getDropList(): DropListProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - height: 100, - minHeight: 100 - }, - value: [], - onlyOneItem: false, - connectedTo: [], - copyOnDrop: false, - allowReplacement: false, - orientation: 'vertical', - highlightReceivingDropList: false, - highlightReceivingDropListColor: '#006064', - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - backgroundColor: '#ededed', - itemBackgroundColor: '#c9e0e0' - } - }; - } - - static getDropdown(): DropdownProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 240, - height: 83 - }, - options: [], - allowUnset: false, - position: ElementPropertyGroupGenerator.generatePositionProps(), - styling: ElementPropertyGroupGenerator.generateBasicStyleProps() - }; - } - - static getHotspotImage(): HotspotImageProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - height: 100 - }, - value: [], - src: null, - position: ElementPropertyGroupGenerator.generatePositionProps(), - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 135 - } - }; - } - - static getRadioButtonGroup(): RadioButtonGroupProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - height: 100 - }, - options: [], - alignment: 'column', - strikeOtherOptions: false, - position: ElementPropertyGroupGenerator.generatePositionProps(), - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 135 - } - }; - } - - static getRadioButtonGroupComplex(): RadioButtonGroupComplexProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - height: 100 - }, - options: [], - itemsPerRow: null, - position: { - ...ElementPropertyGroupGenerator.generatePositionProps(), - marginBottom: { value: 40, unit: 'px' } - }, - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps() - } - }; - } - - static getSlider(): SliderProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - position: ElementPropertyGroupGenerator.generatePositionProps(), - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 135 - }, - minValue: 0, - maxValue: 100, - showValues: true, - barStyle: false, - thumbLabel: false - }; - } - - static getSpellCorrect(): SpellCorrectProperties { - return { - ...ElementPropertyGroupGenerator.generateTextInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 230, - height: 80 - }, - position: ElementPropertyGroupGenerator.generatePositionProps(), - styling: ElementPropertyGroupGenerator.generateBasicStyleProps() - }; - } - - static getTextArea(): TextAreaProperties { - return { - ...ElementPropertyGroupGenerator.generateTextInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 230, - height: 132 - }, - position: ElementPropertyGroupGenerator.generatePositionProps(), - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 135 - }, - appearance: 'outline', - resizeEnabled: false, - hasDynamicRowCount: false, - rowCount: 3, - expectedCharactersCount: 300, - hasReturnKey: false, - hasKeyboardIcon: false - }; - } - - static getTextField(): TextFieldProperties { - return { - ...ElementPropertyGroupGenerator.generateTextInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 180, - height: 120 - }, - position: ElementPropertyGroupGenerator.generatePositionProps(), - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 135 - }, - appearance: 'outline', - minLength: null, - minLengthWarnMessage: 'Eingabe zu kurz', - maxLength: null, - maxLengthWarnMessage: 'Eingabe zu lang', - isLimitedToMaxLength: false, - pattern: null, - patternWarnMessage: 'Eingabe entspricht nicht der Vorgabe', - hasKeyboardIcon: false, - clearable: false - }; - } - - static getAudio(): AudioProperties { - return { - ...ElementPropertyGroupGenerator.generatePlayerElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 250, - height: 90 - }, - position: ElementPropertyGroupGenerator.generatePositionProps(), - src: null - }; - } - - static getVideo(): VideoProperties { - return { - ...ElementPropertyGroupGenerator.generatePlayerElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 280, - height: 230 - }, - position: ElementPropertyGroupGenerator.generatePositionProps(), - src: null, - scale: false - }; - } - - static getImage(): ImageProperties { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - height: 100 - }, - src: null, - alt: 'Bild nicht gefunden', - scale: false, - magnifier: false, - magnifierSize: 100, - magnifierZoom: 1.5, - magnifierUsed: false, - position: ElementPropertyGroupGenerator.generatePositionProps() - }; - } - - static getGeometry(): GeometryProperties { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - position: ElementPropertyGroupGenerator.generatePositionProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 600, - height: 400 - }, - appDefinition: '', - showResetIcon: true, - enableUndoRedo: true, - showToolbar: true, - enableShiftDragZoom: true, - showZoomButtons: true, - showFullscreenButton: true, - customToolbar: '' - }; - } - - static getLikert(): LikertProperties { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 250, - height: 200 - }, - position: { - ...ElementPropertyGroupGenerator.generatePositionProps(), - marginBottom: { value: 35, unit: 'px' } - }, - rows: [], - options: [], - firstColumnSizeRatio: 5, - label: 'Optionentabelle Beschriftung', - label2: 'Beschriftung Erste Spalte', - stickyHeader: false, - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - backgroundColor: 'white', - lineHeight: 135, - lineColoring: true, - lineColoringColor: '#c9e0e0' - } - }; - } - - static getLikertRow(): LikertRowProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - // TODO position? - rowLabel: { text: '', imgSrc: null, imgPosition: 'above' }, - columnCount: 0, - firstColumnSizeRatio: 5, - verticalButtonAlignment: 'center' - }; - } - - static getText(): TextProperties { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - height: 98 - }, - position: ElementPropertyGroupGenerator.generatePositionProps(), - text: 'Lorem ipsum dolor sit amet', - highlightableOrange: false, - highlightableTurquoise: false, - highlightableYellow: false, - hasSelectionPopup: true, - columnCount: 1, - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 135 - } - }; - } - - static getMathfield(): MathFieldProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - enableModeSwitch: false, - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 135 - } - }; - } - - static getCloze(): ClozeProperties { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - position: ElementPropertyGroupGenerator.generatePositionProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - height: 200 - }, - document: ClozeElement.initDocument(), - columnCount: 1, - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 150 - } - }; - } - - static getTextFieldSimple(): TextFieldSimpleProperties { - return { - ...ElementPropertyGroupGenerator.generateTextInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - width: 150, - height: 30, - isWidthFixed: true - }, - minLength: null, - minLengthWarnMessage: 'Eingabe zu kurz', - maxLength: null, - maxLengthWarnMessage: 'Eingabe zu lang', - isLimitedToMaxLength: false, - pattern: null, - patternWarnMessage: 'Eingabe entspricht nicht der Vorgabe', - clearable: false, - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 100 - } - }; - } - - static getToggleButton(): ToggleButtonProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), - height: 30 - }, - options: [], - strikeOtherOptions: false, - strikeSelectedOption: false, - verticalOrientation: false, - styling: { - ...ElementPropertyGroupGenerator.generateBasicStyleProps(), - lineHeight: 100, - selectionColor: '#c7f3d0' - } - }; - } - - static generateElementBlueprint(elementType: UIElementType): UIElementProperties { - return ElementPropertyGenerator.BLUEPRINT_GENERATORS[elementType](); - } - - static BLUEPRINT_GENERATORS: Record<string, () => UIElementProperties> = { - text: ElementPropertyGenerator.getText, - button: ElementPropertyGenerator.getButton, - 'text-field': ElementPropertyGenerator.getTextField, - 'text-field-simple': ElementPropertyGenerator.getTextFieldSimple, - 'text-area': ElementPropertyGenerator.getTextArea, - checkbox: ElementPropertyGenerator.getCheckbox, - dropdown: ElementPropertyGenerator.getDropdown, - radio: ElementPropertyGenerator.getRadioButtonGroup, - image: ElementPropertyGenerator.getImage, - audio: ElementPropertyGenerator.getAudio, - video: ElementPropertyGenerator.getVideo, - likert: ElementPropertyGenerator.getLikert, - 'likert-row': ElementPropertyGenerator.getLikertRow, - 'radio-group-images': ElementPropertyGenerator.getRadioButtonGroupComplex, - 'drop-list': ElementPropertyGenerator.getDropList, - cloze: ElementPropertyGenerator.getCloze, - slider: ElementPropertyGenerator.getSlider, - 'spell-correct': ElementPropertyGenerator.getSpellCorrect, - frame: ElementPropertyGenerator.getFrame, - 'toggle-button': ElementPropertyGenerator.getToggleButton, - geometry: ElementPropertyGenerator.getGeometry, - 'hotspot-image': ElementPropertyGenerator.getHotspotImage, - 'math-field': ElementPropertyGenerator.getMathfield - }; -} diff --git a/projects/editor/src/app/services/default-property-generators/element-property-groups.ts b/projects/editor/src/app/services/default-property-generators/element-property-groups.ts deleted file mode 100644 index dd25acc8f3a09b1761509d1e9c81209d78aa1224..0000000000000000000000000000000000000000 --- a/projects/editor/src/app/services/default-property-generators/element-property-groups.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - BasicStyles, BorderStyles, - DimensionProperties, FontStyles, PlayerProperties, - PositionProperties, Stylings -} from 'common/models/elements/property-group-interfaces'; -import { - InputElementProperties, - PlayerElementBlueprint, - TextInputElementProperties, - UIElementProperties -} from 'common/models/elements/element'; - -export class ElementPropertyGroupGenerator { - static generateElementProps(): UIElementProperties { - return { - id: 'placeholder', - dimensions: ElementPropertyGroupGenerator.generateDimensionProps(), - position: ElementPropertyGroupGenerator.generatePositionProps() - }; - } - - static generateInputElementProps(): InputElementProperties { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - label: 'Beschriftung', - value: null, - required: false, - requiredWarnMessage: 'Eingabe erforderlich', - readOnly: false - }; - } - - static generateTextInputElementProps(): TextInputElementProperties { - return { - ...ElementPropertyGroupGenerator.generateInputElementProps(), - inputAssistancePreset: null, - inputAssistanceCustomKeys: '', - inputAssistancePosition: 'floating', - inputAssistanceFloatingStartPosition: 'startBottom', - restrictedToInputAssistanceChars: true, - hasArrowKeys: false, - hasBackspaceKey: false, - showSoftwareKeyboard: false, - softwareKeyboardShowFrench: false - }; - } - - static generatePositionProps(): PositionProperties { - return { - xPosition: 0, - yPosition: 0, - gridColumn: null, - gridColumnRange: 1, - gridRow: null, - gridRowRange: 1, - marginLeft: { value: 0, unit: 'px' }, - marginRight: { value: 0, unit: 'px' }, - marginTop: { value: 0, unit: 'px' }, - marginBottom: { value: 0, unit: 'px' }, - zIndex: 0 - }; - } - - static generateDimensionProps(): DimensionProperties { - return { - width: 180, - height: 60, - isWidthFixed: false, - isHeightFixed: false, - minWidth: null, - maxWidth: null, - minHeight: null, - maxHeight: null - }; - } - - static generateBasicStyleProps(defaults: Partial<BasicStyles> = {}): BasicStyles { - return { - backgroundColor: 'transparent', - ...ElementPropertyGroupGenerator.generateFontStylingProps(defaults) - }; - } - - static generateFontStylingProps(defaults: Partial<FontStyles> = {}): FontStyles { - return { - fontColor: defaults.fontColor !== undefined ? defaults.fontColor as string : '#000000', - font: defaults?.font !== undefined ? defaults.font as string : 'Roboto', - fontSize: defaults?.fontSize !== undefined ? defaults.fontSize as number : 20, - bold: defaults?.bold !== undefined ? defaults.bold as boolean : false, - italic: defaults?.italic !== undefined ? defaults.italic as boolean : false, - underline: defaults?.underline !== undefined ? defaults.underline as boolean : false - }; - } - - static generateBorderStylingProps(defaults: Partial<Stylings> = {}): BorderStyles { - return { - borderWidth: defaults.borderWidth !== undefined ? defaults.borderWidth : 0, - borderColor: defaults.borderColor !== undefined ? defaults.borderColor : 'black', - borderStyle: defaults.borderStyle !== undefined ? defaults.borderStyle : 'solid', - borderRadius: defaults.borderRadius !== undefined ? defaults.borderRadius : 0 - }; - } - - static generatePlayerElementProps(): PlayerElementBlueprint { - return { - ...ElementPropertyGroupGenerator.generateElementProps(), - player: ElementPropertyGroupGenerator.generatePlayerProps() - }; - } - - static generatePlayerProps(properties: Partial<PlayerProperties> = {}): PlayerProperties { - return { - autostart: properties.autostart !== undefined ? properties.autostart as boolean : false, - autostartDelay: properties.autostartDelay !== undefined ? properties.autostartDelay as number : 0, - loop: properties.loop !== undefined ? properties.loop as boolean : false, - startControl: properties.startControl !== undefined ? properties.startControl as boolean : true, - pauseControl: properties.pauseControl !== undefined ? properties.pauseControl as boolean : false, - progressBar: properties.progressBar !== undefined ? properties.progressBar as boolean : true, - interactiveProgressbar: properties.interactiveProgressbar !== undefined ? - properties.interactiveProgressbar as boolean : - false, - volumeControl: properties.volumeControl !== undefined ? properties.volumeControl as boolean : true, - defaultVolume: properties.defaultVolume !== undefined ? properties.defaultVolume as number : 0.8, - minVolume: properties.minVolume !== undefined ? properties.minVolume as number : 0, - muteControl: properties.muteControl !== undefined ? properties.muteControl as boolean : true, - interactiveMuteControl: properties.interactiveMuteControl !== undefined ? - properties.interactiveMuteControl as boolean : - false, - hintLabel: properties.hintLabel !== undefined ? properties.hintLabel as string : '', - hintLabelDelay: properties.hintLabelDelay !== undefined ? properties.hintLabelDelay as number : 0, - activeAfterID: properties.activeAfterID !== undefined ? properties.activeAfterID as string : '', - minRuns: properties.minRuns !== undefined ? properties.minRuns as number : 1, - maxRuns: properties.maxRuns !== undefined ? properties.maxRuns as number | null : null, - showRestRuns: properties.showRestRuns !== undefined ? properties.showRestRuns as boolean : false, - showRestTime: properties.showRestTime !== undefined ? properties.showRestTime as boolean : true, - playbackTime: properties.playbackTime !== undefined ? properties.playbackTime as number : 0 - }; - } -} diff --git a/projects/editor/src/app/services/default-property-generators/unit-properties.ts b/projects/editor/src/app/services/default-property-generators/unit-properties.ts deleted file mode 100644 index 64675f09a0d61ced8d65d86c2f87f0b8b3ffc816..0000000000000000000000000000000000000000 --- a/projects/editor/src/app/services/default-property-generators/unit-properties.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PageProperties } from 'common/models/page'; -import { SectionProperties } from 'common/models/section'; -import { UnitProperties } from 'common/models/unit'; -import packageJSON from '../../../../../../package.json'; -import { VisibilityRule } from 'common/models/visibility-rule'; - -export class UnitPropertyGenerator { - static generateUnitProps(): UnitProperties { - return { - type: 'aspect-unit-definition', - version: packageJSON.config.unit_definition_version, - stateVariables: [], - pages: [] - }; - } - - static generatePageProps(): PageProperties { - return { - sections: [], - hasMaxWidth: true, - maxWidth: 750, - margin: 30, - backgroundColor: '#ffffff', - alwaysVisible: false, - alwaysVisiblePagePosition: 'left', - alwaysVisibleAspectRatio: 50 - }; - } - - static generateSectionProps(): SectionProperties { - return { - elements: [], - height: 400, - backgroundColor: '#ffffff', - dynamicPositioning: true, - autoColumnSize: true, - autoRowSize: true, - gridColumnSizes: [{ value: 1, unit: 'fr' }, { value: 1, unit: 'fr' }], - gridRowSizes: [{ value: 1, unit: 'fr' }], - visibilityDelay: 0, - animatedVisibility: false, - enableReHide: false, - visibilityRules: [] - }; - } -} diff --git a/projects/editor/src/app/services/dialog.service.ts b/projects/editor/src/app/services/dialog.service.ts index cb12f36a2fcf42cb148f659d0ae1e231058dfb26..4cd0b56ffa5a4957fecde405f3134e0c3a453bd0 100644 --- a/projects/editor/src/app/services/dialog.service.ts +++ b/projects/editor/src/app/services/dialog.service.ts @@ -21,7 +21,9 @@ import { VisibilityRulesDialogComponent } from 'editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component'; import { StateVariable } from 'common/models/state-variable'; +import { UnitDefErrorDialogComponent } from 'common/components/unit-def-error-dialog.component'; import { ReferenceList } from 'editor/src/app/services/reference-manager'; +import { SanitizationDialogComponent } from 'editor/src/app/components/dialogs/sanitization-dialog.component'; import { ConfirmationDialogComponent } from '../components/dialogs/confirmation-dialog.component'; import { TextEditDialogComponent } from '../components/dialogs/text-edit-dialog.component'; import { TextEditMultilineDialogComponent } from '../components/dialogs/text-edit-multiline-dialog.component'; @@ -51,6 +53,13 @@ export class DialogService { return dialogRef.afterClosed(); } + showUnitDefErrorDialog(text: string): void { + this.dialog.open(UnitDefErrorDialogComponent, { + data: { text }, + disableClose: true + }); + } + showDeleteReferenceDialog(refs: ReferenceList[]): Observable<boolean> { const dialogRef = this.dialog.open(DeleteReferenceDialogComponent, { data: { refs }, @@ -59,6 +68,12 @@ export class DialogService { return dialogRef.afterClosed(); } + showSanitizationDialog(): Observable<boolean> { + const dialogRef = this.dialog.open(SanitizationDialogComponent, + { disableClose: true }); + return dialogRef.afterClosed(); + } + showTextEditDialog(text: string): Observable<string> { const dialogRef = this.dialog.open(TextEditDialogComponent, { data: { text } diff --git a/projects/editor/src/app/services/reference-manager.spec.ts b/projects/editor/src/app/services/reference-manager.spec.ts index 24f0c07caeb67ea2dcc95bbeeed35201778989a4..4375705bf42360c93b784d1a41ee59e29640fe45 100644 --- a/projects/editor/src/app/services/reference-manager.spec.ts +++ b/projects/editor/src/app/services/reference-manager.spec.ts @@ -2,22 +2,23 @@ import { TestBed } from '@angular/core/testing'; import * as singleElement from 'test-data/unit-definitions/reference-testing/single-element.json'; import * as elementRef from 'test-data/unit-definitions/reference-testing/element-ref.json'; import * as elementRef2 from 'test-data/unit-definitions/reference-testing/2elements-ref.json'; -import { default as section1 } from 'test-data/unit-definitions/reference-testing/section-deletion.json'; -import { default as section2 } from 'test-data/unit-definitions/reference-testing/section2.json'; -import { default as pageRefs } from 'test-data/unit-definitions/reference-testing/pageRefs.json'; -import { default as cloze } from 'test-data/unit-definitions/reference-testing/cloze.json'; -import { default as pageNav } from 'test-data/unit-definitions/reference-testing/pageNav.json'; +import * as section1 from 'test-data/unit-definitions/reference-testing/section-deletion.json'; +import * as section2 from 'test-data/unit-definitions/reference-testing/section2.json'; +import * as pageRefs from 'test-data/unit-definitions/reference-testing/pageRefs.json'; +import * as cloze from 'test-data/unit-definitions/reference-testing/cloze.json'; +import * as pageNav from 'test-data/unit-definitions/reference-testing/pageNav.json'; import { DropListElement } from 'common/models/elements/input-elements/drop-list'; import { APIService } from 'common/shared.module'; -import { UnitService } from 'editor/src/app/services/unit.service'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatDialogModule } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AudioElement } from 'common/models/elements/media-elements/audio'; import { Section } from 'common/models/section'; -import { Page } from 'common/models/page'; -import { ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; +import { Page, PageProperties } from 'common/models/page'; +import { ClozeElement, ClozeProperties } from 'common/models/elements/compound-elements/cloze/cloze'; +import { ReferenceManager } from 'editor/src/app/services/reference-manager'; +import { Unit, UnitProperties } from 'common/models/unit'; describe('ReferenceManager', () => { class ApiStubService { @@ -27,7 +28,6 @@ describe('ReferenceManager', () => { } } - let unitService: UnitService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ @@ -35,7 +35,6 @@ describe('ReferenceManager', () => { ], imports: [MatSnackBarModule, MatDialogModule, BrowserAnimationsModule, TranslateModule.forRoot()] }); - unitService = TestBed.inject(UnitService); }); it('should load data', () => { @@ -43,7 +42,7 @@ describe('ReferenceManager', () => { }); it('should find no refs for single element', () => { - unitService.loadUnitDefinition(JSON.stringify(singleElement)); + const refMan = new ReferenceManager(new Unit(singleElement as unknown as UnitProperties)); const element = { type: 'drop-list', id: 'drop-list_1', @@ -51,13 +50,12 @@ describe('ReferenceManager', () => { value: [], connectedTo: [] } as unknown as DropListElement; - expect(unitService.referenceManager.getElementsReferences([element])) + expect(refMan.getElementsReferences([element])) .toEqual([]); }); it('should find refs when deleting element', () => { - unitService.loadUnitDefinition(JSON.stringify(elementRef)); - + const refMan = new ReferenceManager(new Unit(elementRef as unknown as UnitProperties)); const element = { type: 'drop-list', id: 'drop-list_1', @@ -66,11 +64,11 @@ describe('ReferenceManager', () => { connectedTo: [] } as unknown as DropListElement; - expect(unitService.referenceManager.getElementsReferences([element]).length) - .toEqual(1); - expect(unitService.referenceManager.getElementsReferences([element])[0].element) + expect(refMan.getElementsReferences([element]) + .length).toEqual(1); + expect(refMan.getElementsReferences([element])[0].element) .toEqual(jasmine.objectContaining({ ...element })); - expect(unitService.referenceManager.getElementsReferences([element])[0].refs[0]) + expect(refMan.getElementsReferences([element])[0].refs[0]) .toEqual(jasmine.objectContaining({ type: 'drop-list', id: 'drop-list_2' @@ -78,8 +76,7 @@ describe('ReferenceManager', () => { }); it('should find 2 refs when deleting 2 elements', () => { - unitService.loadUnitDefinition(JSON.stringify(elementRef2)); - + const refMan = new ReferenceManager(new Unit(elementRef2 as unknown as UnitProperties)); const element1 = { type: 'drop-list', id: 'drop-list_1', @@ -91,7 +88,7 @@ describe('ReferenceManager', () => { id: 'audio_1' } as AudioElement; - const refs = unitService.referenceManager.getElementsReferences([element1, element2]); + const refs = refMan.getElementsReferences([element1, element2]); expect(refs.length) .toEqual(2); @@ -110,9 +107,9 @@ describe('ReferenceManager', () => { }); it('should find ref when deleting section', () => { - unitService.loadUnitDefinition(JSON.stringify(section1)); - const section = section1.pages[0].sections[0] as unknown as Section; - const refs = unitService.referenceManager.getSectionElementsReferences([section]); + const refMan = new ReferenceManager(new Unit(section1 as unknown as UnitProperties)); + const section = JSON.parse(JSON.stringify(section1)).pages[0].sections[0] as unknown as Section; + const refs = refMan.getSectionElementsReferences([section]); expect(refs.length) .toEqual(1); @@ -124,9 +121,9 @@ describe('ReferenceManager', () => { }); it('should find refs when deleting section but ignore refs within same section', () => { - unitService.loadUnitDefinition(JSON.stringify(section2)); - const section = section2.pages[0].sections[0] as unknown as Section; - const refs = unitService.referenceManager.getSectionElementsReferences([section]); + const refMan = new ReferenceManager(new Unit(section2 as unknown as UnitProperties)); + const section = JSON.parse(JSON.stringify(section2)).pages[0].sections[0] as unknown as Section; + const refs = refMan.getSectionElementsReferences([section]); expect(refs.length) .toEqual(1); @@ -138,9 +135,9 @@ describe('ReferenceManager', () => { }); it('should ignore refs within same page', () => { - unitService.loadUnitDefinition(JSON.stringify(pageRefs)); - const page = new Page(pageRefs.pages[0]); - const refs = unitService.referenceManager.getPageElementsReferences(page); + const refMan = new ReferenceManager(new Unit(pageRefs as unknown as UnitProperties)); + const page = new Page(JSON.parse(JSON.stringify(pageRefs)).pages[0] as unknown as PageProperties); + const refs = refMan.getPageElementsReferences(page); expect(refs.length) .toEqual(1); @@ -152,10 +149,10 @@ describe('ReferenceManager', () => { }); it('should find cloze refs but ignore refs within same cloze', () => { - unitService.loadUnitDefinition(JSON.stringify(cloze)); - const clozeElement = - new ClozeElement(cloze.pages[0].sections[0].elements[0] as unknown as Partial<ClozeElement>); - const refs = unitService.referenceManager.getElementsReferences([clozeElement]); + const refMan = new ReferenceManager(new Unit(cloze as unknown as UnitProperties)); + const clozeElement = new ClozeElement( + JSON.parse(JSON.stringify(cloze)).pages[0].sections[0].elements[0] as unknown as ClozeProperties); + const refs = refMan.getElementsReferences([clozeElement]); expect(refs.length) .toEqual(1); @@ -167,12 +164,14 @@ describe('ReferenceManager', () => { }); it('should find page refs via buttons', () => { - unitService.loadUnitDefinition(JSON.stringify(pageNav)); - const refs = unitService.referenceManager.getPageButtonReferences(0); + const refMan = new ReferenceManager(new Unit(pageNav as unknown as UnitProperties)); + const refs = refMan.getButtonReferencesForPage(0); expect(refs.length) .toEqual(1); - expect(refs[0]) + expect(refs[0].refs.length) + .toEqual(1); + expect(refs[0].refs[0]) .toEqual(jasmine.objectContaining({ type: 'button', id: 'button_2' diff --git a/projects/editor/src/app/services/reference-manager.ts b/projects/editor/src/app/services/reference-manager.ts index 3f85f20ac8332a077e17ac1836ec581fea1f68c9..68632db25aa2d852bd2337a69e882aaa15ca6632 100644 --- a/projects/editor/src/app/services/reference-manager.ts +++ b/projects/editor/src/app/services/reference-manager.ts @@ -17,14 +17,70 @@ export class ReferenceManager { this.unit = unit; } - getPageButtonReferences(pageIndex: number): ButtonElement[] { + getAllInvalidRefs(): UIElement[] { + return [...this.getInvalidPageRefs(), ...this.getInvalidDropListRefs(), ...this.getInvalidAudioRefs()]; + } + + getInvalidPageRefs(): ButtonElement[] { + const allRefs: ButtonElement[] = []; + const allButtons = this.unit.getAllElements('button') as ButtonElement[]; + const validPageRange = this.unit.pages.length; + allButtons.forEach(button => { + if (button.action === 'pageNav' && + typeof button.actionParam === 'number' && + (button.actionParam + 1) > validPageRange) { + allRefs.push(button); + } + }); + return allRefs; + } + + private getInvalidDropListRefs(): DropListElement[] { + const allDropLists = this.unit.getAllElements('drop-list') as DropListElement[]; + const allDropListIDs = allDropLists.map(dropList => dropList.id); + return allDropLists.filter(dropList => dropList.connectedTo + .filter(connectedList => !allDropListIDs.includes(connectedList)).length > 0); + } + + private getInvalidAudioRefs(): AudioElement[] { + const allAudios = this.unit.getAllElements('audio') as AudioElement[]; + const allAudioIDs = allAudios.map(audio => audio.id); + return allAudios.filter(audio => !allAudioIDs.includes(audio.player.activeAfterID)); + } + + removeInvalidRefs(refs: UIElement[]): void { + refs.forEach(ref => { + switch (ref.type) { + case 'button': + (ref as ButtonElement).actionParam = null; + break; + case 'drop-list': + (ref as DropListElement).connectedTo = (ref as DropListElement).connectedTo + .filter(connectedList => this.unit.getAllElements('drop-list') + .map(dropList => dropList.id).includes(connectedList)); + break; + case 'audio': + (ref as AudioElement).player.activeAfterID = ''; + break; + // no default + } + }); + } + + getButtonReferencesForPage(pageIndex: number): ReferenceList[] { const page = this.unit.pages[pageIndex]; const allButtons = this.unit.getAllElements('button') as ButtonElement[]; const pageButtonIDs = (page.getAllElements('button') as ButtonElement[]) .map(pageButton => pageButton.id); - return allButtons - .filter(button => button.action === 'pageNav' && button.actionParam === pageIndex) - .filter(button => !pageButtonIDs.includes(button.id)); + return [{ + element: { + id: `Seite ${pageIndex + 1}`, + type: 'page' + }, + refs: allButtons + .filter(button => button.action === 'pageNav' && button.actionParam === pageIndex) + .filter(button => !pageButtonIDs.includes(button.id)) + }]; } getPageElementsReferences(page: Page): ReferenceList[] { @@ -106,11 +162,9 @@ export class ReferenceManager { } getTextReferences(textElements: TextElement[], ignoredElementIDs: string[] = []): ReferenceList[] { - // console.log('getTextReferences', textElements, ignoredElementIDs); - const x = textElements + return textElements .map(textElement => this.getTextAnchorReferences(textElement.getAnchorIDs(), ignoredElementIDs)) .flat(); - return x; } getTextAnchorReferences(deletedAnchorIDs: string[], ignoredElementIDs: string[] = []): ReferenceList[] { diff --git a/projects/editor/src/app/services/sanitizer.ts b/projects/editor/src/app/services/sanitizer.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5c8d94d2d0b997bd7e54a2f1d38e2a3c35ca53e --- /dev/null +++ b/projects/editor/src/app/services/sanitizer.ts @@ -0,0 +1,133 @@ +import { Page } from 'common/models/page'; +import { Section } from 'common/models/section'; +import { UIElement } from 'common/models/elements/element'; +import { ClozeDocument, ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; + +export abstract class UnitDefinitionSanitizer { + static sanitizeUnit(unitDefinition: Record<string, any>): Record<string, unknown> { + return { + ...unitDefinition, + pages: unitDefinition.pages.map((page: Page) => UnitDefinitionSanitizer.sanitizePage(page)) + }; + } + + private static sanitizePage(page: Record<string, any>) { + return { + ...page, + sections: page.sections.map((section: Section) => UnitDefinitionSanitizer.sanitizeSection(section)) + }; + } + + private static sanitizeSection(section: Record<string, any>) { + return { + ...UnitDefinitionSanitizer + .sanitizeSectionVisibility( + UnitDefinitionSanitizer + .sanitizeSectionGridSizes(section) + ), + elements: section.elements.map((element: UIElement) => this.sanitizeElement(element)) + }; + } + + private static sanitizeSectionVisibility(section: Record<string, unknown>) { + return { + ...section, + visibilityDelay: section.activeAfterIdDelay, + visibilityRules: section.activeAfterID ? [{ id: section.activeAfterID, operator: '≥', value: '1' }] : [], + animatedVisibility: !!section.activeAfterID + }; + } + + /* Transform grid sizes from string to array with value and unit. + "gridColumnSizes": "1fr 1fr" + -> + "gridColumnSizes": [ + { + "value": 1, + "unit": "fr" + }, + { + "value": 1, + "unit": "fr" + } + ] + */ + private static sanitizeSectionGridSizes(section: Record<string, any>) { + 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 + }; + } + + private static sanitizeElement(element: Record<string, unknown>) { + if (element.type === 'cloze') UnitDefinitionSanitizer.sanitizeClose(element); + return { + ...element, + dimensions: UnitDefinitionSanitizer.sanitizeDimensionProps(element), + position: element.position ? + UnitDefinitionSanitizer.sanitizePositionProps(element.position as Record<string, unknown>) : undefined + }; + } + + private static sanitizeDimensionProps(element: Record<string, any>) { + return { + width: element.width, + height: element.height, + isWidthFixed: element.position?.fixedSize, + isHeightFixed: element.position?.fixedSize, + minWidth: element.position?.fixedSize ? null : element.width, + minHeight: !element.position?.fixedSize && element.position?.useMinHeight ? element.height : null + }; + } + + private static sanitizePositionProps(position: Record<string, unknown>) { + delete position.dynamicPositioning; + delete position.fixedSize; + delete position.useMinHeight; + return { + ...UnitDefinitionSanitizer.sanitizePositionMargins(position) + }; + } + + private static sanitizePositionMargins(position: Record<string, unknown>) { + return { + ...position, + marginLeft: typeof position.marginLeft === 'number' ? + { value: position.marginLeft, unit: 'px' } : position.marginLeft, + marginRight: typeof position.marginRight === 'number' ? + { value: position.marginRight, unit: 'px' } : position.marginRight, + marginTop: typeof position.marginTop === 'number' ? + { value: position.marginTop, unit: 'px' } : position.marginTop, + marginBottom: typeof position.marginBottom === 'number' ? + { value: position.marginBottom, unit: 'px' } : position.marginBottom + }; + } + + private static sanitizeClose(cloze: Record<string, unknown>) { + const children = ClozeElement.getDocumentChildElements(cloze.document as ClozeDocument); + children.forEach((child: Record<string, unknown>) => { + child.dimensions = { + width: child.width, + height: child.height, + isWidthFixed: true, + isHeightFixed: true, + minWidth: null, + minHeight: null + }; + }); + const toggleButtons = children.filter(child => child.type === 'toggle-button'); + toggleButtons.forEach(toggleButton => { + toggleButton.dimensions.isWidthFixed = !toggleButton.dynamicWidth; + }); + return cloze; + } +} diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts index e29a063efc520073c6deea483024bc775129074d..fb6984ea2d176180824ea7830d78b75451c76bf8 100644 --- a/projects/editor/src/app/services/unit.service.ts +++ b/projects/editor/src/app/services/unit.service.ts @@ -6,8 +6,7 @@ import { TranslateService } from '@ngx-translate/core'; import { FileService } from 'common/services/file.service'; import { MessageService } from 'common/services/message.service'; import { ArrayUtils } from 'common/util/array'; -import { SanitizationService } from 'common/services/sanitization.service'; -import { Unit } from 'common/models/unit'; +import { Unit, UnitProperties } from 'common/models/unit'; import { PlayerProperties, PositionProperties } from 'common/models/elements/property-group-interfaces'; import { DragNDropValueObject, TextLabel } from 'common/models/elements/label-interfaces'; import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; @@ -28,13 +27,13 @@ import { VideoProperties } from 'common/models/elements/media-elements/video'; import { ImageProperties } from 'common/models/elements/media-elements/image'; import { StateVariable } from 'common/models/state-variable'; import { VisibilityRule } from 'common/models/visibility-rule'; +import { VersionManager } from 'common/services/version-manager'; import { ReferenceManager } from 'editor/src/app/services/reference-manager'; import { DialogService } from './dialog.service'; import { VeronaAPIService } from './verona-api.service'; import { SelectionService } from './selection.service'; import { IDService } from './id.service'; -import { UnitPropertyGenerator } from './default-property-generators/unit-properties'; -import { ElementPropertyGenerator } from './default-property-generators/element-properties'; +import { UnitDefinitionSanitizer } from './sanitizer'; @Injectable({ providedIn: 'root' @@ -50,39 +49,50 @@ export class UnitService { private veronaApiService: VeronaAPIService, private messageService: MessageService, private dialogService: DialogService, - private sanitizationService: SanitizationService, private sanitizer: DomSanitizer, private translateService: TranslateService, private idService: IDService) { - this.unit = UnitService.createEmptyUnit(); + this.unit = new Unit(); this.referenceManager = new ReferenceManager(this.unit); } - private static createEmptyUnit(): Unit { - return new Unit({ - ...UnitPropertyGenerator.generateUnitProps(), - pages: [new Page({ - ...UnitPropertyGenerator.generatePageProps(), - sections: [new Section(UnitPropertyGenerator.generateSectionProps())] - })] - }); - } - loadUnitDefinition(unitDefinition: string): void { - this.idService.reset(); - if (unitDefinition) { - let unitDef; - try { - unitDef = JSON.parse(unitDefinition); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - this.messageService.showError('Unit definition konnte nicht gelesen werden!'); + try { + if (!unitDefinition) { + throw Error('Unit-Definition nicht gefunden.'); + } + let unitDef = JSON.parse(unitDefinition); + if (!VersionManager.hasCompatibleVersion(unitDef)) { + if (VersionManager.isNewer(unitDef)) { + throw Error('Unit-Version ist neuer als dieser Editor. Bitte mit der neuesten Version öffnen.'); + } + if (!VersionManager.needsSanitization(unitDef)) { + throw Error('Unit-Version ist veraltet. Sie kann mit Version 1.38/1.39 aktualisiert werden.'); + } + this.dialogService.showSanitizationDialog().subscribe(() => { + unitDef = UnitDefinitionSanitizer.sanitizeUnit(unitDef); + this.loadUnit(unitDef); + const invalidRefs = this.referenceManager.getAllInvalidRefs(); + if (invalidRefs.length > 0) { + this.referenceManager.removeInvalidRefs(invalidRefs); + this.messageService.showFixedReferencePanel(invalidRefs); + } + }); + } else { + this.loadUnit(unitDef); } - this.unit = new Unit(unitDef); - this.referenceManager = new ReferenceManager(this.unit); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + if (e instanceof Error) this.dialogService.showUnitDefErrorDialog(e.message); } + } + + private loadUnit(parsedUnitDefinition?: string): void { + this.idService.reset(); + this.unit = new Unit(parsedUnitDefinition as unknown as UnitProperties); this.idService.registerUnitIds(this.unit); + this.referenceManager = new ReferenceManager(this.unit); } unitUpdated(): void { @@ -90,10 +100,7 @@ export class UnitService { } addPage(): void { - this.unit.pages.push(new Page({ - ...UnitPropertyGenerator.generatePageProps(), - sections: [new Section(UnitPropertyGenerator.generateSectionProps())] - })); + this.unit.pages.push(new Page()); this.selectionService.selectedPageIndex = this.unit.pages.length - 1; this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); } @@ -125,7 +132,7 @@ export class UnitService { addSection(page: Page, section?: Section): void { page.sections.push( - section || new Section(UnitPropertyGenerator.generateSectionProps()) + section || new Section() ); this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); } @@ -162,7 +169,7 @@ export class UnitService { async addElementToSection(elementType: UIElementType, section: Section, coordinates?: { x: number, y: number }): Promise<void> { - const newElementProperties = ElementPropertyGenerator.generateElementBlueprint(elementType); + const newElementProperties: Partial<UIElementProperties> = {}; if (['geometry'].includes(elementType)) { (newElementProperties as GeometryProperties).appDefinition = await firstValueFrom(this.dialogService.showGeogebraAppDefinitionDialog()); @@ -195,16 +202,12 @@ export class UnitService { } as PositionProperties; } - section.addElement(this.createElement(elementType, newElementProperties) as PositionedUIElement); - this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); - } - - private createElement(elementType: UIElementType, props: UIElementProperties): UIElement { - return ElementFactory.createElement({ + section.addElement(ElementFactory.createElement({ type: elementType, - ...props, + ...newElementProperties, id: this.idService.getAndRegisterNewID(elementType) - }); + }) as PositionedUIElement); + this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); } deleteElements(elements: UIElement[]): void { diff --git a/projects/editor/src/app/text-editor/angular-node-views/button-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/button-component-extension.ts index 1695ae233da570f07fc0d9cdc9a751899bbff28f..691dcbe0e38bd2aa9f02398d7fd3e59e861a9962 100644 --- a/projects/editor/src/app/text-editor/angular-node-views/button-component-extension.ts +++ b/projects/editor/src/app/text-editor/angular-node-views/button-component-extension.ts @@ -1,12 +1,8 @@ import { Injector } from '@angular/core'; import { Node, mergeAttributes } from '@tiptap/core'; import { AngularNodeViewRenderer } from 'ngx-tiptap'; -import { ButtonElement } from 'common/models/elements/button/button'; +import { ButtonElement, ButtonProperties } from 'common/models/elements/button/button'; import { ButtonNodeviewComponent } from 'editor/src/app/text-editor/angular-node-views/button-nodeview.component'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element-properties'; -import { - ElementPropertyGroupGenerator -} from 'editor/src/app/services/default-property-generators/element-property-groups'; const ButtonComponentExtension = (injector: Injector): Node => { return Node.create({ @@ -18,15 +14,13 @@ const ButtonComponentExtension = (injector: Injector): Node => { return { model: { default: new ButtonElement({ - ...ElementPropertyGenerator.getButton(), id: 'cloze-child-id-placeholder', dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), height: 30 }, asLink: true, position: undefined - }) + } as ButtonProperties) } }; }, diff --git a/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts index 1f5f236bc4532dc0f1405c85085412f09a4f04c6..1bd82b67354e23f7eb810b4248764406f3690890 100644 --- a/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts +++ b/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts @@ -1,11 +1,7 @@ import { Injector } from '@angular/core'; import { Node, mergeAttributes } from '@tiptap/core'; import { AngularNodeViewRenderer } from 'ngx-tiptap'; -import { DropListElement } from 'common/models/elements/input-elements/drop-list'; -import { - ElementPropertyGroupGenerator -} from 'editor/src/app/services/default-property-generators/element-property-groups'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element-properties'; +import { DropListElement, DropListProperties } from 'common/models/elements/input-elements/drop-list'; import { DropListNodeviewComponent } from './drop-list-nodeview.component'; const DropListComponentExtension = (injector: Injector): Node => { @@ -18,17 +14,17 @@ const DropListComponentExtension = (injector: Injector): Node => { return { model: { default: new DropListElement({ - ...ElementPropertyGenerator.getDropList(), id: 'cloze-child-id-placeholder', onlyOneItem: true, dimensions: { - ...ElementPropertyGroupGenerator.generateDimensionProps(), width: 150, height: 30, - isWidthFixed: true + isWidthFixed: true, + isHeightFixed: true, + minHeight: null }, position: undefined - }) + } as DropListProperties) } }; }, diff --git a/projects/editor/src/app/text-editor/angular-node-views/text-field-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/text-field-component-extension.ts index 3c8d28adc6f7fc4778b646b565f9f954dc80f7ba..84c892e051c64edcf0392083b9a5533c5a512f73 100644 --- a/projects/editor/src/app/text-editor/angular-node-views/text-field-component-extension.ts +++ b/projects/editor/src/app/text-editor/angular-node-views/text-field-component-extension.ts @@ -2,9 +2,8 @@ import { Injector } from '@angular/core'; import { Node, mergeAttributes } from '@tiptap/core'; import { AngularNodeViewRenderer } from 'ngx-tiptap'; import { - TextFieldSimpleElement + TextFieldSimpleElement, TextFieldSimpleProperties } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element-properties'; import { TextFieldNodeviewComponent } from './text-field-nodeview.component'; const TextFieldComponentExtension = (injector: Injector): Node => { @@ -17,10 +16,16 @@ const TextFieldComponentExtension = (injector: Injector): Node => { return { model: { default: new TextFieldSimpleElement({ - ...ElementPropertyGenerator.getTextFieldSimple(), id: 'cloze-child-id-placeholder', + dimensions: { + width: 150, + height: 30, + isWidthFixed: true, + isHeightFixed: true, + minHeight: null + }, position: undefined - }) + } as TextFieldSimpleProperties) } }; }, diff --git a/projects/editor/src/app/text-editor/angular-node-views/toggle-button-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/toggle-button-component-extension.ts index 653cb1096e320b003d750da76766b57b88a742a5..c3215f2249b2f58a5e25c5ef176f58594b33c36c 100644 --- a/projects/editor/src/app/text-editor/angular-node-views/toggle-button-component-extension.ts +++ b/projects/editor/src/app/text-editor/angular-node-views/toggle-button-component-extension.ts @@ -1,8 +1,10 @@ import { Injector } from '@angular/core'; import { Node, mergeAttributes } from '@tiptap/core'; import { AngularNodeViewRenderer } from 'ngx-tiptap'; -import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; -import { ElementPropertyGenerator } from 'editor/src/app/services/default-property-generators/element-properties'; +import { + ToggleButtonElement, + ToggleButtonProperties +} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; import { ToggleButtonNodeviewComponent } from './toggle-button-nodeview.component'; const ToggleButtonComponentExtension = (injector: Injector): Node => { @@ -15,10 +17,16 @@ const ToggleButtonComponentExtension = (injector: Injector): Node => { return { model: { default: new ToggleButtonElement({ - ...ElementPropertyGenerator.getToggleButton(), id: 'cloze-child-id-placeholder', + dimensions: { + width: 150, + height: 30, + isWidthFixed: false, + isHeightFixed: true, + minHeight: null + }, position: undefined - }) + } as ToggleButtonProperties) } }; }, diff --git a/projects/editor/src/environments/environment.ts b/projects/editor/src/environments/environment.ts index f56ff47022c7a46eac3aee615d6d9150338524dc..5af3bc6144dcb7130101d13ebf4d680bbc5d4989 100644 --- a/projects/editor/src/environments/environment.ts +++ b/projects/editor/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + strictInstantiation: false }; /* diff --git a/projects/player/src/app/components/elements/base-group-element/base-group-element.component.spec.ts b/projects/player/src/app/components/elements/base-group-element/base-group-element.component.spec.ts index 5b8f5320066669a54afb3354b9ced47372a7d369..f6f42910200903aef4da8ca5466621d8c861c7ee 100644 --- a/projects/player/src/app/components/elements/base-group-element/base-group-element.component.spec.ts +++ b/projects/player/src/app/components/elements/base-group-element/base-group-element.component.spec.ts @@ -29,12 +29,7 @@ describe('BaseGroupElementComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(BaseGroupElementComponent); component = fixture.componentInstance; - component.elementModel = new FrameElement({ - type: 'frame', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new FrameElement(); fixture.detectChanges(); }); diff --git a/projects/player/src/app/components/elements/compound-group-element/compound-group-element.component.spec.ts b/projects/player/src/app/components/elements/compound-group-element/compound-group-element.component.spec.ts index 95fb38482354d41f2c31563352c9974243647f03..fcd9646fc149c66d4a2d5e6dfaaabdaf8e73385d 100644 --- a/projects/player/src/app/components/elements/compound-group-element/compound-group-element.component.spec.ts +++ b/projects/player/src/app/components/elements/compound-group-element/compound-group-element.component.spec.ts @@ -39,13 +39,7 @@ describe('CompoundGroupElementComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CompoundGroupElementComponent); component = fixture.componentInstance; - component.elementModel = new LikertElement({ - type: 'likert', - id: 'test', - width: 0, - height: 0, - rows: [] - }); + component.elementModel = new LikertElement(); fixture.detectChanges(); }); diff --git a/projects/player/src/app/components/elements/element-group-selection/element-group-selection.component.spec.ts b/projects/player/src/app/components/elements/element-group-selection/element-group-selection.component.spec.ts index 8cfb519aec8ee62e933155a80b2cb2d2efe5c685..e1f92257fcf5f49671916c0661f88d4fdd455202 100644 --- a/projects/player/src/app/components/elements/element-group-selection/element-group-selection.component.spec.ts +++ b/projects/player/src/app/components/elements/element-group-selection/element-group-selection.component.spec.ts @@ -1,5 +1,5 @@ +// eslint-disable-next-line max-classes-per-file import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ElementGroupSelectionComponent } from './element-group-selection.component'; import { Component, Input } from '@angular/core'; import { UIElement } from 'common/models/elements/element'; import { CheckboxElement } from 'common/models/elements/input-elements/checkbox'; @@ -18,6 +18,7 @@ import { ClozeElement } from 'common/models/elements/compound-elements/cloze/clo import { FrameElement } from 'common/models/elements/frame/frame'; import { ImageElement } from 'common/models/elements/media-elements/image'; import { ButtonElement } from 'common/models/elements/button/button'; +import { ElementGroupSelectionComponent } from './element-group-selection.component'; describe('ElementGroupSelectionComponent', () => { let component: ElementGroupSelectionComponent; @@ -65,7 +66,6 @@ describe('ElementGroupSelectionComponent', () => { @Input() pageIndex!: number; } - beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ @@ -88,200 +88,109 @@ describe('ElementGroupSelectionComponent', () => { }); it('should create', () => { - component.elementModel = new CheckboxElement({ - type: 'checkbox', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new CheckboxElement(); expect(component).toBeTruthy(); }); it('should select textGroup', () => { - component.elementModel = new TextElement({ - type: 'text', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new TextElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('textGroup'); }); it('should select inputGroup', () => { - component.elementModel = new CheckboxElement({ - type: 'checkbox', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new CheckboxElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('inputGroup'); }); it('should select inputGroup', () => { - component.elementModel = new RadioButtonGroupElement({ - type: 'radio', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new RadioButtonGroupElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('inputGroup'); }); it('should select inputGroup', () => { - component.elementModel = new DropdownElement({ - type: 'dropdown', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new DropdownElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('inputGroup'); }); it('should select inputGroup', () => { - component.elementModel = new DropListElement({ - type: 'drop-list', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new DropListElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('inputGroup'); }); it('should select inputGroup', () => { - component.elementModel = new SliderElement({ - type: 'slider', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new SliderElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('inputGroup'); }); it('should select inputGroup', () => { - component.elementModel = new RadioButtonGroupElement({ - type: 'radio-group-images', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new RadioButtonGroupElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('inputGroup'); }); it('should select textInputGroup', () => { - component.elementModel = new TextFieldElement({ - type: 'text-field', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new TextFieldElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('textInputGroup'); }); it('should select textInputGroup', () => { - component.elementModel = new TextAreaElement({ - type: 'text-area', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new TextAreaElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('textInputGroup'); }); it('should select textInputGroup', () => { - component.elementModel = new SpellCorrectElement({ - type: 'spell-correct', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new SpellCorrectElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('textInputGroup'); }); it('should select mediaPlayerGroup', () => { - component.elementModel = new AudioElement({ - type: 'audio', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new AudioElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('mediaPlayerGroup'); }); it('should select mediaPlayerGroup', () => { - component.elementModel = new VideoElement({ - type: 'video', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new VideoElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('mediaPlayerGroup'); }); it('should select compoundGroup', () => { - component.elementModel = new LikertElement({ - type: 'likert', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new LikertElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('compoundGroup'); }); it('should select mediaPlayerGroup', () => { - component.elementModel = new ClozeElement({ - type: 'cloze', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new ClozeElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('compoundGroup'); }); it('should select no group (undefined)', () => { - component.elementModel = new FrameElement({ - type: 'frame', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new FrameElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual(undefined); }); it('should select interactiveGroup', () => { - component.elementModel = new ImageElement({ - type: 'image', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new ImageElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('interactiveGroup'); }); it('should select interactiveGroup', () => { - component.elementModel = new ButtonElement({ - type: 'button', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new ButtonElement(); fixture.detectChanges(); expect(component.selectedGroup).toEqual('interactiveGroup'); }); - }); diff --git a/projects/player/src/app/components/elements/external-app-group-element/external-app-group-element.component.spec.ts b/projects/player/src/app/components/elements/external-app-group-element/external-app-group-element.component.spec.ts index 5548af0cc733fb014b0988efdee631dd6a3fb7cb..82151a5b641ba1ae75d86045f68460dce4415776 100644 --- a/projects/player/src/app/components/elements/external-app-group-element/external-app-group-element.component.spec.ts +++ b/projects/player/src/app/components/elements/external-app-group-element/external-app-group-element.component.spec.ts @@ -36,12 +36,7 @@ describe('ExternalAppGroupElementComponent', () => { spyOn(mockUnitStateService, 'getElementCodeById').withArgs('test').and .returnValue({ id: 'test', status: 'NOT_REACHED', value: 0 }); component = fixture.componentInstance; - component.elementModel = new GeometryElement({ - type: 'geometry', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new GeometryElement(); fixture.detectChanges(); }); diff --git a/projects/player/src/app/components/elements/input-group-element/input-group-element.component.spec.ts b/projects/player/src/app/components/elements/input-group-element/input-group-element.component.spec.ts index 44f462eed22977f677d374de0ff4b567b68b5da6..ecf41c51f99d77b435c2647ca765943c465e6985 100644 --- a/projects/player/src/app/components/elements/input-group-element/input-group-element.component.spec.ts +++ b/projects/player/src/app/components/elements/input-group-element/input-group-element.component.spec.ts @@ -40,12 +40,7 @@ describe('InputGroupElementComponent', () => { spyOn(mockUnitStateService, 'getElementCodeById').withArgs('test').and .returnValue({ id: 'test', status: 'NOT_REACHED', value: 0 }); component = fixture.componentInstance; - component.elementModel = new RadioButtonGroupElement({ - type: 'radio', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new RadioButtonGroupElement(); fixture.detectChanges(); }); diff --git a/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.spec.ts b/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.spec.ts index b4966e21ad44b949ba256fe7ffb0cb8e442ae1be..af95d94bad30d7e2a9bd5e5e6ad2647c90a11002 100644 --- a/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.spec.ts +++ b/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.spec.ts @@ -1,9 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { InteractiveGroupElementComponent } from './interactive-group-element.component'; import { Component, Input } from '@angular/core'; import { CastPipe } from 'player/src/app/pipes/cast.pipe'; import { UnitStateService } from 'player/src/app/services/unit-state.service'; import { ButtonElement } from 'common/models/elements/button/button'; +import { InteractiveGroupElementComponent } from './interactive-group-element.component'; describe('InteractiveGroupElementComponent', () => { let component: InteractiveGroupElementComponent; @@ -33,17 +33,11 @@ describe('InteractiveGroupElementComponent', () => { spyOn(mockUnitStateService, 'getElementCodeById').withArgs('test').and .returnValue({ id: 'test', status: 'NOT_REACHED', value: 0 }); component = fixture.componentInstance; - component.elementModel = new ButtonElement({ - type: 'button', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new ButtonElement(); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); - }); diff --git a/projects/player/src/app/components/elements/media-player-group-element/media-player-group-element.component.spec.ts b/projects/player/src/app/components/elements/media-player-group-element/media-player-group-element.component.spec.ts index 00a4a46564576b94f9cf1b1b9f6679eef12d1c36..e8431a8102244424d898db1e5dd7747c766fcb5f 100644 --- a/projects/player/src/app/components/elements/media-player-group-element/media-player-group-element.component.spec.ts +++ b/projects/player/src/app/components/elements/media-player-group-element/media-player-group-element.component.spec.ts @@ -44,12 +44,7 @@ describe('MediaPlayerGroupElementComponent', () => { spyOn(mockUnitStateService, 'getElementCodeById').withArgs('test').and .returnValue({ id: 'test', status: 'NOT_REACHED', value: 0 }); component = fixture.componentInstance; - component.elementModel = new AudioElement({ - type: 'audio', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new AudioElement(); fixture.detectChanges(); }); diff --git a/projects/player/src/app/components/elements/text-group-element/text-group-element.component.spec.ts b/projects/player/src/app/components/elements/text-group-element/text-group-element.component.spec.ts index efb31943d6a2ca72d2f967e99cd3a1ba6665fcaa..cb6a6575bdec483569cbbe989890f635a7b4744a 100644 --- a/projects/player/src/app/components/elements/text-group-element/text-group-element.component.spec.ts +++ b/projects/player/src/app/components/elements/text-group-element/text-group-element.component.spec.ts @@ -49,12 +49,7 @@ describe('TextGroupElementComponent', () => { spyOn(mockUnitStateService, 'getElementCodeById').withArgs('test').and .returnValue({ id: 'test', status: 'NOT_REACHED', value: [] }); component = fixture.componentInstance; - component.elementModel = new TextElement({ - type: 'text', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new TextElement(); }); it('should create', () => { diff --git a/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.spec.ts b/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.spec.ts index b3927e9c9c29da2a113c5b0eeafda0b644d958a2..72f98b542293937694150b05796dbc22940f4709 100644 --- a/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.spec.ts +++ b/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.spec.ts @@ -42,12 +42,7 @@ describe('TextInputGroupElementComponent', () => { spyOn(mockUnitStateService, 'getElementCodeById').withArgs('test').and .returnValue({ id: 'test', status: 'NOT_REACHED', value: 'test' }); component = fixture.componentInstance; - component.elementModel = new TextFieldElement({ - type: 'text-field', - id: 'test', - width: 0, - height: 0 - }); + component.elementModel = new TextFieldElement(); fixture.detectChanges(); }); diff --git a/projects/player/src/app/components/section/section.component.spec.ts b/projects/player/src/app/components/section/section.component.spec.ts index b22cfb2b8bc6b9c9a4d8232f251d86870f25d500..a0074a7235ed387d130390b53cd4a5197ab5a5c6 100644 --- a/projects/player/src/app/components/section/section.component.spec.ts +++ b/projects/player/src/app/components/section/section.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; import { Section } from 'common/models/section'; import { MeasurePipe } from 'common/pipes/measure.pipe'; import { SectionComponent } from './section.component'; @@ -11,7 +12,8 @@ describe('SectionComponent', () => { await TestBed.configureTestingModule({ declarations: [ SectionComponent, MeasurePipe - ] + ], + imports: [MatDialogModule] }) .compileComponents(); }); @@ -19,16 +21,7 @@ describe('SectionComponent', () => { beforeEach(async () => { fixture = TestBed.createComponent(SectionComponent); component = fixture.componentInstance; - component.section = new Section({ - elements: [], - height: 400, - backgroundColor: '#ffffff', - dynamicPositioning: true, - autoColumnSize: true, - autoRowSize: true, - gridColumnSizes: '1fr 1fr', - gridRowSizes: '1fr' - }); + component.section = new Section(); fixture.detectChanges(); }); diff --git a/projects/player/src/app/components/unit/unit.component.spec.ts b/projects/player/src/app/components/unit/unit.component.spec.ts index fcd478f14e3ef55993a9edff62421c3a1b6badce..afaa956a0f98833fb56df5b21cb849882618706a 100644 --- a/projects/player/src/app/components/unit/unit.component.spec.ts +++ b/projects/player/src/app/components/unit/unit.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { MatDialogModule } from '@angular/material/dialog'; import { UnitComponent } from './unit.component'; describe('UnitComponent', () => { @@ -8,7 +8,8 @@ describe('UnitComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ UnitComponent ] + declarations: [UnitComponent], + imports: [MatDialogModule] }).compileComponents(); }); diff --git a/projects/player/src/app/components/unit/unit.component.ts b/projects/player/src/app/components/unit/unit.component.ts index 9fd60be6bc35643a7ae36caa95ca2b829fd8de33..69fbabff0229b77b9953e344bb65143db0dee2f2 100644 --- a/projects/player/src/app/components/unit/unit.component.ts +++ b/projects/player/src/app/components/unit/unit.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; import { PlayerConfig, VopStartCommand } from 'player/modules/verona/models/verona'; import { Unit } from 'common/models/unit'; import { LogService } from 'player/modules/logging/services/log.service'; @@ -15,6 +15,10 @@ import { MetaDataService } from 'player/src/app/services/meta-data.service'; import { AnchorService } from 'player/src/app/services/anchor.service'; import { DragNDropValueObject } from 'common/models/elements/label-interfaces'; +import { VersionManager } from 'common/services/version-manager'; +import { InstantiationEror } from 'common/util/errors'; +import { MatDialog } from '@angular/material/dialog'; +import { UnitDefErrorDialogComponent } from 'common/components/unit-def-error-dialog.component'; @Component({ selector: 'aspect-unit', @@ -32,6 +36,7 @@ export class UnitComponent implements OnInit { private elementModelElementCodeMappingService: ElementModelElementCodeMappingService, private sanitizationService: SanitizationService, private anchorService: AnchorService, + private dialog: MatDialog, private changeDetectorRef: ChangeDetectorRef) { } @@ -42,27 +47,51 @@ export class UnitComponent implements OnInit { private configureUnit(message: VopStartCommand): void { this.reset(); - - if (message.unitDefinition) { - const unitDefinition: Unit = new Unit( - this.sanitizationService.sanitizeUnitDefinition(JSON.parse(message.unitDefinition)) - ); - LogService.debug('player: unitDefinition', unitDefinition); - this.pages = unitDefinition.pages; + try { + if (!message.unitDefinition) { + throw Error('Unit-Definition nicht gefunden.'); + } + LogService.debug('player: unitDefinition', message.unitDefinition); + const unitDefinition = JSON.parse(message.unitDefinition as string); + if (!VersionManager.hasCompatibleVersion(unitDefinition)) { + if (VersionManager.isNewer(unitDefinition)) { + throw Error('Unit-Version ist neuer als dieser Player. Bitte mit der neuesten Version öffnen.'); + } + throw Error('Unit-Version ist veraltet. Sie kann im neuesten Editor geöffnet und aktualisiert werden.'); + } + const unit: Unit = new Unit(unitDefinition); + this.pages = unit.pages; this.playerConfig = message.playerConfig || {}; LogService.info('player: unitStateElementCodes', this.unitStateService.elementCodes); this.metaDataService.resourceURL = this.playerConfig.directDownloadUrl; this.veronaPostService.sessionID = message.sessionId; - this.initUnitStateService(message, unitDefinition); + this.initUnitStateService(message, unit); this.elementModelElementCodeMappingService.dragNDropValueObjects = [ - ...unitDefinition.getAllElements('drop-list'), - ...unitDefinition.getAllElements('drop-list-simple')] + ...unit.getAllElements('drop-list'), + ...unit.getAllElements('drop-list-simple')] .map(element => ((element as InputElement).value as DragNDropValueObject[])).flat(); - } else { - LogService.warn('player: message has no unitDefinition'); + } catch (e: unknown) { + // eslint-disable-next-line no-console + console.error(e); + if (e instanceof InstantiationEror) { + // eslint-disable-next-line no-console + console.error('Failing element blueprint: ', e.faultyBlueprint); + this.showErrorDialog('Unit definition konnte nicht gelesen werden!'); + } else if (e instanceof Error) { + this.showErrorDialog(e.message); + } else { + this.showErrorDialog('Unit definition konnte nicht gelesen werden!'); + } } } + private showErrorDialog(text: string): void { + this.dialog.open(UnitDefErrorDialogComponent, { + data: { text: text }, + disableClose: true + }); + } + private initUnitStateService(message: VopStartCommand, unitDefinition: Unit): void { this.unitStateService.elementCodes = message.unitState?.dataParts?.elementCodes ? JSON.parse(message.unitState.dataParts.elementCodes) : [];