fix: Improved Image Deletion Logic, Image ID Issue in Modals and Performance Optimization in Editor (#2092)

* added improved delete logic in modals

* added better ts support

* impoved complexity to O(1) from O(n) for large docs

* regression: removed ts nocheck
This commit is contained in:
M. Palanikannan 2023-09-07 12:22:02 +05:30 committed by GitHub
parent 85f797058d
commit b47c7d363f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 146 additions and 135 deletions

View File

@ -32,122 +32,122 @@ export const TiptapExtensions = (
workspaceSlug: string, workspaceSlug: string,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) => [ ) => [
StarterKit.configure({ StarterKit.configure({
bulletList: { bulletList: {
HTMLAttributes: { HTMLAttributes: {
class: "list-disc list-outside leading-3 -mt-2", class: "list-disc list-outside leading-3 -mt-2",
},
}, },
}, orderedList: {
orderedList: { HTMLAttributes: {
HTMLAttributes: { class: "list-decimal list-outside leading-3 -mt-2",
class: "list-decimal list-outside leading-3 -mt-2", },
}, },
}, listItem: {
listItem: { HTMLAttributes: {
HTMLAttributes: { class: "leading-normal -mb-2",
class: "leading-normal -mb-2", },
}, },
}, blockquote: {
blockquote: { HTMLAttributes: {
HTMLAttributes: { class: "border-l-4 border-custom-border-300",
class: "border-l-4 border-custom-border-300", },
}, },
}, code: {
code: { HTMLAttributes: {
class:
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
spellcheck: "false",
},
},
codeBlock: false,
horizontalRule: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
width: 2,
},
gapcursor: false,
}),
CodeBlockLowlight.configure({
lowlight,
}),
HorizontalRule.extend({
addInputRules() {
return [
new InputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
handler: ({ state, range, commands }) => {
commands.splitBlock();
const attributes = {};
const { tr } = state;
const start = range.from;
const end = range.to;
// @ts-ignore
tr.replaceWith(start - 1, end, this.type.create(attributes));
},
}),
];
},
}).configure({
HTMLAttributes: {
class: "mb-6 border-t border-custom-border-300",
},
}),
Gapcursor,
TiptapLink.configure({
protocols: ["http", "https"],
validate: (url) => isValidHttpUrl(url),
HTMLAttributes: { HTMLAttributes: {
class: class:
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
spellcheck: "false",
}, },
}, }),
codeBlock: false, UpdatedImage.configure({
horizontalRule: false, HTMLAttributes: {
dropcursor: { class: "rounded-lg border border-custom-border-300",
color: "rgba(var(--color-text-100))", },
width: 2, }),
}, Placeholder.configure({
gapcursor: false, placeholder: ({ node }) => {
}), if (node.type.name === "heading") {
CodeBlockLowlight.configure({ return `Heading ${node.attrs.level}`;
lowlight, }
}), if (node.type.name === "image" || node.type.name === "table") {
HorizontalRule.extend({ return "";
addInputRules() { }
return [
new InputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
handler: ({ state, range, commands }) => {
commands.splitBlock();
const attributes = {}; return "Press '/' for commands...";
const { tr } = state; },
const start = range.from; includeChildren: true,
const end = range.to; }),
// @ts-ignore UniqueID.configure({
tr.replaceWith(start - 1, end, this.type.create(attributes)); types: ["image"],
}, }),
}), SlashCommand(workspaceSlug, setIsSubmitting),
]; TiptapUnderline,
}, TextStyle,
}).configure({ Color,
HTMLAttributes: { Highlight.configure({
class: "mb-6 border-t border-custom-border-300", multicolor: true,
}, }),
}), TaskList.configure({
Gapcursor, HTMLAttributes: {
TiptapLink.configure({ class: "not-prose pl-2",
protocols: ["http", "https"], },
validate: (url) => isValidHttpUrl(url), }),
HTMLAttributes: { TaskItem.configure({
class: HTMLAttributes: {
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", class: "flex items-start my-4",
}, },
}), nested: true,
UpdatedImage.configure({ }),
HTMLAttributes: { Markdown.configure({
class: "rounded-lg border border-custom-border-300", html: true,
}, transformCopiedText: true,
}), }),
Placeholder.configure({ Table,
placeholder: ({ node }) => { TableHeader,
if (node.type.name === "heading") { CustomTableCell,
return `Heading ${node.attrs.level}`; TableRow,
} ];
if (node.type.name === "image" || node.type.name === "table") {
return "";
}
return "Press '/' for commands...";
},
includeChildren: true,
}),
UniqueID.configure({
types: ["image"],
}),
SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline,
TextStyle,
Color,
Highlight.configure({
multicolor: true,
}),
TaskList.configure({
HTMLAttributes: {
class: "not-prose pl-2",
},
}),
TaskItem.configure({
HTMLAttributes: {
class: "flex items-start my-4",
},
nested: true,
}),
Markdown.configure({
html: true,
transformCopiedText: true,
}),
Table,
TableHeader,
CustomTableCell,
TableRow,
];

