diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts index c863276def1b5c16a7e8fd36780899790cb8b5e9..019bd19aa3caac239887caf70167bb84dada2eb7 100644 --- a/projects/editor/src/app/app.module.ts +++ b/projects/editor/src/app/app.module.ts @@ -100,9 +100,6 @@ import { ElementStylePropertiesComponent } from './components/properties-panel/style-properties-tab/element-style-properties.component'; import { ElementModelPropertiesComponent, IsInputElementPipe } from './components/properties-panel/model-properties-tab/element-model-properties.component'; -import { DynamicSectionHelperGridComponent } from 'editor/src/app/components/unit-view/section/dynamic-section-helper-grid.component'; -import { ElementGridChangeListenerDirective } from 'editor/src/app/components/unit-view/section/element-grid-change-listener.directive'; - import { OptionsFieldSetComponent } from './components/properties-panel/model-properties-tab/input-groups/options-field-set.component'; import { SelectPropertiesComponent } from @@ -129,6 +126,10 @@ import { ReferenceListComponent } from 'editor/src/app/components/reference-list import { ElementListComponent } from 'editor/src/app/components/element-list.component'; import { MeasurePipe } from 'common/pipes/measure.pipe'; import { SectionComponent } from 'editor/src/app/components/unit-view/section/section.component'; +import { RadioWizardDialogComponent } from 'editor/src/app/components/dialogs/wizards/radio.dialog.component'; +import { TextWizardDialogComponent } from 'editor/src/app/components/dialogs/wizards/text.dialog.component'; +import { LikertWizardDialogComponent } from 'editor/src/app/components/dialogs/wizards/likert.dialog.component'; +import { InputWizardDialogComponent } from 'editor/src/app/components/dialogs/wizards/input.dialog.component'; /** Custom options the configure the tooltip's default show/hide delays. */ export const myCustomTooltipDefaults: MatTooltipDefaultOptions = { @@ -191,7 +192,11 @@ export const myCustomTooltipDefaults: MatTooltipDefaultOptions = { SanitizationDialogComponent, TooltipPropertiesDialogComponent, GetValidAudioVideoIDsPipe, - InputAssistancePropertiesComponent + InputAssistancePropertiesComponent, + RadioWizardDialogComponent, + TextWizardDialogComponent, + LikertWizardDialogComponent, + InputWizardDialogComponent ], imports: [ BrowserModule, diff --git a/projects/editor/src/app/components/dialogs/wizards/input.dialog.component.ts b/projects/editor/src/app/components/dialogs/wizards/input.dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..773b91b60b90a9a5f991e30fac14af6b248de54d --- /dev/null +++ b/projects/editor/src/app/components/dialogs/wizards/input.dialog.component.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'aspect-editor-input-wizard-dialog', + template: ` + <div mat-dialog-title>Antworteingabe-Assistent</div> + <div mat-dialog-content> + <h3>Text</h3> + <aspect-rich-text-editor [(content)]="text" [showReducedControls]="true" + [style.min-height.px]="200"></aspect-rich-text-editor> + + <div class="row" [style]="'justify-content: space-around;'"> + <div class="column"> + <h3>Anzahl Antwortfelder</h3> + <mat-form-field class="align-start"> + <input matInput type="number" maxlength="1" [(ngModel)]="answerCount"> + </mat-form-field> + </div> + + <div class="column"> + <h3>Nummerierung</h3> + <mat-form-field class="align-start"> + <mat-select [disabled]="answerCount < 2" [(ngModel)]="numbering"> + <mat-option [value]="'latin'">a), b), ...</mat-option> + <mat-option [value]="'decimal'">1), 2), ...</mat-option> + <mat-option [value]="'bullets'">., ., ...</mat-option> + <mat-option [value]="'none'">keine</mat-option> + </mat-select> + </mat-form-field> + </div> + </div> + + <h3>Antwortlänge</h3> + <mat-checkbox [(ngModel)]="useTextAreas" [style]="'margin-bottom: 20px;'"> + Mehrzeilige Antworten + </mat-checkbox> + + <div class="row" [style]="'justify-content: space-evenly;'"> + <div class="column"> + <h3>Länge der Antworten</h3> + <mat-form-field [style]="'width: 270px;'"> + <mat-select [(ngModel)]="fieldLength" [disabled]="useTextAreas"> + <mat-option [value]="'large'">lang (<12 Wörter)</mat-option> + <mat-option [value]="'medium'">mittel (<7 Wörter)</mat-option> + <mat-option [value]="'small'">klein (<3 Wörter)</mat-option> + <mat-option [value]="'very-small'">sehr klein (< vierstellige Zahl)</mat-option> + </mat-select> + </mat-form-field> + </div> + + <div class="column"> + <h3>Erwartete Zeichenanzahl</h3> + <mat-form-field> + <input matInput type="number" maxlength="4" + [(ngModel)]="expectedCharsCount" [disabled]="!useTextAreas"> + </mat-form-field> + </div> + </div> + + </div> + <div mat-dialog-actions> + <button mat-button + [mat-dialog-close]="{ text, answerCount, useTextAreas, numbering, fieldLength, expectedCharsCount }"> + {{ 'confirm' | translate }} + </button> + <button mat-button mat-dialog-close>{{ 'cancel' | translate }}</button> + </div> + `, + styles: ` + .mat-mdc-dialog-content {display: flex; flex-direction: column;} + .row {display: flex; flex-direction: row;} + .column {display: flex; flex-direction: column;} + .align-start {align-self: start;} + mat-button-toggle-group {align-self: center;} + ` +}) +export class InputWizardDialogComponent { + text: string = 'Testtext 1'; + answerCount: number = 1; + useTextAreas: boolean = false; + numbering: 'latin' | 'decimal' | 'bullets' | 'none' = 'latin'; + fieldLength: 'very-small' | 'small' | 'medium' | 'large' = 'large'; + expectedCharsCount: number = 136; +} diff --git a/projects/editor/src/app/components/dialogs/wizards/likert.dialog.component.ts b/projects/editor/src/app/components/dialogs/wizards/likert.dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..72eaa7e614049b427579fc21984a458a43e46f99 --- /dev/null +++ b/projects/editor/src/app/components/dialogs/wizards/likert.dialog.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'aspect-editor-likert-wizard-dialog', + template: ` + <div mat-dialog-title>Richtig/Falsch-Assistent</div> + <div mat-dialog-content> + Text + <aspect-rich-text-editor [(content)]="text1"></aspect-rich-text-editor> + + Satzanfang + <mat-form-field appearance="fill"> + <textarea matInput type="text" [(ngModel)]="text2"></textarea> + </mat-form-field> + + + </div> + <div mat-dialog-actions> + <button mat-button [mat-dialog-close]="{ }">{{'confirm' | translate }}</button> + <button mat-button mat-dialog-close>{{'cancel' | translate }}</button> + </div> + ` +}) +export class LikertWizardDialogComponent { + text1: string = 'Testtext 1'; + text2: string = 'Testtext 2'; +} diff --git a/projects/editor/src/app/components/dialogs/wizards/radio.dialog.component.ts b/projects/editor/src/app/components/dialogs/wizards/radio.dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..98df5b93d70178692efa92a4b5a0a9386acbad24 --- /dev/null +++ b/projects/editor/src/app/components/dialogs/wizards/radio.dialog.component.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { Label } from 'common/models/elements/label-interfaces'; + +@Component({ + selector: 'aspect-editor-radio-wizard-dialog', + template: ` + <div mat-dialog-title>Auswahl-Assistent</div> + <div mat-dialog-content> + <mat-label class="label1">Frage</mat-label> + <aspect-rich-text-editor class="input1" [(content)]="label1" [showReducedControls]="true"> + </aspect-rich-text-editor> + + <mat-divider class="divider1"></mat-divider> + + <mat-label class="label2">Satzanfang</mat-label> + <mat-form-field class="input2" appearance="fill"> + <textarea matInput type="text" [(ngModel)]="label2"></textarea> + </mat-form-field> + + <mat-divider class="divider2"></mat-divider> + + <mat-label class="label3">Optionen</mat-label> + <aspect-option-list-panel class="options" [textFieldLabel]="'Neue Option'" + [itemList]="options" + [localMode]="true"> + </aspect-option-list-panel> + </div> + <div mat-dialog-actions> + <button mat-button [mat-dialog-close]="{ label1, label2, options }">{{'confirm' | translate }}</button> + <button mat-button mat-dialog-close>{{'cancel' | translate }}</button> + </div> + `, + styles: ` + .mat-mdc-dialog-content {display: grid; column-gap: 30px;} + mat-label {align-self: center; font-size: larger; font-weight: bold;} + mat-divider {margin: 10px 0;} + .label1 {grid-row: 1; grid-column: 1;} + .input1 {grid-row: 1; grid-column: 2; min-height: 200px;} + .divider1 {grid-row: 2; grid-column: 1 / 3 ;} + .label2 {grid-row: 3; grid-column: 1;} + .input2 {grid-row: 3; grid-column: 2;} + .divider2 {grid-row: 4; grid-column: 1 / 3 ;} + .label3 {grid-row: 5; grid-column: 1;} + .options {grid-row: 5; grid-column: 2;} + ` +}) +export class RadioWizardDialogComponent { + label1: string = 'Hier steht die Frage der Teilaufgabe mit Multiple Choice (MC).'; + label2: string = 'Hier könnte ein Satzanfang stehen ...'; + options: Label[] = []; +} diff --git a/projects/editor/src/app/components/dialogs/wizards/text.dialog.component.ts b/projects/editor/src/app/components/dialogs/wizards/text.dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9b6fb3f182a7df9d78072f6a312434caa316d1e --- /dev/null +++ b/projects/editor/src/app/components/dialogs/wizards/text.dialog.component.ts @@ -0,0 +1,60 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'aspect-editor-text-wizard-dialog', + template: ` + <div mat-dialog-title>Assistent: Stimulus-Text</div> + <div mat-dialog-content> + <mat-label class="label1">Text</mat-label> + <aspect-rich-text-editor class="input1" [(content)]="text1"></aspect-rich-text-editor> + + <mat-divider class="divider1"></mat-divider> + + <mat-label class="label2">Markieren</mat-label> + <div class="radios"> + <mat-checkbox [(ngModel)]="highlightableYellow"> + {{'propertiesPanel.highlightableYellow' | translate }} + </mat-checkbox> + <mat-checkbox [(ngModel)]="highlightableTurquoise"> + {{'propertiesPanel.highlightableTurquoise' | translate }} + </mat-checkbox> + <mat-checkbox [(ngModel)]="highlightableOrange"> + {{'propertiesPanel.highlightableOrange' | translate }} + </mat-checkbox> + </div> + + <mat-divider class="divider2"></mat-divider> + + <mat-label class="label3">Quelle</mat-label> + <aspect-rich-text-editor class="text2" [(content)]="text2" [showReducedControls]="true"> + </aspect-rich-text-editor> + </div> + <div mat-dialog-actions> + <button mat-button + [mat-dialog-close]="{ text1, text2, highlightableOrange, highlightableTurquoise, highlightableYellow }"> + {{'confirm' | translate }} + </button> + <button mat-button mat-dialog-close>{{'cancel' | translate }}</button> + </div> + `, + styles: ` + .mat-mdc-dialog-content {display: grid; column-gap: 30px;} + mat-label {align-self: center; font-size: larger; font-weight: bold;} + mat-divider {margin: 10px 0;} + .label1 {grid-row: 1; grid-column: 1} + .input1 {grid-row: 1; grid-column: 2; min-height: 400px;} + .divider1 {grid-row: 2; grid-column: 1 / 3 ;} + .label2 {grid-row: 3; grid-column: 1} + .radios {grid-row: 3; grid-column: 2; display: flex; flex-direction: row; gap: 10px;} + .divider2 {grid-row: 4; grid-column: 1 / 3 ;} + .label3 {grid-row: 5; grid-column: 1} + .text2 {grid-row: 5; grid-column: 2; min-height: 200px;} + ` +}) +export class TextWizardDialogComponent { + text1: string = 'Lorem ipsum dolor sit amet'; + text2: string = 'Lorem ipsum dolor sit amet'; + highlightableOrange: boolean = false; + highlightableTurquoise: boolean = false; + highlightableYellow: boolean = false; +} diff --git a/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.html b/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.html index 8b39bf705defa7f8160580052f39618ae433a8e3..2ab0004043f8c6358ef4b19889fbd94f93b80bc6 100644 --- a/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.html +++ b/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.html @@ -227,9 +227,17 @@ </svg> </ng-template> <div class="template-list"> - <button mat-stroked-button (click)="applyTemplate('table')"> - <mat-icon>table_view</mat-icon> - Tabellenassistent + <button mat-stroked-button (click)="applyTemplate('text')"> + Stimulus: Text + </button> + <button mat-stroked-button (click)="applyTemplate('input')"> + Eingabe + </button> + <button mat-stroked-button (click)="applyTemplate('radio')"> + Auswahl + </button> + <button mat-stroked-button (click)="applyTemplate('likert')"> + Likert </button> </div> </mat-tab> diff --git a/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.ts b/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.ts index 3a14a65365929f15970920afd05ff2717b177161..60f4b208909863b22c1ef06342b8e6115a6ac8ae 100644 --- a/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.ts +++ b/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.ts @@ -25,6 +25,7 @@ import { TriggerElement } from 'common/models/elements/trigger/trigger'; import { TextElement } from 'common/models/elements/text/text'; import { DragNDropService } from 'editor/src/app/services/drag-n-drop.service'; import { TableElement } from 'common/models/elements/compound-elements/table/table'; +import { TemplateService } from 'editor/src/app/services/template.service'; @Component({ selector: 'aspect-ui-element-toolbox', @@ -37,6 +38,7 @@ export class UiElementToolboxComponent { constructor(private selectionService: SelectionService, public unitService: UnitService, + private templateService: TemplateService, private elementService: ElementService, protected dragNDropService: DragNDropService) { } @@ -56,7 +58,7 @@ export class UiElementToolboxComponent { } applyTemplate(templateName: string) { - this.unitService.applyTemplate(templateName); + this.templateService.applyTemplate(templateName); } protected readonly ClozeElement = ClozeElement; diff --git a/projects/editor/src/app/services/template.service.ts b/projects/editor/src/app/services/template.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..c11e4dc6f8848eaa4c15422aee3532e9df732160 --- /dev/null +++ b/projects/editor/src/app/services/template.service.ts @@ -0,0 +1,210 @@ +import { inject, Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { RadioWizardDialogComponent } from 'editor/src/app/components/dialogs/wizards/radio.dialog.component'; +import { ElementFactory } from 'common/util/element.factory'; +import { PositionProperties, PropertyGroupGenerators } from 'common/models/elements/property-group-interfaces'; +import { Section, SectionProperties } from 'common/models/section'; +import { UnitService } from 'editor/src/app/services/unit-services/unit.service'; +import { IDService } from 'editor/src/app/services/id.service'; +import { TextLabel } from 'common/models/elements/label-interfaces'; +import { PositionedUIElement, UIElement, UIElementType } from 'common/models/elements/element'; +import { TextWizardDialogComponent } from 'editor/src/app/components/dialogs/wizards/text.dialog.component'; +import { LikertWizardDialogComponent } from 'editor/src/app/components/dialogs/wizards/likert.dialog.component'; +import { InputWizardDialogComponent } from 'editor/src/app/components/dialogs/wizards/input.dialog.component'; + +@Injectable({ + providedIn: 'root' +}) +export class TemplateService { + readonly dialog = inject(MatDialog); + + constructor(private unitService: UnitService, private idService: IDService) { } + + async applyTemplate(templateName: string) { + const templateSection: Section = await this.createTemplateSection(templateName); + this.unitService.getSelectedPage().addSection(templateSection); + } + + private createTemplateSection(templateName: string): Promise<Section> { + return new Promise(resolve => { + switch (templateName) { + case 'text': + this.dialog.open(TextWizardDialogComponent, {}) + .afterClosed().subscribe((result: { + text1: string, + text2: string, + highlightableOrange: boolean, + highlightableTurquoise: boolean, + highlightableYellow: boolean + }) => { + if (result) resolve(this.createTextSection(result)); + }); + break; + case 'input': + this.dialog.open(InputWizardDialogComponent, {}) + .afterClosed().subscribe((result: { + text: string, + answerCount: number, + useTextAreas: boolean, + numbering: 'latin' | 'decimal' | 'bullets' | 'none', + fieldLength: 'very-small' | 'small' | 'medium' | 'large', + expectedCharsCount: number + }) => { + if (result) resolve(this.createInputSection(result)); + }); + break; + case 'radio': + this.dialog.open(RadioWizardDialogComponent, {}) + .afterClosed().subscribe((result: { label1: string, label2: string, options: TextLabel[] }) => { + if (result) resolve(this.createRadioSection(result)); + }); + break; + case 'likert': + this.dialog.open(LikertWizardDialogComponent, {}) + .afterClosed().subscribe(() => { + resolve(this.createLikertSection()); + }); + break; + default: + throw Error(`Template name not found: ${templateName}`); + } + }); + } + + private createTextSection(config: { text1: string, text2: string, + highlightableOrange: boolean, highlightableTurquoise: boolean, highlightableYellow: boolean }): Section { + return new Section({ + elements: [ + this.createElement( + 'text', + { gridRow: 1, gridColumn: 1, marginBottom: { value: 30, unit: 'px' } }, + { + text: config.text1, + highlightableOrange: config.highlightableOrange, + highlightableTurquoise: config.highlightableTurquoise, + highlightableYellow: config.highlightableYellow + }), + this.createElement( + 'text', + { gridRow: 2, gridColumn: 1 }, + { text: config.text2, styling: { fontSize: 16 } } + ) + ] + } as SectionProperties); + } + + private createInputSection(config: { text: string, answerCount: number, useTextAreas: boolean, + numbering: 'latin' | 'decimal' | 'bullets' | 'none', fieldLength: 'very-small' | 'small' | 'medium' | 'large', + expectedCharsCount: number }): Section { + const useNumbering = config.answerCount > 1 && config.numbering !== 'none'; + + const sectionElements: UIElement[] = [ + this.createElement( + 'text', + { + gridRow: 1, + gridColumn: 1, + gridColumnRange: useNumbering ? 2 : 1, + marginBottom: { value: config.useTextAreas ? 10 : 0, unit: 'px' } + }, + { text: config.text } + ) + ]; + + const numberingChars : string[] = TemplateService.prepareNumberingChars(config.answerCount, config.numbering); + for (let i = 0; i < config.answerCount; i++) { + if (useNumbering) { + sectionElements.push( + this.createElement( + 'text', + { gridRow: i + 2, gridColumn: 1 }, + { text: config.numbering !== 'bullets' ? `${numberingChars[i]})` : '•' } + ) + ); + } + let marginBottom = config.useTextAreas ? -6 : -25; + if (i === config.answerCount - 1) marginBottom = config.useTextAreas ? 10 : 0; + sectionElements.push( + this.createElement( + config.useTextAreas ? 'text-area' : 'text-field', + { + gridRow: i + 2, + gridColumn: useNumbering ? 2 : 1, + marginBottom: { value: marginBottom, unit: 'px' } + }, + { + dimensions: { + maxWidth: TemplateService.getWidth(config.fieldLength) + }, + ...!config.useTextAreas ? { + showSoftwareKeyboard: true, + addInputAssistanceToKeyboard: true + } : { + showSoftwareKeyboard: true, + addInputAssistanceToKeyboard: true, + hasDynamicRowCount: true, + expectedCharactersCount: config.expectedCharsCount * 1.5 || 136 + } + } + ) + ); + } + + return new Section({ + elements: sectionElements, + ...useNumbering && { autoColumnSize: false }, + ...useNumbering && { gridColumnSizes: [{ value: 25, unit: 'px' }, { value: 1, unit: 'fr' }] } + } as SectionProperties); + } + + private static prepareNumberingChars(answerCount: number, + numbering: 'latin' | 'decimal' | 'bullets' | 'none'): string[] { + const latinChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']; + switch (numbering) { + case 'latin': return latinChars.slice(0, answerCount); + case 'decimal': return Array.from(Array(answerCount).keys()).map(char => String(char + 1)); + default: throw Error(`Unexpected numbering: ${numbering}`); + } + } + + private static getWidth(length: 'very-small' | 'small' | 'medium' | 'large'): number { + switch (length) { + case 'large': return 750; + case 'medium': return 500; + case 'small': return 250; + case 'very-small': return 75; + default: throw Error(`Unexpected length: ${length}`); + } + } + + private createRadioSection(config: { label1: string, label2: string, options: TextLabel[] }): Section { + return new Section({ + elements: [ + this.createElement( + 'text', + { gridRow: 1, gridColumn: 1, marginBottom: { value: 10, unit: 'px' } }, + { text: config.label1 }), + this.createElement( + 'radio', + { gridRow: 2, gridColumn: 1, marginBottom: { value: 30, unit: 'px' } }, + { label: config.label2, options: config.options }) + ] + } as SectionProperties); + } + + private createLikertSection(): Section { + return new Section({ + // elements: [] + } as SectionProperties); + } + + private createElement(elType: UIElementType, coords: Partial<PositionProperties>, + params?: Record<string, any>): PositionedUIElement { + return ElementFactory.createElement({ + type: elType, + id: this.idService.getAndRegisterNewID(elType), + position: PropertyGroupGenerators.generatePositionProps(coords), + ...params + }) as PositionedUIElement; + } +} diff --git a/projects/editor/src/app/services/unit-services/unit.service.ts b/projects/editor/src/app/services/unit-services/unit.service.ts index f2baae27ec2f2d1bd53a57426032b4f42378dffb..33537ebfff49ca5ba6f2df7cc7e54c6e4f0fa3b4 100644 --- a/projects/editor/src/app/services/unit-services/unit.service.ts +++ b/projects/editor/src/app/services/unit-services/unit.service.ts @@ -208,10 +208,6 @@ export class UnitService { }); } - applyTemplate(templateName: string) { - // TODO - } - updateSectionCounter(): void { SectionCounter.reset(); // Wait for the change to propagate through the components