This commit is contained in:
Benjamin Toby
2025-07-20 10:35:54 +01:00
parent dd1d05251d
commit a0a0ab8ee4
99 changed files with 5678 additions and 909 deletions
+22 -8
View File
@@ -1,5 +1,6 @@
import React, { MutableRefObject } from "react";
import { twMerge } from "tailwind-merge";
import AceEditorModes from "./ace-editor-modes";
export type AceEditorComponentType = {
editorRef?: MutableRefObject<AceAjax.Editor>;
@@ -8,11 +9,16 @@ export type AceEditorComponentType = {
ctrlEnterFn?: (editor: AceAjax.Editor) => void;
content?: string;
placeholder?: string;
mode?: any;
mode?: (typeof AceEditorModes)[number];
fontSize?: string;
previewMode?: boolean;
onChange?: (value: string) => void;
delay?: number;
wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
refresh?: number;
};
let timeout: any;
@@ -34,10 +40,13 @@ export default function AceEditor({
previewMode,
onChange,
delay = 500,
refresh: externalRefresh,
wrapperProps,
}: AceEditorComponentType) {
try {
const editorElementRef = React.useRef<HTMLDivElement>();
const editorRefInstance = React.useRef<AceAjax.Editor>();
const editorElementRef = React.useRef<HTMLDivElement>(null);
// const editorRefInstance = React.useRef<AceAjax.Editor>(null);
const editorRefInstance = React.useRef<any>(null);
const [refresh, setRefresh] = React.useState(0);
const [darkMode, setDarkMode] = React.useState(false);
@@ -58,7 +67,7 @@ export default function AceEditor({
editor.setOptions({
mode: `ace/mode/${mode ? mode : "javascript"}`,
theme: darkMode
? "ace/theme/tomorrow_night_bright"
? "ace/theme/tomorrow_night_eighties"
: "ace/theme/ace_light",
value: content,
placeholder: placeholder ? placeholder : "",
@@ -89,14 +98,17 @@ export default function AceEditor({
setTimeout(() => {
onChange(editor.getValue());
console.log(editor.getValue());
}, delay);
}
});
editorRefInstance.current = editor;
if (editorRef) editorRef.current = editor;
}, [refresh, darkMode, ready]);
return function () {
editor.destroy();
};
}, [refresh, darkMode, ready, externalRefresh]);
React.useEffect(() => {
const htmlClassName = document.documentElement.className;
@@ -109,10 +121,12 @@ export default function AceEditor({
return (
<React.Fragment>
<div
{...wrapperProps}
className={twMerge(
"w-full h-[400px] block rounded-md overflow-hidden",
"w-full h-[400px] block rounded-default overflow-hidden",
"border border-slate-200 border-solid",
"dark:border-white/20"
"dark:border-white/20",
wrapperProps?.className
)}
>
<div
+132 -22
View File
@@ -1,8 +1,10 @@
import React from "react";
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";
export type TinyMCEEditorProps = {
export type TinyMCEEditorProps<KeyType extends string> = {
tinyMCE?: TinyMCE | null;
options?: RawEditorOptions;
editorRef?: React.MutableRefObject<Editor | null>;
@@ -11,7 +13,17 @@ export type TinyMCEEditorProps = {
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;
};
let interval: any;
@@ -20,60 +32,158 @@ let interval: any;
* # Tiny MCE Editor Component
* @className_wrapper twui-rte-wrapper
*/
export default function TinyMCEEditor({
export default function TinyMCEEditor<KeyType extends string>({
options,
editorRef,
setEditor,
tinyMCE,
wrapperProps,
defaultValue,
}: TinyMCEEditorProps) {
changeHandler,
wrapperWrapperProps,
borderProps,
name,
showLabel,
useParentCSS,
placeholder,
}: TinyMCEEditorProps<KeyType>) {
const editorComponentRef = React.useRef<HTMLDivElement>(null);
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 title = name ? twuiSlugToNormalText(name) : "Rich Text";
React.useEffect(() => {
if (!editorComponentRef.current) {
const htmlClassName = document.documentElement.className;
if (htmlClassName.match(/dark/i)) setDarkMode(true);
setTimeout(() => {
setThemeReady(true);
}, 200);
}, []);
React.useEffect(() => {
if (!editorComponentRef.current || !themeReady) {
return;
}
tinyMCE?.init({
height: FINAL_HEIGHT,
menubar: false,
plugins: [
"advlist lists link image charmap print preview anchor",
"searchreplace visualblocks code fullscreen",
"insertdatetime media table paste code help wordcount",
],
plugins:
"advlist lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table code help wordcount",
toolbar:
"undo redo | blocks | bold italic | bullist numlist outdent indent | removeformat",
"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 }",
"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("input", (e) => {
changeHandler?.(editor.getContent());
});
if (useParentCSS) {
useParentStyles(editor);
}
},
base_url: "https://datasquirel.com/tinymce-public",
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,
});
}, [tinyMCE]);
return function () {
tinyMCE?.remove();
};
}, [tinyMCE, themeReady]);
return (
<div
{...wrapperProps}
ref={editorComponentRef}
style={{
height: FINAL_HEIGHT + "px",
...wrapperProps?.style,
}}
{...wrapperWrapperProps}
className={twMerge(
"bg-slate-200 dark:bg-slate-700 rounded-sm",
"twui-rte-wrapper"
"relative w-full [&_.tox-tinymce]:!border-none",
"bg-background-light dark:bg-background-dark",
wrapperWrapperProps?.className
)}
/>
>
{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={name || "twui-tinymce"}
>
{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={name || "twui-tinymce"}
></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);
}
}
}
+125
View File
@@ -0,0 +1,125 @@
const AceEditorModes = [
"abap",
"abc",
"actionscript",
"ada",
"apache_conf",
"asciidoc",
"assembly_x86",
"autohotkey",
"batchfile",
"c9search",
"c_cpp",
"cirru",
"clojure",
"cobol",
"coffee",
"coldfusion",
"csharp",
"css",
"curly",
"d",
"dart",
"diff",
"dockerfile",
"dot",
"dummy",
"dummysyntax",
"eiffel",
"ejs",
"elixir",
"elm",
"erlang",
"forth",
"ftl",
"gcode",
"gherkin",
"gitignore",
"glsl",
"golang",
"groovy",
"haml",
"handlebars",
"haskell",
"haxe",
"html",
"html_ruby",
"ini",
"io",
"jack",
"jade",
"java",
"javascript",
"json",
"jsoniq",
"jsp",
"jsx",
"julia",
"latex",
"less",
"liquid",
"lisp",
"livescript",
"logiql",
"lsl",
"lua",
"luapage",
"lucene",
"makefile",
"markdown",
"mask",
"matlab",
"mel",
"mushcode",
"mysql",
"nix",
"objectivec",
"ocaml",
"pascal",
"perl",
"pgsql",
"php",
"powershell",
"praat",
"prolog",
"properties",
"protobuf",
"python",
"r",
"rdoc",
"rhtml",
"ruby",
"rust",
"sass",
"scad",
"scala",
"scheme",
"scss",
"sh",
"sjs",
"smarty",
"snippets",
"soy_template",
"space",
"sql",
"stylus",
"svg",
"tcl",
"tex",
"text",
"textile",
"toml",
"twig",
"typescript",
"vala",
"vbscript",
"velocity",
"verilog",
"vhdl",
"xml",
"xquery",
"yaml",
"shell",
] as const;
export default AceEditorModes;