View File

@ -1,43 +1,51 @@
import { Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import fileService from "services/file.service"; import fileService from "services/file.service";
const deleteKey = new PluginKey("delete-image"); const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image";
const TrackImageDeletionPlugin = () => interface ImageNode extends ProseMirrorNode {
attrs: {
src: string;
id: string;
};
}
const TrackImageDeletionPlugin = (): Plugin =>
new Plugin({ new Plugin({
key: deleteKey, key: deleteKey,
appendTransaction: (transactions, oldState, newState) => { appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
const newImageSources = new Set();
newState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) {
newImageSources.add(node.attrs.src);
}
});
transactions.forEach((transaction) => { transactions.forEach((transaction) => {
if (!transaction.docChanged) return; if (!transaction.docChanged) return;
const removedImages: ProseMirrorNode[] = []; const removedImages: ImageNode[] = [];
oldState.doc.descendants((oldNode, oldPos) => { oldState.doc.descendants((oldNode, oldPos) => {
if (oldNode.type.name !== "image") return; if (oldNode.type.name !== IMAGE_NODE_TYPE) return;
if (oldPos < 0 || oldPos > newState.doc.content.size) return; if (oldPos < 0 || oldPos > newState.doc.content.size) return;
if (!newState.doc.resolve(oldPos).parent) return; if (!newState.doc.resolve(oldPos).parent) return;
const newNode = newState.doc.nodeAt(oldPos); const newNode = newState.doc.nodeAt(oldPos);
// Check if the node has been deleted or replaced // Check if the node has been deleted or replaced
if (!newNode || newNode.type.name !== "image") { if (!newNode || newNode.type.name !== IMAGE_NODE_TYPE) {
// Check if the node still exists elsewhere in the document if (!newImageSources.has(oldNode.attrs.src)) {
let nodeExists = false; removedImages.push(oldNode as ImageNode);
newState.doc.descendants((node) => {
if (node.attrs.id === oldNode.attrs.id) {
nodeExists = true;
}
});
if (!nodeExists) {
removedImages.push(oldNode as ProseMirrorNode);
} }
} }
}); });
removedImages.forEach((node) => { removedImages.forEach(async (node) => {
const src = node.attrs.src; const src = node.attrs.src;
onNodeDeleted(src); await onNodeDeleted(src);
}); });
}); });
@ -47,10 +55,14 @@ const TrackImageDeletionPlugin = () =>
export default TrackImageDeletionPlugin; export default TrackImageDeletionPlugin;
async function onNodeDeleted(src: string) { async function onNodeDeleted(src: string): Promise<void> {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); try {
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
if (resStatus === 204) { const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
console.log("Image deleted successfully"); if (resStatus === 204) {
console.log("Image deleted successfully");
}
} catch (error) {
console.error("Error deleting image: ", error);
} }
} }

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
import fileService from "services/file.service"; import fileService from "services/file.service";