diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.scss b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.scss index 008f137b3cf9..4c084c80d989 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.scss +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.scss @@ -88,6 +88,10 @@ .markupEditorArea { margin-bottom: 14px; + + .markdown-editor { + border: 1px solid var(--border-color); + } } .mapping-numbers-wrapper { diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-question-list-edit.component.ts b/src/main/webapp/app/exercises/quiz/manage/quiz-question-list-edit.component.ts index bf0754da46d0..3d3da811b347 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-question-list-edit.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-question-list-edit.component.ts @@ -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'; diff --git a/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.html b/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.html index 3fd0ce91a5b3..8372aea83c43 100644 --- a/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.html @@ -277,24 +277,18 @@

- @if (!reEvaluationInProgress) { -
-
-
-
-
-
- } @if (!reEvaluationInProgress) {
-
} diff --git a/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.ts b/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.ts index 6e99de7c0fb8..538966d0c15f 100644 --- a/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/short-answer-question/short-answer-question-edit.component.ts @@ -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'; @@ -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', @@ -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() @@ -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 **/ @@ -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[] = []; @@ -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); @@ -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(); @@ -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); }); } @@ -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; } @@ -354,36 +356,17 @@ 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 }); } /** @@ -391,21 +374,7 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte * @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(); } /** @@ -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); @@ -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); @@ -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(); } @@ -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(); } @@ -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()); } /** @@ -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(); } @@ -786,6 +751,6 @@ export class ShortAnswerQuestionEditComponent implements OnInit, OnChanges, Afte } setQuestionEditorValue(text: string): void { - this.questionEditor.getEditor().setValue(text); + this.questionEditor.markdown = text; } } diff --git a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html index 27cd1906c609..075014915cf8 100644 --- a/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html +++ b/src/main/webapp/app/shared/markdown-editor/monaco/markdown-editor-monaco.component.html @@ -2,14 +2,15 @@