diff --git a/packages/editor/core/src/lib/utils.ts b/packages/editor/core/src/lib/utils.ts
index 5c7a8f08f..c943d4c60 100644
--- a/packages/editor/core/src/lib/utils.ts
+++ b/packages/editor/core/src/lib/utils.ts
@@ -1,3 +1,4 @@
+import { Selection } from "@tiptap/pm/state";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
interface EditorClassNames {
@@ -18,6 +19,19 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+// Helper function to find the parent node of a specific type
+export function findParentNodeOfType(selection: Selection, typeName: string) {
+ let depth = selection.$anchor.depth;
+ while (depth > 0) {
+ const node = selection.$anchor.node(depth);
+ if (node.type.name === typeName) {
+ return { node, pos: selection.$anchor.start(depth) - 1 };
+ }
+ depth--;
+ }
+ return null;
+}
+
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
while (node !== null && node.nodeName !== "TABLE") {
node = node.parentNode;
diff --git a/packages/editor/core/src/ui/components/editor-container.tsx b/packages/editor/core/src/ui/components/editor-container.tsx
index 5480a51e9..1b2504b58 100644
--- a/packages/editor/core/src/ui/components/editor-container.tsx
+++ b/packages/editor/core/src/ui/components/editor-container.tsx
@@ -1,5 +1,5 @@
import { Editor } from "@tiptap/react";
-import { ReactNode } from "react";
+import { FC, ReactNode } from "react";
interface EditorContainerProps {
editor: Editor | null;
@@ -8,17 +8,54 @@ interface EditorContainerProps {
hideDragHandle?: () => void;
}
-export const EditorContainer = ({ editor, editorClassNames, hideDragHandle, children }: EditorContainerProps) => (
-
{
- editor?.chain().focus(undefined, { scrollIntoView: false }).run();
- }}
- onMouseLeave={() => {
- hideDragHandle?.();
- }}
- className={`cursor-text ${editorClassNames}`}
- >
- {children}
-
-);
+export const EditorContainer: FC = (props) => {
+ const { editor, editorClassNames, hideDragHandle, children } = props;
+
+ const handleContainerClick = () => {
+ if (!editor) return;
+ if (!editor.isEditable) return;
+ if (editor.isFocused) return; // If editor is already focused, do nothing
+
+ const { selection } = editor.state;
+ const currentNode = selection.$from.node();
+
+ 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;
+ }
+
+ // 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 (
+ {
+ hideDragHandle?.();
+ }}
+ className={`cursor-text ${editorClassNames}`}
+ >
+ {children}
+
+ );
+};
diff --git a/packages/editor/core/src/ui/extensions/image/index.tsx b/packages/editor/core/src/ui/extensions/image/index.tsx
index db8b1c97b..1431b7755 100644
--- a/packages/editor/core/src/ui/extensions/image/index.tsx
+++ b/packages/editor/core/src/ui/extensions/image/index.tsx
@@ -5,6 +5,8 @@ import ImageExt from "@tiptap/extension-image";
import { onNodeDeleted, onNodeRestored } from "src/ui/plugins/delete-image";
import { DeleteImage } from "src/types/delete-image";
import { RestoreImage } from "src/types/restore-image";
+import { insertLineBelowImageAction } from "./utilities/insert-line-below-image";
+import { insertLineAboveImageAction } from "./utilities/insert-line-above-image";
interface ImageNode extends ProseMirrorNode {
attrs: {
@@ -18,6 +20,12 @@ const IMAGE_NODE_TYPE = "image";
export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
ImageExt.extend({
+ addKeyboardShortcuts() {
+ return {
+ ArrowDown: insertLineBelowImageAction,
+ ArrowUp: insertLineAboveImageAction,
+ };
+ },
addProseMirrorPlugins() {
return [
UploadImagesPlugin(cancelUploadImage),
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
new file mode 100644
index 000000000..a18576b46
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts
@@ -0,0 +1,45 @@
+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;
+
+ 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;
+ });
+
+ if (imageNode === null || imagePos === null) return false;
+
+ // Since we want to insert above the image, we use the imagePos directly
+ const insertPos = imagePos;
+
+ if (insertPos < 0) return false;
+
+ // 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 (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;
+};
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
new file mode 100644
index 000000000..e998c728b
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts
@@ -0,0 +1,46 @@
+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;
+
+ 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;
+ });
+
+ 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;
+};
diff --git a/packages/editor/core/src/ui/extensions/table/table/table.ts b/packages/editor/core/src/ui/extensions/table/table/table.ts
index ef595eee2..5fd06caf6 100644
--- a/packages/editor/core/src/ui/extensions/table/table/table.ts
+++ b/packages/editor/core/src/ui/extensions/table/table/table.ts
@@ -25,6 +25,8 @@ import { tableControls } from "src/ui/extensions/table/table/table-controls";
import { TableView } from "src/ui/extensions/table/table/table-view";
import { createTable } from "src/ui/extensions/table/table/utilities/create-table";
import { deleteTableWhenAllCellsSelected } from "src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected";
+import { insertLineBelowTableAction } from "./utilities/insert-line-below-table-action";
+import { insertLineAboveTableAction } from "./utilities/insert-line-above-table-action";
export interface TableOptions {
HTMLAttributes: Record;
@@ -231,6 +233,8 @@ export const Table = Node.create({
"Mod-Backspace": deleteTableWhenAllCellsSelected,
Delete: deleteTableWhenAllCellsSelected,
"Mod-Delete": deleteTableWhenAllCellsSelected,
+ ArrowDown: insertLineBelowTableAction,
+ ArrowUp: insertLineAboveTableAction,
};
},
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
new file mode 100644
index 000000000..d61d21c5b
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts
@@ -0,0 +1,50 @@
+import { KeyboardShortcutCommand } from "@tiptap/core";
+import { findParentNodeOfType } from "src/lib/utils";
+
+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;
+
+ // Find the table node and its position
+ const tableNode = findParentNodeOfType(selection, "table");
+ if (!tableNode) return false;
+
+ 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);
+
+ 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();
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+};
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
new file mode 100644
index 000000000..28b46084a
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts
@@ -0,0 +1,48 @@
+import { KeyboardShortcutCommand } from "@tiptap/core";
+import { findParentNodeOfType } from "src/lib/utils";
+
+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;
+
+ // Find the table node and its position
+ const tableNode = findParentNodeOfType(selection, "table");
+ if (!tableNode) return false;
+
+ 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);
+
+ if (!selectionInLastRow) return false;
+
+ // 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);
+
+ 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;
+};
diff --git a/packages/editor/document-editor/src/ui/components/page-renderer.tsx b/packages/editor/document-editor/src/ui/components/page-renderer.tsx
index 06b9e70ff..d82719c87 100644
--- a/packages/editor/document-editor/src/ui/components/page-renderer.tsx
+++ b/packages/editor/document-editor/src/ui/components/page-renderer.tsx
@@ -152,7 +152,7 @@ export const PageRenderer = (props: IPageRenderer) => {
);
return (
-
+
{!readonly ? (
handlePageTitleChange(e.target.value)}
diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx
index 2aff5d265..eeac3d2ef 100644
--- a/packages/editor/rich-text-editor/src/ui/index.tsx
+++ b/packages/editor/rich-text-editor/src/ui/index.tsx
@@ -100,10 +100,10 @@ const RichTextEditor = ({
customClassName,
});
- React.useEffect(() => {
- if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue);
- }, [editor, initialValue]);
-
+ // React.useEffect(() => {
+ // if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue);
+ // }, [editor, initialValue]);
+ //
if (!editor) return null;
return (
diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx
index 2e7dd25b8..f96e7293e 100644
--- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx
+++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx
@@ -121,7 +121,10 @@ export const EditorBubbleMenu: FC
= (props: any) => {