import { useState } from "react"; import { GlobalStyles } from "tss-react"; import { alpha, styled, useTheme } from "@mui/material"; import { Editor } from "@tinymce/tinymce-react"; // TinyMCE so the global var exists import "tinymce/tinymce.min.js"; // Theme import "tinymce/themes/silver"; // Toolbar icons import "tinymce/icons/default"; // Editor styles /* eslint import/no-webpack-loader-syntax: off */ import skinCss from "tinymce/skins/ui/oxide/skin.min.css?inline"; import skinDarkCss from "tinymce/skins/ui/oxide-dark/skin.min.css?inline"; // Content styles, including inline UI like fake cursors /* eslint import/no-webpack-loader-syntax: off */ import contentCss from "tinymce/skins/content/default/content.min.css?inline"; import contentUiCss from "tinymce/skins/ui/oxide/content.min.css?inline"; import contentCssDark from "tinymce/skins/content/dark/content.min.css?inline"; import contentUiCssDark from "tinymce/skins/ui/oxide-dark/content.min.css?inline"; // Plugins import "tinymce/plugins/autoresize"; import "tinymce/plugins/lists"; import "tinymce/plugins/link"; import "tinymce/plugins/image"; import "tinymce/plugins/paste"; import "tinymce/plugins/help"; import "tinymce/plugins/code"; import "tinymce/plugins/fullscreen"; import { useAtom } from "jotai"; import { firebaseStorageAtom } from "@src/sources/ProjectSourceFirebase"; import { projectScope } from "@src/atoms/projectScope"; import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage"; import { generateId } from "@src/utils/table"; import { useSnackbar } from "notistack"; import { ColumnConfig, TableRowRef } from "@src/types/table"; import { IMAGE_MIME_TYPES } from "./fields/Image"; const Styles = styled("div", { shouldForwardProp: (prop) => prop !== "focus", })<{ focus?: boolean; disabled?: boolean }>(({ theme, focus, disabled }) => ({ "& .tox": { "&.tox-tinymce": { borderRadius: theme.shape.borderRadius, border: "none", backgroundColor: theme.palette.action.input, boxShadow: `0 -1px 0 0 ${theme.palette.text.disabled} inset, 0 0 0 1px ${theme.palette.action.inputOutline} inset`, transition: theme.transitions.create("box-shadow", { duration: theme.transitions.duration.short, }), "&:hover": { boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset, 0 0 0 1px ${theme.palette.action.inputOutline} inset`, }, "&.tox-fullscreen": { zIndex: theme.zIndex.modal, backgroundColor: theme.palette.background.paper, }, }, "& .tox-toolbar-overlord, & .tox-edit-area__iframe, & .tox-toolbar__primary": { background: "transparent", borderRadius: theme.shape.borderRadius, }, "& .tox-edit-area__iframe": { colorScheme: "auto" }, "& .tox-toolbar__group": { border: "none !important", "& .tox-tbtn": { "&:hover:": { backgroundColor: "inherit", }, "&:focus": { backgroundColor: "inherit", }, }, "& .tox-tbtn__select-chevron": { transition: theme.transitions.create("transform", { duration: theme.transitions.duration.short, }), }, "& .tox-tbtn--select": { "& .tox-tbtn__select-chevron": { transform: "none", }, }, "& .tox-tbtn--active": { "& .tox-tbtn__select-chevron": { transform: "rotate(180deg)", }, }, }, "& .tox-tbtn": { borderRadius: theme.shape.borderRadius, color: theme.palette.text.secondary, cursor: "pointer", margin: 0, transition: theme.transitions.create(["color", "background-color"], { duration: theme.transitions.duration.shortest, }), "&:hover": { color: theme.palette.text.primary, backgroundColor: "transparent", }, "& svg": { fill: "currentColor" }, }, "& .tox-tbtn--enabled, & .tox-tbtn--enabled:hover": { backgroundColor: theme.palette.action.selected + " !important", color: theme.palette.text.primary, }, "& .tox.tox-tinymce, & .tox.tox-tinymce:hover": disabled ? { backgroundColor: theme.palette.mode === "dark" ? "transparent" : theme.palette.action.disabledBackground, } : focus ? { boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset, 0 0 0 1px ${theme.palette.action.inputOutline} inset`, } : {}, }, })); export interface IRichTextEditorProps { value?: string; onChange: (value: string) => void; disabled?: boolean; id: string; onFocus?: () => void; onBlur?: () => void; column: ColumnConfig; _rowy_ref: TableRowRef; } export default function RichTextEditor({ value, onChange, disabled, id, onFocus, onBlur, column, _rowy_ref, }: IRichTextEditorProps) { const theme = useTheme(); const [focus, setFocus] = useState(false); const [firebaseStorage] = useAtom(firebaseStorageAtom, projectScope); const { enqueueSnackbar } = useSnackbar(); const handleImageUpload = (file: any) => { return new Promise((resolve, reject) => { const path = _rowy_ref.path; const key = column.key; const storageRef = ref( firebaseStorage, `${path}/${key}/${generateId()}-${file.name}` ); const uploadTask = uploadBytesResumable(storageRef, file, { cacheControl: "public, max-age=31536000", }); uploadTask.on( "state_changed", null, (error: any) => { reject(error); }, () => { getDownloadURL(uploadTask.snapshot.ref).then( (downloadURL: string) => { resolve(downloadURL); } ); } ); }); }; return ( { const input = document.createElement("input"); input.setAttribute("type", "file"); input.setAttribute("accept", IMAGE_MIME_TYPES.join(",")); // Handle file selection input.onchange = async () => { const file = input && input.files && input.files[0]; try { const imageUrl = await handleImageUpload(file); // Upload the image to Firebase Storage // Create the image object to be inserted into the editor const imageObj = { src: imageUrl, alt: file && file.name, }; // Pass the image object to the callback function callback(imageUrl, imageObj); } catch (error) { enqueueSnackbar("Error uploading image", { variant: "error", }); } }; input.click(); }, }} value={value} onEditorChange={onChange} onFocus={() => { setFocus(true); if (onFocus) onFocus(); }} onBlur={() => { setFocus(false); if (onBlur) onBlur(); }} /> ); }