diff --git a/projects/common/id.service.ts b/projects/common/id.service.ts index 1aa8fdfb20a0925aab2a04109b339b4322759555..8acc04e0e65dffafc451cda8198fd673661ca525 100644 --- a/projects/common/id.service.ts +++ b/projects/common/id.service.ts @@ -16,6 +16,8 @@ export class IdService { video: 0, likert: 0, likert_row: 0, + slider: 0, + 'spell-correct': 0, 'radio-group-images': 0, 'drop-list': 0, cloze: 0, diff --git a/projects/common/models/uI-element.ts b/projects/common/models/uI-element.ts index 19042331aaaa1673af7d7d099c63dd20b3f86f1d..6af596c9cbacfa49a6df5342457250323d2e960b 100644 --- a/projects/common/models/uI-element.ts +++ b/projects/common/models/uI-element.ts @@ -3,7 +3,7 @@ import { IdService } from '../id.service'; export type UIElementType = 'text' | 'button' | 'text-field' | 'text-area' | 'checkbox' | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert_row' | 'radio-group-images' -| 'drop-list' | 'cloze'; +| 'drop-list' | 'cloze' | 'spell-correct' | 'slider'; export type InputElementValue = string[] | string | number | boolean | DragNDropValueObject | null; export type DragNDropValueObject = { id: string; @@ -50,7 +50,7 @@ export abstract class UIElement { // This can be overwritten by elements if they need to handle some property specifics. Likert does. setProperty(property: string, - value: InputElementValue | string[] | LikertColumn[] | LikertRow[] | DragNDropValueObject[]): void { + value: InputElementValue | string[] | LikertColumn[] | LikertRow[]): void { if (this.fontProps && property in this.fontProps) { this.fontProps[property] = value as string | number | boolean; } else if (this.surfaceProps && property in this.surfaceProps) { diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts index 7d1bd4732076a6d2a8583509d13897d5ec83d740..cbd0a066106a21d4c5549af8c1fa04e6a726fdbb 100644 --- a/projects/common/shared.module.ts +++ b/projects/common/shared.module.ts @@ -43,6 +43,8 @@ import { RadioGroupImagesComponent } from './ui-elements/radio-with-images/radio import { DropListComponent } from './ui-elements/drop-list/drop-list.component'; import { ClozeComponent } from './ui-elements/cloze/cloze.component'; import { TextFieldSimpleComponent } from './ui-elements/textfield-simple/text-field-simple.component'; +import { SliderComponent } from './ui-elements/slider/slider.component'; +import { SpellCorrectComponent } from './ui-elements/spell-correct/spell-correct.component'; import { DropListSimpleComponent } from './ui-elements/drop-list-simple/drop-list-simple.component'; @NgModule({ @@ -88,7 +90,10 @@ import { DropListSimpleComponent } from './ui-elements/drop-list-simple/drop-lis DropListComponent, ClozeComponent, TextFieldSimpleComponent, - DropListSimpleComponent + DropListSimpleComponent, + SliderComponent, + SpellCorrectComponent, + TextFieldSimpleComponent ], exports: [ CommonModule, diff --git a/projects/common/ui-elements/likert/likert-radio-button-group.component.ts b/projects/common/ui-elements/likert/likert-radio-button-group.component.ts index 6f2ea7a2062679df989a3f5436f9816ed30efadd..8162cc539cade651454c8863bb6a23253fb60d83 100644 --- a/projects/common/ui-elements/likert/likert-radio-button-group.component.ts +++ b/projects/common/ui-elements/likert/likert-radio-button-group.component.ts @@ -1,5 +1,4 @@ import { Component, Input } from '@angular/core'; -import { FormGroup } from '@angular/forms'; import { FormElementComponent } from '../../directives/form-element-component.directive'; import { LikertElementRow } from './likert-element-row'; diff --git a/projects/common/ui-elements/likert/likert.component.ts b/projects/common/ui-elements/likert/likert.component.ts index 30c22a510ea7de977dc15c2b7898d50c0b4e8176..7ed8be0968eb90820cf11388d8029a525852a859 100644 --- a/projects/common/ui-elements/likert/likert.component.ts +++ b/projects/common/ui-elements/likert/likert.component.ts @@ -1,9 +1,7 @@ import { - Component, EventEmitter, Output, QueryList, ViewChildren + Component, QueryList, ViewChildren } from '@angular/core'; -import { FormGroup } from '@angular/forms'; import { LikertElement } from './likert-element'; -import { ValueChangeElement } from '../../models/uI-element'; import { LikertElementRow } from './likert-element-row'; import { LikertRadioButtonGroupComponent } from './likert-radio-button-group.component'; import { CompoundElementComponent } from '../../directives/compound-element.directive'; diff --git a/projects/common/ui-elements/slider/slider-element.ts b/projects/common/ui-elements/slider/slider-element.ts new file mode 100644 index 0000000000000000000000000000000000000000..21907a4c3c96d5eac3e0f94e50fc132519057fe4 --- /dev/null +++ b/projects/common/ui-elements/slider/slider-element.ts @@ -0,0 +1,33 @@ +import { + FontElement, + FontProperties, + InputElement, + PositionProperties, + SurfaceElement, SurfaceProperties, + UIElement +} from '../../models/uI-element'; +import { initFontElement, initPositionedElement, initSurfaceElement } from '../../util/unit-interface-initializer'; + +export class SliderElement extends InputElement implements FontElement, SurfaceElement { + positionProps: PositionProperties; + fontProps: FontProperties; + surfaceProps: SurfaceProperties; + minValue: number = 0; + maxValue: number = 100; + showValues: boolean = true; + barStyle: boolean = false; + thumbLabel: boolean = false; + + constructor(serializedElement: UIElement) { + super(serializedElement); + Object.assign(this, serializedElement); + this.positionProps = initPositionedElement(serializedElement); + this.fontProps = initFontElement(serializedElement); + this.surfaceProps = initSurfaceElement(serializedElement); + + this.surfaceProps.backgroundColor = + serializedElement.surfaceProps?.backgroundColor as string || 'transparent'; + this.height = serializedElement.height || 75; + this.width = serializedElement.width || 300; + } +} diff --git a/projects/common/ui-elements/slider/slider.component.ts b/projects/common/ui-elements/slider/slider.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fc323cdd42aece95cfd3e31ced9851bbe076675 --- /dev/null +++ b/projects/common/ui-elements/slider/slider.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ValidatorFn, Validators } from '@angular/forms'; +import { MatSlider } from '@angular/material/slider'; +import { SliderElement } from './slider-element'; +import { FormElementComponent } from '../../directives/form-element-component.directive'; + +@Component({ + selector: 'app-slider', + template: ` + <div fxLayout="column" + [style.background-color]="elementModel.surfaceProps.backgroundColor" + [style.width.%]="100" + [style.height.%]="100"> + <div *ngIf="elementModel.label" + [style.color]="elementModel.fontProps.fontColor" + [style.font-family]="elementModel.fontProps.font" + [style.font-size.px]="elementModel.fontProps.fontSize" + [style.line-height.%]="elementModel.fontProps.lineHeight" + [style.font-weight]="elementModel.fontProps.bold ? 'bold' : ''" + [style.font-style]="elementModel.fontProps.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.fontProps.underline ? 'underline' : ''"> + {{elementModel.label}} + </div> + <div fxFlex fxLayout="row"> + <div *ngIf="elementModel.showValues" fxFlex + [style.color]="elementModel.fontProps.fontColor" + [style.font-family]="elementModel.fontProps.font" + [style.font-size.px]="elementModel.fontProps.fontSize" + [style.line-height.%]="elementModel.fontProps.lineHeight" + [style.font-weight]="elementModel.fontProps.bold ? 'bold' : ''" + [style.font-style]="elementModel.fontProps.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.fontProps.underline ? 'underline' : ''"> + {{elementModel.minValue | number:'.0'}} + </div> + <div [style.display]="'flex'" + [style.flex-direction]="'column'" + [style.width.%]="100" + [style.height.%]="100"> + <mat-slider + [class]="elementModel.barStyle ? 'bar-style' : ''" + [thumbLabel]="elementModel.thumbLabel" + [formControl]="elementFormControl" + [style.width.%]="100" + [max]="elementModel.maxValue" + [min]="elementModel.minValue"> + </mat-slider> + <mat-error *ngIf="elementFormControl.touched && elementFormControl.errors"> + {{elementModel.requiredWarnMessage}} + </mat-error> + </div> + <div *ngIf="elementModel.showValues" + [style.color]="elementModel.fontProps.fontColor" + [style.font-family]="elementModel.fontProps.font" + [style.font-size.px]="elementModel.fontProps.fontSize" + [style.line-height.%]="elementModel.fontProps.lineHeight" + [style.font-weight]="elementModel.fontProps.bold ? 'bold' : ''" + [style.font-style]="elementModel.fontProps.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.fontProps.underline ? 'underline' : ''"> + {{elementModel.maxValue | number:'.0'}}</div> + </div> + </div> + `, + styles: [ + ':host ::ng-deep .bar-style .mat-slider-thumb {border-radius: 0; width: 10px; height: 40px; bottom: -15px}' + ] +}) +export class SliderComponent extends FormElementComponent implements OnInit { + @ViewChild(MatSlider) inputElement!: MatSlider; + elementModel!: SliderElement; + + get validators(): ValidatorFn[] { + const validators: ValidatorFn[] = []; + if (this.elementModel.required) { + validators.push(Validators.min(this.elementModel.minValue + 1)); + } + return validators; + } + + ngOnInit(): void { + super.ngOnInit(); + if (this.inputElement) { + this.inputElement.disabled = this.elementModel.readOnly; + } + } +} diff --git a/projects/common/ui-elements/spell-correct/spell-correct-element.ts b/projects/common/ui-elements/spell-correct/spell-correct-element.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f0b69998c57fe19c787a110efc977477f593691 --- /dev/null +++ b/projects/common/ui-elements/spell-correct/spell-correct-element.ts @@ -0,0 +1,28 @@ +import { initFontElement, initPositionedElement, initSurfaceElement } from '../../util/unit-interface-initializer'; +import { + FontElement, + FontProperties, + InputElement, + PositionProperties, + SurfaceElement, SurfaceProperties, + UIElement +} from '../../models/uI-element'; + +export class SpellCorrectElement extends InputElement implements FontElement, SurfaceElement { + positionProps: PositionProperties; + fontProps: FontProperties; + surfaceProps: SurfaceProperties; + + constructor(serializedElement: UIElement) { + super(serializedElement); + Object.assign(this, serializedElement); + this.positionProps = initPositionedElement(serializedElement); + this.fontProps = initFontElement(serializedElement); + this.surfaceProps = initSurfaceElement(serializedElement); + + this.surfaceProps.backgroundColor = + serializedElement.surfaceProps?.backgroundColor as string || 'transparent'; + this.height = serializedElement.height || 80; + this.width = serializedElement.width || 230; + } +} diff --git a/projects/common/ui-elements/spell-correct/spell-correct.component.ts b/projects/common/ui-elements/spell-correct/spell-correct.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..16c87fc6c507a2a25f77c322a7dad3fb2edb6cc0 --- /dev/null +++ b/projects/common/ui-elements/spell-correct/spell-correct.component.ts @@ -0,0 +1,74 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatInput } from '@angular/material/input'; +import { MatButton } from '@angular/material/button'; +import { FormElementComponent } from '../../directives/form-element-component.directive'; +import { SpellCorrectElement } from './spell-correct-element'; + +@Component({ + selector: 'app-spell-correct', + template: ` + <div fxFlex + fxLayout="column" + [style.width.%]="100" + appInputBackgroundColor [backgroundColor]="elementModel.surfaceProps.backgroundColor" + [style.height.%]="100"> + <mat-form-field class="small-input"> + <input matInput type="text" + [style.text-align]="'center'" + autocomplete="off" + [formControl]="elementFormControl"> + </mat-form-field> + <button mat-button + [style.color]="elementModel.fontProps.fontColor" + [style.font-family]="elementModel.fontProps.font" + [style.font-size.px]="elementModel.fontProps.fontSize" + [style.font-weight]="elementModel.fontProps.bold ? 'bold' : ''" + [style.font-style]="elementModel.fontProps.italic ? 'italic' : ''" + [style.width.%]="100" + [style.margin-top]="'-20px'" + [style.text-decoration-line]="strikethrough() ? 'line-through' : ''" + (click)="onClick($event)"> + {{elementModel.label}} + </button> + </div> + `, + styles: [ + '::ng-deep app-spell-correct .small-input div.mat-form-field-infix {border-top: none; padding: 0.75em 0 0.25em 0;}' + ] +}) +export class SpellCorrectComponent extends FormElementComponent implements OnInit { + elementModel!: SpellCorrectElement; + @ViewChild(MatInput) inputElement!: MatInput; + @ViewChild(MatButton) buttonElement!: MatButton; + + ngOnInit(): void { + super.ngOnInit(); + if (this.inputElement && this.elementModel.readOnly) { + this.inputElement.readonly = true; + } + if (this.buttonElement && this.elementModel.readOnly) { + this.buttonElement.disabled = true; + } + } + + strikethrough(): boolean { + if (this.inputElement) { + const value = this.inputElement.value; + if (value === null) return false; + if (value === undefined) return false; + return value.length > 0; + } + return false; + } + + onClick(event: MouseEvent) : void { + if (this.strikethrough()) { + this.elementFormControl.setValue(''); + } else { + this.elementFormControl.setValue(this.elementModel.label); + this.inputElement.focus(); + } + event.preventDefault(); + event.stopPropagation(); + } +} diff --git a/projects/common/util/element.factory.ts b/projects/common/util/element.factory.ts index 8baa07ccab337ae44567765a9e44913b4c4fc7f4..deddd7dcdb3b24e1278aed20f88dfcf8b91de897 100644 --- a/projects/common/util/element.factory.ts +++ b/projects/common/util/element.factory.ts @@ -28,6 +28,10 @@ import { DropListComponent } from '../ui-elements/drop-list/drop-list.component' import { DropListElement } from '../ui-elements/drop-list/drop-list'; import { ClozeComponent } from '../ui-elements/cloze/cloze.component'; import { ClozeElement } from '../ui-elements/cloze/cloze-element'; +import { SliderElement } from '../ui-elements/slider/slider-element'; +import { SpellCorrectElement } from '../ui-elements/spell-correct/spell-correct-element'; +import { SliderComponent } from '../ui-elements/slider/slider.component'; +import { SpellCorrectComponent } from '../ui-elements/spell-correct/spell-correct.component'; export function createElement(elementModel: UIElement): UIElement { let newElement: UIElement; @@ -74,6 +78,12 @@ export function createElement(elementModel: UIElement): UIElement { case 'cloze': newElement = new ClozeElement(elementModel); break; + case 'slider': + newElement = new SliderElement(elementModel); + break; + case 'spell-correct': + newElement = new SpellCorrectElement(elementModel); + break; default: throw new Error(`ElementType ${elementModel.type} not found!`); } @@ -114,6 +124,10 @@ export function getComponentFactory( return componentFactoryResolver.resolveComponentFactory(DropListComponent); case 'cloze': return componentFactoryResolver.resolveComponentFactory(ClozeComponent); + case 'slider': + return componentFactoryResolver.resolveComponentFactory(SliderComponent); + case 'spell-correct': + return componentFactoryResolver.resolveComponentFactory(SpellCorrectComponent); default: throw new Error('unknown element'); } diff --git a/projects/editor/src/app/app.component.ts b/projects/editor/src/app/app.component.ts index ab2c3121cd85b1ad4ff136b82625311514f31fbf..e6e3ee5bdad31aa33827700a306e26551cfeaaf5 100644 --- a/projects/editor/src/app/app.component.ts +++ b/projects/editor/src/app/app.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { registerLocaleData } from '@angular/common'; +import localeDe from '@angular/common/locales/de'; import { VeronaAPIService } from './services/verona-api.service'; import { UnitService } from './services/unit.service'; @@ -37,5 +39,6 @@ export class AppComponent implements OnInit { }); this.veronaApiService.sendVoeReadyNotification(); + registerLocaleData(localeDe); } } diff --git a/projects/editor/src/app/components/unit-view/page-view/new-ui-element-panel/ui-element-toolbox.component.html b/projects/editor/src/app/components/unit-view/page-view/new-ui-element-panel/ui-element-toolbox.component.html index 403717932005fbdfbed3181e7f24c64d3a741f9b..5b5deeb3f8577b403e1150d39dae9dc3a6448772 100644 --- a/projects/editor/src/app/components/unit-view/page-view/new-ui-element-panel/ui-element-toolbox.component.html +++ b/projects/editor/src/app/components/unit-view/page-view/new-ui-element-panel/ui-element-toolbox.component.html @@ -81,6 +81,16 @@ <mat-icon>vertical_split</mat-icon> Lückentext </button> + <button mat-raised-button (click)="addUIElement('slider')" + draggable="true" (dragstart)="$event.dataTransfer?.setData('elementType','slider')"> + <mat-icon>linear_scale</mat-icon> + Schieberegler + </button> + <button mat-raised-button (click)="addUIElement('spell-correct')" + draggable="true" (dragstart)="$event.dataTransfer?.setData('elementType','spell-correct')"> + <mat-icon>format_strikethrough</mat-icon> + Wort korrigieren + </button> </div> </mat-tab> </mat-tab-group> diff --git a/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties-component.component.ts b/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties-component.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a83c691a2880541058646c8974517e08d3da8e6e --- /dev/null +++ b/projects/editor/src/app/components/unit-view/page-view/properties-panel/element-model-properties-component.component.ts @@ -0,0 +1,618 @@ +import { + Component, EventEmitter, + Input, Output +} from '@angular/core'; +import { CdkDragDrop } from '@angular/cdk/drag-drop/drag-events'; +import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { + InputElementValue, LikertColumn, LikertRow, UIElement +} from '../../../../../../../common/models/uI-element'; +import { LikertElement } from '../../../../../../../common/ui-elements/likert/likert-element'; +import { LikertElementRow } from '../../../../../../../common/ui-elements/likert/likert-element-row'; +import { UnitService } from '../../../../services/unit.service'; +import { FileService } from '../../../../../../../common/file.service'; + +@Component({ + selector: 'app-element-model-properties-component', + template: ` + <div fxLayout="column"> + <mat-form-field appearance="fill"> + <mat-label>{{'propertiesPanel.id' | translate }}</mat-label> + <input matInput type="text" *ngIf="selectedElements.length === 1" [value]="combinedProperties.id" + (input)="updateModel.emit({property: 'id', value: $any($event.target).value })"> + <input matInput type="text" disabled *ngIf="selectedElements.length > 1" [value]="'Muss eindeutig sein'"> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.label !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.label' | translate }}</mat-label> + <input matInput type="text" [value]="combinedProperties.label" + (input)="updateModel.emit({property: 'label', value: $any($event.target).value })"> + </mat-form-field> + + <ng-container *ngIf="combinedProperties.text"> + {{'propertiesPanel.text' | translate }} + <div class="text-text" [innerHTML]="$any(combinedProperties.text) | safeResourceHTML" + (click)="unitService.showDefaultEditDialog(selectedElements[0])"> + </div> + </ng-container> + + <!-- Autostart for detecting a player-element --> + <ng-container *ngIf="combinedProperties.playerProps"> + <button (click)="unitService.showDefaultEditDialog(selectedElements[0])"> + <mat-icon>build_circle</mat-icon> + </button> + </ng-container> + + {{'propertiesPanel.highlightable' | translate }} + <mat-checkbox *ngIf="combinedProperties.highlightableYellow !== undefined" + [checked]="$any(combinedProperties.highlightableYellow)" + (change)="updateModel.emit({ property: 'highlightableYellow', value: $event.checked })"> + {{'propertiesPanel.highlightableYellow' | translate }} + </mat-checkbox> + <mat-checkbox *ngIf="combinedProperties.highlightableTurquoise !== undefined" + [checked]="$any(combinedProperties.highlightableTurquoise)" + (change)="updateModel.emit({ property: 'highlightableTurquoise', value: $event.checked })"> + {{'propertiesPanel.highlightableTurquoise' | translate }} + </mat-checkbox> + <mat-checkbox *ngIf="combinedProperties.highlightableOrange !== undefined" + [checked]="$any(combinedProperties.highlightableOrange)" + (change)="updateModel.emit({ property: 'highlightableOrange', value: $event.checked })"> + {{'propertiesPanel.highlightableOrange' | translate }} + </mat-checkbox> + + <mat-checkbox *ngIf="combinedProperties.strikeOtherOptions !== undefined" + [checked]="$any(combinedProperties.strikeOtherOptions)" + (change)="updateModel.emit({ property: 'strikeOtherOptions', value: $event.checked })"> + {{'propertiesPanel.strikeOtherOptions' | translate }} + </mat-checkbox> + <mat-checkbox *ngIf="combinedProperties.allowUnset !== undefined" + [checked]="$any(combinedProperties.allowUnset)" + (change)="updateModel.emit({ property: 'allowUnset', value: $event.checked })"> + {{'propertiesPanel.allowUnset' | translate }} + </mat-checkbox> + + <mat-checkbox *ngIf="combinedProperties.readOnly !== undefined" + [checked]="$any(combinedProperties.readOnly)" + (change)="updateModel.emit({ property: 'readOnly', value: $event.checked })"> + {{'propertiesPanel.readOnly' | translate }} + </mat-checkbox> + <mat-checkbox *ngIf="combinedProperties.required !== undefined" + [checked]="$any(combinedProperties.required)" + (change)="updateModel.emit({ property: 'required', value: $event.checked })"> + {{'propertiesPanel.requiredField' | translate }} + </mat-checkbox> + <mat-form-field *ngIf="combinedProperties.required" + appearance="fill"> + <mat-label>{{'propertiesPanel.requiredWarnMessage' | translate }}</mat-label> + <input matInput type="text" [value]="combinedProperties.requiredWarnMessage" + (input)="updateModel.emit({ property: 'requiredWarnMessage', value: $any($event.target).value })"> + </mat-form-field> + + <mat-form-field disabled="true" *ngIf="combinedProperties.options !== undefined"> + <ng-container> + <mat-label>{{'propertiesPanel.options' | translate }}</mat-label> + <div class="drop-list" cdkDropList [cdkDropListData]="combinedProperties.options" + (cdkDropListDropped)="reorderOptions('options', $any($event))"> + <div *ngFor="let option of $any(combinedProperties.options); let i = index" cdkDrag + class="list-items" fxLayout="row" fxLayoutAlign="end center"> + <div fxFlex="70"> + {{option}} + </div> + <button mat-icon-button color="primary" + (click)="editTextOption('options', i)"> + <mat-icon>build</mat-icon> + </button> + <button mat-icon-button color="primary" + (click)="removeOption('options', option)"> + <mat-icon>clear</mat-icon> + </button> + </div> + </div> + </ng-container> + <div fxLayout="row" fxLayoutAlign="center center"> + <button mat-icon-button matPrefix + (click)="addOption('options', newOption.value); newOption.select()"> + <mat-icon>add</mat-icon> + </button> + <input #newOption matInput type="text" placeholder="Optionstext" + (keyup.enter)="addOption('options', newOption.value); newOption.select()"> + </div> + </mat-form-field> + + <mat-form-field disabled="true" *ngIf="combinedProperties.connectedTo !== undefined"> + <ng-container> + <mat-label>{{'preset' | translate }}</mat-label> + <div class="drop-list" cdkDropList [cdkDropListData]="combinedProperties.value" + (cdkDropListDropped)="reorderOptions('value', $any($event))"> + <div *ngFor="let value of $any(combinedProperties.value); let i = index" cdkDrag + class="list-items" fxLayout="row" fxLayoutAlign="end center"> + <div fxFlex="70"> + {{value}} + </div> + <button mat-icon-button color="primary" + (click)="editTextOption('value', i)"> + <mat-icon>build</mat-icon> + </button> + <button mat-icon-button color="primary" + (click)="removeOption('value', value)"> + <mat-icon>clear</mat-icon> + </button> + </div> + </div> + </ng-container> + <div fxLayout="row" fxLayoutAlign="center center"> + <button mat-icon-button matPrefix + (click)="addOption('value', newValue.value); newValue.select()"> + <mat-icon>add</mat-icon> + </button> + <input #newValue matInput type="text" + (keyup.enter)="addOption('value', newValue.value); newValue.select()"> + </div> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.alignment" appearance="fill"> + <mat-label>{{'propertiesPanel.alignment' | translate }}</mat-label> + <mat-select [value]="combinedProperties.alignment" + (selectionChange)="updateModel.emit({ property: 'alignment', value: $event.value })"> + <mat-option *ngFor="let option of ['column', 'row']" + [value]="option"> + {{ 'propertiesPanel.' + option | translate }} + </mat-option> + </mat-select> + </mat-form-field> + + <mat-checkbox *ngIf="combinedProperties.resizeEnabled !== undefined" + [checked]="$any(combinedProperties.resizeEnabled)" + (change)="updateModel.emit({ property: 'resizeEnabled', value: $event.checked })"> + {{'propertiesPanel.resizeEnabled' | translate }} + </mat-checkbox> + + <mat-form-field *ngIf="combinedProperties.rowCount != null" + appearance="fill" class="mdInput textsingleline"> + <mat-label>{{'rows' | translate }}</mat-label> + <input matInput type="number" [value]="combinedProperties.rowCount" + (input)="updateModel.emit({ property: 'rowCount', value: $any($event.target).value })"> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.action !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.action' | translate }}</mat-label> + <mat-select [value]="combinedProperties.action" + (selectionChange)="updateModel.emit({ property: 'action', value: $event.value })"> + <mat-option *ngFor="let option of [undefined, 'previous', 'next', 'first', 'last', 'end']" + [value]="option"> + {{ 'propertiesPanel.' + option | translate }} + </mat-option> + </mat-select> + </mat-form-field> + + <ng-container *ngIf="combinedProperties.imageSrc !== undefined"> + <input #imageUpload type="file" hidden (click)="loadImage()"> + <button mat-raised-button (click)="imageUpload.click()">{{'loadImage' | translate }}</button> + <button mat-raised-button (click)="removeImage()">{{'removeImage' | translate }}</button> + <img [src]="combinedProperties.imageSrc" + [style.object-fit]="'scale-down'" + [width]="200"> + </ng-container> + + <mat-form-field *ngIf="combinedProperties.options !== undefined && !combinedProperties.connectedTo" + appearance="fill"> + <mat-label>{{'preset' | translate }}</mat-label> + <mat-select [value]="combinedProperties.value" + (selectionChange)="updateModel.emit({ property: 'value', value: $event.value })"> + <mat-option [value]="null">{{'propertiesPanel.undefined' | translate }}</mat-option> + <mat-option *ngFor="let option of $any(combinedProperties.options); let i = index" [value]="i"> + {{option}} (Index: {{i}}) + </mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.columns !== undefined && combinedProperties.rows === undefined" + appearance="fill"> + <mat-label>{{'preset' | translate }}</mat-label> + <mat-select [value]="combinedProperties.value" + (selectionChange)="updateModel.emit({ property: 'value', value: $event.value })"> + <mat-option [value]="null">{{'propertiesPanel.undefined' | translate }}</mat-option> + <mat-option *ngFor="let column of $any(combinedProperties.columns); let i = index" [value]="i"> + {{column.name}} (Index: {{i}}) + </mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.minValue !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.minValue' | translate }}</mat-label> + <input matInput type="number" #minValue="ngModel" + [ngModel]="combinedProperties.minValue" + (ngModelChange)="updateModel.emit({ + property: 'minValue', + value: $event, + isInputValid: minValue.valid})"> + </mat-form-field> + <mat-form-field *ngIf="combinedProperties.maxValue !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.maxValue' | translate }}</mat-label> + <input matInput type="number" #maxValue="ngModel" + [ngModel]="combinedProperties.maxValue" + (ngModelChange)="updateModel.emit({ + property: 'maxValue', + value: $event, + isInputValid: maxValue.valid})"> + </mat-form-field> + <mat-checkbox *ngIf="combinedProperties.showValues !== undefined" + [checked]="$any(combinedProperties.showValues)" + (change)="updateModel.emit({ property: 'showValues', value: $event.checked })"> + {{'propertiesPanel.showValues' | translate }} + </mat-checkbox> + <mat-checkbox *ngIf="combinedProperties.barStyle !== undefined" + [checked]="$any(combinedProperties.barStyle)" + (change)="updateModel.emit({ property: 'barStyle', value: $event.checked })"> + {{'propertiesPanel.barStyle' | translate }} + </mat-checkbox> + <mat-checkbox *ngIf="combinedProperties.thumbLabel !== undefined" + [checked]="$any(combinedProperties.thumbLabel)" + (change)="updateModel.emit({ property: 'thumbLabel', value: $event.checked })"> + {{'propertiesPanel.thumbLabel' | translate }} + </mat-checkbox> + + <ng-container *ngIf="combinedProperties.value === true || combinedProperties.value === false"> + {{'preset' | translate }} + <mat-button-toggle-group [value]="combinedProperties.value" + (change)="updateModel.emit({ property: 'value', value: $event.value })"> + <mat-button-toggle [value]="true">{{'propertiesPanel.true' | translate }}</mat-button-toggle> + <mat-button-toggle [value]="false">{{'propertiesPanel.false' | translate }}</mat-button-toggle> + </mat-button-toggle-group> + </ng-container> + <mat-form-field *ngIf="combinedProperties.minValue !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.preset' | translate }}</mat-label> + <input matInput type="number" #presetValue="ngModel" + [ngModel]="combinedProperties.value" + (ngModelChange)="updateModel.emit({ + property: 'value', + value: $event, + isInputValid: presetValue.valid})"> + </mat-form-field> + + <!-- TODO wtf--> + <mat-form-field *ngIf="combinedProperties.value !== undefined && combinedProperties.minValue === undefined && + !combinedProperties.options && !combinedProperties.columns && + combinedProperties.connectedTo === undefined && + combinedProperties.value !== true && combinedProperties.value !== false" + appearance="fill"> + <mat-label>{{'preset' | translate }}</mat-label> + <textarea matInput type="text" + [value]="combinedProperties.value" + (input)="updateModel.emit({ property: 'value', value: $any($event.target).value })"> + </textarea> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.appearance !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.appearance' | translate }}</mat-label> + <mat-select [value]="combinedProperties.appearance" + (selectionChange)="updateModel.emit({ property: 'appearance', value: $event.value })"> + <mat-option *ngFor="let option of [{displayValue: 'standard', value: 'standard'}, + {displayValue: 'legacy', value: 'legacy'}, + {displayValue: 'fill', value: 'fill'}, + {displayValue: 'outline', value: 'outline'}]" + [value]="option.value"> + {{option.displayValue}} + </mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.minLength !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.minLength' | translate }}</mat-label> + <input matInput type="number" #minLength="ngModel" min="0" + [ngModel]="combinedProperties.minLength" + (ngModelChange)="updateModel.emit({ + property: 'minLength', + value: $event, + isInputValid: minLength.valid })"> + </mat-form-field> + <mat-form-field *ngIf="combinedProperties.minLength && + $any(combinedProperties.minLength) > 0" + appearance="fill"> + <mat-label>{{'propertiesPanel.minLengthWarnMessage' | translate }}</mat-label> + <input matInput type="text" [value]="combinedProperties.minLengthWarnMessage" + (input)="updateModel.emit({ property: 'minLengthWarnMessage', value: $any($event.target).value })"> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.maxLength !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.maxLength' | translate }}</mat-label> + <input matInput type="number" #maxLength="ngModel" min="0" + [ngModel]="combinedProperties.maxLength" + (ngModelChange)="updateModel.emit({ + property: 'maxLength', + value: $event, + isInputValid: maxLength.valid })"> + </mat-form-field> + <mat-form-field *ngIf="combinedProperties.maxLength && + $any(combinedProperties.maxLength) > 0" + appearance="fill"> + <mat-label>{{'propertiesPanel.maxLengthWarnMessage' | translate }}</mat-label> + <input matInput type="text" [value]="combinedProperties.maxLengthWarnMessage" + (input)="updateModel.emit({ property: 'maxLengthWarnMessage', value: $any($event.target).value })"> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.pattern !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.pattern' | translate }}</mat-label> + <input matInput [value]="combinedProperties.pattern" + (input)="updateModel.emit({ property: 'pattern', value: $any($event.target).value })"> + </mat-form-field> + <mat-form-field *ngIf="combinedProperties.pattern && $any(combinedProperties.pattern) !== ''" + appearance="fill" + matTooltip="Angabe als regulärer Ausdruck."> + <mat-label>{{'propertiesPanel.patternWarnMessage' | translate }}</mat-label> + <input matInput type="text" [value]="combinedProperties.patternWarnMessage" + (input)="updateModel.emit({ property: 'patternWarnMessage', value: $any($event.target).value })"> + </mat-form-field> + + <mat-checkbox *ngIf="combinedProperties.clearable !== undefined" + [checked]="$any(combinedProperties.clearable)" + (change)="updateModel.emit({ property: 'clearable', value: $event.checked })"> + {{'propertiesPanel.clearable' | translate }} + </mat-checkbox> + + <mat-form-field *ngIf="combinedProperties.inputAssistancePreset !== undefined" appearance="fill"> + <mat-label>{{'propertiesPanel.inputAssistance' | translate }}</mat-label> + <mat-select [value]="combinedProperties.inputAssistancePreset" + (selectionChange)="updateModel.emit({ property: 'inputAssistancePreset', value: $event.value })"> + <mat-option *ngFor="let option of ['none', 'french', 'numbers', 'numbersAndOperators']" + [value]="option"> + {{ 'propertiesPanel.' + option | translate }} + </mat-option> + </mat-select> + </mat-form-field> + <mat-form-field *ngIf="combinedProperties.inputAssistancePreset !== 'none' && + combinedProperties.inputAssistancePosition !== undefined" + appearance="fill"> + <mat-label>{{'propertiesPanel.inputAssistancePosition' | translate }}</mat-label> + <mat-select [value]="combinedProperties.inputAssistancePosition" + (selectionChange)="updateModel.emit({ property: 'inputAssistancePosition', value: $event.value })"> + <mat-option *ngFor="let option of ['floating', 'right']" + [value]="option"> + {{ 'propertiesPanel.' + option | translate }} + </mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field disabled="true" *ngIf="combinedProperties.rows !== undefined"> + <ng-container> + <mat-label>{{'rows' | translate }}</mat-label> + <div class="drop-list" cdkDropList [cdkDropListData]="combinedProperties.rows" + (cdkDropListDropped)="reorderOptions('rows', $any($event))"> + <div *ngFor="let row of $any(combinedProperties.rows); let i = index" cdkDrag + class="list-items" fxLayout="row" fxLayoutAlign="end center"> + <div fxFlex="70"> + {{row.text}} + </div> + <button mat-icon-button color="primary" + (click)="editRowOption(i)"> + <mat-icon>build</mat-icon> + </button> + <button mat-icon-button color="primary" + (click)="removeOption('rows', row)"> + <mat-icon>clear</mat-icon> + </button> + </div> + </div> + </ng-container> + <div fxLayout="row" fxLayoutAlign="center center"> + <button mat-icon-button matPrefix + (click)="addRow(newRow.value); newRow.select()"> + <mat-icon>add</mat-icon> + </button> + <input #newRow matInput type="text" placeholder="Fragetext" + (keyup.enter)="addRow(newRow.value); newRow.select()"> + </div> + </mat-form-field> + + <mat-form-field disabled="true" *ngIf="combinedProperties.columns !== undefined"> + <ng-container> + <mat-label>{{'columns' | translate }}</mat-label> + <div class="drop-list" cdkDropList [cdkDropListData]="combinedProperties.columns" + (cdkDropListDropped)="reorderOptions('columns', $any($event))"> + <div *ngFor="let column of $any(combinedProperties.columns); let i = index" cdkDrag + class="list-items" fxLayout="row" fxLayoutAlign="end center"> + <div fxFlex="70"> + {{column.text}} + </div> + <img [src]="column.imgSrc" + [style.object-fit]="'scale-down'" + [style.height.px]="40"> + <button mat-icon-button color="primary" + (click)="editColumnOption(i)"> + <mat-icon>build</mat-icon> + </button> + <button mat-icon-button color="primary" + (click)="removeOption('columns', column)"> + <mat-icon>clear</mat-icon> + </button> + </div> + </div> + </ng-container> + <div fxLayout="row" fxLayoutAlign="center center"> + <button mat-icon-button matPrefix + (click)="addColumn(newColumn.value); newColumn.select()"> + <mat-icon>add</mat-icon> + </button> + <input #newColumn matInput type="text" placeholder="Antworttext" + (keyup.enter)="addColumn(newColumn.value); newColumn.select()"> + </div> + </mat-form-field> + + <mat-checkbox *ngIf="combinedProperties.lineColoring !== undefined" + [checked]="$any(combinedProperties.lineColoring)" + (change)="updateModel.emit({ property: 'lineColoring', value: $event.checked })"> + {{'propertiesPanel.lineColoring' | translate }} + </mat-checkbox> + + <mat-form-field *ngIf="combinedProperties.lineColoring && combinedProperties.lineColoringColor" + appearance="fill" class="mdInput textsingleline"> + <mat-label>{{'propertiesPanel.lineColoringColor' | translate }}</mat-label> + <input matInput type="color" [value]="combinedProperties.lineColoringColor" + (input)="updateModel.emit({ property: 'lineColoringColor', value: $any($event.target).value })"> + </mat-form-field> + + <mat-checkbox *ngIf="combinedProperties.magnifier !== undefined" + [checked]="$any(combinedProperties.magnifier)" + (change)="updateModel.emit({ property: 'magnifier', value: $event.checked })"> + {{'propertiesPanel.magnifier' | translate }} + </mat-checkbox> + <mat-form-field *ngIf="combinedProperties.magnifier" appearance="fill"> + <mat-label>{{'propertiesPanel.magnifierSize' | translate }} in px</mat-label> + <input matInput type="number" #magnifierSize="ngModel" min="0" + [ngModel]="combinedProperties.magnifierSize" + (ngModelChange)="updateModel.emit({ + property: 'magnifierSize', + value: $event, + isInputValid: magnifierSize.valid})"> + </mat-form-field> + + <ng-container *ngIf="combinedProperties.magnifier"> + {{'propertiesPanel.magnifierZoom' | translate }} + <mat-slider min="1" max="3" step="0.1" [ngModel]="combinedProperties.magnifierZoom" + (change)="updateModel.emit({ property: 'magnifierZoom', value: $event.value })"> + </mat-slider> + <div *ngIf="combinedProperties.magnifier"> + {{combinedProperties.magnifierZoom}} + </div> + </ng-container> + + <mat-form-field disabled="true" *ngIf="combinedProperties.connectedTo !== undefined"> + <ng-container> + <mat-label>{{'propertiesPanel.connectedDropList' | translate }}</mat-label> + <div class="drop-list" cdkDropList [cdkDropListData]="combinedProperties.connectedTo" + (cdkDropListDropped)="reorderOptions('connectedTo', $any($event))"> + <div *ngFor="let connectedTo of $any(combinedProperties.connectedTo); let i = index" cdkDrag + class="list-items" fxLayout="row" fxLayoutAlign="end center"> + <div fxFlex="70"> + {{connectedTo}} + </div> + <button mat-icon-button color="primary" + (click)="editTextOption('connectedTo', i)"> + <mat-icon>build</mat-icon> + </button> + <button mat-icon-button color="primary" + (click)="removeOption('connectedTo', connectedTo)"> + <mat-icon>clear</mat-icon> + </button> + </div> + </div> + </ng-container> + <div fxLayout="row" fxLayoutAlign="center center"> + <button mat-icon-button matPrefix + (click)="addOption('connectedTo', newconnectedTo.value); newconnectedTo.select()"> + <mat-icon>add</mat-icon> + </button> + <input #newconnectedTo matInput type="text" + (keyup.enter)="addOption('connectedTo', newconnectedTo.value); newconnectedTo.select()"> + </div> + </mat-form-field> + + <mat-form-field *ngIf="combinedProperties.orientation !== undefined" + appearance="fill"> + <mat-label>{{'propertiesPanel.alignment' | translate }}</mat-label> + <mat-select [value]="combinedProperties.orientation" + (selectionChange)="updateModel.emit({ property: 'orientation', value: $event.value })"> + <mat-option *ngFor="let option of ['vertical', 'horizontal']" + [value]="option"> + {{ 'propertiesPanel.' + option | translate }} + </mat-option> + </mat-select> + </mat-form-field> + + <mat-checkbox *ngIf="combinedProperties.onlyOneItem !== undefined" + [checked]="$any(combinedProperties.onlyOneItem)" + (change)="updateModel.emit({ property: 'onlyOneItem', value: $event.checked })"> + {{'propertiesPanel.onlyOneItem' | translate }} + </mat-checkbox> + + <mat-checkbox *ngIf="combinedProperties.highlightReceivingDropList !== undefined" + [checked]="$any(combinedProperties.highlightReceivingDropList)" + (change)="updateModel.emit({ property: 'highlightReceivingDropList', value: $event.checked })"> + {{'propertiesPanel.highlightReceivingDropList' | translate }} + </mat-checkbox> + <mat-form-field *ngIf="combinedProperties.highlightReceivingDropList" + appearance="fill" class="mdInput textsingleline"> + <mat-label>{{'propertiesPanel.highlightReceivingDropListColor' | translate }}</mat-label> + <input matInput type="text" [value]="combinedProperties.highlightReceivingDropListColor" + (input)="updateModel.emit({ + property: 'highlightReceivingDropListColor', + value: $any($event.target).value })"> + </mat-form-field> + <mat-form-field *ngIf="combinedProperties.firstColumnSizeRatio != null" + matTooltip="{{'propertiesPanel.firstColumnSizeRatioExplanation' | translate }}" + appearance="fill" class="mdInput textsingleline"> + <mat-label>{{'propertiesPanel.firstColumnSizeRatio' | translate }}</mat-label> + <input matInput type="number" [value]="combinedProperties.firstColumnSizeRatio" + (input)="updateModel.emit({ property: 'firstColumnSizeRatio', value: $any($event.target).value })"> + </mat-form-field> + </div> + `, + styleUrls: ['./element-model-properties.component.css'] +}) +export class ElementModelPropertiesComponentComponent { + @Input() combinedProperties: UIElement = {} as UIElement; + @Input() selectedElements: UIElement[] = []; + @Output() updateModel = new EventEmitter<{ + property: string; + value: InputElementValue | LikertColumn[] | LikertRow[], + isInputValid?: boolean | null + }>(); + + constructor(public unitService: UnitService) { } + + addOption(property: string, value: string): void { + (this.combinedProperties[property] as string[]).push(value); + this.updateModel.emit({ property: property, value: this.combinedProperties[property] as string[] }); + } + + reorderOptions(property: string, event: CdkDragDrop<string[]>): void { + moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); + this.updateModel.emit({ property: property, value: event.container.data }); + } + + /* Putting the actual types for option does not work because indexOf throws an error + about the types not being assignable. */ + removeOption(property: string, option: any): void { + const valueList = this.combinedProperties[property] as string[] | LikertElementRow[] | LikertColumn[]; + valueList.splice(valueList.indexOf(option), 1); + this.updateModel.emit({ property: property, value: valueList }); + } + + async editTextOption(property: string, optionIndex: number): Promise<void> { + await this.unitService.editTextOption(property, optionIndex); + } + + async editColumnOption(optionIndex: number): Promise<void> { + await this.unitService.editLikertColumn(this.selectedElements as LikertElement[], optionIndex); + } + + async editRowOption(optionIndex: number): Promise<void> { + await this.unitService.editLikertRow( + (this.combinedProperties.rows as LikertElementRow[])[optionIndex] as LikertElementRow, + this.combinedProperties.columns as LikertColumn[] + ); + } + + addColumn(value: string): void { + const column = UnitService.createLikertColumn(value); + (this.combinedProperties.columns as LikertColumn[]).push(column); + this.updateModel.emit({ property: 'columns', value: this.combinedProperties.columns as LikertColumn[] }); + } + + addRow(question: string): void { + const newRow = UnitService.createLikertRow( + question, + (this.combinedProperties.columns as LikertColumn[]).length + ); + (this.combinedProperties.rows as LikertElementRow[]).push(newRow); + this.updateModel.emit({ property: 'rows', value: this.combinedProperties.rows as LikertElementRow[] }); + } + + async loadImage(): Promise<void> { + this.updateModel.emit({ property: 'imageSrc', value: await FileService.loadImage() }); + } + + removeImage(): void { + this.updateModel.emit({ property: 'imageSrc', value: null }); + } +} diff --git a/projects/editor/src/assets/i18n/de.json b/projects/editor/src/assets/i18n/de.json index 54a515a64eaf40548b4a36dd5a6de5e997ba32a3..207daba3b24d7f156d20fe3e4405c449705f5940 100644 --- a/projects/editor/src/assets/i18n/de.json +++ b/projects/editor/src/assets/i18n/de.json @@ -93,8 +93,13 @@ "appearance": "Aussehen", "minLength": "Minimallänge", + "minValue": "Minimalwert", "minLengthWarnMessage": "Minimalwert Warnmeldung", "maxLength": "Maximalwert", + "maxValue": "Maximalwert", + "showValues": "Zeige Start- und Endwert", + "barStyle": "Balken statt Kreis", + "thumbLabel": "Zeige gewählten Wert", "maxLengthWarnMessage": "Maximalwert Warnmeldung", "pattern": "Muster", "patternWarnMessage": "Muster Warnmeldung", @@ -121,7 +126,8 @@ "firstColumnSizeRatioExplanation": "Größenverhältnis der ersten Spalte zu allen anderen", "duplicateElement": "Element duplizieren", "deleteElement": "Element löschen", - "noElementSelected": "Kein Element ausgewählt" + "noElementSelected": "Kein Element ausgewählt", + "spellCorrectButtonLabel": "Wort zum Korrigieren" }, "player": { "autoStart": "Autostart", diff --git a/projects/player/src/app/app.component.ts b/projects/player/src/app/app.component.ts index 895a91e2b362e368c00e05de2e21ae6cf1e91a10..6215c291c33e9f6ca9d4c25768c086ef44b16b1d 100644 --- a/projects/player/src/app/app.component.ts +++ b/projects/player/src/app/app.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; +import { registerLocaleData } from '@angular/common'; +import localeDe from '@angular/common/locales/de'; import { Unit } from '../../../common/models/unit'; @@ -42,6 +44,7 @@ export class AppComponent implements OnInit { this.veronaPostService.sendVopReadyNotification(this.metaDataService.playerMetadata); this.translateService.addLangs(['de']); this.translateService.setDefaultLang('de'); + registerLocaleData(localeDe); } private initSubscriptions(): void {