From 7d4bb3e12b607cedb94031cf38542aab53bd4414 Mon Sep 17 00:00:00 2001
From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com>
Date: Mon, 1 Jul 2024 12:49:10 +0530
Subject: [PATCH] Fix: image insertion in node boundaries and missing
extensions (#4977)
* fix: insertion of image at node boundaries fixed
* fix: remove unecessary things
* fix: history conflicting
* fix: generate json from html properly
---
.../components/editors/document/helpers.ts | 19 ++++
.../core/components/editors/document/index.ts | 1 +
.../core/extensions/core-without-props.tsx | 19 +---
.../extensions/document-without-props.tsx | 3 +
.../editor/src/core/extensions/extensions.tsx | 1 +
.../src/core/extensions/image/extension.tsx | 9 +-
.../image/image-extension-without-props.tsx | 18 ----
.../utilities/insert-line-above-image.ts | 52 ----------
.../utilities/insert-line-below-image.ts | 51 ----------
packages/editor/src/core/extensions/index.ts | 1 +
.../src/core/extensions/issue-embed/index.ts | 1 +
.../issue-embed/issue-embed-without-props.ts | 41 ++++++++
packages/editor/src/core/helpers/common.ts | 17 ----
...insert-empty-paragraph-at-node-boundary.ts | 96 +++++++++++++++++++
packages/editor/src/index.ts | 1 +
web/core/hooks/use-page-description.ts | 9 +-
16 files changed, 176 insertions(+), 163 deletions(-)
create mode 100644 packages/editor/src/core/components/editors/document/helpers.ts
create mode 100644 packages/editor/src/core/extensions/document-without-props.tsx
delete mode 100644 packages/editor/src/core/extensions/image/utilities/insert-line-above-image.ts
delete mode 100644 packages/editor/src/core/extensions/image/utilities/insert-line-below-image.ts
create mode 100644 packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts
create mode 100644 packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts
diff --git a/packages/editor/src/core/components/editors/document/helpers.ts b/packages/editor/src/core/components/editors/document/helpers.ts
new file mode 100644
index 000000000..b2ef91fb7
--- /dev/null
+++ b/packages/editor/src/core/components/editors/document/helpers.ts
@@ -0,0 +1,19 @@
+import { Extensions, generateJSON, getSchema } from "@tiptap/core";
+import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@/extensions";
+
+/**
+ * @description return an object with contentJSON and editorSchema
+ * @description contentJSON- ProseMirror JSON from HTML content
+ * @description editorSchema- editor schema from extensions
+ * @param {string} html
+ * @returns {object} {contentJSON, editorSchema}
+ */
+export const generateJSONfromHTMLForDocumentEditor = (html: string) => {
+ const extensions = [...CoreEditorExtensionsWithoutProps(), ...DocumentEditorExtensionsWithoutProps()];
+ const contentJSON = generateJSON(html ?? "
", extensions as Extensions);
+ const editorSchema = getSchema(extensions as Extensions);
+ return {
+ contentJSON,
+ editorSchema,
+ };
+};
diff --git a/packages/editor/src/core/components/editors/document/index.ts b/packages/editor/src/core/components/editors/document/index.ts
index 6c48975bd..574c613be 100644
--- a/packages/editor/src/core/components/editors/document/index.ts
+++ b/packages/editor/src/core/components/editors/document/index.ts
@@ -1,3 +1,4 @@
export * from "./editor";
export * from "./page-renderer";
export * from "./read-only-editor";
+export * from "./helpers";
diff --git a/packages/editor/src/core/extensions/core-without-props.tsx b/packages/editor/src/core/extensions/core-without-props.tsx
index be1b9ab24..101511ce0 100644
--- a/packages/editor/src/core/extensions/core-without-props.tsx
+++ b/packages/editor/src/core/extensions/core-without-props.tsx
@@ -1,4 +1,3 @@
-import Placeholder from "@tiptap/extension-placeholder";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import TextStyle from "@tiptap/extension-text-style";
@@ -46,10 +45,7 @@ export const CoreEditorExtensionsWithoutProps = () => [
codeBlock: false,
horizontalRule: false,
blockquote: false,
- dropcursor: {
- color: "rgba(var(--color-text-100))",
- width: 1,
- },
+ dropcursor: false,
}),
CustomQuoteExtension,
CustomHorizontalRule.configure({
@@ -58,7 +54,6 @@ export const CoreEditorExtensionsWithoutProps = () => [
},
}),
CustomKeymap,
- // ListKeymap,
CustomLinkExtension.configure({
openOnClick: true,
autolink: true,
@@ -105,16 +100,4 @@ export const CoreEditorExtensionsWithoutProps = () => [
TableCell,
TableRow,
CustomMentionWithoutProps(),
- Placeholder.configure({
- placeholder: ({ editor, node }) => {
- if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
-
- const shouldHidePlaceholder =
- editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");
- if (shouldHidePlaceholder) return "";
-
- return "Press '/' for commands...";
- },
- includeChildren: true,
- }),
];
diff --git a/packages/editor/src/core/extensions/document-without-props.tsx b/packages/editor/src/core/extensions/document-without-props.tsx
new file mode 100644
index 000000000..2202510ec
--- /dev/null
+++ b/packages/editor/src/core/extensions/document-without-props.tsx
@@ -0,0 +1,3 @@
+import { IssueWidgetWithoutProps } from "@/extensions/issue-embed";
+
+export const DocumentEditorExtensionsWithoutProps = () => [IssueWidgetWithoutProps()];
diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx
index 6cabd9e3f..bb53395ac 100644
--- a/packages/editor/src/core/extensions/extensions.tsx
+++ b/packages/editor/src/core/extensions/extensions.tsx
@@ -70,6 +70,7 @@ export const CoreEditorExtensions = ({
codeBlock: false,
horizontalRule: false,
blockquote: false,
+ history: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
width: 1,
diff --git a/packages/editor/src/core/extensions/image/extension.tsx b/packages/editor/src/core/extensions/image/extension.tsx
index 04555fd45..98961b7f0 100644
--- a/packages/editor/src/core/extensions/image/extension.tsx
+++ b/packages/editor/src/core/extensions/image/extension.tsx
@@ -1,4 +1,6 @@
import ImageExt from "@tiptap/extension-image";
+// helpers
+import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
// plugins
import {
IMAGE_NODE_TYPE,
@@ -9,16 +11,13 @@ import {
} from "@/plugins/image";
// types
import { DeleteImage, RestoreImage } from "@/types";
-// helpers
-import { insertLineAboveImageAction } from "./utilities/insert-line-above-image";
-import { insertLineBelowImageAction } from "./utilities/insert-line-below-image";
export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreImage, cancelUploadImage?: () => void) =>
ImageExt.extend({
addKeyboardShortcuts() {
return {
- ArrowDown: insertLineBelowImageAction,
- ArrowUp: insertLineAboveImageAction,
+ ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", "image"),
+ ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", "image"),
};
},
addProseMirrorPlugins() {
diff --git a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx b/packages/editor/src/core/extensions/image/image-extension-without-props.tsx
index ee9d76dab..0d505000c 100644
--- a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx
+++ b/packages/editor/src/core/extensions/image/image-extension-without-props.tsx
@@ -1,25 +1,7 @@
import ImageExt from "@tiptap/extension-image";
-// helpers
-import { insertLineAboveImageAction } from "./utilities/insert-line-above-image";
-import { insertLineBelowImageAction } from "./utilities/insert-line-below-image";
export const ImageExtensionWithoutProps = () =>
ImageExt.extend({
- addKeyboardShortcuts() {
- return {
- ArrowDown: insertLineBelowImageAction,
- ArrowUp: insertLineAboveImageAction,
- };
- },
-
- // storage to keep track of image states Map
- addStorage() {
- return {
- images: new Map(),
- uploadInProgress: false,
- };
- },
-
addAttributes() {
return {
...this.parent?.(),
diff --git a/packages/editor/src/core/extensions/image/utilities/insert-line-above-image.ts b/packages/editor/src/core/extensions/image/utilities/insert-line-above-image.ts
deleted file mode 100644
index 9370f33ab..000000000
--- a/packages/editor/src/core/extensions/image/utilities/insert-line-above-image.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { KeyboardShortcutCommand } from "@tiptap/core";
-import { Node as ProseMirrorNode } from "@tiptap/pm/model";
-
-export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor }) => {
- try {
- 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;
-
- const docSize = editor.state.doc.content.size;
-
- if (insertPos < 0 || insertPos > docSize) 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;
- } catch (error) {
- console.error("An error occurred while inserting a line above the image:", error);
- return false;
- }
-};
diff --git a/packages/editor/src/core/extensions/image/utilities/insert-line-below-image.ts b/packages/editor/src/core/extensions/image/utilities/insert-line-below-image.ts
deleted file mode 100644
index 4825386e3..000000000
--- a/packages/editor/src/core/extensions/image/utilities/insert-line-below-image.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { KeyboardShortcutCommand } from "@tiptap/core";
-import { Node as ProseMirrorNode } from "@tiptap/pm/model";
-
-export const insertLineBelowImageAction: KeyboardShortcutCommand = ({ editor }) => {
- try {
- 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;
- } catch (error) {
- console.error("An error occurred while inserting a line below the image:", error);
- return false;
- }
-};
diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts
index 2fb9d8974..220a11757 100644
--- a/packages/editor/src/core/extensions/index.ts
+++ b/packages/editor/src/core/extensions/index.ts
@@ -8,6 +8,7 @@ export * from "./mentions";
export * from "./table";
export * from "./typography";
export * from "./core-without-props";
+export * from "./document-without-props";
export * from "./custom-code-inline";
export * from "./drag-drop";
export * from "./drop";
diff --git a/packages/editor/src/core/extensions/issue-embed/index.ts b/packages/editor/src/core/extensions/issue-embed/index.ts
index f6674719e..f47619a03 100644
--- a/packages/editor/src/core/extensions/issue-embed/index.ts
+++ b/packages/editor/src/core/extensions/issue-embed/index.ts
@@ -1 +1,2 @@
export * from "./widget-node";
+export * from "./issue-embed-without-props";
diff --git a/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts b/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts
new file mode 100644
index 000000000..bef366cba
--- /dev/null
+++ b/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts
@@ -0,0 +1,41 @@
+import { mergeAttributes, Node } from "@tiptap/core";
+
+export const IssueWidgetWithoutProps = () =>
+ Node.create({
+ name: "issue-embed-component",
+ group: "block",
+ atom: true,
+ selectable: true,
+ draggable: true,
+
+ addAttributes() {
+ return {
+ entity_identifier: {
+ default: undefined,
+ },
+ project_identifier: {
+ default: undefined,
+ },
+ workspace_identifier: {
+ default: undefined,
+ },
+ id: {
+ default: undefined,
+ },
+ entity_name: {
+ default: undefined,
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: "issue-embed-component",
+ },
+ ];
+ },
+ renderHTML({ HTMLAttributes }) {
+ return ["issue-embed-component", mergeAttributes(HTMLAttributes)];
+ },
+ });
diff --git a/packages/editor/src/core/helpers/common.ts b/packages/editor/src/core/helpers/common.ts
index 5fbff4510..98930d94f 100644
--- a/packages/editor/src/core/helpers/common.ts
+++ b/packages/editor/src/core/helpers/common.ts
@@ -61,20 +61,3 @@ export const isValidHttpUrl = (string: string): boolean => {
return url.protocol === "http:" || url.protocol === "https:";
};
-
-/**
- * @description return an object with contentJSON and editorSchema
- * @description contentJSON- ProseMirror JSON from HTML content
- * @description editorSchema- editor schema from extensions
- * @param {string} html
- * @returns {object} {contentJSON, editorSchema}
- */
-export const generateJSONfromHTML = (html: string) => {
- const extensions = CoreEditorExtensionsWithoutProps();
- const contentJSON = generateJSON(html ?? "", extensions as Extensions);
- const editorSchema = getSchema(extensions as Extensions);
- return {
- contentJSON,
- editorSchema,
- };
-};
diff --git a/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts b/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts
new file mode 100644
index 000000000..ffad88d4e
--- /dev/null
+++ b/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts
@@ -0,0 +1,96 @@
+import { KeyboardShortcutCommand } from "@tiptap/core";
+import { Node as ProseMirrorNode } from "@tiptap/pm/model";
+
+type Direction = "up" | "down";
+
+export const insertEmptyParagraphAtNodeBoundaries: (
+ direction: Direction,
+ nodeType: string
+) => KeyboardShortcutCommand =
+ (direction, nodeType) =>
+ ({ editor }) => {
+ try {
+ const { selection, doc } = editor.state;
+ const { $from, $to } = selection;
+
+ let targetNode: ProseMirrorNode | null = null;
+ let targetNodePos: number | null = null;
+
+ // Check if the selection itself is the target node
+ doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
+ if (node.type.name === nodeType) {
+ targetNode = node;
+ targetNodePos = pos;
+ return false; // Stop iterating once the target node is found
+ }
+ return true;
+ });
+
+ if (targetNode === null || targetNodePos === null) return false;
+
+ const docSize = doc.content.size; // Get the size of the document
+
+ switch (direction) {
+ case "up": {
+ const insertPosUp = targetNodePos;
+
+ // Ensure the insert position is within the document boundaries
+ if (insertPosUp < 0 || insertPosUp > docSize) return false;
+
+ if (insertPosUp === 0) {
+ // If at the very start of the document, insert a new paragraph at the start
+ editor.chain().insertContentAt(insertPosUp, { type: "paragraph" }).run();
+ editor.chain().setTextSelection(insertPosUp).run(); // Set the cursor to the new paragraph
+ } else {
+ // Otherwise, check the node immediately before the target node
+ const prevNode = doc.nodeAt(insertPosUp - 1);
+
+ if (prevNode && prevNode.type.name === "paragraph") {
+ // If the previous node is a paragraph, move the cursor there
+ editor
+ .chain()
+ .setTextSelection(insertPosUp - 1)
+ .run();
+ } else {
+ return false; // If the previous node is not a paragraph, do not proceed
+ }
+ }
+ break;
+ }
+
+ case "down": {
+ const insertPosDown = targetNodePos + (targetNode as ProseMirrorNode).nodeSize;
+
+ // Ensure the insert position is within the document boundaries
+ if (insertPosDown < 0 || insertPosDown > docSize) return false;
+
+ // Check the node immediately after the target node
+ const nextNode = doc.nodeAt(insertPosDown);
+
+ if (nextNode && nextNode.type.name === "paragraph") {
+ // If the next node is a paragraph, move the cursor to the end of it
+ const endOfParagraphPos = insertPosDown + nextNode.nodeSize - 1;
+ editor.chain().setTextSelection(endOfParagraphPos).run();
+ } else if (!nextNode) {
+ // If there is no next node (end of document), insert a new paragraph
+ editor.chain().insertContentAt(insertPosDown, { type: "paragraph" }).run();
+ editor
+ .chain()
+ .setTextSelection(insertPosDown + 1)
+ .run(); // Set the cursor to the new paragraph
+ } else {
+ return false; // If the next node is not a paragraph, do not proceed
+ }
+ break;
+ }
+
+ default:
+ return false; // If the direction is not recognized, do not proceed
+ }
+
+ return true; // Return true if the operation was successful
+ } catch (error) {
+ console.error(`An error occurred while inserting a line ${direction} the ${nodeType}:`, error);
+ return false; // Return false if an error occurred
+ }
+ };
diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts
index 1dae4e8b3..828fab021 100644
--- a/packages/editor/src/index.ts
+++ b/packages/editor/src/index.ts
@@ -19,6 +19,7 @@ export { isCellSelection } from "@/extensions/table/table/utilities/is-cell-sele
// helpers
export * from "@/helpers/common";
+export * from "@/components/editors/document/helpers";
export * from "@/helpers/editor-commands";
export * from "@/helpers/yjs";
export * from "@/extensions/table/table";
diff --git a/web/core/hooks/use-page-description.ts b/web/core/hooks/use-page-description.ts
index 49edb3d41..f7b467d4d 100644
--- a/web/core/hooks/use-page-description.ts
+++ b/web/core/hooks/use-page-description.ts
@@ -1,7 +1,12 @@
import React, { useCallback, useEffect, useState } from "react";
import useSWR from "swr";
-import { EditorRefApi, generateJSONfromHTML, proseMirrorJSONToBinaryString, applyUpdates } from "@plane/editor";
+import {
+ EditorRefApi,
+ proseMirrorJSONToBinaryString,
+ applyUpdates,
+ generateJSONfromHTMLForDocumentEditor,
+} from "@plane/editor";
// hooks
import { setToast, TOAST_TYPE } from "@plane/ui";
@@ -71,7 +76,7 @@ export const usePageDescription = (props: Props) => {
const changeHTMLToBinary = async () => {
if (!pageDescriptionYJS || !pageDescription) return;
if (pageDescriptionYJS.length === 0) {
- const { contentJSON, editorSchema } = generateJSONfromHTML(pageDescription ?? "");
+ const { contentJSON, editorSchema } = generateJSONfromHTMLForDocumentEditor(pageDescription ?? "");
const yDocBinaryString = proseMirrorJSONToBinaryString(contentJSON, "default", editorSchema);
try {