mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 05:02:36 +10:00
fix editor file handling
This commit is contained in:
@ -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;
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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, {
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export const TiptapImage = Image.extend<ImageOptions>({
|
|||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
return [
|
return [
|
||||||
ImageUploadPlugin({
|
ImageUploadPlugin({
|
||||||
placeHolderClass: "image-upload",
|
placeholderClass: "image-upload",
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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, {
|
||||||
|
|||||||
@ -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",
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user