Skip to content

Commit

Permalink
Quiz exercises: Change the short answer quiz question editor to Monaco (
Browse files Browse the repository at this point in the history
  • Loading branch information
pzdr7 authored and JohannesWt committed Sep 23, 2024
1 parent ecb3f32 commit a387bd5
Show file tree
Hide file tree
Showing 17 changed files with 463 additions and 131 deletions.
4 changes: 4 additions & 0 deletions src/main/webapp/app/exercises/quiz/manage/quiz-exercise.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@

.markupEditorArea {
margin-bottom: 14px;

.markdown-editor {
border: 1px solid var(--border-color);
}
}

.mapping-numbers-wrapper {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export class QuizQuestionListEditComponent {
'Enter your long question if needed\n\n' +
'Select a part of the text and click on Add Spot to automatically create an input field and the corresponding mapping\n\n' +
'You can define a input field like this: This [-spot 1] an [-spot 2] field.\n\n' +
'To define the solution for the input fields you need to create a mapping (multiple mapping also possible):\n\n' +
'To define the solution for the input fields you need to create a mapping (multiple mapping also possible):\n\n\n' +
'[-option 1] is\n' +
'[-option 2] input\n' +
'[-option 1,2] correctInBothFields';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,24 +277,18 @@ <h3 class="mb-0"><span class="badge bg-success align-text-top" style="width: 60p
</div>
}
<div class="markupEditorArea" [hidden]="showVisualMode">
@if (!reEvaluationInProgress) {
<div class="toolbar">
<div class="btn-group">
<div class="btn btn-outline-secondary" jhiTranslate="artemisApp.shortAnswerQuestion.editor.addSpot" (click)="addSpotAtCursor()"></div>
<div class="btn btn-outline-secondary" jhiTranslate="artemisApp.shortAnswerQuestion.editor.addOption" (click)="addOption()"></div>
</div>
</div>
}
@if (!reEvaluationInProgress) {
<div class="question-content">
<jhi-ace-editor
<jhi-markdown-editor-monaco
#questionEditor
[(text)]="questionEditorText"
[mode]="questionEditorMode"
[autoUpdateContent]="questionEditorAutoUpdate"
(textChange)="onTextChange($event)"
style="min-height: 200px; width: 100%; overflow: auto"
class="form-control"
[enableResize]="false"
[enableFileUpload]="false"
[showPreviewButton]="false"
[defaultActions]="markdownActions"
[colorAction]="undefined"
[initialEditorHeight]="'external'"
[(markdown)]="questionEditorText"
(markdownChange)="onTextChange($event)"
/>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ import {
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { ArtemisMarkdownService } from 'app/shared/markdown.service';
import { AceEditorComponent } from 'app/shared/markdown-editor/ace-editor/ace-editor.component';
import 'brace/theme/chrome';
import 'brace/mode/markdown';
import { ShortAnswerQuestionUtil } from 'app/exercises/quiz/shared/short-answer-question-util.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ShortAnswerQuestion } from 'app/entities/quiz/short-answer-question.model';
Expand All @@ -29,6 +25,19 @@ import { markdownForHtml } from 'app/shared/util/markdown.conversion.util';
import { generateExerciseHintExplanation, parseExerciseHintExplanation } from 'app/shared/util/markdown.util';
import { faAngleDown, faAngleRight, faBan, faBars, faChevronDown, faChevronUp, faTrash, faUndo, faUnlink } from '@fortawesome/free-solid-svg-icons';
import { MAX_QUIZ_QUESTION_POINTS, MAX_QUIZ_SHORT_ANSWER_TEXT_LENGTH } from 'app/shared/constants/input.constants';
import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component';
import { MarkdownEditorHeight } from 'app/shared/markdown-editor/markdown-editor.component';
import { MonacoBoldAction } from 'app/shared/monaco-editor/model/actions/monaco-bold.action';
import { MonacoItalicAction } from 'app/shared/monaco-editor/model/actions/monaco-italic.action';
import { MonacoUnderlineAction } from 'app/shared/monaco-editor/model/actions/monaco-underline.action';
import { MonacoCodeAction } from 'app/shared/monaco-editor/model/actions/monaco-code.action';
import { MonacoUrlAction } from 'app/shared/monaco-editor/model/actions/monaco-url.action';
import { MonacoUnorderedListAction } from 'app/shared/monaco-editor/model/actions/monaco-unordered-list.action';
import { MonacoOrderedListAction } from 'app/shared/monaco-editor/model/actions/monaco-ordered-list.action';
import { MonacoInsertShortAnswerSpotAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-insert-short-answer-spot.action';
import { MonacoEditorAction } from 'app/shared/monaco-editor/model/actions/monaco-editor-action.model';
import { MonacoInsertShortAnswerOptionAction } from 'app/shared/monaco-editor/model/actions/quiz/monaco-insert-short-answer-option.action';
import { SHORT_ANSWER_QUIZ_QUESTION_EDITOR_OPTIONS } from 'app/shared/monaco-editor/monaco-editor-option.helper';

@Component({
selector: 'jhi-short-answer-question-edit',
Expand All @@ -38,12 +47,16 @@ import { MAX_QUIZ_QUESTION_POINTS, MAX_QUIZ_SHORT_ANSWER_TEXT_LENGTH } from 'app
})
export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, AfterViewInit, QuizQuestionEdit {
@ViewChild('questionEditor', { static: false })
private questionEditor: AceEditorComponent;
private questionEditor: MarkdownEditorMonacoComponent;
@ViewChild('clickLayer', { static: false })
private clickLayer: ElementRef;
@ViewChild('question', { static: false })
questionElement: ElementRef;

markdownActions: MonacoEditorAction[];
insertShortAnswerOptionAction = new MonacoInsertShortAnswerOptionAction();
insertShortAnswerSpotAction = new MonacoInsertShortAnswerSpotAction(this.insertShortAnswerOptionAction);

shortAnswerQuestion: ShortAnswerQuestion;

@Input()
Expand All @@ -68,10 +81,7 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte

readonly MAX_CHARACTER_COUNT = MAX_QUIZ_SHORT_ANSWER_TEXT_LENGTH;

/** Ace Editor configuration constants **/
questionEditorText: any = '';
questionEditorMode = 'markdown';
questionEditorAutoUpdate = true;
questionEditorText = '';
showVisualMode: boolean;

/** Status boolean for collapse status **/
Expand All @@ -80,8 +90,6 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
/** Variables needed for the setup of editorText **/
// equals the highest spotNr
numberOfSpot = 1;
// defines the first gap between text and solutions when
firstPressed = 1;
// has all solution options with their mapping (each spotNr)
optionsWithID: string[] = [];

Expand All @@ -101,16 +109,28 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
faAngleRight = faAngleRight;
faAngleDown = faAngleDown;

readonly MAX_POINTS = MAX_QUIZ_QUESTION_POINTS;
protected readonly MAX_POINTS = MAX_QUIZ_QUESTION_POINTS;
protected readonly MarkdownEditorHeight = MarkdownEditorHeight;

constructor(
private artemisMarkdown: ArtemisMarkdownService,
public shortAnswerQuestionUtil: ShortAnswerQuestionUtil,
private modalService: NgbModal,
private changeDetector: ChangeDetectorRef,
) {}

ngOnInit(): void {
this.markdownActions = [
new MonacoBoldAction(),
new MonacoItalicAction(),
new MonacoUnderlineAction(),
new MonacoCodeAction(),
new MonacoUrlAction(),
new MonacoUnorderedListAction(),
new MonacoOrderedListAction(),
this.insertShortAnswerSpotAction,
this.insertShortAnswerOptionAction,
];

// create deepcopy
this.backupQuestion = cloneDeep(this.shortAnswerQuestion);

Expand Down Expand Up @@ -185,28 +205,9 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
setupQuestionEditor(): void {
// Sets the counter to the highest spotNr and generates solution options with their mapping (each spotNr)
this.numberOfSpot = this.shortAnswerQuestion.spots!.length + 1;

// Default editor settings for inline markup editor
this.questionEditor.getEditor().renderer.setShowGutter(false);
this.questionEditor.getEditor().renderer.setPadding(10);
this.questionEditor.getEditor().renderer.setScrollMargin(8, 8);
this.questionEditor.getEditor().setHighlightActiveLine(false);
this.questionEditor.getEditor().setShowPrintMargin(false);

this.questionEditor.applyOptionPreset(SHORT_ANSWER_QUIZ_QUESTION_EDITOR_OPTIONS);
// Generate markdown from question and show result in editor
this.questionEditorText = this.generateMarkdown();
this.questionEditor.getEditor().clearSelection();

// Register the onBlur listener
this.questionEditor.getEditor().on(
'blur',
() => {
// Parse the markdown in the editor and update question accordingly
this.parseMarkdown(this.questionEditorText);
this.questionUpdated.emit();
},
this,
);
this.changeDetector.detectChanges();
this.parseMarkdown(this.questionEditorText);
this.questionUpdated.emit();
Expand All @@ -233,7 +234,7 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
option += ',' + this.shortAnswerQuestion.spots?.filter((spot) => this.shortAnswerQuestionUtil.isSameSpot(spot, spotForSolution))[0].spotNr;
}
});
option += ']';
option += option === '[-option ' ? '#]' : ']';
this.optionsWithID.push(option);
});
}
Expand All @@ -247,10 +248,11 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
*/
generateMarkdown(): string {
this.setOptionsWithID();
const markdownText =
generateExerciseHintExplanation(this.shortAnswerQuestion) +
'\n\n\n' +
this.shortAnswerQuestion.solutions?.map((solution, index) => this.optionsWithID[index] + ' ' + solution.text!.trim()).join('\n');
let markdownText = generateExerciseHintExplanation(this.shortAnswerQuestion);

if (this.shortAnswerQuestion.solutions?.length) {
markdownText += '\n\n\n' + this.shortAnswerQuestion.solutions.map((solution, index) => this.optionsWithID[index] + ' ' + solution.text!.trim()).join('\n');
}
return markdownText;
}

Expand Down Expand Up @@ -354,58 +356,25 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
* an option connected to the spot below the last visible row
*/
addSpotAtCursor(): void {
const editor = this.questionEditor.getEditor();
const optionText = editor.getCopyText();
const currentSpotNumber = this.numberOfSpot;
const addedText = '[-spot ' + currentSpotNumber + ']';
editor.focus();
editor.insert(addedText);
editor.moveCursorTo(editor.getLastVisibleRow() + this.numberOfSpot, Number.POSITIVE_INFINITY);
this.addOptionToSpot(editor, currentSpotNumber, optionText, this.firstPressed);

this.firstPressed++;
this.insertShortAnswerSpotAction.executeInCurrentEditor({ spotNumber: this.numberOfSpot });
}

/**
* add the markdown for a solution option below the last visible row, which is connected to a spot in the given editor
*
* @param editor {object} the editor into which the solution option markdown will be inserted
* @param numberOfSpot
* @param optionText
* @param firstPressed
*/
addOptionToSpot(editor: any, numberOfSpot: number, optionText: string, firstPressed: number) {
let addedText: string;
if (numberOfSpot === 1 && firstPressed === 1) {
addedText = '\n\n\n[-option ' + numberOfSpot + '] ' + optionText;
} else {
addedText = '\n[-option ' + numberOfSpot + '] ' + optionText;
}
editor.focus();
editor.clearSelection();
editor.insert(addedText);
* @param numberOfSpot the number of the spot to which the option should be connected
* @param optionText the text of the option
*/
addOptionToSpot(numberOfSpot: number, optionText: string) {
this.insertShortAnswerOptionAction.executeInCurrentEditor({ spotNumber: numberOfSpot, optionText });
}

/**
* @function addOption
* @desc Add the markdown for a solution option below the last visible row
*/
addOption(): void {
const editor = this.questionEditor.getEditor();
let addedText: string;
if (this.firstPressed === 1) {
addedText = '\n\n\n[-option #] Please enter here one answer option and do not forget to replace # with a number';
} else {
addedText = '\n[-option #] Please enter here one answer option and do not forget to replace # with a number';
}
editor.clearSelection();
editor.moveCursorTo(editor.getLastVisibleRow(), Number.POSITIVE_INFINITY);
editor.insert(addedText);
const range = editor.selection.getRange();
range.setStart(range.start.row, 12);
editor.selection.setRange(range);

this.firstPressed++;
this.insertShortAnswerOptionAction.executeInCurrentEditor();
}

/**
Expand All @@ -426,7 +395,6 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
return;
}

const editor = this.questionEditor.getEditor();
// ID 'element-row-column' is divided into array of [row, column]
const selectedTextRowColumn = selection.focusNode!.parentNode!.parentElement!.id.split('-').slice(1);

Expand Down Expand Up @@ -458,8 +426,8 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
const currentSpotNumber = this.numberOfSpot;

// split text before first option tag
const questionText = editor
.getValue()
const questionText = this.questionEditor.monacoEditor
.getText()
.split(/\[-option /g)[0]
.trim();
this.textParts = this.shortAnswerQuestionUtil.divideQuestionTextIntoTextParts(questionText);
Expand All @@ -470,12 +438,9 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
this.shortAnswerQuestion.text = this.textParts.map((textPart) => textPart.join(' ')).join('\n');
const textParts = this.shortAnswerQuestionUtil.divideQuestionTextIntoTextParts(this.shortAnswerQuestion.text);
this.textParts = this.shortAnswerQuestionUtil.transformTextPartsIntoHTML(textParts);
editor.setValue(this.generateMarkdown());
editor.moveCursorTo(editor.getLastVisibleRow() + currentSpotNumber, Number.POSITIVE_INFINITY);
this.addOptionToSpot(editor, currentSpotNumber, markedText, this.firstPressed);
this.parseMarkdown(editor.getValue());

this.firstPressed++;
this.setQuestionEditorValue(this.generateMarkdown());
this.addOptionToSpot(currentSpotNumber, markedText);
this.parseMarkdown(this.questionEditor.monacoEditor.getText());

this.questionUpdated.emit();
}
Expand All @@ -490,9 +455,8 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
this.shortAnswerQuestion.solutions = [];
}
const solution = new ShortAnswerSolution();
solution.text = 'Please enter here your text';
this.shortAnswerQuestion.solutions.push(solution);
this.questionEditorText = this.generateMarkdown();
solution.text = MonacoInsertShortAnswerOptionAction.DEFAULT_TEXT_SHORT;
this.insertShortAnswerOptionAction.executeInCurrentEditor({ optionText: solution.text });
this.questionUpdated.emit();
}

Expand Down Expand Up @@ -636,8 +600,7 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
const textParts = this.shortAnswerQuestionUtil.divideQuestionTextIntoTextParts(this.shortAnswerQuestion.text!);
this.textParts = this.shortAnswerQuestionUtil.transformTextPartsIntoHTML(textParts);

this.questionEditor.getEditor().setValue(this.generateMarkdown());
this.questionEditor.getEditor().clearSelection();
this.setQuestionEditorValue(this.generateMarkdown());
}

/**
Expand Down Expand Up @@ -767,7 +730,9 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
}

onTextChange(newText: string) {
this.parseMarkdown(this.questionEditorText);
this.numberOfSpot = this.getHighestSpotNumbers(newText) + 1;
this.insertShortAnswerSpotAction.spotNumber = this.numberOfSpot;
this.questionUpdated.emit();
}

Expand All @@ -786,6 +751,6 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte
}

setQuestionEditorValue(text: string): void {
this.questionEditor.getEditor().setValue(text);
this.questionEditor.markdown = text;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
<div
#wrapper
class="markdown-editor-wrapper"
[style.overflow]="inPreviewMode ? 'auto' : initialEditorHeight === 'external' ? 'hidden' : 'visible'"
[style.overflow]="inPreviewMode ? 'auto' : 'visible'"
[style.height.px]="inPreviewMode ? undefined : targetWrapperHeight"
[style.cursor]="isResizing ? 'ns-resize' : undefined"
>
<nav ngbNav #actionPalette #nav="ngbNav" class="nav-tabs" [destroyOnHide]="false" (navChange)="onNavChanged($event)">
<!-- TODO if show edit -->
<ng-container ngbNavItem="editor_edit">
<a ngbNavLink class="btn-sm text-normal px-2 py-0 m-0"><span jhiTranslate="entity.action.edit"></span></a>
@if (showEditButton) {
<a ngbNavLink class="btn-sm text-normal px-2 py-0 m-0"><span jhiTranslate="entity.action.edit"></span></a>
}
<ng-template ngbNavContent>
<div class="markdown-editor markdown-editor-wrapper flex-column" [ngClass]="{ 'd-flex': !inPreviewMode, 'd-none': inPreviewMode }">
<jhi-monaco-editor
Expand Down Expand Up @@ -48,7 +49,9 @@
</ng-template>
</ng-container>
<ng-container ngbNavItem="editor_preview">
<a ngbNavLink class="btn-sm text-normal px-2 py-0 m-0"><span jhiTranslate="entity.action.preview"></span></a>
@if (showPreviewButton) {
<a ngbNavLink class="btn-sm text-normal px-2 py-0 m-0"><span jhiTranslate="entity.action.preview"></span></a>
}
<ng-template ngbNavContent>
<ng-content class="overflow-auto" select="[#previewMonaco]" />
<!-- TODO preview handling for other exercise types -->
Expand Down
Loading

0 comments on commit a387bd5

Please sign in to comment.