[WIKI-419] chore: new asset duplicate endpoint added (#7172)
* chore: new asset duplicate endpoint added * chore: change the type in url * chore: added rate limiting for image duplication endpoint * chore: added rate limiting per asset id * chore: added throttle class * chore: added validations for entity * chore: added extra validations * chore: removed the comment * chore: reverted the frontend code * chore: added the response key * feat: handle image duplication for web * feat: custom image duplication update * fix: remove paste logic for image * fix : remove entity validation * refactor: remove entity id for duplication * feat: handle duplication in utils * feat: add asset duplication registry * chore: update the set attribute method * fix: add ref for api check * chore :remove logs * chore : add entity types types * refactor: rename duplication success status value * chore: update attribute to enums * chore: update variable name * chore: set uploading state * chore : update enum name * chore : update replace command * chore: fix retry UI * chore: remove default logic * refactor: optimize imports in custom image extension files and improve error handling in image duplication * fix:type error * Update packages/editor/src/core/extensions/custom-image/components/node-view.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: enhance asset duplication handler to ignore HTTP sources --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: VipinDevelops <vipinchaudhary1809@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
parent
d462546055
commit
83679806fd
33 changed files with 581 additions and 55 deletions
|
|
@ -4,7 +4,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from
|
|||
import { cn } from "@plane/utils";
|
||||
// local imports
|
||||
import type { Pixel, TCustomImageAttributes, TCustomImageSize } from "../types";
|
||||
import { ensurePixelString, getImageBlockId } from "../utils";
|
||||
import { ensurePixelString, getImageBlockId, isImageDuplicating } from "../utils";
|
||||
import type { CustomImageNodeViewProps } from "./node-view";
|
||||
import { ImageToolbarRoot } from "./toolbar";
|
||||
import { ImageUploadStatus } from "./upload-status";
|
||||
|
|
@ -42,6 +42,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
aspectRatio: nodeAspectRatio,
|
||||
src: imgNodeSrc,
|
||||
alignment: nodeAlignment,
|
||||
status,
|
||||
} = node.attrs;
|
||||
// states
|
||||
const [size, setSize] = useState<TCustomImageSize>({
|
||||
|
|
@ -202,15 +203,16 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
[editor, getPos, isTouchDevice]
|
||||
);
|
||||
|
||||
const isDuplicating = isImageDuplicating(status);
|
||||
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
|
||||
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
|
||||
const showImageLoader = !(resolvedImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
|
||||
// show the image upload status only when the resolvedImageSrc is not ready
|
||||
const showUploadStatus = !resolvedImageSrc;
|
||||
const showImageLoader =
|
||||
(!resolvedImageSrc && !isDuplicating) || !initialResizeComplete || hasErroredOnFirstLoad || isDuplicating; // show the image upload status only when the resolvedImageSrc is not ready
|
||||
const showUploadStatus = !resolvedImageSrc && !isDuplicating;
|
||||
// show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
const showImageToolbar = resolvedImageSrc && resolvedDownloadSrc && initialResizeComplete;
|
||||
const showImageToolbar = resolvedImageSrc && resolvedDownloadSrc && initialResizeComplete && !isDuplicating;
|
||||
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
const showImageResizer = editor.isEditable && resolvedImageSrc && initialResizeComplete;
|
||||
const showImageResizer = editor.isEditable && resolvedImageSrc && initialResizeComplete && !isDuplicating;
|
||||
// show the preview image from the file system if the remote image's src is not set
|
||||
const displayedImageSrc = resolvedImageSrc || imageFromFileSystem;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import type { NodeViewProps } from "@tiptap/react";
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
// local imports
|
||||
import type { CustomImageExtensionType, TCustomImageAttributes } from "../types";
|
||||
import { ECustomImageStatus } from "../types";
|
||||
import { hasImageDuplicationFailed } from "../utils";
|
||||
import { CustomImageBlock } from "./block";
|
||||
import { CustomImageUploader } from "./uploader";
|
||||
|
||||
|
|
@ -15,8 +17,8 @@ export type CustomImageNodeViewProps = Omit<NodeViewProps, "extension" | "update
|
|||
};
|
||||
|
||||
export const CustomImageNodeView: React.FC<CustomImageNodeViewProps> = (props) => {
|
||||
const { editor, extension, node } = props;
|
||||
const { src: imgNodeSrc } = node.attrs;
|
||||
const { editor, extension, node, updateAttributes } = props;
|
||||
const { src: imgNodeSrc, status } = node.attrs;
|
||||
|
||||
const [isUploaded, setIsUploaded] = useState(!!imgNodeSrc);
|
||||
const [resolvedSrc, setResolvedSrc] = useState<string | undefined>(undefined);
|
||||
|
|
@ -26,6 +28,8 @@ export const CustomImageNodeView: React.FC<CustomImageNodeViewProps> = (props) =
|
|||
|
||||
const [editorContainer, setEditorContainer] = useState<HTMLDivElement | null>(null);
|
||||
const imageComponentRef = useRef<HTMLDivElement>(null);
|
||||
const hasRetriedOnMount = useRef(false);
|
||||
const isDuplicatingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const closestEditorContainer = imageComponentRef.current?.closest(".editor-container");
|
||||
|
|
@ -61,10 +65,66 @@ export const CustomImageNodeView: React.FC<CustomImageNodeViewProps> = (props) =
|
|||
getImageSource();
|
||||
}, [imgNodeSrc, extension.options]);
|
||||
|
||||
// Handle image duplication when status is duplicating
|
||||
useEffect(() => {
|
||||
const handleDuplication = async () => {
|
||||
if (status !== ECustomImageStatus.DUPLICATING || !extension.options.duplicateImage || !imgNodeSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicate calls - check if already duplicating this asset
|
||||
if (isDuplicatingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDuplicatingRef.current = true;
|
||||
try {
|
||||
hasRetriedOnMount.current = true;
|
||||
|
||||
const newAssetId = await extension.options.duplicateImage!(imgNodeSrc);
|
||||
|
||||
if (!newAssetId) {
|
||||
throw new Error("Duplication returned invalid asset ID");
|
||||
}
|
||||
|
||||
// Update node with new source and success status
|
||||
updateAttributes({
|
||||
src: newAssetId,
|
||||
status: ECustomImageStatus.UPLOADED,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to duplicate image:", error);
|
||||
// Update status to failed
|
||||
updateAttributes({ status: ECustomImageStatus.DUPLICATION_FAILED });
|
||||
} finally {
|
||||
isDuplicatingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
handleDuplication();
|
||||
}, [status, imgNodeSrc, extension.options.duplicateImage, updateAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasImageDuplicationFailed(status) && !hasRetriedOnMount.current && imgNodeSrc) {
|
||||
hasRetriedOnMount.current = true;
|
||||
// Add a small delay before retrying to avoid immediate retries
|
||||
updateAttributes({ status: ECustomImageStatus.DUPLICATING });
|
||||
}
|
||||
}, [status, imgNodeSrc, updateAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === ECustomImageStatus.UPLOADED) {
|
||||
hasRetriedOnMount.current = false;
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const hasDuplicationFailed = hasImageDuplicationFailed(status);
|
||||
const shouldShowBlock = (isUploaded || imageFromFileSystem) && !failedToLoadImage;
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<div className="p-0 mx-0 my-2" data-drag-handle ref={imageComponentRef}>
|
||||
{(isUploaded || imageFromFileSystem) && !failedToLoadImage ? (
|
||||
{shouldShowBlock && !hasDuplicationFailed ? (
|
||||
<CustomImageBlock
|
||||
editorContainer={editorContainer}
|
||||
imageFromFileSystem={imageFromFileSystem}
|
||||
|
|
@ -77,6 +137,7 @@ export const CustomImageNodeView: React.FC<CustomImageNodeViewProps> = (props) =
|
|||
) : (
|
||||
<CustomImageUploader
|
||||
failedToLoadImage={failedToLoadImage}
|
||||
hasDuplicationFailed={hasDuplicationFailed}
|
||||
loadImageFromFileSystem={setImageFromFileSystem}
|
||||
maxFileSize={editor.storage.imageComponent?.maxFileSize}
|
||||
setIsUploaded={setIsUploaded}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ImageIcon } from "lucide-react";
|
||||
import { ImageIcon, RotateCcw } from "lucide-react";
|
||||
import type { ChangeEvent } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
// plane imports
|
||||
|
|
@ -11,11 +11,13 @@ import type { EFileError } from "@/helpers/file";
|
|||
// hooks
|
||||
import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
// local imports
|
||||
import { ECustomImageStatus } from "../types";
|
||||
import { getImageComponentImageFileMap } from "../utils";
|
||||
import type { CustomImageNodeViewProps } from "./node-view";
|
||||
|
||||
type CustomImageUploaderProps = CustomImageNodeViewProps & {
|
||||
failedToLoadImage: boolean;
|
||||
hasDuplicationFailed: boolean;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
maxFileSize: number;
|
||||
setIsUploaded: (isUploaded: boolean) => void;
|
||||
|
|
@ -33,6 +35,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
selected,
|
||||
setIsUploaded,
|
||||
updateAttributes,
|
||||
hasDuplicationFailed,
|
||||
} = props;
|
||||
// refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -50,6 +53,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
// Update the node view's src attribute post upload
|
||||
updateAttributes({
|
||||
src: url,
|
||||
status: ECustomImageStatus.UPLOADED,
|
||||
});
|
||||
imageComponentImageFileMap?.delete(imageEntityId);
|
||||
|
||||
|
|
@ -84,8 +88,11 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
);
|
||||
|
||||
const uploadImageEditorCommand = useCallback(
|
||||
async (file: File) => await extension.options.uploadImage?.(imageEntityId ?? "", file),
|
||||
[extension.options, imageEntityId]
|
||||
async (file: File) => {
|
||||
updateAttributes({ status: ECustomImageStatus.UPLOADING });
|
||||
return await extension.options.uploadImage?.(imageEntityId ?? "", file);
|
||||
},
|
||||
[extension.options, imageEntityId, updateAttributes]
|
||||
);
|
||||
|
||||
const handleProgressStatus = useCallback(
|
||||
|
|
@ -161,7 +168,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
|
||||
const getDisplayMessage = useCallback(() => {
|
||||
const isUploading = isImageBeingUploaded;
|
||||
if (failedToLoadImage) {
|
||||
if (failedToLoadImage || hasDuplicationFailed) {
|
||||
return "Error loading image";
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +181,17 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
}
|
||||
|
||||
return "Add an image";
|
||||
}, [draggedInside, editor.isEditable, failedToLoadImage, isImageBeingUploaded]);
|
||||
}, [draggedInside, editor.isEditable, failedToLoadImage, isImageBeingUploaded, hasDuplicationFailed]);
|
||||
|
||||
const handleRetryClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (hasDuplicationFailed && editor.isEditable) {
|
||||
updateAttributes({ status: ECustomImageStatus.DUPLICATING });
|
||||
}
|
||||
},
|
||||
[hasDuplicationFailed, editor.isEditable, updateAttributes]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -185,10 +202,10 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
"bg-custom-background-80 text-custom-text-200": draggedInside && editor.isEditable,
|
||||
"text-custom-primary-200 bg-custom-primary-100/10 border-custom-primary-200/10 hover:bg-custom-primary-100/10 hover:text-custom-primary-200":
|
||||
selected && editor.isEditable,
|
||||
"text-red-500 cursor-default": failedToLoadImage,
|
||||
"hover:text-red-500": failedToLoadImage && editor.isEditable,
|
||||
"bg-red-500/10": failedToLoadImage && selected,
|
||||
"hover:bg-red-500/10": failedToLoadImage && selected && editor.isEditable,
|
||||
"text-red-500 cursor-default": failedToLoadImage || hasDuplicationFailed,
|
||||
"hover:text-red-500": (failedToLoadImage || hasDuplicationFailed) && editor.isEditable,
|
||||
"bg-red-500/10": (failedToLoadImage || hasDuplicationFailed) && selected,
|
||||
"hover:bg-red-500/10": (failedToLoadImage || hasDuplicationFailed) && selected && editor.isEditable,
|
||||
}
|
||||
)}
|
||||
onDrop={onDrop}
|
||||
|
|
@ -196,13 +213,29 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
|||
onDragLeave={onDragLeave}
|
||||
contentEditable={false}
|
||||
onClick={() => {
|
||||
if (!failedToLoadImage && editor.isEditable) {
|
||||
if (!failedToLoadImage && editor.isEditable && !hasDuplicationFailed) {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ImageIcon className="size-4" />
|
||||
<div className="text-base font-medium">{getDisplayMessage()}</div>
|
||||
<div className="text-base font-medium flex-1">{getDisplayMessage()}</div>
|
||||
{hasDuplicationFailed && editor.isEditable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRetryClick}
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-2 py-1 text-xs font-medium text-custom-text-300 hover:bg-custom-background-90 hover:text-custom-text-200 rounded-md transition-all duration-200 ease-in-out",
|
||||
{
|
||||
"hover:bg-red-500/20": selected,
|
||||
}
|
||||
)}
|
||||
title="Retry duplication"
|
||||
>
|
||||
<RotateCcw className="size-3" />
|
||||
Retry
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
className="size-0 overflow-hidden"
|
||||
ref={fileInputRef}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type { CustomImageNodeViewProps } from "./components/node-view";
|
|||
import { CustomImageNodeView } from "./components/node-view";
|
||||
import { CustomImageExtensionConfig } from "./extension-config";
|
||||
import type { CustomImageExtensionOptions, CustomImageExtensionStorage } from "./types";
|
||||
import { ECustomImageAttributeNames, ECustomImageStatus } from "./types";
|
||||
import { getImageComponentImageFileMap } from "./utils";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -30,13 +31,14 @@ export const CustomImageExtension = (props: Props) => {
|
|||
|
||||
addOptions() {
|
||||
const upload = "upload" in fileHandler ? fileHandler.upload : undefined;
|
||||
|
||||
const duplicate = "duplicate" in fileHandler ? fileHandler.duplicate : undefined;
|
||||
return {
|
||||
...this.parent?.(),
|
||||
getImageDownloadSource: getAssetDownloadSrc,
|
||||
getImageSource: getAssetSrc,
|
||||
restoreImage: restoreImageFn,
|
||||
uploadImage: upload,
|
||||
duplicateImage: duplicate,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -93,7 +95,8 @@ export const CustomImageExtension = (props: Props) => {
|
|||
}
|
||||
|
||||
const attributes = {
|
||||
id: fileId,
|
||||
[ECustomImageAttributeNames.ID]: fileId,
|
||||
[ECustomImageAttributeNames.STATUS]: ECustomImageStatus.PENDING,
|
||||
};
|
||||
|
||||
if (props.pos) {
|
||||
|
|
@ -116,7 +119,6 @@ export const CustomImageExtension = (props: Props) => {
|
|||
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer((props) => (
|
||||
<CustomImageNodeView {...props} node={props.node as CustomImageNodeViewProps["node"]} />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export enum ECustomImageAttributeNames {
|
|||
ASPECT_RATIO = "aspectRatio",
|
||||
SOURCE = "src",
|
||||
ALIGNMENT = "alignment",
|
||||
STATUS = "status",
|
||||
}
|
||||
|
||||
export type Pixel = `${number}px`;
|
||||
|
|
@ -23,6 +24,14 @@ export type TCustomImageSize = {
|
|||
|
||||
export type TCustomImageAlignment = "left" | "center" | "right";
|
||||
|
||||
export enum ECustomImageStatus {
|
||||
PENDING = "pending",
|
||||
UPLOADING = "uploading",
|
||||
UPLOADED = "uploaded",
|
||||
DUPLICATING = "duplicating",
|
||||
DUPLICATION_FAILED = "duplication-failed",
|
||||
}
|
||||
|
||||
export type TCustomImageAttributes = {
|
||||
[ECustomImageAttributeNames.ID]: string | null;
|
||||
[ECustomImageAttributeNames.WIDTH]: PixelAttribute<"35%" | number> | null;
|
||||
|
|
@ -30,6 +39,7 @@ export type TCustomImageAttributes = {
|
|||
[ECustomImageAttributeNames.ASPECT_RATIO]: number | null;
|
||||
[ECustomImageAttributeNames.SOURCE]: string | null;
|
||||
[ECustomImageAttributeNames.ALIGNMENT]: TCustomImageAlignment;
|
||||
[ECustomImageAttributeNames.STATUS]: ECustomImageStatus;
|
||||
};
|
||||
|
||||
export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean };
|
||||
|
|
@ -45,6 +55,7 @@ export type CustomImageExtensionOptions = {
|
|||
getImageSource: TFileHandler["getAssetSrc"];
|
||||
restoreImage: TFileHandler["restore"];
|
||||
uploadImage?: TFileHandler["upload"];
|
||||
duplicateImage?: TFileHandler["duplicate"];
|
||||
};
|
||||
|
||||
export type CustomImageExtensionStorage = {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { Editor } from "@tiptap/core";
|
|||
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
// local imports
|
||||
import { ECustomImageAttributeNames } from "./types";
|
||||
import { ECustomImageAttributeNames, ECustomImageStatus } from "./types";
|
||||
import type { TCustomImageAlignment, Pixel, TCustomImageAttributes } from "./types";
|
||||
|
||||
export const DEFAULT_CUSTOM_IMAGE_ATTRIBUTES: TCustomImageAttributes = {
|
||||
|
|
@ -12,6 +12,7 @@ export const DEFAULT_CUSTOM_IMAGE_ATTRIBUTES: TCustomImageAttributes = {
|
|||
[ECustomImageAttributeNames.HEIGHT]: "auto",
|
||||
[ECustomImageAttributeNames.ASPECT_RATIO]: null,
|
||||
[ECustomImageAttributeNames.ALIGNMENT]: "left",
|
||||
[ECustomImageAttributeNames.STATUS]: ECustomImageStatus.PENDING,
|
||||
};
|
||||
|
||||
export const getImageComponentImageFileMap = (editor: Editor) => editor.storage.imageComponent?.fileMap;
|
||||
|
|
@ -53,3 +54,11 @@ export const IMAGE_ALIGNMENT_OPTIONS: {
|
|||
},
|
||||
];
|
||||
export const getImageBlockId = (id: string) => `editor-image-block-${id}`;
|
||||
|
||||
export const isImageDuplicating = (status: ECustomImageStatus) => status === ECustomImageStatus.DUPLICATING;
|
||||
|
||||
export const isImageDuplicationComplete = (status: ECustomImageStatus) =>
|
||||
status === ECustomImageStatus.UPLOADED || status === ECustomImageStatus.DUPLICATION_FAILED;
|
||||
|
||||
export const hasImageDuplicationFailed = (status: ECustomImageStatus) =>
|
||||
status === ECustomImageStatus.DUPLICATION_FAILED;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { DropHandlerPlugin } from "@/plugins/drop";
|
|||
import { FilePlugins } from "@/plugins/file/root";
|
||||
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
||||
// types
|
||||
import { PasteAssetPlugin } from "@/plugins/paste-asset";
|
||||
import type { IEditorProps, TEditorAsset, TFileHandler } from "@/types";
|
||||
|
||||
type TActiveDropbarExtensions =
|
||||
|
|
@ -80,6 +81,7 @@ export const UtilityExtension = (props: Props) => {
|
|||
disabledExtensions,
|
||||
editor: this.editor,
|
||||
}),
|
||||
PasteAssetPlugin(),
|
||||
];
|
||||
},
|
||||
|
||||
|
|
|
|||
77
packages/editor/src/core/plugins/paste-asset.ts
Normal file
77
packages/editor/src/core/plugins/paste-asset.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { assetDuplicationHandlers } from "@/plane-editor/helpers/asset-duplication";
|
||||
|
||||
export const PasteAssetPlugin = (): Plugin =>
|
||||
new Plugin({
|
||||
key: new PluginKey("paste-asset-duplication"),
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
if (!event.clipboardData) return false;
|
||||
|
||||
const htmlContent = event.clipboardData.getData("text/html");
|
||||
if (!htmlContent || htmlContent.includes('data-uploaded="true"')) return false;
|
||||
|
||||
// Process the HTML content using the registry
|
||||
const { processedHtml, hasChanges } = processAssetDuplication(htmlContent);
|
||||
|
||||
if (!hasChanges) return false;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Mark the content as already processed to avoid infinite loops
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = processedHtml;
|
||||
const metaTag = tempDiv.querySelector("meta[charset='utf-8']");
|
||||
if (metaTag) {
|
||||
metaTag.setAttribute("data-uploaded", "true");
|
||||
}
|
||||
const finalHtml = tempDiv.innerHTML;
|
||||
|
||||
const newDataTransfer = new DataTransfer();
|
||||
newDataTransfer.setData("text/html", finalHtml);
|
||||
if (event.clipboardData) {
|
||||
newDataTransfer.setData("text/plain", event.clipboardData.getData("text/plain"));
|
||||
}
|
||||
|
||||
const pasteEvent = new ClipboardEvent("paste", {
|
||||
clipboardData: newDataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
view.dom.dispatchEvent(pasteEvent);
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Utility function to process HTML content with all registered handlers
|
||||
const processAssetDuplication = (htmlContent: string): { processedHtml: string; hasChanges: boolean } => {
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
|
||||
let processedHtml = htmlContent;
|
||||
let hasChanges = false;
|
||||
|
||||
// Process each registered component type
|
||||
for (const [componentName, handler] of Object.entries(assetDuplicationHandlers)) {
|
||||
const elements = tempDiv.querySelectorAll(componentName);
|
||||
|
||||
if (elements.length > 0) {
|
||||
elements.forEach((element) => {
|
||||
const result = handler({ element, originalHtml: processedHtml });
|
||||
if (result.shouldProcess) {
|
||||
processedHtml = result.modifiedHtml;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update tempDiv with processed HTML for next iteration
|
||||
tempDiv.innerHTML = processedHtml;
|
||||
}
|
||||
}
|
||||
|
||||
return { processedHtml, hasChanges };
|
||||
};
|
||||
|
|
@ -27,8 +27,5 @@ export const CoreEditorProps = (props: TArgs): EditorProps => {
|
|||
}
|
||||
},
|
||||
},
|
||||
transformPastedHTML(html) {
|
||||
return html.replace(/<img.*?>/g, "");
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type TFileHandler = {
|
|||
getAssetSrc: (path: string) => Promise<string>;
|
||||
restore: (assetSrc: string) => Promise<void>;
|
||||
upload: (blockId: string, file: File) => Promise<string>;
|
||||
duplicate: (assetId: string) => Promise<string>;
|
||||
validation: {
|
||||
/**
|
||||
* @description max file size in bytes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue