[WIKI-181] refactor: make file handling generic in editor (#7046)
* refactor: make file handling generic * fix: useeffect dependency array * chore: remove mime type to extension conversion
This commit is contained in:
parent
e68d344410
commit
dc16f2862e
17 changed files with 334 additions and 223 deletions
|
|
@ -1,9 +1,10 @@
|
|||
import { Extensions } from "@tiptap/core";
|
||||
// types
|
||||
import { TExtensions } from "@/types";
|
||||
import { TExtensions, TFileHandler } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: TFileHandler;
|
||||
};
|
||||
|
||||
export const CoreEditorAdditionalExtensions = (props: Props): Extensions => {
|
||||
|
|
|
|||
|
|
@ -8,5 +8,55 @@ export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
|
|||
wideLayout: false,
|
||||
};
|
||||
|
||||
export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"];
|
||||
export const ACCEPTED_FILE_EXTENSIONS = ACCEPTED_FILE_MIME_TYPES.map((type) => `.${type.split("/")[1]}`);
|
||||
export const ACCEPTED_IMAGE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"];
|
||||
|
||||
export const ACCEPTED_ATTACHMENT_MIME_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/svg+xml",
|
||||
"image/webp",
|
||||
"image/tiff",
|
||||
"image/bmp",
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"text/plain",
|
||||
"application/rtf",
|
||||
"audio/mpeg",
|
||||
"audio/wav",
|
||||
"audio/ogg",
|
||||
"audio/midi",
|
||||
"audio/x-midi",
|
||||
"audio/aac",
|
||||
"audio/flac",
|
||||
"audio/x-m4a",
|
||||
"video/mp4",
|
||||
"video/mpeg",
|
||||
"video/ogg",
|
||||
"video/webm",
|
||||
"video/quicktime",
|
||||
"video/x-msvideo",
|
||||
"video/x-ms-wmv",
|
||||
"application/zip",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-tar",
|
||||
"application/gzip",
|
||||
"model/gltf-binary",
|
||||
"model/gltf+json",
|
||||
"application/octet-stream",
|
||||
"font/ttf",
|
||||
"font/otf",
|
||||
"font/woff",
|
||||
"font/woff2",
|
||||
"text/css",
|
||||
"text/javascript",
|
||||
"application/json",
|
||||
"text/xml",
|
||||
"text/csv",
|
||||
"application/xml",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
|
|||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { ACCEPTED_FILE_EXTENSIONS } from "@/constants/config";
|
||||
import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||
// extensions
|
||||
import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
// hooks
|
||||
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
|
||||
type CustomImageUploaderProps = CustoBaseImageNodeViewProps & {
|
||||
maxFileSize: number;
|
||||
|
|
@ -41,7 +41,9 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
if (!imageEntityId) return;
|
||||
setIsUploaded(true);
|
||||
// Update the node view's src attribute post upload
|
||||
updateAttributes({ src: url });
|
||||
updateAttributes({
|
||||
src: url,
|
||||
});
|
||||
imageComponentImageFileMap?.delete(imageEntityId);
|
||||
|
||||
const pos = getPos();
|
||||
|
|
@ -51,7 +53,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
|
||||
// only if the cursor is at the current image component, manipulate
|
||||
// the cursor position
|
||||
if (currentNode && currentNode.type.name === "imageComponent" && currentNode.attrs.src === url) {
|
||||
if (currentNode && currentNode.type.name === node.type.name && currentNode.attrs.src === url) {
|
||||
// control cursor position after upload
|
||||
const nextNode = editor.state.doc.nodeAt(pos + 1);
|
||||
|
||||
|
|
@ -68,17 +70,23 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
[imageComponentImageFileMap, imageEntityId, updateAttributes, getPos]
|
||||
);
|
||||
// hooks
|
||||
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({
|
||||
blockId: imageEntityId ?? "",
|
||||
editor,
|
||||
loadImageFromFileSystem,
|
||||
const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
// @ts-expect-error - TODO: fix typings, and don't remove await from here for now
|
||||
editorCommand: async (file) => await editor?.commands.uploadImage(imageEntityId, file),
|
||||
handleProgressStatus: (isUploading) => {
|
||||
editor.storage.imageComponent.uploadInProgress = isUploading;
|
||||
},
|
||||
loadFileFromFileSystem: loadImageFromFileSystem,
|
||||
maxFileSize,
|
||||
onUpload,
|
||||
});
|
||||
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
editor,
|
||||
maxFileSize,
|
||||
pos: getPos(),
|
||||
type: "image",
|
||||
uploader: uploadFile,
|
||||
});
|
||||
|
||||
|
|
@ -110,11 +118,13 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
if (!filesList) {
|
||||
return;
|
||||
}
|
||||
await uploadFirstImageAndInsertRemaining({
|
||||
await uploadFirstFileAndInsertRemaining({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
pos: getPos(),
|
||||
type: "image",
|
||||
uploader: uploadFile,
|
||||
});
|
||||
},
|
||||
|
|
@ -170,7 +180,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
ref={fileInputRef}
|
||||
hidden
|
||||
type="file"
|
||||
accept={ACCEPTED_FILE_EXTENSIONS.join(",")}
|
||||
accept={ACCEPTED_IMAGE_MIME_TYPES.join(",")}
|
||||
onChange={onFileChange}
|
||||
multiple
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ import { Editor, mergeAttributes } from "@tiptap/core";
|
|||
import { Image } from "@tiptap/extension-image";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// constants
|
||||
import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||
// extensions
|
||||
import { CustomImageNode } from "@/extensions/custom-image";
|
||||
// helpers
|
||||
import { isFileValid } from "@/helpers/file";
|
||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||
// plugins
|
||||
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
|
||||
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
|
||||
|
|
@ -146,6 +149,7 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
if (
|
||||
props?.file &&
|
||||
!isFileValid({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
file: props.file,
|
||||
maxFileSize,
|
||||
})
|
||||
|
|
|
|||
127
packages/editor/src/core/extensions/drop.ts
Normal file
127
packages/editor/src/core/extensions/drop.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { Extension, Editor } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
// constants
|
||||
import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
|
||||
export const DropHandlerExtension = Extension.create({
|
||||
name: "dropHandler",
|
||||
priority: 1000,
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("drop-handler-plugin"),
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
if (
|
||||
editor.isEditable &&
|
||||
event.clipboardData &&
|
||||
event.clipboardData.files &&
|
||||
event.clipboardData.files.length > 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
const files = Array.from(event.clipboardData.files);
|
||||
const acceptedFiles = files.filter(
|
||||
(f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type)
|
||||
);
|
||||
|
||||
if (acceptedFiles.length) {
|
||||
const pos = view.state.selection.from;
|
||||
insertFilesSafely({
|
||||
editor,
|
||||
files: acceptedFiles,
|
||||
initialPos: pos,
|
||||
event: "drop",
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handleDrop: (view, event, _slice, moved) => {
|
||||
if (
|
||||
editor.isEditable &&
|
||||
!moved &&
|
||||
event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files.length > 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
const acceptedFiles = files.filter(
|
||||
(f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type)
|
||||
);
|
||||
|
||||
if (acceptedFiles.length) {
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
if (coordinates) {
|
||||
const pos = coordinates.pos;
|
||||
insertFilesSafely({
|
||||
editor,
|
||||
files: acceptedFiles,
|
||||
initialPos: pos,
|
||||
event: "drop",
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
type InsertFilesSafelyArgs = {
|
||||
editor: Editor;
|
||||
event: "insert" | "drop";
|
||||
files: File[];
|
||||
initialPos: number;
|
||||
type?: Extract<TEditorCommands, "attachment" | "image">;
|
||||
};
|
||||
|
||||
export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => {
|
||||
const { editor, event, files, initialPos, type } = args;
|
||||
let pos = initialPos;
|
||||
|
||||
for (const file of files) {
|
||||
// safe insertion
|
||||
const docSize = editor.state.doc.content.size;
|
||||
pos = Math.min(pos, docSize);
|
||||
|
||||
let fileType: "image" | "attachment" | null = null;
|
||||
|
||||
try {
|
||||
if (type) {
|
||||
if (["image", "attachment"].includes(type)) fileType = type;
|
||||
else throw new Error("Wrong file type passed");
|
||||
} else {
|
||||
if (ACCEPTED_IMAGE_MIME_TYPES.includes(file.type)) fileType = "image";
|
||||
else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment";
|
||||
}
|
||||
// insert file depending on the type at the current position
|
||||
if (fileType === "image") {
|
||||
editor.commands.insertImageComponent({
|
||||
file,
|
||||
pos,
|
||||
event,
|
||||
});
|
||||
} else if (fileType === "attachment") {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error while ${event}ing file:`, error);
|
||||
}
|
||||
|
||||
// Move to the next position
|
||||
pos += 1;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import { Extension, Editor } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { EditorView } from "@tiptap/pm/view";
|
||||
|
||||
export const DropHandlerExtension = Extension.create({
|
||||
name: "dropHandler",
|
||||
priority: 1000,
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("drop-handler-plugin"),
|
||||
props: {
|
||||
handlePaste: (view: EditorView, event: ClipboardEvent) => {
|
||||
if (
|
||||
editor.isEditable &&
|
||||
event.clipboardData &&
|
||||
event.clipboardData.files &&
|
||||
event.clipboardData.files.length > 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
const files = Array.from(event.clipboardData.files);
|
||||
const imageFiles = files.filter((file) => file.type.startsWith("image"));
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
const pos = view.state.selection.from;
|
||||
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => {
|
||||
if (
|
||||
editor.isEditable &&
|
||||
!moved &&
|
||||
event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files.length > 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
const imageFiles = files.filter((file) => file.type.startsWith("image"));
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
if (coordinates) {
|
||||
const pos = coordinates.pos;
|
||||
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
export const insertImagesSafely = async ({
|
||||
editor,
|
||||
files,
|
||||
initialPos,
|
||||
event,
|
||||
}: {
|
||||
editor: Editor;
|
||||
files: File[];
|
||||
initialPos: number;
|
||||
event: "insert" | "drop";
|
||||
}) => {
|
||||
let pos = initialPos;
|
||||
|
||||
for (const file of files) {
|
||||
// safe insertion
|
||||
const docSize = editor.state.doc.content.size;
|
||||
pos = Math.min(pos, docSize);
|
||||
|
||||
try {
|
||||
// Insert the image at the current position
|
||||
editor.commands.insertImageComponent({ file, pos, event });
|
||||
} catch (error) {
|
||||
console.error(`Error while ${event}ing image:`, error);
|
||||
}
|
||||
|
||||
// Move to the next position
|
||||
pos += 1;
|
||||
}
|
||||
};
|
||||
|
|
@ -172,6 +172,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
|||
CustomColorExtension,
|
||||
...CoreEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Editor, Range } from "@tiptap/core";
|
||||
// types
|
||||
import { InsertImageComponentProps } from "@/extensions";
|
||||
// extensions
|
||||
import { InsertImageComponentProps } from "@/extensions";
|
||||
import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text";
|
||||
// helpers
|
||||
import { findTableAncestor } from "@/helpers/common";
|
||||
|
|
@ -206,6 +205,7 @@ export const insertHorizontalRule = (editor: Editor, range?: Range) => {
|
|||
if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
else editor.chain().focus().setHorizontalRule().run();
|
||||
};
|
||||
|
||||
export const insertCallout = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
|
||||
else editor.chain().focus().insertCallout().run();
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
// constants
|
||||
import { ACCEPTED_FILE_MIME_TYPES } from "@/constants/config";
|
||||
|
||||
type TArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
file: File;
|
||||
maxFileSize: number;
|
||||
};
|
||||
|
||||
export const isFileValid = (args: TArgs): boolean => {
|
||||
const { file, maxFileSize } = args;
|
||||
const { acceptedMimeTypes, file, maxFileSize } = args;
|
||||
|
||||
if (!file) {
|
||||
alert("No file selected. Please select a file to upload.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ACCEPTED_FILE_MIME_TYPES.includes(file.type)) {
|
||||
if (!acceptedMimeTypes.includes(file.type)) {
|
||||
alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP or GIF file.");
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1,87 +1,87 @@
|
|||
import { DragEvent, useCallback, useEffect, useState } from "react";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { DragEvent, useCallback, useEffect, useState } from "react";
|
||||
// extensions
|
||||
import { insertImagesSafely } from "@/extensions/drop";
|
||||
import { insertFilesSafely } from "@/extensions/drop";
|
||||
// plugins
|
||||
import { isFileValid } from "@/plugins/image";
|
||||
import { isFileValid } from "@/helpers/file";
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
|
||||
type TUploaderArgs = {
|
||||
blockId: string;
|
||||
editor: Editor;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
acceptedMimeTypes: string[];
|
||||
editorCommand: (file: File) => Promise<string>;
|
||||
handleProgressStatus?: (isUploading: boolean) => void;
|
||||
loadFileFromFileSystem?: (file: string) => void;
|
||||
maxFileSize: number;
|
||||
onUpload: (url: string) => void;
|
||||
onUpload: (url: string, file: File) => void;
|
||||
};
|
||||
|
||||
export const useUploader = (args: TUploaderArgs) => {
|
||||
const { blockId, editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
|
||||
const { acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload } =
|
||||
args;
|
||||
// states
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const uploadFile = useCallback(
|
||||
async (file: File) => {
|
||||
const setImageUploadInProgress = (isUploading: boolean) => {
|
||||
if (editor.storage.imageComponent) {
|
||||
editor.storage.imageComponent.uploadInProgress = isUploading;
|
||||
}
|
||||
};
|
||||
setImageUploadInProgress(true);
|
||||
setUploading(true);
|
||||
const fileNameTrimmed = trimFileName(file.name);
|
||||
const fileWithTrimmedName = new File([file], fileNameTrimmed, { type: file.type });
|
||||
handleProgressStatus?.(true);
|
||||
setIsUploading(true);
|
||||
const isValid = isFileValid({
|
||||
file: fileWithTrimmedName,
|
||||
acceptedMimeTypes,
|
||||
file,
|
||||
maxFileSize,
|
||||
});
|
||||
if (!isValid) {
|
||||
setImageUploadInProgress(false);
|
||||
handleProgressStatus?.(false);
|
||||
setIsUploading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (reader.result) {
|
||||
loadImageFromFileSystem(reader.result as string);
|
||||
} else {
|
||||
console.error("Failed to read the file: reader.result is null");
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error("Error reading file");
|
||||
};
|
||||
reader.readAsDataURL(fileWithTrimmedName);
|
||||
// @ts-expect-error - TODO: fix typings, and don't remove await from
|
||||
// here for now
|
||||
const url: string = await editor?.commands.uploadImage(blockId, fileWithTrimmedName);
|
||||
if (loadFileFromFileSystem) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (reader.result) {
|
||||
loadFileFromFileSystem(reader.result as string);
|
||||
} else {
|
||||
console.error("Failed to read the file: reader.result is null");
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error("Error reading file");
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
const url: string = await editorCommand(file);
|
||||
|
||||
if (!url) {
|
||||
throw new Error("Something went wrong while uploading the image");
|
||||
throw new Error("Something went wrong while uploading the file.");
|
||||
}
|
||||
onUpload(url);
|
||||
} catch (errPayload: any) {
|
||||
console.log(errPayload);
|
||||
onUpload(url, file);
|
||||
} catch (errPayload) {
|
||||
const error = errPayload?.response?.data?.error || "Something went wrong";
|
||||
console.error(error);
|
||||
} finally {
|
||||
setImageUploadInProgress(false);
|
||||
setUploading(false);
|
||||
handleProgressStatus?.(false);
|
||||
setIsUploading(false);
|
||||
}
|
||||
},
|
||||
[onUpload]
|
||||
[acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload]
|
||||
);
|
||||
|
||||
return { uploading, uploadFile };
|
||||
return { isUploading, uploadFile };
|
||||
};
|
||||
|
||||
type TDropzoneArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editor: Editor;
|
||||
maxFileSize: number;
|
||||
pos: number;
|
||||
type: Extract<TEditorCommands, "attachment" | "image">;
|
||||
uploader: (file: File) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useDropZone = (args: TDropzoneArgs) => {
|
||||
const { editor, maxFileSize, pos, uploader } = args;
|
||||
const { acceptedMimeTypes, editor, maxFileSize, pos, type, uploader } = args;
|
||||
// states
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [draggedInside, setDraggedInside] = useState<boolean>(false);
|
||||
|
|
@ -112,83 +112,79 @@ export const useDropZone = (args: TDropzoneArgs) => {
|
|||
return;
|
||||
}
|
||||
const filesList = e.dataTransfer.files;
|
||||
await uploadFirstImageAndInsertRemaining({
|
||||
await uploadFirstFileAndInsertRemaining({
|
||||
acceptedMimeTypes,
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
pos,
|
||||
type,
|
||||
uploader,
|
||||
});
|
||||
},
|
||||
[uploader, editor, pos]
|
||||
[acceptedMimeTypes, editor, maxFileSize, pos, type, uploader]
|
||||
);
|
||||
const onDragEnter = useCallback(() => setDraggedInside(true), []);
|
||||
const onDragLeave = useCallback(() => setDraggedInside(false), []);
|
||||
|
||||
const onDragEnter = () => {
|
||||
setDraggedInside(true);
|
||||
return {
|
||||
isDragging,
|
||||
draggedInside,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
setDraggedInside(false);
|
||||
};
|
||||
|
||||
return { isDragging, draggedInside, onDragEnter, onDragLeave, onDrop };
|
||||
};
|
||||
|
||||
function trimFileName(fileName: string, maxLength = 100) {
|
||||
if (fileName.length > maxLength) {
|
||||
const extension = fileName.split(".").pop();
|
||||
const nameWithoutExtension = fileName.slice(0, -(extension?.length ?? 0 + 1));
|
||||
const allowedNameLength = maxLength - (extension?.length ?? 0) - 1; // -1 for the dot
|
||||
return `${nameWithoutExtension.slice(0, allowedNameLength)}.${extension}`;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
type TMultipleImagesArgs = {
|
||||
type TMultipleFileArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editor: Editor;
|
||||
filesList: FileList;
|
||||
maxFileSize: number;
|
||||
pos: number;
|
||||
type: Extract<TEditorCommands, "attachment" | "image">;
|
||||
uploader: (file: File) => Promise<void>;
|
||||
};
|
||||
|
||||
// Upload the first image and insert the remaining images for uploading multiple image
|
||||
// post insertion of image-component
|
||||
export async function uploadFirstImageAndInsertRemaining(args: TMultipleImagesArgs) {
|
||||
const { editor, filesList, maxFileSize, pos, uploader } = args;
|
||||
// Upload the first file and insert the remaining ones for uploading multiple files
|
||||
export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => {
|
||||
const { acceptedMimeTypes, editor, filesList, maxFileSize, pos, type, uploader } = args;
|
||||
const filteredFiles: File[] = [];
|
||||
for (let i = 0; i < filesList.length; i += 1) {
|
||||
const item = filesList.item(i);
|
||||
const file = filesList.item(i);
|
||||
if (
|
||||
item &&
|
||||
item.type.indexOf("image") !== -1 &&
|
||||
file &&
|
||||
isFileValid({
|
||||
file: item,
|
||||
acceptedMimeTypes,
|
||||
file,
|
||||
maxFileSize,
|
||||
})
|
||||
) {
|
||||
filteredFiles.push(item);
|
||||
filteredFiles.push(file);
|
||||
}
|
||||
}
|
||||
if (filteredFiles.length !== filesList.length) {
|
||||
console.warn("Some files were not images and have been ignored.");
|
||||
console.warn("Some files were invalid and have been ignored.");
|
||||
}
|
||||
if (filteredFiles.length === 0) {
|
||||
console.error("No image files found to upload");
|
||||
console.error("No files found to upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the first image
|
||||
// Upload the first file
|
||||
const firstFile = filteredFiles[0];
|
||||
uploader(firstFile);
|
||||
|
||||
// Insert the remaining images
|
||||
// Insert the remaining files
|
||||
const remainingFiles = filteredFiles.slice(1);
|
||||
|
||||
if (remainingFiles.length > 0) {
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const posOfNextImageToBeInserted = Math.min(pos + 1, docSize);
|
||||
insertImagesSafely({ editor, files: remainingFiles, initialPos: posOfNextImageToBeInserted, event: "drop" });
|
||||
const posOfNextFileToBeInserted = Math.min(pos + 1, docSize);
|
||||
insertFilesSafely({
|
||||
editor,
|
||||
files: remainingFiles,
|
||||
initialPos: posOfNextFileToBeInserted,
|
||||
event: "drop",
|
||||
type,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ const verticalEllipsisIcon =
|
|||
|
||||
const generalSelectors = [
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
"p.editor-paragraph-block:not(:first-child)",
|
||||
".code-block",
|
||||
"blockquote",
|
||||
"h1, h2, h3, h4, h5, h6",
|
||||
"h1.editor-heading-block, h2.editor-heading-block, h3.editor-heading-block, h4.editor-heading-block, h5.editor-heading-block, h6.editor-heading-block",
|
||||
"[data-type=horizontalRule]",
|
||||
".table-wrapper",
|
||||
".issue-embed",
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
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";
|
||||
|
|
@ -37,20 +37,16 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag
|
|||
|
||||
removedImages.forEach(async (node) => {
|
||||
const src = node.attrs.src;
|
||||
editor.storage[nodeType].deletedImageSet.set(src, true);
|
||||
await onNodeDeleted(src, deleteImage);
|
||||
editor.storage[nodeType].deletedImageSet?.set(src, true);
|
||||
if (!src) return;
|
||||
try {
|
||||
await deleteImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error deleting image:", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
|
||||
if (!src) return;
|
||||
try {
|
||||
await deleteImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error deleting image: ", error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
export * from "./types";
|
||||
export * from "./utils";
|
||||
export * from "./constants";
|
||||
export * from "./delete-image";
|
||||
export * from "./restore-image";
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export * from "./validate-file";
|
||||
32
packages/utils/src/attachment.ts
Normal file
32
packages/utils/src/attachment.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export const generateFileName = (fileName: string) => {
|
||||
const date = new Date();
|
||||
const timestamp = date.getTime();
|
||||
|
||||
const _fileName = getFileName(fileName);
|
||||
const nameWithoutExtension = _fileName.length > 80 ? _fileName.substring(0, 80) : _fileName;
|
||||
const extension = getFileExtension(fileName);
|
||||
|
||||
return `${nameWithoutExtension}-${timestamp}.${extension}`;
|
||||
};
|
||||
|
||||
export const getFileExtension = (filename: string) => filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2);
|
||||
|
||||
export const getFileName = (fileName: string) => {
|
||||
const dotIndex = fileName.lastIndexOf(".");
|
||||
|
||||
const nameWithoutExtension = fileName.substring(0, dotIndex);
|
||||
|
||||
return nameWithoutExtension;
|
||||
};
|
||||
|
||||
export const convertBytesToSize = (bytes: number) => {
|
||||
let size;
|
||||
|
||||
if (bytes < 1024 * 1024) {
|
||||
size = Math.round(bytes / 1024) + " KB";
|
||||
} else {
|
||||
size = Math.round(bytes / (1024 * 1024)) + " MB";
|
||||
}
|
||||
|
||||
return size;
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./array";
|
||||
export * from "./attachment";
|
||||
export * from "./auth";
|
||||
export * from "./datetime";
|
||||
export * from "./color";
|
||||
|
|
@ -16,4 +17,3 @@ export * from "./work-item";
|
|||
export * from "./get-icon-for-link";
|
||||
|
||||
export * from "./subscription";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue