editor improvements

* add callout, youtube embed, image, video, table, detail, math
* fix attachments module
* other fixes
This commit is contained in:
Philipinho
2024-06-20 14:57:00 +01:00
parent c7925739cb
commit 1f4bd129a8
74 changed files with 5205 additions and 381 deletions

View File

@ -0,0 +1,151 @@
import {
BubbleMenu as BaseBubbleMenu,
findParentNode,
posToDOMRect,
} from "@tiptap/react";
import React, { useCallback } from "react";
import { sticky } from "tippy.js";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Tooltip } from "@mantine/core";
import {
IconLayoutAlignCenter,
IconLayoutAlignLeft,
IconLayoutAlignRight,
} from "@tabler/icons-react";
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
export function ImageMenu({ editor }: EditorMenuProps) {
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
return false;
}
return editor.isActive("image");
},
[editor],
);
const getReferenceClientRect = useCallback(() => {
const { selection } = editor.state;
const predicate = (node: PMNode) => node.type.name === "image";
const parent = findParentNode(predicate)(selection);
if (parent) {
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
return dom.getBoundingClientRect();
}
return posToDOMRect(editor.view, selection.from, selection.to);
}, [editor]);
const alignImageLeft = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setImageAlign("left")
.run();
}, [editor]);
const alignImageCenter = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setImageAlign("center")
.run();
}, [editor]);
const alignImageRight = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setImageAlign("right")
.run();
}, [editor]);
const onWidthChange = useCallback(
(value: number) => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setImageWidth(value)
.run();
},
[editor],
);
return (
<BaseBubbleMenu
editor={editor}
pluginKey={`image-menu}`}
updateDelay={0}
tippyOptions={{
getReferenceClientRect,
offset: [0, 8],
zIndex: 99,
popperOptions: {
modifiers: [{ name: "flip", enabled: false }],
},
plugins: [sticky],
sticky: "popper",
}}
shouldShow={shouldShow}
>
<ActionIcon.Group className="actionIconGroup">
<Tooltip position="top" label="Align image left">
<ActionIcon
onClick={alignImageLeft}
size="lg"
aria-label="Align image left"
variant={
editor.isActive("image", { align: "left" }) ? "light" : "default"
}
>
<IconLayoutAlignLeft size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Align image center">
<ActionIcon
onClick={alignImageCenter}
size="lg"
aria-label="Align image center"
variant={
editor.isActive("image", { align: "center" })
? "light"
: "default"
}
>
<IconLayoutAlignCenter size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Align image right">
<ActionIcon
onClick={alignImageRight}
size="lg"
aria-label="Align image right"
variant={
editor.isActive("image", { align: "right" }) ? "light" : "default"
}
>
<IconLayoutAlignRight size={18} />
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
{editor.getAttributes("image")?.width && (
<NodeWidthResize
onChange={onWidthChange}
value={parseInt(editor.getAttributes("image").width)}
/>
)}
</BaseBubbleMenu>
);
}
export default ImageMenu;

View File

@ -0,0 +1,33 @@
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { useMemo } from "react";
import { Image } from "@mantine/core";
import { getBackendUrl } from "@/lib/config.ts";
export default function ImageView(props: NodeViewProps) {
const { node, selected } = props;
const { src, width, align } = node.attrs;
const flexJustifyContent = useMemo(() => {
if (align === "center") return "center";
if (align === "right") return "flex-end";
return "flex-start";
}, [align]);
return (
<NodeViewWrapper
style={{
position: "relative",
display: "flex",
justifyContent: flexJustifyContent,
}}
>
<Image
radius="md"
src={getBackendUrl() + src}
fit="contain"
w={width}
className={selected && "ProseMirror-selectednode"}
/>
</NodeViewWrapper>
);
}

View File

@ -0,0 +1,24 @@
import { handleImageUpload } from "@docmost/editor-ext";
import { uploadFile } from "@/features/page/services/page-service.ts";
export const uploadImageAction = handleImageUpload({
onUpload: async (file: File, pageId: string): Promise<any> => {
try {
console.log("dont upload");
return await uploadFile(file, pageId);
} catch (err) {
console.error("failed to upload image", err);
throw err;
}
},
validateFn: (file) => {
if (!file.type.includes("image/")) {
return false;
}
if (file.size / 1024 / 1024 > 20) {
//error("File size too big (max 20MB).");
return false;
}
return true;
},
});