-
Notifications
You must be signed in to change notification settings - Fork 301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Communication
: Add additional input formatting options
#9657
Changes from all commits
1f903b6
9f3a180
c702918
48d7fc1
0601ea6
7637a83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,4 +1,4 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
import { Component, EventEmitter, Input, Output } from '@angular/core'; | ||||||||||||||||||||||||||||||||||||||||||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||||||||||||||||||||||||||||||||||||||||||||
import { PostingContentPart, ReferenceType } from '../../metis.util'; | ||||||||||||||||||||||||||||||||||||||||||||||
import { FileService } from 'app/shared/http/file.service'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -26,7 +26,7 @@ import { AccountService } from 'app/core/auth/account.service'; | |||||||||||||||||||||||||||||||||||||||||||||
templateUrl: './posting-content-part.component.html', | ||||||||||||||||||||||||||||||||||||||||||||||
styleUrls: ['./../../metis.component.scss'], | ||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||
export class PostingContentPartComponent { | ||||||||||||||||||||||||||||||||||||||||||||||
export class PostingContentPartComponent implements OnInit { | ||||||||||||||||||||||||||||||||||||||||||||||
@Input() postingContentPart: PostingContentPart; | ||||||||||||||||||||||||||||||||||||||||||||||
@Output() userReferenceClicked = new EventEmitter<string>(); | ||||||||||||||||||||||||||||||||||||||||||||||
@Output() channelReferenceClicked = new EventEmitter<number>(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -35,7 +35,7 @@ export class PostingContentPartComponent { | |||||||||||||||||||||||||||||||||||||||||||||
hasClickedUserReference = false; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
// Only allow certain html tags and attributes | ||||||||||||||||||||||||||||||||||||||||||||||
allowedHtmlTags: string[] = ['a', 'b', 'br', 'blockquote', 'code', 'del', 'em', 'i', 'ins', 'li', 'mark', 'ol', 'p', 'pre', 'small', 'span', 'strong', 'sub', 'sup', 'ul']; | ||||||||||||||||||||||||||||||||||||||||||||||
allowedHtmlTags: string[] = ['a', 'b', 'br', 'blockquote', 'code', 'del', 'em', 'i', 'ins', 'mark', 'p', 'pre', 'small', 's', 'span', 'strong', 'sub', 'sup']; | ||||||||||||||||||||||||||||||||||||||||||||||
allowedHtmlAttributes: string[] = ['href']; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
// icons | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -45,13 +45,19 @@ export class PostingContentPartComponent { | |||||||||||||||||||||||||||||||||||||||||||||
protected readonly faHashtag = faHashtag; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
protected readonly ReferenceType = ReferenceType; | ||||||||||||||||||||||||||||||||||||||||||||||
processedContentBeforeReference: string; | ||||||||||||||||||||||||||||||||||||||||||||||
processedContentAfterReference: string; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
constructor( | ||||||||||||||||||||||||||||||||||||||||||||||
private fileService: FileService, | ||||||||||||||||||||||||||||||||||||||||||||||
private dialog: MatDialog, | ||||||||||||||||||||||||||||||||||||||||||||||
private accountService: AccountService, | ||||||||||||||||||||||||||||||||||||||||||||||
) {} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
ngOnInit() { | ||||||||||||||||||||||||||||||||||||||||||||||
this.processContent(); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||
* Opens an attachment with the given URL in a new window | ||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -65,6 +71,20 @@ export class PostingContentPartComponent { | |||||||||||||||||||||||||||||||||||||||||||||
this.imageNotFound = true; | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
processContent() { | ||||||||||||||||||||||||||||||||||||||||||||||
if (this.postingContentPart.contentBeforeReference) { | ||||||||||||||||||||||||||||||||||||||||||||||
this.processedContentBeforeReference = this.escapeNumberedList(this.postingContentPart.contentBeforeReference); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
if (this.postingContentPart.contentAfterReference) { | ||||||||||||||||||||||||||||||||||||||||||||||
this.processedContentAfterReference = this.escapeNumberedList(this.postingContentPart.contentAfterReference); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+74
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling in processContent method. The method should handle cases where postingContentPart is undefined or null to prevent runtime errors. processContent() {
+ if (!this.postingContentPart) {
+ return;
+ }
+
if (this.postingContentPart.contentBeforeReference) {
this.processedContentBeforeReference = this.escapeNumberedList(this.postingContentPart.contentBeforeReference);
}
if (this.postingContentPart.contentAfterReference) {
this.processedContentAfterReference = this.escapeNumberedList(this.postingContentPart.contentAfterReference);
}
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
escapeNumberedList(content: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||
return content.replace(/^(\s*\d+)\. /gm, '$1\\. '); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||
* Opens a dialog to display the image in full size | ||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { faListUl } from '@fortawesome/free-solid-svg-icons'; | ||
import { ListAction } from './list.action'; | ||
|
||
const BULLET_PREFIX = '• '; | ||
|
||
/** | ||
* Action used to add or modify a bullet-point list in the text editor. | ||
*/ | ||
export class BulletedListAction extends ListAction { | ||
static readonly ID = 'bulletedList.action'; | ||
|
||
protected readonly PREFIX = BULLET_PREFIX; | ||
|
||
constructor() { | ||
super(BulletedListAction.ID, 'artemisApp.multipleChoiceQuestion.editor.unorderedList', faListUl, undefined); | ||
} | ||
|
||
protected getPrefix(lineNumber: number): string { | ||
void lineNumber; | ||
return this.PREFIX; | ||
} | ||
Comment on lines
+18
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve handling of unused lineNumber parameter The
If the parameter is truly unused, apply this change: - protected getPrefix(lineNumber: number): string {
- void lineNumber;
- return this.PREFIX;
- }
+ protected getPrefix(): string {
+ return this.PREFIX;
+ }
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import { TextEditorAction } from 'app/shared/monaco-editor/model/actions/text-editor-action.model'; | ||
import { TextEditor } from 'app/shared/monaco-editor/model/actions/adapter/text-editor.interface'; | ||
import { TextEditorRange } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-range.model'; | ||
import { TextEditorPosition } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-position.model'; | ||
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; | ||
import { TextEditorKeybinding } from 'app/shared/monaco-editor/model/actions/adapter/text-editor-keybinding.model'; | ||
|
||
/** | ||
* Abstract class representing a list action in a text editor. | ||
* This class handles adding and removing list prefixes and supports event listeners | ||
* for features like continuing lists with Shift/Cmd+Enter. | ||
* | ||
* @abstract | ||
* @extends TextEditorAction | ||
*/ | ||
export abstract class ListAction extends TextEditorAction { | ||
protected static editorsWithListener = new WeakMap<TextEditor, boolean>(); | ||
|
||
protected abstract readonly PREFIX: string; | ||
protected abstract getPrefix(lineNumber: number): string; | ||
|
||
/** | ||
* Constructor for the ListAction class. | ||
* @param {string} id - The unique ID of the action. | ||
* @param {string} label - The label of the action. | ||
* @param {IconDefinition} icon - The icon to display for the action. | ||
* @param {TextEditorKeybinding[]} shortcut - The keyboard shortcut for the action. | ||
*/ | ||
protected constructor(id: string, label: string, icon: IconDefinition | undefined, shortcut: TextEditorKeybinding[] | undefined) { | ||
super(id, label, icon, shortcut); | ||
} | ||
|
||
/** | ||
* Removes any list prefix (either bullet or numbered) from the given line. | ||
* @param {string} line - The line to process. | ||
* @returns {string} - The line without any list prefix. | ||
*/ | ||
protected stripAnyListPrefix(line: string): string { | ||
const numberedListRegex = /^\s*\d+\.\s+/; | ||
const bulletListRegex = /^\s*[-*+•]\s+/; | ||
|
||
if (numberedListRegex.test(line)) { | ||
return line.replace(numberedListRegex, ''); | ||
} else if (bulletListRegex.test(line)) { | ||
return line.replace(bulletListRegex, ''); | ||
} | ||
Comment on lines
+39
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical Issue] Correct the regular expressions to match list prefixes accurately The regular expressions used to detect bullet list prefixes may incorrectly interpret the hyphen Apply the following diff to fix the regular expressions: In `stripAnyListPrefix` method (lines 39-46):
- const bulletListRegex = /^\s*[-*+•]\s+/;
+ const bulletListRegex = /^\s*[*+•-]\s+/;
In `hasPrefix` method (lines 132-135):
- const bulletListRegex = /^\s*[•\-*+]\s+/;
+ const bulletListRegex = /^\s*[*+•-]\s+/;
In `handleBackspace` method (lines 179-190):
- const linePrefixMatch = lineContent.match(/^\s*(\d+\.\s+|[-*+•]\s+)/);
+ const linePrefixMatch = lineContent.match(/^\s*(\d+\.\s+|[*+•-]\s+)/); Also applies to: 132-135, 179-190 |
||
return line; | ||
} | ||
|
||
/** | ||
* Adds or removes a list prefix from the selected text. | ||
* Also handles the Shift/Cmd+Enter key combination to continue the list. | ||
* @param {TextEditor} editor - The editor instance where the action is applied. | ||
*/ | ||
run(editor: TextEditor) { | ||
if (!ListAction.editorsWithListener.has(editor)) { | ||
ListAction.editorsWithListener.set(editor, true); | ||
|
||
editor.getDomNode()?.addEventListener('keydown', (event: KeyboardEvent) => { | ||
if (event.key === 'Enter' && event.shiftKey) { | ||
event.preventDefault(); | ||
this.handleShiftEnter(editor); | ||
} else if (event.key === 'Enter' && event.metaKey) { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
this.handleShiftEnter(editor); | ||
} else if (event.key === 'Backspace') { | ||
this.handleBackspace(editor, event); | ||
} | ||
}); | ||
} | ||
|
||
const selection = editor.getSelection(); | ||
if (!selection) { | ||
return; | ||
} | ||
|
||
const selectedText = editor.getTextAtRange(selection); | ||
const lines = selectedText.split('\n'); | ||
|
||
// Check if the cursor is at the end of the line to add or remove the prefix | ||
let position = editor.getPosition(); | ||
if (position) { | ||
const currentLineText = editor.getLineText(position.getLineNumber()); | ||
|
||
if (!selectedText && position.getColumn() <= currentLineText.length) { | ||
const endPosition = new TextEditorPosition(position.getLineNumber(), currentLineText.length + 1); | ||
editor.setPosition(endPosition); | ||
editor.focus(); | ||
position = endPosition; | ||
} | ||
|
||
if (position.getColumn() === currentLineText.length + 1) { | ||
const lineWithoutPrefix = this.stripAnyListPrefix(currentLineText); | ||
const newPrefix = this.getPrefix(1); | ||
|
||
const updatedLine = currentLineText.startsWith(newPrefix) ? lineWithoutPrefix : newPrefix + lineWithoutPrefix; | ||
|
||
editor.replaceTextAtRange( | ||
new TextEditorRange(new TextEditorPosition(position.getLineNumber(), 1), new TextEditorPosition(position.getLineNumber(), currentLineText.length + 1)), | ||
updatedLine, | ||
); | ||
editor.focus(); | ||
return; | ||
} | ||
} | ||
|
||
const startLineNumber = selection.getStartPosition().getLineNumber(); | ||
const currentPrefix = this.getPrefix(startLineNumber); | ||
|
||
// Determine if all lines have the current prefix | ||
let allLinesHaveCurrentPrefix; | ||
if (this.getPrefix(1) != '• ') { | ||
const numberedListRegex = /^\s*\d+\.\s+/; | ||
allLinesHaveCurrentPrefix = lines.every((line) => numberedListRegex.test(line)); | ||
} else { | ||
allLinesHaveCurrentPrefix = lines.every((line) => line.startsWith(currentPrefix)); | ||
} | ||
|
||
let updatedLines: string[]; | ||
if (allLinesHaveCurrentPrefix) { | ||
updatedLines = lines.map((line) => this.stripAnyListPrefix(line)); | ||
} else { | ||
const linesWithoutPrefix = lines.map((line) => this.stripAnyListPrefix(line)); | ||
|
||
updatedLines = linesWithoutPrefix.map((line, index) => { | ||
const prefix = this.getPrefix(index) != '• ' ? this.getPrefix(index + 1) : this.getPrefix(startLineNumber + index); | ||
return prefix + line; | ||
}); | ||
} | ||
|
||
const updatedText = updatedLines.join('\n'); | ||
editor.replaceTextAtRange(selection, updatedText); | ||
editor.focus(); | ||
} | ||
|
||
/** | ||
* Checks if a line has any list prefix (either bullet or numbered). | ||
* @param {string} line - The line to check. | ||
* @returns {boolean} - True if the line has a prefix, false otherwise. | ||
*/ | ||
protected hasPrefix(line: string): boolean { | ||
const numberedListRegex = /^\s*\d+\.\s+/; | ||
const bulletListRegex = /^\s*[•\-*+]\s+/; | ||
return numberedListRegex.test(line) || bulletListRegex.test(line); | ||
} | ||
|
||
/** | ||
* Handles the Shift+Enter key combination to continue the list. | ||
* @param {TextEditor} editor - The editor instance. | ||
*/ | ||
protected handleShiftEnter(editor: TextEditor) { | ||
const position = editor.getPosition(); | ||
if (position) { | ||
const currentLineText = editor.getLineText(position.getLineNumber()); | ||
let nextLinePrefix = ''; | ||
|
||
// Check if the current line starts with a prefix and continue the list | ||
if (this.hasPrefix(currentLineText)) { | ||
const isNumbered = /^\s*\d+\.\s+/.test(currentLineText); | ||
|
||
if (isNumbered) { | ||
const match = currentLineText.match(/^\s*(\d+)\.\s+/); | ||
const currentNumber = match ? parseInt(match[1], 10) : 0; | ||
nextLinePrefix = `${currentNumber + 1}. `; | ||
} else { | ||
nextLinePrefix = '• '; | ||
} | ||
} | ||
|
||
editor.replaceTextAtRange(new TextEditorRange(position, position), '\n' + nextLinePrefix); | ||
|
||
const newLineNumber = position.getLineNumber() + 1; | ||
const newColumnPosition = nextLinePrefix ? nextLinePrefix.length + 1 : 1; | ||
editor.setPosition(new TextEditorPosition(newLineNumber, newColumnPosition)); | ||
editor.focus(); | ||
} | ||
} | ||
|
||
/** | ||
* Handles the Backspace key press to remove a prefix if the cursor is just after it. | ||
* @param {TextEditor} editor - The editor instance. | ||
* @param {KeyboardEvent} event - The keyboard event. | ||
*/ | ||
protected handleBackspace(editor: TextEditor, event: KeyboardEvent) { | ||
const position = editor.getPosition(); | ||
if (position) { | ||
const lineNumber = position.getLineNumber(); | ||
const lineContent = editor.getLineText(lineNumber); | ||
const linePrefixMatch = lineContent.match(/^\s*(\d+\.\s+|[-*+•]\s+)/); | ||
|
||
if (linePrefixMatch) { | ||
const prefixLength = linePrefixMatch[0].length; | ||
// Check if the cursor is just after the prefix | ||
if (position.getColumn() === prefixLength + 1) { | ||
event.preventDefault(); | ||
editor.replaceTextAtRange(new TextEditorRange(new TextEditorPosition(lineNumber, 1), new TextEditorPosition(lineNumber, prefixLength + 1)), ' '); | ||
editor.focus(); | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codebase verification
Remaining references to UnorderedListAction need to be updated
src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts
: Tests are still using UnorderedListActionsrc/main/webapp/app/shared/monaco-editor/model/actions/unordered-list.action.ts
: The old action file still exists and needs to be removedThe codebase still contains references to the old UnorderedListAction in test files and the original action file. These need to be updated to use BulletedListAction for consistency.
🔗 Analysis chain
Verify the replacement of UnorderedListAction with BulletedListAction.
The changes look good, but let's verify that all existing usages of UnorderedListAction have been updated to use BulletedListAction to prevent any broken functionality.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
Length of output: 20864