new-personal-site/components/lib/editors/TinyMCE/index.tsx
2025-12-02 16:30:46 +01:00

225 lines
7.3 KiB
TypeScript

import React, { ComponentProps } from "react";
import { RawEditorOptions, TinyMCE, Editor } from "./tinymce";
import { twMerge } from "tailwind-merge";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import Border from "../../elements/Border";
import useTinyMCE from "./useTinyMCE";
export type TinyMCEEditorProps<KeyType extends string> = {
options?: RawEditorOptions;
editorRef?: React.MutableRefObject<Editor | null>;
setEditor?: React.Dispatch<React.SetStateAction<Editor>>;
wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
wrapperWrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
borderProps?: ComponentProps<typeof Border>;
defaultValue?: string;
name?: KeyType;
changeHandler?: (content: string) => void;
showLabel?: boolean;
useParentCSS?: boolean;
placeholder?: string;
};
/**
* # Tiny MCE Editor Component
* @className_wrapper twui-rte-wrapper
*/
export default function TinyMCEEditor<KeyType extends string>({
options,
editorRef,
setEditor: passedSetEditor,
wrapperProps,
defaultValue,
changeHandler,
wrapperWrapperProps,
borderProps,
name,
showLabel,
useParentCSS,
placeholder,
}: TinyMCEEditorProps<KeyType>) {
const { tinyMCE } = useTinyMCE();
const editorComponentRef = React.useRef<HTMLDivElement>(null);
const EDITOR_VALUE_CHANGE_TIMEOUT = 500;
const FINAL_HEIGHT = options?.height || 500;
const [themeReady, setThemeReady] = React.useState(false);
const [ready, setReady] = React.useState(false);
const [darkMode, setDarkMode] = React.useState(false);
const [refresh, setRefresh] = React.useState(0);
const [editor, setEditor] = React.useState<Editor>();
const title = name ? twuiSlugToNormalText(name) : "Rich Text";
React.useEffect(() => {
if (!tinyMCE) {
return;
}
const htmlClassName = document.documentElement.className;
if (htmlClassName.match(/dark/i)) setDarkMode(true);
setTimeout(() => {
setThemeReady(true);
}, 200);
}, [tinyMCE]);
let valueTimeout: any;
const id = crypto.randomUUID();
React.useEffect(() => {
if (!editorComponentRef.current || !themeReady || !tinyMCE) {
return;
}
const baseUrl =
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
"https://www.datasquirel.com/tinymce-public";
tinyMCE.init({
height: FINAL_HEIGHT,
menubar: false,
plugins:
"advlist lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table code help wordcount",
toolbar:
"undo redo | blocks | bold italic underline link image | bullist numlist outdent indent | removeformat code searchreplace wordcount preview insertdatetime",
content_style:
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }",
init_instance_callback: (editor) => {
setEditor(editor as any);
if (editorRef) editorRef.current = editor as any;
if (defaultValue) editor.setContent(defaultValue);
setReady(true);
// editor.on("change", (e) => {
// changeHandler?.(editor.getContent());
// });
editor.on("input", (e) => {
if (changeHandler) {
window.clearTimeout(valueTimeout);
valueTimeout = setTimeout(() => {
changeHandler(editor.getContent());
}, EDITOR_VALUE_CHANGE_TIMEOUT);
}
});
if (useParentCSS) {
useParentStyles(editor);
}
},
base_url: baseUrl,
body_class: "twui-tinymce",
placeholder,
relative_urls: true,
remove_script_host: true,
convert_urls: false,
...options,
license_key: "gpl",
target: editorComponentRef.current,
content_css: darkMode ? "dark" : undefined,
skin: darkMode ? "oxide-dark" : undefined,
});
return function () {
if (!ready) return;
const instance = editorComponentRef.current
? tinyMCE?.get(editorComponentRef.current?.id)
: undefined;
instance?.remove();
};
}, [tinyMCE, themeReady, refresh]);
return (
<div
{...wrapperWrapperProps}
className={twMerge(
"relative w-full [&_.tox-tinymce]:!border-none",
"bg-background-light dark:bg-background-dark",
wrapperWrapperProps?.className
)}
onInput={(e) => {
console.log(`Input Detected`);
}}
>
{showLabel && (
<label
className={twMerge(
"absolute z-10 -top-[7px] left-[10px] px-2 text-xs",
"bg-background-light dark:bg-background-dark text-gray-500",
"dark:text-white/80 rounded"
)}
htmlFor={id}
>
{title}
</label>
)}
<Border
{...borderProps}
className={twMerge(
"dark:border-white/30 p-0 pt-2",
borderProps?.className
)}
>
<div
{...wrapperProps}
ref={editorComponentRef}
style={{
height:
String(FINAL_HEIGHT).replace(/[^\d]/g, "") + "px",
...wrapperProps?.style,
}}
className={twMerge(
"bg-slate-200 dark:bg-slate-700 rounded-sm w-full",
"twui-rte-wrapper"
)}
id={id}
></div>
</Border>
</div>
);
}
function useParentStyles(editor: Editor) {
const doc = editor.getDoc();
const parentStylesheets = document.styleSheets;
for (const sheet of parentStylesheets) {
try {
if (sheet.href) {
const link = doc.createElement("link");
link.rel = "stylesheet";
link.href = sheet.href;
doc.head.appendChild(link);
} else {
const rules = sheet.cssRules || sheet.rules;
if (rules) {
const style = doc.createElement("style");
for (const rule of rules) {
try {
style.appendChild(doc.createTextNode(rule.cssText));
} catch (e) {
console.warn("Could not copy CSS rule:", rule, e);
}
}
doc.head.appendChild(style);
}
}
} catch (e) {
console.warn("Error processing stylesheet:", sheet, e);
}
}
}