fix editor file handling

This commit is contained in:
Philipinho
2024-06-22 03:33:54 +01:00
parent 2aec5a3fe7
commit df1840cf67
12 changed files with 71 additions and 75 deletions

View File

@ -0,0 +1,45 @@
import type { EditorView } from "@tiptap/pm/view";
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
export const handleFilePaste = (
view: EditorView,
event: ClipboardEvent,
pageId: string,
) => {
if (event.clipboardData?.files.length) {
event.preventDefault();
const [file] = Array.from(event.clipboardData.files);
const pos = view.state.selection.from;
if (file) {
uploadImageAction(file, view, pos, pageId);
uploadVideoAction(file, view, pos, pageId);
}
return true;
}
return false;
};
export const handleFileDrop = (
view: EditorView,
event: DragEvent,
moved: boolean,
pageId: string,
) => {
if (!moved && event.dataTransfer?.files.length) {
event.preventDefault();
const [file] = Array.from(event.dataTransfer.files);
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
if (file) {
uploadImageAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
uploadVideoAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
}
return true;
}
return false;
};

View File

@ -1,7 +1,7 @@
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { useMemo } from "react"; import { useMemo } from "react";
import { Image } from "@mantine/core"; import { Image } from "@mantine/core";
import { getBackendUrl } from "@/lib/config.ts"; import { getFileUrl } from "@/lib/config.ts";
export default function ImageView(props: NodeViewProps) { export default function ImageView(props: NodeViewProps) {
const { node, selected } = props; const { node, selected } = props;
@ -23,9 +23,9 @@ export default function ImageView(props: NodeViewProps) {
> >
<Image <Image
radius="md" radius="md"
src={getBackendUrl() + src}
fit="contain" fit="contain"
w={width} w={width}
src={getFileUrl(src)}
className={selected && "ProseMirror-selectednode"} className={selected && "ProseMirror-selectednode"}
/> />
</NodeViewWrapper> </NodeViewWrapper>

View File

@ -4,7 +4,6 @@ import { uploadFile } from "@/features/page/services/page-service.ts";
export const uploadImageAction = handleImageUpload({ export const uploadImageAction = handleImageUpload({
onUpload: async (file: File, pageId: string): Promise<any> => { onUpload: async (file: File, pageId: string): Promise<any> => {
try { try {
console.log("dont upload");
return await uploadFile(file, pageId); return await uploadFile(file, pageId);
} catch (err) { } catch (err) {
console.error("failed to upload image", err); console.error("failed to upload image", err);

View File

@ -1,6 +1,6 @@
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { useMemo } from "react"; import { useMemo } from "react";
import { getBackendUrl } from "@/lib/config.ts"; import { getFileUrl } from "@/lib/config.ts";
export default function VideoView(props: NodeViewProps) { export default function VideoView(props: NodeViewProps) {
const { node, selected } = props; const { node, selected } = props;
@ -24,7 +24,7 @@ export default function VideoView(props: NodeViewProps) {
preload="metadata" preload="metadata"
width={width} width={width}
controls controls
src={getBackendUrl() + src} src={getFileUrl(src)}
className={selected && "ProseMirror-selectednode"} className={selected && "ProseMirror-selectednode"}
/> />
</NodeViewWrapper> </NodeViewWrapper>

View File

@ -29,12 +29,15 @@ import EditorSkeleton from "@/features/editor/components/editor-skeleton";
import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu"; import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu";
import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx"; import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx";
import TableMenu from "@/features/editor/components/table/table-menu.tsx"; import TableMenu from "@/features/editor/components/table/table-menu.tsx";
import { handleMediaDrop, handleMediaPaste } from "@docmost/editor-ext";
import ImageMenu from "@/features/editor/components/image/image-menu.tsx"; import ImageMenu from "@/features/editor/components/image/image-menu.tsx";
import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx"; import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx";
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx"; import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx"; import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
import VideoMenu from "@/features/editor/components/video/video-menu.tsx"; import VideoMenu from "@/features/editor/components/video/video-menu.tsx";
import {
handleFileDrop,
handleFilePaste,
} from "@/features/editor/components/common/file-upload-handler.tsx";
interface PageEditorProps { interface PageEditorProps {
pageId: string; pageId: string;
@ -112,14 +115,9 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
} }
}, },
}, },
handlePaste: (view, event) => { handlePaste: (view, event) => handleFilePaste(view, event, pageId),
handleMediaPaste(view, event, uploadImageAction, pageId); handleDrop: (view, event, _slice, moved) =>
handleMediaPaste(view, event, uploadVideoAction, pageId); handleFileDrop(view, event, moved, pageId),
},
handleDrop: (view, event, _slice, moved) => {
handleMediaDrop(view, event, moved, uploadImageAction, pageId);
handleMediaDrop(view, event, moved, uploadVideoAction, pageId);
},
}, },
onCreate({ editor }) { onCreate({ editor }) {
if (editor) { if (editor) {

View File

@ -5,6 +5,9 @@
} }
.node-image, .node-video { .node-image, .node-video {
margin-top: 8px;
margin-bottom: 8px;
&.ProseMirror-selectednode { &.ProseMirror-selectednode {
outline: none; outline: none;
} }

View File

@ -42,3 +42,7 @@ export function getAvatarUrl(avatarUrl: string) {
export function getSpaceUrl(spaceSlug: string) { export function getSpaceUrl(spaceSlug: string) {
return "/s/" + spaceSlug; return "/s/" + spaceSlug;
} }
export function getFileUrl(src: string) {
return src.startsWith("/files/") ? getBackendUrl() + src : src;
}

View File

@ -6,9 +6,9 @@ import { MediaUploadOptions, UploadFn } from "../media-utils";
const uploadKey = new PluginKey("image-upload"); const uploadKey = new PluginKey("image-upload");
export const ImageUploadPlugin = ({ export const ImageUploadPlugin = ({
placeHolderClass, placeholderClass,
}: { }: {
placeHolderClass: string; placeholderClass: string;
}) => }) =>
new Plugin({ new Plugin({
key: uploadKey, key: uploadKey,
@ -27,7 +27,7 @@ export const ImageUploadPlugin = ({
const placeholder = document.createElement("div"); const placeholder = document.createElement("div");
placeholder.setAttribute("class", "img-placeholder"); placeholder.setAttribute("class", "img-placeholder");
const image = document.createElement("img"); const image = document.createElement("img");
image.setAttribute("class", placeHolderClass); image.setAttribute("class", placeholderClass);
image.src = src; image.src = src;
placeholder.appendChild(image); placeholder.appendChild(image);
const deco = Decoration.widget(pos + 1, placeholder, { const deco = Decoration.widget(pos + 1, placeholder, {

View File

@ -141,7 +141,7 @@ export const TiptapImage = Image.extend<ImageOptions>({
addProseMirrorPlugins() { addProseMirrorPlugins() {
return [ return [
ImageUploadPlugin({ ImageUploadPlugin({
placeHolderClass: "image-upload", placeholderClass: "image-upload",
}), }),
]; ];
}, },

View File

@ -7,44 +7,6 @@ export type UploadFn = (
pageId: string, pageId: string,
) => void; ) => void;
export const handleMediaPaste = (
view: EditorView,
event: ClipboardEvent,
uploadFn: UploadFn,
pageId: string,
) => {
if (event.clipboardData?.files.length) {
event.preventDefault();
const [file] = Array.from(event.clipboardData.files);
const pos = view.state.selection.from;
if (file) uploadFn(file, view, pos, pageId);
return true;
}
return false;
};
export const handleMediaDrop = (
view: EditorView,
event: DragEvent,
moved: boolean,
uploadFn: UploadFn,
pageId: string,
) => {
if (!moved && event.dataTransfer?.files.length) {
event.preventDefault();
const [file] = Array.from(event.dataTransfer.files);
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
if (file) uploadFn(file, view, coordinates?.pos ?? 0 - 1, pageId);
return true;
}
return false;
};
export interface MediaUploadOptions { export interface MediaUploadOptions {
validateFn?: (file: File) => void; validateFn?: (file: File) => void;
onUpload: (file: File, pageId: string) => Promise<any>; onUpload: (file: File, pageId: string) => Promise<any>;

View File

@ -6,9 +6,9 @@ import { MediaUploadOptions, UploadFn } from "../media-utils";
const uploadKey = new PluginKey("video-upload"); const uploadKey = new PluginKey("video-upload");
export const VideoUploadPlugin = ({ export const VideoUploadPlugin = ({
placeHolderClass, placeholderClass,
}: { }: {
placeHolderClass: string; placeholderClass: string;
}) => }) =>
new Plugin({ new Plugin({
key: uploadKey, key: uploadKey,
@ -27,7 +27,7 @@ export const VideoUploadPlugin = ({
const placeholder = document.createElement("div"); const placeholder = document.createElement("div");
placeholder.setAttribute("class", "video-placeholder"); placeholder.setAttribute("class", "video-placeholder");
const video = document.createElement("video"); const video = document.createElement("video");
video.setAttribute("class", placeHolderClass); video.setAttribute("class", placeholderClass);
video.src = src; video.src = src;
placeholder.appendChild(video); placeholder.appendChild(video);
const deco = Decoration.widget(pos + 1, placeholder, { const deco = Decoration.widget(pos + 1, placeholder, {

View File

@ -28,8 +28,6 @@ declare module "@tiptap/core" {
} }
} }
const VIDEO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
export const TiptapVideo = Node.create<VideoOptions>({ export const TiptapVideo = Node.create<VideoOptions>({
name: "video", name: "video",
@ -123,23 +121,10 @@ export const TiptapVideo = Node.create<VideoOptions>({
return ReactNodeViewRenderer(this.options.view); return ReactNodeViewRenderer(this.options.view);
}, },
addInputRules() {
return [
nodeInputRule({
find: VIDEO_INPUT_REGEX,
type: this.type,
getAttributes: (match) => {
const [, , src] = match;
return { src };
},
}),
];
},
addProseMirrorPlugins() { addProseMirrorPlugins() {
return [ return [
VideoUploadPlugin({ VideoUploadPlugin({
placeHolderClass: "video-upload", placeholderClass: "video-upload",
}), }),
]; ];
}, },