Skip to content

Commit

Permalink
[WEB-1244] fix: add better image insertion and replacement logic in t…
Browse files Browse the repository at this point in the history
…he editor (#4508)

* fix: add better image insertion and replacement logic

* refactor: image handling in editor

* chore: remove passing uploadKey around

* refactor: remove unused code

* fix: redundant files removed

* fix: add is editor ready to discard api to control behvaiours from our app

* fix: focus issues and image insertion position when not using slash command

* fix: import order fixed
  • Loading branch information
Palanikannan1437 authored May 29, 2024
1 parent 061a447 commit ade6ede
Show file tree
Hide file tree
Showing 22 changed files with 483 additions and 366 deletions.
3 changes: 2 additions & 1 deletion packages/editor/core/src/hooks/use-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export const useEditor = ({
const item = getEditorMenuItem(itemName);
if (item) {
if (item.key === "image") {
item.command(savedSelection);
item.command(savedSelectionRef.current);
} else {
item.command();
}
Expand Down Expand Up @@ -186,6 +186,7 @@ export const useEditor = ({
if (!editorRef.current) return;
scrollSummary(editorRef.current, marking);
},
isEditorReadyToDiscard: () => editorRef.current?.storage.image.uploadInProgress === false,
setFocusAtPosition: (position: number) => {
if (!editorRef.current || editorRef.current.isDestroyed) {
console.error("Editor reference is not available or has been destroyed.");
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export { isCellSelection } from "src/ui/extensions/table/table/utilities/is-cell
// utils
export * from "src/lib/utils";
export * from "src/ui/extensions/table/table";
export { startImageUpload } from "src/ui/plugins/upload-image";
export { startImageUpload } from "src/ui/plugins/image/image-upload-handler";

// components
export { EditorContainer } from "src/ui/components/editor-container";
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/core/src/lib/editor-commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Editor, Range } from "@tiptap/core";
import { startImageUpload } from "src/ui/plugins/upload-image";
import { startImageUpload } from "src/ui/plugins/image/image-upload-handler";
import { findTableAncestor } from "src/lib/utils";
import { Selection } from "@tiptap/pm/state";
import { UploadImage } from "src/types/upload-image";
Expand Down Expand Up @@ -194,7 +194,7 @@ export const insertImageCommand = (
if (range) editor.chain().focus().deleteRange(range).run();
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.accept = ".jpeg, .jpg, .png, .webp, .svg";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
Expand Down
1 change: 1 addition & 0 deletions packages/editor/core/src/types/editor-ref-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
isMenuItemActive: (itemName: EditorMenuItemNames) => boolean;
onStateChange: (callback: () => void) => () => void;
setFocusAtPosition: (position: number) => void;
isEditorReadyToDiscard: () => boolean;
}
2 changes: 1 addition & 1 deletion packages/editor/core/src/ui/extensions/drop.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "prosemirror-state";
import { UploadImage } from "src/types/upload-image";
import { startImageUpload } from "../plugins/upload-image";
import { startImageUpload } from "src/ui/plugins/image/image-upload-handler";

export const DropHandlerExtension = (uploadFile: UploadImage) =>
Extension.create({
Expand Down
100 changes: 11 additions & 89 deletions packages/editor/core/src/ui/extensions/image/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { UploadImagesPlugin } from "src/ui/plugins/upload-image";
import { UploadImagesPlugin } from "src/ui/plugins/image/upload-image";
import ImageExt from "@tiptap/extension-image";
import { onNodeDeleted, onNodeRestored } from "src/ui/plugins/delete-image";
import { TrackImageDeletionPlugin } from "src/ui/plugins/image/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";
import { TrackImageRestorationPlugin } from "src/ui/plugins/image/restore-image";
import { IMAGE_NODE_TYPE } from "src/ui/plugins/image/constants";
import { ImageExtensionStorage } from "src/ui/plugins/image/types/image-node";

interface ImageNode extends ProseMirrorNode {
attrs: {
src: string;
id: string;
};
}

const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image";

export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => void) =>
ImageExt.extend({
export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreImage, cancelUploadImage?: () => void) =>
ImageExt.extend<any, ImageExtensionStorage>({
addKeyboardShortcuts() {
return {
ArrowDown: insertLineBelowImageAction,
Expand All @@ -29,77 +20,8 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
addProseMirrorPlugins() {
return [
UploadImagesPlugin(this.editor, cancelUploadImage),
new Plugin({
key: deleteKey,
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
const newImageSources = new Set<string>();
newState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) {
newImageSources.add(node.attrs.src);
}
});

transactions.forEach((transaction) => {
// transaction could be a selection
if (!transaction.docChanged) return;

const removedImages: ImageNode[] = [];

// iterate through all the nodes in the old state
oldState.doc.descendants((oldNode, oldPos) => {
// if the node is not an image, then return as no point in checking
if (oldNode.type.name !== IMAGE_NODE_TYPE) return;

// Check if the node has been deleted or replaced
if (!newImageSources.has(oldNode.attrs.src)) {
removedImages.push(oldNode as ImageNode);
}
});

removedImages.forEach(async (node) => {
const src = node.attrs.src;
this.storage.images.set(src, true);
await onNodeDeleted(src, deleteImage);
});
});

return null;
},
}),
new Plugin({
key: new PluginKey("imageRestoration"),
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
const oldImageSources = new Set<string>();
oldState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) {
oldImageSources.add(node.attrs.src);
}
});

transactions.forEach((transaction) => {
if (!transaction.docChanged) return;

const addedImages: ImageNode[] = [];

newState.doc.descendants((node, pos) => {
if (node.type.name !== IMAGE_NODE_TYPE) return;
if (pos < 0 || pos > newState.doc.content.size) return;
if (oldImageSources.has(node.attrs.src)) return;
addedImages.push(node as ImageNode);
});

addedImages.forEach(async (image) => {
const wasDeleted = this.storage.images.get(image.attrs.src);
if (wasDeleted === undefined) {
this.storage.images.set(image.attrs.src, false);
} else if (wasDeleted === true) {
await onNodeRestored(image.attrs.src, restoreFile);
}
});
});
return null;
},
}),
TrackImageDeletionPlugin(this.editor, deleteImage),
TrackImageRestorationPlugin(this.editor, restoreImage),
];
},

Expand All @@ -113,7 +35,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
imageSources.forEach(async (src) => {
try {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
await restoreFile(assetUrlWithWorkspaceId);
await restoreImage(assetUrlWithWorkspaceId);
} catch (error) {
console.error("Error restoring image: ", error);
}
Expand All @@ -123,7 +45,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
// storage to keep track of image states Map<src, isDeleted>
addStorage() {
return {
images: new Map<string, boolean>(),
deletedImageSet: new Map<string, boolean>(),
uploadInProgress: false,
};
},
Expand Down
3 changes: 3 additions & 0 deletions packages/editor/core/src/ui/extensions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,11 @@ export const CoreEditorExtensions = ({
placeholder: ({ editor, node }) => {
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;

if (editor.storage.image.uploadInProgress) return "";

const shouldHidePlaceholder =
editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");

if (shouldHidePlaceholder) return "";

if (placeholder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,5 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag
];
}

export type EditorMenuItemNames = ReturnType<typeof getEditorMenuItems> extends (infer U)[]
? U extends { key: infer N }
? N
: never
: never;
export type EditorMenuItemNames =
ReturnType<typeof getEditorMenuItems> extends (infer U)[] ? (U extends { key: infer N } ? N : never) : never;
73 changes: 0 additions & 73 deletions packages/editor/core/src/ui/plugins/delete-image.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions packages/editor/core/src/ui/plugins/image/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PluginKey } from "@tiptap/pm/state";

export const uploadKey = new PluginKey("upload-image");
export const deleteKey = new PluginKey("delete-image");
export const restoreKey = new PluginKey("restore-image");

export const IMAGE_NODE_TYPE = "image";
54 changes: 54 additions & 0 deletions packages/editor/core/src/ui/plugins/image/delete-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { EditorState, Plugin, Transaction } from "@tiptap/pm/state";
import { DeleteImage } from "src/types/delete-image";
import { Editor } from "@tiptap/core";

import { type ImageNode } from "src/ui/plugins/image/types/image-node";
import { deleteKey, IMAGE_NODE_TYPE } from "src/ui/plugins/image/constants";

export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImage): Plugin =>
new Plugin({
key: deleteKey,
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
const newImageSources = new Set<string>();
newState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) {
newImageSources.add(node.attrs.src);
}
});

transactions.forEach((transaction) => {
// transaction could be a selection
if (!transaction.docChanged) return;

const removedImages: ImageNode[] = [];

// iterate through all the nodes in the old state
oldState.doc.descendants((oldNode) => {
// if the node is not an image, then return as no point in checking
if (oldNode.type.name !== IMAGE_NODE_TYPE) return;

// Check if the node has been deleted or replaced
if (!newImageSources.has(oldNode.attrs.src)) {
removedImages.push(oldNode as ImageNode);
}
});

removedImages.forEach(async (node) => {
const src = node.attrs.src;
editor.storage.image.deletedImageSet.set(src, true);
await onNodeDeleted(src, deleteImage);
});
});

return null;
},
});

async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
try {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
await deleteImage(assetUrlWithWorkspaceId);
} catch (error) {
console.error("Error deleting image: ", error);
}
}
Loading

0 comments on commit ade6ede

Please sign in to comment.