diff --git a/frontend/src/app/shared/components/editor/components/ckeditor/ckeditor.types.ts b/frontend/src/app/shared/components/editor/components/ckeditor/ckeditor.types.ts index 35dd84b2a9b..3b578071b5c 100644 --- a/frontend/src/app/shared/components/editor/components/ckeditor/ckeditor.types.ts +++ b/frontend/src/app/shared/components/editor/components/ckeditor/ckeditor.types.ts @@ -20,6 +20,10 @@ export interface CKEditorDomEventData { keyCode:number; } +// Opaque handle for a parsed CKEditor model fragment (DocumentFragment). +// We never inspect its internals, only pass it from data.parse to model.insertContent. +export type CKEditorModelFragment = unknown; + export interface ICKEditorInstance { id:string; @@ -39,9 +43,13 @@ export interface ICKEditorInstance { listenTo(node:unknown, key:string, callback:(evt:CKEditorEvent, data:CKEditorDomEventData) => unknown, options:CKEditorListenOptions):void; + data:{ + parse(data:string):CKEditorModelFragment; + }; model:{ on(ev:string, callback:() => unknown):void fire(ev:string, data:unknown):void + insertContent(content:CKEditorModelFragment):void document:{ on(ev:string, callback:() => unknown):void }; diff --git a/frontend/src/stimulus/controllers/dynamic/work-packages/activities-tab/quote-comment.controller.ts b/frontend/src/stimulus/controllers/dynamic/work-packages/activities-tab/quote-comment.controller.ts index 40d836b354c..25a26ff7916 100644 --- a/frontend/src/stimulus/controllers/dynamic/work-packages/activities-tab/quote-comment.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/work-packages/activities-tab/quote-comment.controller.ts @@ -65,19 +65,19 @@ export default class QuoteCommentController extends Controller { .join(''); // if we ever change CKEditor or how @mentions work this will break - return `@${userName} ${textWrote}:\n\n${quoted}`; + return `@${userName} ${textWrote}:\n\n${quoted}\n\n`; } private insertQuoteOnExistingEditor(quotedText:string) { - if (this.ckEditorInstance) { - const editorData = this.ckEditorInstance.getData({ trim: false }); + const editor = this.ckEditorInstance; + if (!editor) return; - if (editorData.endsWith('
') || editorData.endsWith('\n')) { - this.ckEditorInstance.setData(`${editorData}${quotedText}`); - } else { - this.ckEditorInstance.setData(`${editorData}\n\n${quotedText}`); - } - } + // insert at the current cursor position (preserved by CKEditor across blur/focus), + // then place focus so the user can type immediately after the blockquote + const fragment = editor.data.parse(quotedText); + editor.model.insertContent(fragment); + + this.workPackagesActivitiesTabEditorOutlet.focusEditor(); } private setCommentRestriction(isInternal:boolean) { diff --git a/spec/features/activities/work_package/activities_spec.rb b/spec/features/activities/work_package/activities_spec.rb index a51a855b57b..eaf918ae0fd 100644 --- a/spec/features/activities/work_package/activities_spec.rb +++ b/spec/features/activities/work_package/activities_spec.rb @@ -919,6 +919,9 @@ RSpec.describe "Work package activity", :js, :with_cuprite, with_ee: %i[internal # expect the quoted comment to be shown activity_tab.ckeditor.expect_include_value("@A Member wrote:\nFirst comment by member") + + # expect the editor to be focused so the user can type immediately + activity_tab.expect_focus_on_editor end end @@ -937,8 +940,11 @@ RSpec.describe "Work package activity", :js, :with_cuprite, with_ee: %i[internal # quote other user's comment activity_tab.quote_comment(first_comment_by_member) - # expect the original comment and quote are shown + # expect the original comment and quote are shown (quote appended after existing content) activity_tab.ckeditor.expect_include_value("Partial message:\n@A Member wrote:\nFirst comment by member") + + # expect the editor to be focused so the user can type immediately + activity_tab.expect_focus_on_editor end end end