mirror of
https://github.com/makeplane/plane.git
synced 2026-09-03 12:50:29 +02:00
fix: image insertion at pos and loading effect added
This commit is contained in:
@@ -189,16 +189,17 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({
|
||||
icon: TableIcon,
|
||||
});
|
||||
|
||||
export const ImageItem = (editor: Editor, uploadFile: UploadImage) =>
|
||||
export const ImageItem = (editor: Editor) =>
|
||||
({
|
||||
key: "image",
|
||||
name: "Image",
|
||||
isActive: () => editor?.isActive("image"),
|
||||
command: (savedSelection: Selection | null) => editor?.commands.setImageUpload({ event: "insert" }),
|
||||
command: (savedSelection: Selection | null) =>
|
||||
editor?.commands.setImageUpload({ event: "insert", pos: savedSelection?.from }),
|
||||
icon: ImageIcon,
|
||||
}) as const;
|
||||
|
||||
export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImage) {
|
||||
export function getEditorMenuItems(editor: Editor | null) {
|
||||
if (!editor) {
|
||||
return [];
|
||||
}
|
||||
@@ -220,6 +221,6 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag
|
||||
NumberedListItem(editor),
|
||||
QuoteItem(editor),
|
||||
TableItem(editor),
|
||||
ImageItem(editor, uploadFile),
|
||||
ImageItem(editor),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CustomCodeInlineExtension } from "./code-inline";
|
||||
import { CustomLinkExtension } from "./custom-link";
|
||||
import { CustomHorizontalRule } from "./horizontal-rule";
|
||||
import { ImageExtensionWithoutProps } from "./image";
|
||||
import { ImageBlockWithoutProps } from "./image/image-block-without-props";
|
||||
import { CustomImageComponentWithoutProps } from "./image/image-component-without-props";
|
||||
import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props";
|
||||
import { CustomMentionWithoutProps } from "./mentions/mentions-without-props";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
@@ -62,7 +62,7 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
ImageBlockWithoutProps(),
|
||||
CustomImageComponentWithoutProps(),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// types
|
||||
import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types";
|
||||
import { ImageBlock } from "./image-block";
|
||||
import { CustomImageComponent } from "./image-block";
|
||||
|
||||
type TArguments = {
|
||||
enableHistory: boolean;
|
||||
@@ -108,7 +108,7 @@ export const CoreEditorExtensions = ({
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
ImageBlock({
|
||||
CustomImageComponent({
|
||||
deleteFile,
|
||||
restoreFile,
|
||||
uploadFile,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useRef, useState, useCallback, useEffect } from "react";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { ImageShimmer } from "./image-loader";
|
||||
|
||||
interface ImageBlockViewProps {
|
||||
editor: Editor;
|
||||
@@ -27,6 +28,7 @@ export const ImageBlockView: React.FC<ImageBlockViewProps> = (props) => {
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const isResizing = useRef(false);
|
||||
const aspectRatio = useRef(1);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (imageRef.current) {
|
||||
@@ -39,6 +41,7 @@ export const ImageBlockView: React.FC<ImageBlockViewProps> = (props) => {
|
||||
const newHeight = newWidth / aspectRatio.current;
|
||||
setSize({ width: `${newWidth}px`, height: `${newHeight}px` });
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
}
|
||||
}, [src, width, height]);
|
||||
@@ -97,6 +100,7 @@ export const ImageBlockView: React.FC<ImageBlockViewProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative inline-block" onMouseDown={handleMouseDown} data-drag-handle>
|
||||
{isLoading ? <ImageShimmer width={size.width} height={size.height} /> : null}
|
||||
<img
|
||||
ref={imageRef}
|
||||
src={src}
|
||||
@@ -105,6 +109,7 @@ export const ImageBlockView: React.FC<ImageBlockViewProps> = (props) => {
|
||||
style={{
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
display: isLoading ? "none" : "block",
|
||||
}}
|
||||
/>
|
||||
{isSelected && (
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const ImageShimmer: React.FC<{ width: string; height: string }> = ({ width, height }) => (
|
||||
<div className="animate-pulse bg-custom-background-80 rounded-md" style={{ width, height }} />
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mergeAttributes, Range } from "@tiptap/core";
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -9,19 +9,14 @@ import { ImageUpload } from "../image-upload/view";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
imageBlock: {
|
||||
setImageBlock: (attributes: { src: string; width?: number; height?: number }) => ReturnType;
|
||||
setImageBlockAt: (attributes: {
|
||||
src: string;
|
||||
pos: number | Range;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}) => ReturnType;
|
||||
imageComponent: {
|
||||
setImageUpload: ({ file, pos, event }: { file?: File; pos?: number; event: "insert" | "drop" }) => ReturnType;
|
||||
uploadImage: (file: File) => () => Promise<string> | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const ImageBlock = ({
|
||||
export const CustomImageComponent = ({
|
||||
uploadFile,
|
||||
// deleteFile,
|
||||
// restoreFile,
|
||||
@@ -33,7 +28,7 @@ export const ImageBlock = ({
|
||||
cancelUploadImage?: () => void;
|
||||
}) =>
|
||||
Image.extend<{}, UploadImageExtensionStorage>({
|
||||
name: "imageBlock",
|
||||
name: "imageComponent",
|
||||
group: "inline",
|
||||
draggable: true,
|
||||
|
||||
@@ -64,13 +59,13 @@ export const ImageBlock = ({
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "image-block",
|
||||
tag: "image-component",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["image-block", mergeAttributes(HTMLAttributes)];
|
||||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
@@ -81,31 +76,17 @@ export const ImageBlock = ({
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setImageBlock:
|
||||
(attrs) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: { src: attrs.src },
|
||||
}),
|
||||
setImageBlockAt:
|
||||
(attrs) =>
|
||||
({ commands }) =>
|
||||
commands.insertContentAt(attrs.pos, {
|
||||
type: this.name,
|
||||
attrs: { src: attrs.src },
|
||||
}),
|
||||
setImageUpload:
|
||||
(props: { file?: File; pos?: number; event: "insert" | "replace" | "drop" }) =>
|
||||
(props: { file?: File; pos?: number; event: "insert" | "drop" }) =>
|
||||
({ commands }) => {
|
||||
const fileId = uuidv4();
|
||||
if (props?.file && props?.event === "drop") {
|
||||
(this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, {
|
||||
if (props?.event === "drop" && props.file) {
|
||||
(this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, {
|
||||
file: props.file,
|
||||
event: props.event,
|
||||
});
|
||||
} else if (props.event !== "drop") {
|
||||
(this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, {
|
||||
} else if (props.event === "insert") {
|
||||
(this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, {
|
||||
event: props.event,
|
||||
});
|
||||
}
|
||||
@@ -115,6 +96,12 @@ export const ImageBlock = ({
|
||||
"data-file": props?.file ? `data-file="${props.file}"` : "",
|
||||
};
|
||||
|
||||
if (props.pos) {
|
||||
return commands.insertContentAt(props.pos, {
|
||||
type: this.name,
|
||||
attrs: attributes,
|
||||
});
|
||||
}
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: attributes,
|
||||
@@ -134,4 +121,4 @@ export const ImageBlock = ({
|
||||
inline: true,
|
||||
});
|
||||
|
||||
export default ImageBlock;
|
||||
export default CustomImageComponent;
|
||||
|
||||
@@ -3,30 +3,11 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { DeleteImage, RestoreImage, UploadImage } from "@/types";
|
||||
import { ImageUpload as ImageUploadComponent } from "./view/image-upload";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
imageUpload: {
|
||||
setImageUpload: ({
|
||||
file,
|
||||
pos,
|
||||
event,
|
||||
}: {
|
||||
file?: File;
|
||||
pos?: number;
|
||||
event: "insert" | "replace" | "drop";
|
||||
}) => ReturnType;
|
||||
uploadImage: (file: File) => () => Promise<string> | undefined;
|
||||
restoreImage: (assetUrlWithWorkspaceId: string) => Promise<ReturnType>;
|
||||
deleteImage: (assetUrlWithWorkspaceId: string) => Promise<ReturnType>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface UploadImageExtensionStorage {
|
||||
fileMap: Map<string, UploadEntity>;
|
||||
}
|
||||
|
||||
export type UploadEntity = ({ event: "insert" } | { event: "replace" } | { event: "drop"; file: File }) & {
|
||||
export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & {
|
||||
pos?: number;
|
||||
};
|
||||
|
||||
@@ -91,16 +72,16 @@ export const ImageUpload = ({
|
||||
addCommands() {
|
||||
return {
|
||||
setImageUpload:
|
||||
(props: { file?: File; pos?: number; event: "insert" | "replace" | "drop" }) =>
|
||||
(props: { file?: File; pos?: number; event: "insert" | "drop" }) =>
|
||||
({ commands }) => {
|
||||
const fileId = uuidv4();
|
||||
if (props?.file && props?.event === "drop") {
|
||||
(this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, {
|
||||
(this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, {
|
||||
file: props.file,
|
||||
event: props.event,
|
||||
});
|
||||
} else if (props.event !== "drop") {
|
||||
(this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, {
|
||||
(this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, {
|
||||
event: props.event,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const ImageUpload: React.FC<ImageUploadProps> = ({ getPos, editor, node,
|
||||
const [isUploaded, setIsUploaded] = useState(!!node.attrs.src);
|
||||
|
||||
const id = node.attrs.id as string;
|
||||
const editorStorage = editor.storage.imageBlock as UploadImageExtensionStorage | undefined;
|
||||
const editorStorage = editor.storage.imageComponent as UploadImageExtensionStorage | undefined;
|
||||
|
||||
const getUploadEntity = useCallback(
|
||||
(): UploadEntity | undefined => editorStorage?.fileMap.get(id),
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
import { mergeAttributes, Range } from "@tiptap/core";
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { UploadImageExtensionStorage } from "../image-upload";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
imageBlock: {
|
||||
setImageBlock: (attributes: { src: string; width?: number; height?: number }) => ReturnType;
|
||||
setImageBlockAt: (attributes: {
|
||||
src: string;
|
||||
pos: number | Range;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const ImageBlockWithoutProps = () =>
|
||||
export const CustomImageComponentWithoutProps = () =>
|
||||
Image.extend<{}, UploadImageExtensionStorage>({
|
||||
name: "imageBlock",
|
||||
name: "imageComponent",
|
||||
group: "inline",
|
||||
draggable: true,
|
||||
|
||||
@@ -49,13 +35,13 @@ export const ImageBlockWithoutProps = () =>
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "image-block",
|
||||
tag: "image-component",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["image-block", mergeAttributes(HTMLAttributes)];
|
||||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
@@ -67,4 +53,4 @@ export const ImageBlockWithoutProps = () =>
|
||||
inline: true,
|
||||
});
|
||||
|
||||
export default ImageBlockWithoutProps;
|
||||
export default CustomImageComponentWithoutProps;
|
||||
@@ -64,7 +64,7 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.node-imageUpload {
|
||||
&.node-imageComponent {
|
||||
--horizontal-offset: 0px;
|
||||
|
||||
&::after {
|
||||
@@ -104,7 +104,7 @@ ol > li:nth-child(n + 100).ProseMirror-selectednode:not(.dragging)::after {
|
||||
margin-left: -35px;
|
||||
}
|
||||
|
||||
.ProseMirror node-imageBlock {
|
||||
.ProseMirror node-imageComponent {
|
||||
transition: filter 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user