This commit is contained in:
2026-03-04 17:35:14 +01:00
parent f73b56cdc4
commit db26e26495
113 changed files with 12433 additions and 169 deletions
+172
View File
@@ -0,0 +1,172 @@
import React, { MutableRefObject } from "react";
import { twMerge } from "tailwind-merge";
import AceEditorModes from "./ace-editor-modes";
import { AceEditorOptions } from "@moduletrace/datasquirel/dist/package-shared/types";
export type AceEditorComponentType = {
editorRef?: MutableRefObject<AceAjax.Editor | undefined>;
readOnly?: boolean;
/** Function to call when Ctrl+Enter is pressed */
ctrlEnterFn?: (editor: AceAjax.Editor) => void;
content?: string;
placeholder?: string;
title?: string;
mode?: (typeof AceEditorModes)[number];
fontSize?: string;
previewMode?: boolean;
onChange?: (value: string) => void;
delay?: number;
wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
refreshDepArr?: any[];
editorOptions?: AceEditorOptions;
showLabel?: boolean;
};
let timeout: any;
/**
* # Powerful Ace Editor
* @note **NOTE** head scripts required
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js`
* @script `https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ext-language_tools.min.js`
*/
export default function AceEditor({
editorRef,
readOnly,
ctrlEnterFn,
content = "",
placeholder,
mode,
fontSize,
previewMode,
onChange,
delay = 500,
refreshDepArr,
wrapperProps,
editorOptions,
showLabel,
title,
}: AceEditorComponentType) {
try {
const editorElementRef = React.useRef<HTMLDivElement>(undefined);
const editorRefInstance = React.useRef<AceAjax.Editor>(undefined);
const [refresh, setRefresh] = React.useState(0);
const [darkMode, setDarkMode] = React.useState(false);
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
if (!ready) return;
if (!ace?.edit || !editorElementRef.current) {
setTimeout(() => {
setRefresh((prev) => prev + 1);
}, 1000);
return;
}
const editor = ace.edit(editorElementRef.current);
editor.setOptions({
mode: `ace/mode/${mode ? mode : "javascript"}`,
theme: darkMode
? "ace/theme/tomorrow_night_eighties"
: "ace/theme/ace_light",
value: (() => {
try {
return JSON.stringify(JSON.parse(content), null, 4);
} catch (error) {
return content;
}
})(),
placeholder: placeholder ? placeholder : "",
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
readOnly: readOnly ? true : false,
fontSize: fontSize ? fontSize : null,
showLineNumbers: previewMode ? false : true,
wrap: true,
wrapMethod: "code",
...editorOptions,
});
editor.commands.addCommand({
name: "myCommand",
bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" },
exec: function (editor) {
if (ctrlEnterFn) ctrlEnterFn(editor);
},
readOnly: true,
});
editor.getSession().on("change", function (e) {
if (onChange) {
clearTimeout(timeout);
setTimeout(() => {
try {
onChange(editor.getValue());
} catch (error) {}
}, delay);
}
});
editorRefInstance.current = editor;
if (editorRef) editorRef.current = editor;
return function () {
editor.destroy();
};
}, [refresh, darkMode, ready, mode, ...(refreshDepArr || [])]);
React.useEffect(() => {
const htmlClassName = document.documentElement.className;
if (htmlClassName.match(/dark/i)) setDarkMode(true);
setTimeout(() => {
setReady(true);
}, 200);
}, []);
return (
<React.Fragment>
<div
{...wrapperProps}
className={twMerge(
"w-full h-[400px] block rounded-default",
"border border-slate-200 border-solid relative",
"dark:border-white/20",
showLabel && title ? "pt-4" : "",
wrapperProps?.className,
)}
>
{showLabel && title ? (
<label
className={twMerge(
"bg-background-light dark:bg-background-dark text-xs",
"-top-3 left-2 px-2 py-1 absolute z-10",
)}
>
{title}
</label>
) : null}
<div
ref={editorElementRef as any}
className="w-full h-full"
></div>
</div>
</React.Fragment>
);
} catch (error: any) {
return (
<React.Fragment>
<span className="m-0">
Editor Error:{" "}
<b className="text-red-600">{error.message}</b>
</span>
</React.Fragment>
);
}
}
+238
View File
@@ -0,0 +1,238 @@
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;
refreshDependencyArray?: any[];
};
/**
* # Tiny MCE Editor Component
* @className_wrapper twui-rte-wrapper
*/
export default function TinyMCEEditor<KeyType extends string>({
options,
editorRef: passedEditorRef,
setEditor: passedSetEditor,
wrapperProps,
defaultValue,
changeHandler,
wrapperWrapperProps,
borderProps,
name,
showLabel,
useParentCSS,
placeholder,
refreshDependencyArray,
}: TinyMCEEditorProps<KeyType>) {
const { tinyMCE } = useTinyMCE();
const editorComponentRef = React.useRef<HTMLDivElement>(null);
const editorRef = passedEditorRef || React.useRef<Editor>(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;
passedSetEditor?.(editor);
}
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, ...(refreshDependencyArray || [])]);
React.useEffect(() => {
const instance = editorRef.current;
if (instance) {
instance.setContent(defaultValue || "");
}
}, [defaultValue]);
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);
}
}
}
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
import React from "react";
import { TinyMCE } from "./tinymce";
let interval: any;
export default function useTinyMCE() {
const [tinyMCE, setTinyMCE] = React.useState<TinyMCE>();
const [refresh, setRefresh] = React.useState(0);
const [scriptLoaded, setScriptLoaded] = React.useState(false);
React.useEffect(() => {
if (refresh >= 5) return;
const clientWindow = window as Window & { tinymce?: TinyMCE };
if (clientWindow.tinymce) {
setScriptLoaded(true);
return;
}
const script = document.createElement("script");
const baseUrl =
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
"https://www.datasquirel.com/tinymce-public";
script.src = `${baseUrl}/tinymce.min.js`;
script.async = true;
script.onload = () => {
setScriptLoaded(true);
};
document.head.appendChild(script);
}, [refresh]);
React.useEffect(() => {
if (!scriptLoaded) return;
const clientWindow = window as Window & { tinymce?: TinyMCE };
let tinyMCE = clientWindow.tinymce;
if (tinyMCE) {
setTinyMCE(tinyMCE);
} else {
setRefresh((prev) => prev + 1);
}
}, [scriptLoaded]);
return { tinyMCE };
}
+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;