From 27db8699c8df2df8a68d7031e921f44be3ea13c1 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:50:45 +0530 Subject: [PATCH] [WEB-480] fix: code block paste from VSCode and indentation (#4198) * fix: stroing the transactions in page * fix: page details changes * chore: page response change * chore: removed duplicated endpoints * chore: optimised the urls * chore: removed archived and favorite pages * chore: revamping pages store and components * mentions loading state part done * fixed mentions not showing in modals * removed comments and cleaned up types * removed unused types * reset: head * chore: pages store and component updates * style: pages list item UI * fix: improved colors and drag handle width * fix: slash commands are no more shown in the code blocks * fix: cleanup/hide drag handles post drop * fix: hide/cleanup drag handles post drag start * fix: aligning the drag handles better with the node post css changes of the length * fix: juggling back and forth of drag handles in ordered and unordered lists * chore: fix imports, ts errors and other things * fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes * chore: clearNodes after delete in case of selections being present * fix: hiding link selector in the bubble menu if inline code block is selected * chore: filtering, ordering and searching implemented * chore: updated pages store and updated UI * chore: new core editor just for document editor created * chore: removed setIsSubmitting prop in doc editor * fix: fixed submitting state for image uploads * refactor: setShouldShowAlert removed * refactor: rerenderOnPropsChange prop removed * chore: type inference magic in ref to expose an api for controlling editor menu items from outside * fix: naming imports * chore: change names of the exposed functions and removing old types * refactor: remove debouncedUpdatesEnabled prop; * refactor: editor heading markings now parsed using html * chore: removed unrelated components from the document editor * refactor: page details granular components * fix: remove onActionCompleteHandler * refactor: removed rerenderOnProps change prop * feat: added getMarkDown function * chore: update dropdown option actions * fix: sidebar markings update logic * chore: add image and to-do list actions to the toolbar * fix: handling refs and populating them via callbacks * feat: scroll to node api exposed * cleaning up editor refs when the editor is destroyed * feat: scrolling added to read only instance of the editor * fix: markings logic * fix: build errors with types * fix: build erros * fix: subscribing to transactions of editor via ref * chore: remove debug statements * fix: type errors * fix: temporary different slash commands for document editor * chore: inline code extension style * chore: remove border from readOnly editor * fix: editor bottom padding * chore: pages improvements * chore: handle Enter key on the page title * feat: added loading indicator logic in mentions * fix: mentions and slash commands now work well with multiple editors in one place * refactor: page store structure, filtering logic * feat: added better seperation in inline code blocks * feat: list autojoining added * fix: pages folder structure * fix: image refocus from external parts * working lists somewhat * chore: implement page reactions * fix: build errors * fix: build errors * fixed drag handles stuff * task list item fixed * working * fix: working on multiple nested lists * chore: remove debug statements * fix: Tab key on first list item handled to not go out of editor focus * feat: threshold auto scroll support added and multi nested list selection fixed * fix: caret color bug with improved inline code blocks * fix: node range error when bulk deleting with list * fix: removed slash commands from working in code blocks * chore: update typography margins * chore: new field added in page model * fix: better type inference in slash commands * chore: code block UI * feat: image insertion at correct position using ref added * feat: added improved mentions support for space * fix: type errors in mentions for comments in web app * sync: core with document-core * fix: build errors * fix: fallback for appendTo not being able to find active container instantly * fix: page store * fix: page description * fix: css quality issues * chore: code cleanup * chore: removed placeholder text in codeblocks * chore: archived pages response change * chore: archived pages response change * fix: initial pages list fetch * fix: pages list filters and ordering * chore: add access change option in the quick actions dropdown * fix: inline code block caret fixed * regression: removing extra text * chore: caret color removed * feat: copy code button added in code blocks * fix: initial load of page details * fix: initial load of page details * fix: image resizing weird behavior on click/expanding it too much fixed now * chore: copy page response * fix: todo list spacing * chore: description html in the copy page * chore: handle latest description on refetch * fix: saner scroll behaviours * fix: block menu positioning * fix: updated empty string description * feat: tab change sync support added * fix: infinite rerendering with markings * fix: block menu finally * fix: intial load on reload bug fixed * fix: nested lists alignment * fix: editor padding * fix: first level list items copyable * chore: list spacing * fix: title change * fix: pages list block items interaction * fix: saving chip position * fix: delete action from block menu to focus properly * fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node * style: table, chore: lite text editor toolbar * fix: page description tab sync * fix: lists spacing and alignment * refactor: document editor props * feat: rich text editor wrapper created and migrated core * feat: created wrapper around lite text editor and merged core * chore: add lite text editor toolbar * fix: build errors * fix: type errors and addead live updation of toolbar * chore: pages migration * fix: inbox issue * refactor: remove redundant package * refactor: unused files * fix: add dompurify to space app * fix: inline code margin * fix: editor className props * fix: build errors * fix: traversing up the tree before assuming the parent is not a list item * fix: drag handle positions for list items fixed * fix: removed focus at end logic after deleting block * fix: image wrapper overflow scroll fix with block menu's position * fix: selection and deletion logic for nested lists fixed!! * fix: hiding the block menu while scrolling in the document/app * fix: merge conflicts resolved from develop * fix: inbox issue description * chore: move page title to the web app * fix: handling edge cases for table selection * chore: lint issues * refactor: list item functions moved to same file * refactor: use mention hook * fix: added try catch blocks for mention suggestions * chore: remove unused code * fix: remove console logs * fix: remove console logs * fix: code block paste handler * fix: tracking image uploading status to prevent sync rerenders * chore: remove unnecessary props * fix: code block pasting logic from vs code handled properly * feat: additional checks for doc bounds * fix: type of cancel button changed to button instead of default submit * feat: editor focus on saved position while syncing via swr * fix: type errors * fix: changing names of plugins and removing packages * fix: readonly editor synced * removed console logs * fix: stringifying instead of dom injection * fix: use-editor try catch error handling * fix: editor container click in try catch * fix: some more error handling * chore: removed commented out code * fix: backspace error handling --------- Co-authored-by: NarayanBavisetti Co-authored-by: sriram veeraghanta Co-authored-by: gurusainath Co-authored-by: Aaryan Khandelwal --- packages/editor/core/package.json | 2 - .../insert-content-at-cursor-position.ts | 23 +- packages/editor/core/src/hooks/use-editor.tsx | 43 ++- .../editor/core/src/lib/editor-commands.ts | 90 ++++- .../src/ui/components/editor-container.tsx | 56 +-- .../ui/extensions/code/code-block-lowlight.ts | 30 ++ .../extensions/code/code-block-node-view.tsx | 2 +- .../core/src/ui/extensions/code/code-block.ts | 346 ++++++++++++++++++ .../core/src/ui/extensions/code/index.tsx | 152 ++++---- .../src/ui/extensions/code/lowlight-plugin.ts | 153 ++++++++ .../custom-list-keymap/list-keymap.ts | 50 ++- .../editor/core/src/ui/extensions/drop.tsx | 2 +- .../utilities/insert-line-above-image.ts | 67 ++-- .../utilities/insert-line-below-image.ts | 75 ++-- .../editor/core/src/ui/extensions/index.tsx | 4 +- .../core/src/ui/extensions/quote/index.tsx | 27 +- .../insert-line-above-table-action.ts | 75 ++-- .../insert-line-below-table-action.ts | 67 ++-- .../core/src/ui/read-only/extensions.tsx | 8 +- yarn.lock | 12 +- 20 files changed, 965 insertions(+), 319 deletions(-) create mode 100644 packages/editor/core/src/ui/extensions/code/code-block-lowlight.ts create mode 100644 packages/editor/core/src/ui/extensions/code/code-block.ts create mode 100644 packages/editor/core/src/ui/extensions/code/lowlight-plugin.ts diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 8c3056e00..8a64ba12c 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -31,8 +31,6 @@ "@plane/ui": "*", "@tiptap/core": "^2.1.13", "@tiptap/extension-blockquote": "^2.1.13", - "@tiptap/extension-code-block-lowlight": "^2.1.13", - "@tiptap/extension-color": "^2.1.13", "@tiptap/extension-image": "^2.1.13", "@tiptap/extension-list-item": "^2.1.13", "@tiptap/extension-mention": "^2.1.13", diff --git a/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts b/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts index 062acafcb..f17858d3b 100644 --- a/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts +++ b/packages/editor/core/src/helpers/insert-content-at-cursor-position.ts @@ -7,11 +7,22 @@ export const insertContentAtSavedSelection = ( content: string, savedSelection: Selection ) => { - if (editorRef.current && savedSelection) { - editorRef.current - .chain() - .focus() - .insertContentAt(savedSelection?.anchor, content) - .run(); + if (!editorRef.current || editorRef.current.isDestroyed) { + console.error("Editor reference is not available or has been destroyed."); + return; + } + + if (!savedSelection) { + console.error("Saved selection is invalid."); + return; + } + + const docSize = editorRef.current.state.doc.content.size; + const safePosition = Math.max(0, Math.min(savedSelection.anchor, docSize)); + + try { + editorRef.current.chain().focus().insertContentAt(safePosition, content).run(); + } catch (error) { + console.error("An error occurred while inserting content at saved selection:", error); } }; diff --git a/packages/editor/core/src/hooks/use-editor.tsx b/packages/editor/core/src/hooks/use-editor.tsx index e432ec6a4..f5500e725 100644 --- a/packages/editor/core/src/hooks/use-editor.tsx +++ b/packages/editor/core/src/hooks/use-editor.tsx @@ -108,15 +108,19 @@ export const useEditor = ({ // supported and value is undefined when the data from swr is not populated if (value === null || value === undefined) return; if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) { - editor.commands.setContent(value); - const currentSavedSelection = savedSelectionRef.current; - if (currentSavedSelection) { - editor.view.focus(); - const docLength = editor.state.doc.content.size; - const relativePosition = Math.min(currentSavedSelection.from, docLength - 1); - editor.commands.setTextSelection(relativePosition); - } else { - editor.commands.focus("end"); + try { + editor.commands.setContent(value); + const currentSavedSelection = savedSelectionRef.current; + if (currentSavedSelection) { + editor.view.focus(); + const docLength = editor.state.doc.content.size; + const relativePosition = Math.min(currentSavedSelection.from, docLength - 1); + editor.commands.setTextSelection(relativePosition); + } else { + editor.commands.focus("end"); + } + } catch (error) { + console.error("Error syncing editor content with external value:", error); } } }, [editor, value, id]); @@ -179,12 +183,21 @@ export const useEditor = ({ scrollSummary(editorRef.current, marking); }, setFocusAtPosition: (position: number) => { - if (!editorRef.current) return; - editorRef.current - .chain() - .insertContentAt(position, [{ type: "paragraph" }]) - .focus() - .run(); + if (!editorRef.current || editorRef.current.isDestroyed) { + console.error("Editor reference is not available or has been destroyed."); + return; + } + try { + const docSize = editorRef.current.state.doc.content.size; + const safePosition = Math.max(0, Math.min(position, docSize)); + editorRef.current + .chain() + .insertContentAt(safePosition, [{ type: "paragraph" }]) + .focus() + .run(); + } catch (error) { + console.error("An error occurred while setting focus at position:", error); + } }, }), [editorRef, savedSelection, uploadFile] diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index ce14502a7..f0c6c85e0 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -34,32 +34,82 @@ export const toggleUnderline = (editor: Editor, range?: Range) => { else editor.chain().focus().toggleUnderline().run(); }; -export const toggleCodeBlock = (editor: Editor, range?: Range) => { - // Check if code block is active then toggle code block - if (editor.isActive("codeBlock")) { - if (range) { - editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); - return; +const replaceCodeBlockWithContent = (editor: Editor) => { + try { + const { schema } = editor.state; + const { paragraph } = schema.nodes; + let replaced = false; + + const replaceCodeBlock = (from: number, to: number, textContent: string) => { + const docSize = editor.state.doc.content.size; + + if (from < 0 || to > docSize || from > to) { + console.error("Invalid range for replacement: ", from, to, "in a document of size", docSize); + return; + } + + // split the textContent by new lines to handle each line as a separate paragraph + const lines = textContent.split(/\r?\n/); + + const tr = editor.state.tr; + + // Calculate the position for inserting the first paragraph + let insertPos = from; + + // Remove the code block first + tr.delete(from, to); + + // For each line, create a paragraph node and insert it + lines.forEach((line) => { + const paragraphNode = paragraph.create({}, schema.text(line)); + tr.insert(insertPos, paragraphNode); + // Update insertPos for the next insertion + insertPos += paragraphNode.nodeSize; + }); + + // Dispatch the transaction + editor.view.dispatch(tr); + replaced = true; + }; + + editor.state.doc.nodesBetween(editor.state.selection.from, editor.state.selection.to, (node, pos) => { + if (node.type === schema.nodes.codeBlock) { + const startPos = pos; + const endPos = pos + node.nodeSize; + const textContent = node.textContent; + replaceCodeBlock(startPos, endPos, textContent); + return false; + } + }); + + if (!replaced) { + console.log("No code block to replace."); } - editor.chain().focus().toggleCodeBlock().run(); - return; + } catch (error) { + console.error("An error occurred while replacing code block content:", error); } +}; - // Check if user hasn't selected any text - const isSelectionEmpty = editor.state.selection.empty; - - if (isSelectionEmpty) { - if (range) { - editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); +export const toggleCodeBlock = (editor: Editor, range?: Range) => { + try { + if (editor.isActive("codeBlock")) { + replaceCodeBlockWithContent(editor); return; } - editor.chain().focus().toggleCodeBlock().run(); - } else { - if (range) { - editor.chain().focus().deleteRange(range).toggleCode().run(); - return; + + const { from, to } = range || editor.state.selection; + const text = editor.state.doc.textBetween(from, to, "\n"); + const isMultiline = text.includes("\n"); + + if (editor.state.selection.empty) { + editor.chain().focus().toggleCodeBlock().run(); + } else if (isMultiline) { + editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, `\`\`\`\n${text}\n\`\`\``).run(); + } else { + editor.chain().focus().toggleCode().run(); } - editor.chain().focus().toggleCode().run(); + } catch (error) { + console.error("An error occurred while toggling code block:", error); } }; diff --git a/packages/editor/core/src/ui/components/editor-container.tsx b/packages/editor/core/src/ui/components/editor-container.tsx index e7d272b9b..cfa80da90 100644 --- a/packages/editor/core/src/ui/components/editor-container.tsx +++ b/packages/editor/core/src/ui/components/editor-container.tsx @@ -15,36 +15,40 @@ export const EditorContainer: FC = (props) => { const handleContainerClick = () => { if (!editor) return; if (!editor.isEditable) return; - if (editor.isFocused) return; // If editor is already focused, do nothing + try { + if (editor.isFocused) return; // If editor is already focused, do nothing - const { selection } = editor.state; - const currentNode = selection.$from.node(); + const { selection } = editor.state; + const currentNode = selection.$from.node(); - editor?.chain().focus("end", { scrollIntoView: false }).run(); // Focus the editor at the end + editor?.chain().focus("end", { scrollIntoView: false }).run(); // Focus the editor at the end - if ( - currentNode.content.size === 0 && // Check if the current node is empty - !( - editor.isActive("orderedList") || - editor.isActive("bulletList") || - editor.isActive("taskItem") || - editor.isActive("table") || - editor.isActive("blockquote") || - editor.isActive("codeBlock") - ) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block - ) { - return; + if ( + currentNode.content.size === 0 && // Check if the current node is empty + !( + editor.isActive("orderedList") || + editor.isActive("bulletList") || + editor.isActive("taskItem") || + editor.isActive("table") || + editor.isActive("blockquote") || + editor.isActive("codeBlock") + ) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block + ) { + return; + } + + // Insert a new paragraph at the end of the document + const endPosition = editor?.state.doc.content.size; + editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run(); + + // Focus the newly added paragraph for immediate editing + editor + .chain() + .setTextSelection(endPosition + 1) + .run(); + } catch (error) { + console.error("An error occurred while handling container click to insert new empty node at bottom:", error); } - - // Insert a new paragraph at the end of the document - const endPosition = editor?.state.doc.content.size; - editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run(); - - // Focus the newly added paragraph for immediate editing - editor - .chain() - .setTextSelection(endPosition + 1) - .run(); }; return ( diff --git a/packages/editor/core/src/ui/extensions/code/code-block-lowlight.ts b/packages/editor/core/src/ui/extensions/code/code-block-lowlight.ts new file mode 100644 index 000000000..ae44d83d6 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/code/code-block-lowlight.ts @@ -0,0 +1,30 @@ +// import CodeBlock, { CodeBlockOptions } from "@tiptap/extension-code-block"; + +import { CodeBlockOptions, CodeBlock } from "./code-block"; +import { LowlightPlugin } from "./lowlight-plugin"; + +export interface CodeBlockLowlightOptions extends CodeBlockOptions { + lowlight: any; + defaultLanguage: string | null | undefined; +} + +export const CodeBlockLowlight = CodeBlock.extend({ + addOptions() { + return { + ...this.parent?.(), + lowlight: {}, + defaultLanguage: null, + }; + }, + + addProseMirrorPlugins() { + return [ + ...(this.parent?.() || []), + LowlightPlugin({ + name: this.name, + lowlight: this.options.lowlight, + defaultLanguage: this.options.defaultLanguage, + }), + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/code/code-block-node-view.tsx b/packages/editor/core/src/ui/extensions/code/code-block-node-view.tsx index f75da438e..f7218986b 100644 --- a/packages/editor/core/src/ui/extensions/code/code-block-node-view.tsx +++ b/packages/editor/core/src/ui/extensions/code/code-block-node-view.tsx @@ -49,7 +49,7 @@ export const CodeBlockComponent: React.FC = ({ node })
-        
+        
       
); diff --git a/packages/editor/core/src/ui/extensions/code/code-block.ts b/packages/editor/core/src/ui/extensions/code/code-block.ts new file mode 100644 index 000000000..b2218ee45 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/code/code-block.ts @@ -0,0 +1,346 @@ +import { mergeAttributes, Node, textblockTypeInputRule } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; + +export interface CodeBlockOptions { + /** + * Adds a prefix to language classes that are applied to code tags. + * Defaults to `'language-'`. + */ + languageClassPrefix: string; + /** + * Define whether the node should be exited on triple enter. + * Defaults to `true`. + */ + exitOnTripleEnter: boolean; + /** + * Define whether the node should be exited on arrow down if there is no node after it. + * Defaults to `true`. + */ + exitOnArrowDown: boolean; + /** + * Custom HTML attributes that should be added to the rendered HTML tag. + */ + HTMLAttributes: Record; +} + +declare module "@tiptap/core" { + interface Commands { + codeBlock: { + /** + * Set a code block + */ + setCodeBlock: (attributes?: { language: string }) => ReturnType; + /** + * Toggle a code block + */ + toggleCodeBlock: (attributes?: { language: string }) => ReturnType; + }; + } +} + +export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/; +export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/; + +export const CodeBlock = Node.create({ + name: "codeBlock", + + addOptions() { + return { + languageClassPrefix: "language-", + exitOnTripleEnter: true, + exitOnArrowDown: true, + HTMLAttributes: {}, + }; + }, + content: "text*", + + marks: "", + + group: "block", + + code: true, + + defining: true, + + addAttributes() { + return { + language: { + default: null, + parseHTML: (element) => { + const { languageClassPrefix } = this.options; + // @ts-expect-error element is a DOM element + const classNames = [...(element.firstElementChild?.classList || [])]; + const languages = classNames + .filter((className) => className.startsWith(languageClassPrefix)) + .map((className) => className.replace(languageClassPrefix, "")); + const language = languages[0]; + + if (!language) { + return null; + } + + return language; + }, + rendered: false, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "pre", + preserveWhitespace: "full", + }, + ]; + }, + + renderHTML({ node, HTMLAttributes }) { + return [ + "pre", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + [ + "code", + { + class: node.attrs.language ? this.options.languageClassPrefix + node.attrs.language : null, + }, + 0, + ], + ]; + }, + + addCommands() { + return { + setCodeBlock: + (attributes) => + ({ commands }) => + commands.setNode(this.name, attributes), + toggleCodeBlock: + (attributes) => + ({ commands }) => + commands.toggleNode(this.name, "paragraph", attributes), + }; + }, + + addKeyboardShortcuts() { + return { + "Mod-Alt-c": () => this.editor.commands.toggleCodeBlock(), + + // remove code block when at start of document or code block is empty + Backspace: () => { + try { + const { empty, $anchor } = this.editor.state.selection; + const isAtStart = $anchor.pos === 1; + + if (!empty || $anchor.parent.type.name !== this.name) { + return false; + } + + if (isAtStart || !$anchor.parent.textContent.length) { + return this.editor.commands.clearNodes(); + } + + return false; + } catch (error) { + console.error("Error handling Backspace in code block:", error); + return false; + } + }, + + // exit node on triple enter + Enter: ({ editor }) => { + try { + if (!this.options.exitOnTripleEnter) { + return false; + } + + const { state } = editor; + const { selection } = state; + const { $from, empty } = selection; + + if (!empty || $from.parent.type !== this.type) { + return false; + } + + const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2; + const endsWithDoubleNewline = $from.parent.textContent.endsWith("\n\n"); + + if (!isAtEnd || !endsWithDoubleNewline) { + return false; + } + + return editor + .chain() + .command(({ tr }) => { + tr.delete($from.pos - 2, $from.pos); + + return true; + }) + .exitCode() + .run(); + } catch (error) { + console.error("Error handling Enter in code block:", error); + return false; + } + }, + + // exit node on arrow down + ArrowDown: ({ editor }) => { + try { + if (!this.options.exitOnArrowDown) { + return false; + } + + const { state } = editor; + const { selection, doc } = state; + const { $from, empty } = selection; + + if (!empty || $from.parent.type !== this.type) { + return false; + } + + const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2; + + if (!isAtEnd) { + return false; + } + + const after = $from.after(); + + if (after === undefined) { + return false; + } + + const nodeAfter = doc.nodeAt(after); + + if (nodeAfter) { + return false; + } + + return editor.commands.exitCode(); + } catch (error) { + console.error("Error handling ArrowDown in code block:", error); + return false; + } + }, + }; + }, + + addInputRules() { + return [ + textblockTypeInputRule({ + find: backtickInputRegex, + type: this.type, + getAttributes: (match) => ({ + language: match[1], + }), + }), + textblockTypeInputRule({ + find: tildeInputRegex, + type: this.type, + getAttributes: (match) => ({ + language: match[1], + }), + }), + ]; + }, + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey("codeBlockVSCodeHandlerCustom"), + props: { + handlePaste: (view, event) => { + try { + if (!event.clipboardData) { + return false; + } + + if (this.editor.isActive(this.type.name)) { + return false; + } + + if (this.editor.isActive("code")) { + // Check if it's an inline code block + event.preventDefault(); + const text = event.clipboardData.getData("text/plain"); + + if (!text) { + console.error("Pasted text is empty."); + return false; + } + + const { tr } = view.state; + const { $from, $to } = tr.selection; + + if ($from.pos > $to.pos) { + console.error("Invalid selection range."); + return false; + } + + const docSize = tr.doc.content.size; + if ($from.pos < 0 || $to.pos > docSize) { + console.error("Selection range is out of document bounds."); + return false; + } + + // Extend the current selection to replace it with the pasted text + // wrapped in an inline code mark + const codeMark = view.state.schema.marks.code.create(); + tr.replaceWith($from.pos, $to.pos, view.state.schema.text(text, [codeMark])); + view.dispatch(tr); + return true; + } + + event.preventDefault(); + const text = event.clipboardData.getData("text/plain"); + const vscode = event.clipboardData.getData("vscode-editor-data"); + const vscodeData = vscode ? JSON.parse(vscode) : undefined; + const language = vscodeData?.mode; + + if (vscodeData && language) { + const { tr } = view.state; + const { $from } = tr.selection; + + // Check if the current line is empty + const isCurrentLineEmpty = !$from.parent.textContent.trim(); + + let insertPos; + + if (isCurrentLineEmpty) { + // If the current line is empty, use the current position + insertPos = $from.pos - 1; + } else { + // If the current line is not empty, insert below the current block node + insertPos = $from.end($from.depth) + 1; + } + + // Ensure insertPos is within document bounds + if (insertPos < 0 || insertPos > tr.doc.content.size) { + console.error("Invalid insert position."); + return false; + } + + // Create a new code block node with the pasted content + const textNode = view.state.schema.text(text.replace(/\r\n?/g, "\n")); + const codeBlock = this.type.create({ language }, textNode); + if (insertPos <= tr.doc.content.size) { + tr.insert(insertPos, codeBlock); + view.dispatch(tr); + return true; + } + + return false; + } else { + // TODO: complicated paste logic, to be handled later + return false; + } + } catch (error) { + console.error("Error handling paste in CodeBlock extension:", error); + return false; + } + }, + }, + }), + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/code/index.tsx b/packages/editor/core/src/ui/extensions/code/index.tsx index d2851d302..206930a87 100644 --- a/packages/editor/core/src/ui/extensions/code/index.tsx +++ b/packages/editor/core/src/ui/extensions/code/index.tsx @@ -1,5 +1,3 @@ -import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; - import { common, createLowlight } from "lowlight"; import ts from "highlight.js/lib/languages/typescript"; @@ -9,6 +7,7 @@ lowlight.register("ts", ts); import { Selection } from "@tiptap/pm/state"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { CodeBlockComponent } from "./code-block-node-view"; +import { CodeBlockLowlight } from "./code-block-lowlight"; export const CustomCodeBlockExtension = CodeBlockLowlight.extend({ addNodeView() { @@ -18,85 +17,100 @@ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({ addKeyboardShortcuts() { return { Tab: ({ editor }) => { - const { state } = editor; - const { selection } = state; - const { $from, empty } = selection; + try { + const { state } = editor; + const { selection } = state; + const { $from, empty } = selection; - if (!empty || $from.parent.type !== this.type) { + if (!empty || $from.parent.type !== this.type) { + return false; + } + + // Use ProseMirror's insertText transaction to insert the tab character + const tr = state.tr.insertText("\t", $from.pos, $from.pos); + editor.view.dispatch(tr); + + return true; + } catch (error) { + console.error("Error handling Tab in CustomCodeBlockExtension:", error); return false; } - - // Use ProseMirror's insertText transaction to insert the tab character - const tr = state.tr.insertText("\t", $from.pos, $from.pos); - editor.view.dispatch(tr); - - return true; }, ArrowUp: ({ editor }) => { - const { state } = editor; - const { selection } = state; - const { $from, empty } = selection; + try { + const { state } = editor; + const { selection } = state; + const { $from, empty } = selection; - if (!empty || $from.parent.type !== this.type) { + if (!empty || $from.parent.type !== this.type) { + return false; + } + + const isAtStart = $from.parentOffset === 0; + + if (!isAtStart) { + return false; + } + + // Check if codeBlock is the first node + const isFirstNode = $from.depth === 1 && $from.index($from.depth - 1) === 0; + + if (isFirstNode) { + // Insert a new paragraph at the start of the document and move the cursor to it + return editor.commands.command(({ tr }) => { + const node = editor.schema.nodes.paragraph.create(); + tr.insert(0, node); + tr.setSelection(Selection.near(tr.doc.resolve(1))); + return true; + }); + } + + return false; + } catch (error) { + console.error("Error handling ArrowUp in CustomCodeBlockExtension:", error); return false; } - - const isAtStart = $from.parentOffset === 0; - - if (!isAtStart) { - return false; - } - - // Check if codeBlock is the first node - const isFirstNode = $from.depth === 1 && $from.index($from.depth - 1) === 0; - - if (isFirstNode) { - // Insert a new paragraph at the start of the document and move the cursor to it - return editor.commands.command(({ tr }) => { - const node = editor.schema.nodes.paragraph.create(); - tr.insert(0, node); - tr.setSelection(Selection.near(tr.doc.resolve(1))); - return true; - }); - } - - return false; }, ArrowDown: ({ editor }) => { - if (!this.options.exitOnArrowDown) { + try { + if (!this.options.exitOnArrowDown) { + return false; + } + + const { state } = editor; + const { selection, doc } = state; + const { $from, empty } = selection; + + if (!empty || $from.parent.type !== this.type) { + return false; + } + + const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2; + + if (!isAtEnd) { + return false; + } + + const after = $from.after(); + + if (after === undefined) { + return false; + } + + const nodeAfter = doc.nodeAt(after); + + if (nodeAfter) { + return editor.commands.command(({ tr }) => { + tr.setSelection(Selection.near(doc.resolve(after))); + return true; + }); + } + + return editor.commands.exitCode(); + } catch (error) { + console.error("Error handling ArrowDown in CustomCodeBlockExtension:", error); return false; } - - const { state } = editor; - const { selection, doc } = state; - const { $from, empty } = selection; - - if (!empty || $from.parent.type !== this.type) { - return false; - } - - const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2; - - if (!isAtEnd) { - return false; - } - - const after = $from.after(); - - if (after === undefined) { - return false; - } - - const nodeAfter = doc.nodeAt(after); - - if (nodeAfter) { - return editor.commands.command(({ tr }) => { - tr.setSelection(Selection.near(doc.resolve(after))); - return true; - }); - } - - return editor.commands.exitCode(); }, }; }, diff --git a/packages/editor/core/src/ui/extensions/code/lowlight-plugin.ts b/packages/editor/core/src/ui/extensions/code/lowlight-plugin.ts new file mode 100644 index 000000000..54aa431c5 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/code/lowlight-plugin.ts @@ -0,0 +1,153 @@ +import { findChildren } from "@tiptap/core"; +import { Node as ProsemirrorNode } from "@tiptap/pm/model"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Decoration, DecorationSet } from "@tiptap/pm/view"; +import highlight from "highlight.js/lib/core"; + +function parseNodes(nodes: any[], className: string[] = []): { text: string; classes: string[] }[] { + return nodes + .map((node) => { + const classes = [...className, ...(node.properties ? node.properties.className : [])]; + + if (node.children) { + return parseNodes(node.children, classes); + } + + return { + text: node.value, + classes, + }; + }) + .flat(); +} + +function getHighlightNodes(result: any) { + // `.value` for lowlight v1, `.children` for lowlight v2 + return result.value || result.children || []; +} + +function registered(aliasOrLanguage: string) { + return Boolean(highlight.getLanguage(aliasOrLanguage)); +} + +function getDecorations({ + doc, + name, + lowlight, + defaultLanguage, +}: { + doc: ProsemirrorNode; + name: string; + lowlight: any; + defaultLanguage: string | null | undefined; +}) { + const decorations: Decoration[] = []; + + findChildren(doc, (node) => node.type.name === name).forEach((block) => { + let from = block.pos + 1; + const language = block.node.attrs.language || defaultLanguage; + const languages = lowlight.listLanguages(); + + const nodes = + language && (languages.includes(language) || registered(language)) + ? getHighlightNodes(lowlight.highlight(language, block.node.textContent)) + : getHighlightNodes(lowlight.highlightAuto(block.node.textContent)); + + parseNodes(nodes).forEach((node) => { + const to = from + node.text.length; + + if (node.classes.length) { + const decoration = Decoration.inline(from, to, { + class: node.classes.join(" "), + }); + + decorations.push(decoration); + } + + from = to; + }); + }); + + return DecorationSet.create(doc, decorations); +} + +function isFunction(param: () => any) { + return typeof param === "function"; +} + +export function LowlightPlugin({ + name, + lowlight, + defaultLanguage, +}: { + name: string; + lowlight: any; + defaultLanguage: string | null | undefined; +}) { + if (!["highlight", "highlightAuto", "listLanguages"].every((api) => isFunction(lowlight[api]))) { + throw Error("You should provide an instance of lowlight to use the code-block-lowlight extension"); + } + + const lowlightPlugin: Plugin = new Plugin({ + key: new PluginKey("lowlight"), + + state: { + init: (_, { doc }) => + getDecorations({ + doc, + name, + lowlight, + defaultLanguage, + }), + apply: (transaction, decorationSet, oldState, newState) => { + const oldNodeName = oldState.selection.$head.parent.type.name; + const newNodeName = newState.selection.$head.parent.type.name; + const oldNodes = findChildren(oldState.doc, (node) => node.type.name === name); + const newNodes = findChildren(newState.doc, (node) => node.type.name === name); + + if ( + transaction.docChanged && + // Apply decorations if: + // selection includes named node, + ([oldNodeName, newNodeName].includes(name) || + // OR transaction adds/removes named node, + newNodes.length !== oldNodes.length || + // OR transaction has changes that completely encapsulte a node + // (for example, a transaction that affects the entire document). + // Such transactions can happen during collab syncing via y-prosemirror, for example. + transaction.steps.some( + (step) => + // @ts-ignore + step.from !== undefined && + // @ts-ignore + step.to !== undefined && + oldNodes.some( + (node) => + // @ts-ignore + node.pos >= step.from && + // @ts-ignore + node.pos + node.node.nodeSize <= step.to + ) + )) + ) { + return getDecorations({ + doc: transaction.doc, + name, + lowlight, + defaultLanguage, + }); + } + + return decorationSet.map(transaction.mapping, transaction.doc); + }, + }, + + props: { + decorations(state) { + return lowlightPlugin.getState(state); + }, + }, + }); + + return lowlightPlugin; +} diff --git a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts index dba6b1f46..928eaff40 100644 --- a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts +++ b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-keymap.ts @@ -46,19 +46,24 @@ export const ListKeymap = Extension.create({ return true; }, Delete: ({ editor }) => { - let handled = false; + try { + let handled = false; - this.options.listTypes.forEach(({ itemName }) => { - if (editor.state.schema.nodes[itemName] === undefined) { - return; - } + this.options.listTypes.forEach(({ itemName }) => { + if (editor.state.schema.nodes[itemName] === undefined) { + return; + } - if (handleDelete(editor, itemName)) { - handled = true; - } - }); + if (handleDelete(editor, itemName)) { + handled = true; + } + }); - return handled; + return handled; + } catch (e) { + console.log("error in handling delete:", e); + return false; + } }, "Mod-Delete": ({ editor }) => { let handled = false; @@ -76,19 +81,24 @@ export const ListKeymap = Extension.create({ return handled; }, Backspace: ({ editor }) => { - let handled = false; + try { + let handled = false; - this.options.listTypes.forEach(({ itemName, wrapperNames }) => { - if (editor.state.schema.nodes[itemName] === undefined) { - return; - } + this.options.listTypes.forEach(({ itemName, wrapperNames }) => { + if (editor.state.schema.nodes[itemName] === undefined) { + return; + } - if (handleBackspace(editor, itemName, wrapperNames)) { - handled = true; - } - }); + if (handleBackspace(editor, itemName, wrapperNames)) { + handled = true; + } + }); - return handled; + return handled; + } catch (e) { + console.log("error in handling Backspace:", e); + return false; + } }, "Mod-Backspace": ({ editor }) => { let handled = false; diff --git a/packages/editor/core/src/ui/extensions/drop.tsx b/packages/editor/core/src/ui/extensions/drop.tsx index 8de48f9e0..ed206bc42 100644 --- a/packages/editor/core/src/ui/extensions/drop.tsx +++ b/packages/editor/core/src/ui/extensions/drop.tsx @@ -11,7 +11,7 @@ export const DropHandlerExtension = (uploadFile: UploadImage) => addProseMirrorPlugins() { return [ new Plugin({ - key: new PluginKey("dropHandler"), + key: new PluginKey("drop-handler-plugin"), props: { handlePaste: (view, event) => { if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { diff --git a/packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts index a18576b46..205ec96b9 100644 --- a/packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts +++ b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts @@ -2,44 +2,51 @@ import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { KeyboardShortcutCommand } from "@tiptap/core"; export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor }) => { - const { selection, doc } = editor.state; - const { $from, $to } = selection; + try { + const { selection, doc } = editor.state; + const { $from, $to } = selection; - let imageNode: ProseMirrorNode | null = null; - let imagePos: number | null = null; + let imageNode: ProseMirrorNode | null = null; + let imagePos: number | null = null; - // Check if the selection itself is an image node - doc.nodesBetween($from.pos, $to.pos, (node, pos) => { - if (node.type.name === "image") { - imageNode = node; - imagePos = pos; - return false; // Stop iterating once an image node is found - } - return true; - }); + // Check if the selection itself is an image node + doc.nodesBetween($from.pos, $to.pos, (node, pos) => { + if (node.type.name === "image") { + imageNode = node; + imagePos = pos; + return false; // Stop iterating once an image node is found + } + return true; + }); - if (imageNode === null || imagePos === null) return false; + if (imageNode === null || imagePos === null) return false; - // Since we want to insert above the image, we use the imagePos directly - const insertPos = imagePos; + // Since we want to insert above the image, we use the imagePos directly + const insertPos = imagePos; - if (insertPos < 0) return false; + const docSize = editor.state.doc.content.size; - // Check for an existing node immediately before the image - if (insertPos === 0) { - // If the previous node doesn't exist or isn't a paragraph, create and insert a new empty node there - editor.chain().insertContentAt(insertPos, { type: "paragraph" }).run(); - editor.chain().setTextSelection(insertPos).run(); - } else { - const prevNode = doc.nodeAt(insertPos); + if (insertPos < 0 || insertPos > docSize) return false; - if (prevNode && prevNode.type.name === "paragraph") { - // If the previous node is a paragraph, move the cursor there + // Check for an existing node immediately before the image + if (insertPos === 0) { + // If the previous node doesn't exist or isn't a paragraph, create and insert a new empty node there + editor.chain().insertContentAt(insertPos, { type: "paragraph" }).run(); editor.chain().setTextSelection(insertPos).run(); } else { - return false; - } - } + const prevNode = doc.nodeAt(insertPos); - return true; + if (prevNode && prevNode.type.name === "paragraph") { + // If the previous node is a paragraph, move the cursor there + editor.chain().setTextSelection(insertPos).run(); + } else { + return false; + } + } + + return true; + } catch (error) { + console.error("An error occurred while inserting a line above the image:", error); + return false; + } }; diff --git a/packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts index e998c728b..fe06ea0d9 100644 --- a/packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts +++ b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts @@ -2,45 +2,50 @@ import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { KeyboardShortcutCommand } from "@tiptap/core"; export const insertLineBelowImageAction: KeyboardShortcutCommand = ({ editor }) => { - const { selection, doc } = editor.state; - const { $from, $to } = selection; + try { + const { selection, doc } = editor.state; + const { $from, $to } = selection; - let imageNode: ProseMirrorNode | null = null; - let imagePos: number | null = null; + let imageNode: ProseMirrorNode | null = null; + let imagePos: number | null = null; - // Check if the selection itself is an image node - doc.nodesBetween($from.pos, $to.pos, (node, pos) => { - if (node.type.name === "image") { - imageNode = node; - imagePos = pos; - return false; // Stop iterating once an image node is found + // Check if the selection itself is an image node + doc.nodesBetween($from.pos, $to.pos, (node, pos) => { + if (node.type.name === "image") { + imageNode = node; + imagePos = pos; + return false; // Stop iterating once an image node is found + } + return true; + }); + + if (imageNode === null || imagePos === null) return false; + + const guaranteedImageNode: ProseMirrorNode = imageNode; + const nextNodePos = imagePos + guaranteedImageNode.nodeSize; + + // Check for an existing node immediately after the image + const nextNode = doc.nodeAt(nextNodePos); + + if (nextNode && nextNode.type.name === "paragraph") { + // If the next node is a paragraph, move the cursor there + const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; + editor.chain().setTextSelection(endOfParagraphPos).run(); + } else if (!nextNode) { + // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there + editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); + editor + .chain() + .setTextSelection(nextNodePos + 1) + .run(); + } else { + // If the next node is not a paragraph, do not proceed + return false; } + return true; - }); - - if (imageNode === null || imagePos === null) return false; - - const guaranteedImageNode: ProseMirrorNode = imageNode; - const nextNodePos = imagePos + guaranteedImageNode.nodeSize; - - // Check for an existing node immediately after the image - const nextNode = doc.nodeAt(nextNodePos); - - if (nextNode && nextNode.type.name === "paragraph") { - // If the next node is a paragraph, move the cursor there - const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; - editor.chain().setTextSelection(endOfParagraphPos).run(); - } else if (!nextNode) { - // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there - editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); - editor - .chain() - .setTextSelection(nextNodePos + 1) - .run(); - } else { - // If the next node is not a paragraph, do not proceed + } catch (error) { + console.error("An error occurred while inserting a line below the image:", error); return false; } - - return true; }; diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index c5a5d5eb9..4b8de461b 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -28,9 +28,9 @@ import { CustomLinkExtension } from "src/ui/extensions/custom-link"; import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline"; import { CustomTypographyExtension } from "src/ui/extensions/typography"; import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule"; -import { CustomCodeMarkPlugin } from "./custom-code-inline/inline-code-plugin"; +import { CustomCodeMarkPlugin } from "src/ui/extensions/custom-code-inline/inline-code-plugin"; import { UploadImage } from "src/types/upload-image"; -import { DropHandlerExtension } from "./drop"; +import { DropHandlerExtension } from "src/ui/extensions/drop"; type TArguments = { mentionConfig: { diff --git a/packages/editor/core/src/ui/extensions/quote/index.tsx b/packages/editor/core/src/ui/extensions/quote/index.tsx index 9dcae6ad7..4ae81ffe4 100644 --- a/packages/editor/core/src/ui/extensions/quote/index.tsx +++ b/packages/editor/core/src/ui/extensions/quote/index.tsx @@ -4,21 +4,26 @@ export const CustomQuoteExtension = Blockquote.extend({ addKeyboardShortcuts() { return { Enter: () => { - const { $from, $to, $head } = this.editor.state.selection; - const parent = $head.node(-1); + try { + const { $from, $to, $head } = this.editor.state.selection; + const parent = $head.node(-1); - if (!parent) return false; + if (!parent) return false; - if (parent.type.name !== "blockquote") { + if (parent.type.name !== "blockquote") { + return false; + } + if ($from.pos !== $to.pos) return false; + // if ($head.parentOffset < $head.parent.content.size) return false; + + // this.editor.commands.insertContentAt(parent.ne); + this.editor.chain().splitBlock().lift(this.name).run(); + + return true; + } catch (error) { + console.error("Error handling Enter in blockquote:", error); return false; } - if ($from.pos !== $to.pos) return false; - // if ($head.parentOffset < $head.parent.content.size) return false; - - // this.editor.commands.insertContentAt(parent.ne); - this.editor.chain().splitBlock().lift(this.name).run(); - - return true; }, }; }, diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts index d61d21c5b..865bce8b7 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts @@ -5,46 +5,51 @@ export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor }) // Check if the current selection or the closest node is a table if (!editor.isActive("table")) return false; - // Get the current selection - const { selection } = editor.state; + try { + // Get the current selection + const { selection } = editor.state; - // Find the table node and its position - const tableNode = findParentNodeOfType(selection, "table"); - if (!tableNode) return false; + // Find the table node and its position + const tableNode = findParentNodeOfType(selection, "table"); + if (!tableNode) return false; - const tablePos = tableNode.pos; + const tablePos = tableNode.pos; - // Determine if the selection is in the first row of the table - const firstRow = tableNode.node.child(0); - const selectionPath = (selection.$anchor as any).path; - const selectionInFirstRow = selectionPath.includes(firstRow); + // Determine if the selection is in the first row of the table + const firstRow = tableNode.node.child(0); + const selectionPath = (selection.$anchor as any).path; + const selectionInFirstRow = selectionPath.includes(firstRow); - if (!selectionInFirstRow) return false; + if (!selectionInFirstRow) return false; - // Check if the table is at the very start of the document or its parent node - if (tablePos === 0) { - // The table is at the start, so just insert a paragraph at the current position - editor.chain().insertContentAt(tablePos, { type: "paragraph" }).run(); - editor - .chain() - .setTextSelection(tablePos + 1) - .run(); - } else { - // The table is not at the start, check for the node immediately before the table - const prevNodePos = tablePos - 1; - - if (prevNodePos <= 0) return false; - - const prevNode = editor.state.doc.nodeAt(prevNodePos - 1); - - if (prevNode && prevNode.type.name === "paragraph") { - // If there's a paragraph before the table, move the cursor to the end of that paragraph - const endOfParagraphPos = tablePos - prevNode.nodeSize; - editor.chain().setTextSelection(endOfParagraphPos).run(); + // Check if the table is at the very start of the document or its parent node + if (tablePos === 0) { + // The table is at the start, so just insert a paragraph at the current position + editor.chain().insertContentAt(tablePos, { type: "paragraph" }).run(); + editor + .chain() + .setTextSelection(tablePos + 1) + .run(); } else { - return false; - } - } + // The table is not at the start, check for the node immediately before the table + const prevNodePos = tablePos - 1; - return true; + if (prevNodePos <= 0) return false; + + const prevNode = editor.state.doc.nodeAt(prevNodePos - 1); + + if (prevNode && prevNode.type.name === "paragraph") { + // If there's a paragraph before the table, move the cursor to the end of that paragraph + const endOfParagraphPos = tablePos - prevNode.nodeSize; + editor.chain().setTextSelection(endOfParagraphPos).run(); + } else { + return false; + } + } + + return true; + } catch (e) { + console.error("failed to insert line above table", e); + return false; + } }; diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts index 28b46084a..6ce0fa4c4 100644 --- a/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts @@ -5,44 +5,49 @@ export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor }) // Check if the current selection or the closest node is a table if (!editor.isActive("table")) return false; - // Get the current selection - const { selection } = editor.state; + try { + // Get the current selection + const { selection } = editor.state; - // Find the table node and its position - const tableNode = findParentNodeOfType(selection, "table"); - if (!tableNode) return false; + // Find the table node and its position + const tableNode = findParentNodeOfType(selection, "table"); + if (!tableNode) return false; - const tablePos = tableNode.pos; - const table = tableNode.node; + const tablePos = tableNode.pos; + const table = tableNode.node; - // Determine if the selection is in the last row of the table - const rowCount = table.childCount; - const lastRow = table.child(rowCount - 1); - const selectionPath = (selection.$anchor as any).path; - const selectionInLastRow = selectionPath.includes(lastRow); + // Determine if the selection is in the last row of the table + const rowCount = table.childCount; + const lastRow = table.child(rowCount - 1); + const selectionPath = (selection.$anchor as any).path; + const selectionInLastRow = selectionPath.includes(lastRow); - if (!selectionInLastRow) return false; + if (!selectionInLastRow) return false; - // Calculate the position immediately after the table - const nextNodePos = tablePos + table.nodeSize; + // Calculate the position immediately after the table + const nextNodePos = tablePos + table.nodeSize; - // Check for an existing node immediately after the table - const nextNode = editor.state.doc.nodeAt(nextNodePos); + // Check for an existing node immediately after the table + const nextNode = editor.state.doc.nodeAt(nextNodePos); - if (nextNode && nextNode.type.name === "paragraph") { - // If the next node is an paragraph, move the cursor there - const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; - editor.chain().setTextSelection(endOfParagraphPos).run(); - } else if (!nextNode) { - // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there - editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); - editor - .chain() - .setTextSelection(nextNodePos + 1) - .run(); - } else { + if (nextNode && nextNode.type.name === "paragraph") { + // If the next node is an paragraph, move the cursor there + const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; + editor.chain().setTextSelection(endOfParagraphPos).run(); + } else if (!nextNode) { + // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there + editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); + editor + .chain() + .setTextSelection(nextNodePos + 1) + .run(); + } else { + return false; + } + + return true; + } catch (e) { + console.error("failed to insert line above table", e); return false; } - - return true; }; diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx index cf12880f8..33853e9b1 100644 --- a/packages/editor/core/src/ui/read-only/extensions.tsx +++ b/packages/editor/core/src/ui/read-only/extensions.tsx @@ -1,7 +1,6 @@ import StarterKit from "@tiptap/starter-kit"; import TiptapUnderline from "@tiptap/extension-underline"; import TextStyle from "@tiptap/extension-text-style"; -import { Color } from "@tiptap/extension-color"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import { Markdown } from "tiptap-markdown"; @@ -50,7 +49,9 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { }), CustomQuoteExtension, CustomHorizontalRule.configure({ - HTMLAttributes: { class: "my-4" }, + HTMLAttributes: { + class: "my-4 border-custom-border-400", + }, }), CustomLinkExtension.configure({ openOnClick: true, @@ -71,7 +72,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { }), TiptapUnderline, TextStyle, - Color, TaskList.configure({ HTMLAttributes: { class: "not-prose pl-2 space-y-2", @@ -85,7 +85,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { }), CustomCodeBlockExtension.configure({ HTMLAttributes: { - class: "bg-custom-background-90 text-custom-text-100 rounded-lg p-8 pl-9 pr-4", + class: "", }, }), CustomCodeInlineExtension, diff --git a/yarn.lock b/yarn.lock index 63fd1d9e7..1fb78bb2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2385,11 +2385,6 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.13.tgz#0a26731ebf98ddfd268884ff1712f7189be7b63c" integrity sha512-NkWlQ5bLPUlcROj6G/d4oqAxMf3j3wfndGOPp0z8OoXJtVbVoXl/aMSlLbVgE6n8r6CS8MYxKhXNxrb7Ll2foA== -"@tiptap/extension-code-block-lowlight@^2.1.13": - version "2.1.13" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.1.13.tgz#91110f44d6cc8a12d95ac92aee0c848fdedefb0d" - integrity sha512-PlU0lzAEbUGqPykl7fYqlAiY7/zFRtQExsbrpi2kctSIzxC+jgMM4vEpWxLS4jZEXl7jVHvBRH6lRNINDHWmQA== - "@tiptap/extension-code-block@^2.1.13": version "2.1.13" resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.1.13.tgz#3e441d171d3ed821e67291dbf4cbad7e2ea29809" @@ -2400,11 +2395,6 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.1.13.tgz#27a5ca5705e59ca97390fad4d6631bf431690480" integrity sha512-f5fLYlSgliVVa44vd7lQGvo49+peC+Z2H0Fn84TKNCH7tkNZzouoJsHYn0/enLaQ9Sq+24YPfqulfiwlxyiT8w== -"@tiptap/extension-color@^2.1.13": - version "2.1.13" - resolved "https://registry.yarnpkg.com/@tiptap/extension-color/-/extension-color-2.1.13.tgz#f1ea3805db93f308aaf99d8ac80b18fcf13de050" - integrity sha512-T3tJXCIfFxzIlGOhvbPVIZa3y36YZRPYIo2TKsgkTz8LiMob6hRXXNFjsrFDp2Fnu3DrBzyvrorsW7767s4eYg== - "@tiptap/extension-document@^2.1.13": version "2.1.13" resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.13.tgz#5b68fa08e8a79eebd41f1360982db2ddd28ad010" @@ -2757,7 +2747,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42": +"@types/react@*", "@types/react@^18.2.42": version "18.2.42" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==