2024-11-10 17:26:16 +02:00

353 lines
11 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { EditorProvider, useCurrentEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Color } from "@tiptap/extension-color";
import ListItem from "@tiptap/extension-list-item";
import TextStyle from "@tiptap/extension-text-style";
import Placeholder from "@tiptap/extension-placeholder";
import Image from "@tiptap/extension-image";
import IconButton from "@mui/material/IconButton";
import FormatBoldIcon from "@mui/icons-material/FormatBold";
import FormatItalicIcon from "@mui/icons-material/FormatItalic";
import StrikethroughSIcon from "@mui/icons-material/StrikethroughS";
import FormatClearIcon from "@mui/icons-material/FormatClear";
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered";
import CodeIcon from "@mui/icons-material/Code";
import ImageIcon from "@mui/icons-material/Image"; // Import Image icon
import FormatQuoteIcon from "@mui/icons-material/FormatQuote";
import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule";
import UndoIcon from "@mui/icons-material/Undo";
import RedoIcon from "@mui/icons-material/Redo";
import FormatHeadingIcon from "@mui/icons-material/FormatSize";
import DeveloperModeIcon from "@mui/icons-material/DeveloperMode";
import Compressor from "compressorjs";
import ImageResize from "tiptap-extension-resize-image"; // Import the ResizeImage extension
import { isMobile } from "../../App";
const MenuBar = ({ setEditorRef, isChat }) => {
const { editor } = useCurrentEditor();
const fileInputRef = useRef(null);
if (!editor) {
return null;
}
useEffect(() => {
if (editor && setEditorRef) {
setEditorRef(editor);
}
}, [editor, setEditorRef]);
const handleImageUpload = async (file) => {
let compressedFile;
await new Promise<void>((resolve) => {
new Compressor(file, {
quality: 0.6,
maxWidth: 1200,
mimeType: "image/webp",
success(result) {
compressedFile = new File([result], "image.webp", {
type: "image/webp",
});
resolve();
},
error(err) {
console.error("Image compression error:", err);
},
});
});
if (compressedFile) {
const reader = new FileReader();
reader.onload = () => {
const url = reader.result;
editor
.chain()
.focus()
.setImage({ src: url, style: "width: auto" })
.run();
fileInputRef.current.value = "";
};
reader.readAsDataURL(compressedFile);
}
};
const triggerImageUpload = () => {
fileInputRef.current.click(); // Trigger the file input click
};
const handlePaste = (event) => {
const items = event.clipboardData.items;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
event.preventDefault(); // Prevent the default paste behavior
handleImageUpload(file); // Call the image upload function
}
}
}
};
useEffect(() => {
if (editor) {
editor.view.dom.addEventListener("paste", handlePaste);
return () => {
editor.view.dom.removeEventListener("paste", handlePaste);
};
}
}, [editor]);
return (
<div className="control-group">
<div className="button-group">
<IconButton
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
sx={{
color: editor.isActive("bold") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatBoldIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
sx={{
color: editor.isActive("italic") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatItalicIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()}
sx={{
color: editor.isActive("strike") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<StrikethroughSIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()}
sx={{
color: editor.isActive("code") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<CodeIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().unsetAllMarks().run()}
sx={{
color:
editor.isActive("bold") ||
editor.isActive("italic") ||
editor.isActive("strike") ||
editor.isActive("code")
? "white"
: "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatClearIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleBulletList().run()}
sx={{
color: editor.isActive("bulletList") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatListBulletedIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleOrderedList().run()}
sx={{
color: editor.isActive("orderedList") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatListNumberedIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
sx={{
color: editor.isActive("codeBlock") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<DeveloperModeIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleBlockquote().run()}
sx={{
color: editor.isActive("blockquote") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatQuoteIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().setHorizontalRule().run()}
disabled={!editor.can().chain().focus().setHorizontalRule().run()}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
>
<HorizontalRuleIcon />
</IconButton>
<IconButton
onClick={() =>
editor.chain().focus().toggleHeading({ level: 1 }).run()
}
sx={{
color: editor.isActive("heading", { level: 1 }) ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatHeadingIcon fontSize="small" />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().chain().focus().undo().run()}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
>
<UndoIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().chain().focus().redo().run()}
sx={{ color: "gray" }}
>
<RedoIcon />
</IconButton>
{!isChat && (
<>
<IconButton
onClick={triggerImageUpload}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
>
<ImageIcon />
</IconButton>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={(event) => handleImageUpload(event.target.files[0])}
accept="image/*"
/>
</>
)}
</div>
</div>
);
};
const extensions = [
Color.configure({ types: [TextStyle.name, ListItem.name] }),
TextStyle.configure({ types: [ListItem.name] }),
StarterKit.configure({
bulletList: {
keepMarks: true,
keepAttributes: false,
},
orderedList: {
keepMarks: true,
keepAttributes: false,
},
}),
Placeholder.configure({
placeholder: "Start typing here...",
}),
ImageResize,
];
const content = ``;
export default ({
setEditorRef,
onEnter,
disableEnter,
isChat,
maxHeightOffset,
setIsFocusedParent,
isFocusedParent,
overrideMobile,
customEditorHeight,
}) => {
const [isFocused, setIsFocused] = useState(false);
const extensionsFiltered = isChat
? extensions.filter((item) => item?.name !== "image")
: extensions;
const editorRef = useRef(null);
const setEditorRefFunc = (editorInstance) => {
editorRef.current = editorInstance;
setEditorRef(editorInstance);
};
const handleFocus = () => {
if (!isMobile) return;
setIsFocusedParent(true);
};
const handleBlur = () => {
const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>") {
// Set focus state based on content
}
};
return (
<EditorProvider
slotBefore={
(isFocusedParent || !isMobile || overrideMobile) && (
<MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} />
)
}
extensions={extensionsFiltered}
content={content}
onCreate={({ editor }) => {
editor.on("focus", handleFocus); // Listen for focus event
editor.on("blur", handleBlur); // Listen for blur event
}}
onUpdate={({ editor }) => {
editor.on('focus', handleFocus); // Ensure focus is updated
editor.on('blur', handleBlur); // Ensure blur is updated
}}
editorProps={{
attributes: {
class: "tiptap-prosemirror",
style:
isMobile ?
`overflow: auto; min-height: ${
customEditorHeight ? "200px" : "0px"
}; max-height:calc(100svh - ${customEditorHeight || "140px"})`: `overflow: auto; max-height: 250px`,
},
handleKeyDown(view, event) {
if (!disableEnter && event.key === "Enter") {
if (event.shiftKey) {
view.dispatch(
view.state.tr.replaceSelectionWith(
view.state.schema.nodes.hardBreak.create()
)
);
return true;
} else {
if (typeof onEnter === "function") {
onEnter();
}
return true;
}
}
return false;
},
}}
/>
);
};