From dadb311f3a26240e28c5918c22b2ae7afe29dd79 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:13:22 +0530 Subject: [PATCH] Refactors editor util functions --- .../components/widgets/WootWriter/Editor.vue | 54 ++++--------- .../dashboard/helper/editorHelper.js | 59 +++++++++++++++ .../helper/specs/editorHelper.spec.js | 75 +++++++++++++++++++ 3 files changed, 147 insertions(+), 41 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index 931659b38..0c07d789c 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -48,6 +48,8 @@ import VariableList from '../conversation/VariableList.vue'; import { appendSignature, removeSignature, + insertAtCursor, + scrollCursorIntoView, } from 'dashboard/helper/editorHelper'; const TYPING_INDICATOR_IDLE_TIME = 4000; @@ -273,6 +275,7 @@ export default { const tr = this.editorView.state.tr.replaceSelectionWith(node); this.editorView.focus(); this.state = this.editorView.state.apply(tr); + this.editorView.updateState(this.state); this.emitOnChange(); this.$emit('clear-selection'); } @@ -395,6 +398,7 @@ export default { state: this.state, dispatchTransaction: tx => { this.state = this.state.apply(tx); + this.editorView.updateState(this.state); this.emitOnChange(); }, handleDOMEvents: { @@ -469,12 +473,7 @@ export default { updatedMessage ); - this.insertNodeIntoEditor( - node, - this.range.from, - this.range.to, - updatedMessage - ); + this.insertNodeIntoEditor(node, this.range.from, this.range.to); this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); return false; @@ -486,9 +485,9 @@ export default { const content = `{{${variable}}}`; let node = this.editorView.state.schema.text(content); - const from = this.range.from; + const { from, to } = this.range; - this.insertNodeIntoEditor(node, from, this.range.to, content); + this.insertNodeIntoEditor(node, from, to); this.showVariables = false; this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE); return false; @@ -546,8 +545,6 @@ export default { }, emitOnChange() { - this.editorView.updateState(this.state); - this.$emit('input', this.contentFromEditor); }, @@ -611,39 +608,14 @@ export default { const from = defaultFrom || this.editorView.state.selection.from || 0; let node = new MessageMarkdownTransformer(messageSchema).parse(content); - this.insertNodeIntoEditor(node, from, undefined, content); + this.insertNodeIntoEditor(node, from, undefined); }, - insertNodeIntoEditor(node, from = 0, to = 0, content = '') { - if (!this.editorView) { - return; - } - - // If the node is of type 'doc' and has only one child which is a paragraph, - // then extract its inline content to be inserted as inline. - if ( - node.type.name === 'doc' && - node.childCount === 1 && - node.firstChild.type.name === 'paragraph' - ) { - node = node.firstChild.content; - } else if (content && node.textContent === content) { - node = this.editorView.state.schema.text(content); - } - - let tr; - if (to) { - tr = this.editorView.state.tr - .replaceWith(from, to, node) - .insertText(` `); - } else { - tr = this.editorView.state.tr.insert(from, node); - } - - this.state = this.editorView.state.apply(tr); + insertNodeIntoEditor(node, from = 0, to = 0) { + this.state = insertAtCursor(this.editorView, node, from, to); this.emitOnChange(); - - tr.scrollIntoView(); - this.editorView.focus(); + this.$nextTick(() => { + scrollCursorIntoView(this.editorView); + }); }, }, }; diff --git a/app/javascript/dashboard/helper/editorHelper.js b/app/javascript/dashboard/helper/editorHelper.js index 795c3af1b..d82fdbaa5 100644 --- a/app/javascript/dashboard/helper/editorHelper.js +++ b/app/javascript/dashboard/helper/editorHelper.js @@ -139,3 +139,62 @@ export function extractTextFromMarkdown(markdown) { .replace(/\n{2,}/g, '\n') // Remove multiple consecutive newlines (blank lines) .trim(); // Trim any extra space } + +/** + * Scrolls the editor view into current cursor position + * + * @param {EditorView} view - The Prosemirror EditorView + * + */ +export const scrollCursorIntoView = view => { + // Get the current selection's head position (where the cursor is). + const pos = view.state.selection.head; + + // Get the corresponding DOM node for that position. + const domAtPos = view.domAtPos(pos); + const node = domAtPos.node; + + // Scroll the node into view. + if (node && node.scrollIntoView) { + node.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } +}; + +/** + * Returns a transaction that inserts a node into editor at the given position + * Has an optional param 'content' to check if the + * + * @param {Node} node - The prosemirror node that needs to be inserted into the editor + * @param {number} from - Position in the editor where the node needs to be inserted + * @param {number} to - Position in the editor where the node needs to be replaced + * + */ +export function insertAtCursor(editorView, node, from, to) { + if (!editorView) { + return undefined; + } + + // This is a workaround to prevent inserting content into new line rather than on the exiting line + // If the node is of type 'doc' and has only one child which is a paragraph, + // then extract its inline content to be inserted as inline. + const isWrappedInParagraph = + node.type.name === 'doc' && + node.childCount === 1 && + node.firstChild.type.name === 'paragraph'; + + if (isWrappedInParagraph) { + node = node.firstChild.content; + } + + let tr; + if (to) { + tr = editorView.state.tr.replaceWith(from, to, node).insertText(` `); + } else { + tr = editorView.state.tr.insert(from, node); + } + const state = editorView.state.apply(tr); + editorView.updateState(state); + editorView.focus(); + + return state; +} diff --git a/app/javascript/dashboard/helper/specs/editorHelper.spec.js b/app/javascript/dashboard/helper/specs/editorHelper.spec.js index 6b1a934b6..cca0d9b21 100644 --- a/app/javascript/dashboard/helper/specs/editorHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/editorHelper.spec.js @@ -5,7 +5,41 @@ import { replaceSignature, cleanSignature, extractTextFromMarkdown, + insertAtCursor, } from '../editorHelper'; +import { EditorState } from 'prosemirror-state'; +import { EditorView } from 'prosemirror-view'; +import { Schema } from 'prosemirror-model'; + +// Define a basic ProseMirror schema +const schema = new Schema({ + nodes: { + doc: { content: 'paragraph+' }, + paragraph: { + content: 'text*', + toDOM: () => ['p', 0], // Represents a paragraph as a
tag in the DOM. + }, + text: { + toDOM: node => node.text, // Represents text as its actual string value. + }, + }, +}); + +// Initialize a basic EditorState for testing +const createEditorState = (content = '') => { + if (!content) { + return EditorState.create({ + schema, + doc: schema.node('doc', null, [schema.node('paragraph')]), + }); + } + return EditorState.create({ + schema, + doc: schema.node('doc', null, [ + schema.node('paragraph', null, [schema.text(content)]), + ]), + }); +}; const NEW_SIGNATURE = 'This is a new signature'; @@ -167,3 +201,44 @@ describe('extractTextFromMarkdown', () => { expect(extractTextFromMarkdown(markdown)).toEqual(expected); }); }); + +describe('insertAtCursor', () => { + it('should return undefined if editorView is not provided', () => { + const result = insertAtCursor(undefined, schema.text('Hello'), 0); + expect(result).toBeUndefined(); + }); + + it('should unwrap doc nodes that are wrapped in a paragraph', () => { + const docNode = schema.node('doc', null, [ + schema.node('paragraph', null, [schema.text('Hello')]), + ]); + + const editorState = createEditorState(); + const editorView = new EditorView(document.body, { state: editorState }); + + insertAtCursor(editorView, docNode, 0); + + // Check if node was unwrapped and inserted correctly + expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello'); + }); + + it('should insert node without replacing any content if "to" is not provided', () => { + const editorState = createEditorState(); + const editorView = new EditorView(document.body, { state: editorState }); + + insertAtCursor(editorView, schema.text('Hello'), 0); + + // Check if node was inserted correctly + expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello'); + }); + + it('should replace content between "from" and "to" with the provided node', () => { + const editorState = createEditorState('ReplaceMe'); + const editorView = new EditorView(document.body, { state: editorState }); + + insertAtCursor(editorView, schema.text('Hello'), 0, 8); + + // Check if content was replaced correctly + expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello Me'); + }); +});