UNPKG

strapi-plugin-jodit-editor

Version:

A powerful rich text editor plugin for Strapi v5 using the Jodit Editor with advanced formatting capabilities and seamless integration with Strapi's media library.

620 lines (619 loc) 22.2 kB
import { jsx, jsxs } from "react/jsx-runtime"; import { memo, useRef, useState, useCallback, useMemo } from "react"; import styled from "styled-components"; import { useIntl } from "react-intl"; import { Field, Loader, Flex } from "@strapi/design-system"; import { useFetchClient, useStrapiApp } from "@strapi/strapi/admin"; import JoditEditor from "jodit-react"; import { Pencil } from "@strapi/icons"; const __variableDynamicImportRuntimeHelper = (glob, path, segs) => { const v = glob[path]; if (v) { return typeof v === "function" ? v() : Promise.resolve(v); } return new Promise((_, reject) => { (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)( reject.bind( null, new Error( "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "") ) ) ); }); }; const PLUGIN_ID = "jodit-editor"; const STRAPI_MEDIA_BUTTON_NAME = "strapiMedia"; const DEFAULT_BUTTONS = `source, bold, italic, underline, strikethrough, superscript, subscript, eraser, font, fontsize, brush, paragraph, classSpan, |, ul, ol, indent, outdent, left, center, right, justify, |, link, unlink, ${STRAPI_MEDIA_BUTTON_NAME}, image, file, video, table, hr, symbols, lineHeight, |, copy, cut, paste, copyformat, selectall, undo, redo, fullsize, print, preview, find, spellcheck, about`; const cursorPlaceholder = `current_cursor_placeholder`; const cursorPlaceholderContent = `<${cursorPlaceholder}></${cursorPlaceholder}>`; const JoditContainer = styled.div` h1, h2, h3, h4, h5, h6 { font-weight: 700; } h1 { font-size: 4rem; margin-bottom: 1rem; } h2 { font-size: 3.5rem; margin-bottom: 0.75rem; } h3 { font-size: 3rem; margin-bottom: 0.5rem; } h4, h5, h6 { font-size: 2rem; margin-bottom: 0.25rem; } p { margin-bottom: 1rem; line-height: 1.6; } ul, ol { margin-bottom: 1rem; padding-left: 1.5rem; } ul { list-style-type: disc; } ol { list-style-type: decimal; } ul li, ol li { margin-bottom: 0.5rem; } blockquote { margin: 1rem 0; padding-left: 1rem; border-left: 4px solid #ccc; color: #666; } .jodit-toolbar-button_strapiMedia .jodit-icon { height: 19px; width: 19px; } `; const prefixFileUrlWithBackendUrl = (url) => { return url.startsWith("/") ? `${window.location.origin}${url}` : url; }; const generateMediaHtml = (file) => { const { url, alt = "", mime, name = "media" } = file; if (mime.startsWith("image/")) { return `<img src="${url}" alt="${alt || name}" />`; } else if (mime.startsWith("video/")) { return `<video controls style="max-width: 100%;"> <source src="${url}" type="${mime}"> Your browser does not support the video tag. </video>`; } else if (mime.startsWith("audio/")) { return `<audio controls> <source src="${url}" type="${mime}"> Your browser does not support the audio tag. </audio>`; } else { return `<a href="${url}" download="${name}" target="_blank">${alt || name}</a>`; } }; const IMAGE_SCHEMA_FIELDS = [ "name", "alternativeText", "url", "caption", "width", "height", "formats", "hash", "ext", "mime", "size", "previewUrl", "provider", "provider_metadata", "createdAt", "updatedAt" ]; const pick = (object, keys) => { const entries = keys.map((key) => [key, object[key]]); return Object.fromEntries(entries); }; const MediaLib = ({ isOpen = false, onChange = () => { }, onToggle = () => { } }) => { console.log(useStrapiApp("ImageDialog", (state) => state)); const components = useStrapiApp("ImageDialog", (state) => state.components); console.log(components); const ImageDialog = components["media-library"]; console.log(ImageDialog); const handleSelectAssets = (files) => { const formattedFiles = files.map((f) => { const expectedFile = pick(f, IMAGE_SCHEMA_FIELDS); const nodeFile = { ...expectedFile, alternativeText: expectedFile.alternativeText || expectedFile.name, url: prefixFileUrlWithBackendUrl(f.url), mime: f.mime, name: f.name }; return nodeFile; }); console.log("📎 Jodit: Media library assets selected", formattedFiles); onChange(formattedFiles); }; if (!isOpen) { return null; } return /* @__PURE__ */ jsx( ImageDialog, { onClose: onToggle, onSelectAssets: handleSelectAssets, allowedTypes: [ "files", "images", "videos", "audios" ] } ); }; const JoditInput = ({ name, value, onChange, required = false, disabled = false, error, description, intlLabel, attribute, label, hint, placeholder, fieldSchema, metadatas }) => { const mediaLibButton = { name: "strapiMedia", iconURL: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMycHgiIGhlaWdodD0iMzJweCIgZmlsbD0iIzIxMjEzNCI+PHBhdGggZD0iTTI3IDVIOWEyIDIgMCAwIDAtMiAydjJINWEyIDIgMCAwIDAtMiAydjE0YTIgMiAwIDAgMCAyIDJoMThhMiAyIDAgMCAwIDItMnYtMmgyYTIgMiAwIDAgMCAyLTJWN2EyIDIgMCAwIDAtMi0ybS01LjUgNGExLjUgMS41IDAgMSAxIDAgMyAxLjUgMS41IDAgMCAxIDAtM00yMyAyNUg1VjExaDJ2MTBhMiAyIDAgMCAwIDIgMmgxNHptNC00SDl2LTQuNWw0LjUtNC41IDYuMjA4IDYuMjA4YTEgMSAwIDAgMCAxLjQxMyAwTDI0LjMzIDE1IDI3IDE3LjY3MnoiPjwvcGF0aD48L3N2Zz4=", tooltip: "Strapi Media Library", exec: function(jodit) { console.log(`📎 Jodit: Open Strapi media library`); jodit.selection.insertHTML(cursorPlaceholderContent); const newContent = jodit.value; onChange({ target: { name, value: newContent.split(cursorPlaceholderContent).join("").trim() } }); toggleMediaLib(); } }; const { formatMessage } = useIntl(); const { post } = useFetchClient(); const editorRef = useRef(null); const [mediaLibVisible, setMediaLibVisible] = useState(false); const [initialValue] = useState(value || ""); const [isLoading, setIsLoading] = useState(false); const toggleMediaLib = useCallback(() => { setMediaLibVisible((prev) => !prev); }, []); const fileToMediaObject = async (file, handleFileUpload2, webpEnabled2 = []) => { return new Promise(async (resolve) => { if (handleFileUpload2) { try { console.log("📎 Jodit: Uploading image to media library..."); setIsLoading(true); let uploadedUrl = await handleFileUpload2(file); if (uploadedUrl) { if (webpEnabled2.includes(file.type)) { console.log("📎 Jodit: WebP conversion enabled, converting image...", file.type); uploadedUrl = uploadedUrl.replace(`.${file.type.replace("image/", "")}`, ".webp"); } resolve({ url: uploadedUrl, alt: file.name, mime: file.type, name: file.name }); setIsLoading(false); return; } else { console.warn("📎 Jodit: Upload failed, falling back to base64"); } } catch (error2) { console.error("📎 Jodit: Upload error, falling back to base64:", error2); } setIsLoading(false); } }); }; const handleMediaLibChange = async (files) => { const mediaToInsert = files.length ? files.filter((file) => file.mime?.startsWith("image/") || file.mime?.startsWith("video/") || file.mime?.startsWith("audio/")).map((file) => generateMediaHtml(file)).join("") : ""; const jodit = editorRef.current; const nodeToSelect = jodit?.editor.querySelector(cursorPlaceholder); if (nodeToSelect) { jodit?.selection?.setCursorBefore(nodeToSelect); jodit?.selection.removeNode(nodeToSelect); } jodit?.selection.insertHTML(mediaToInsert, true); setMediaLibVisible(false); }; const options = attribute?.options || {}; const height = options.height || 400; const buttons = options.buttons ? options.buttons.split(",").map((btn) => btn.trim()) : DEFAULT_BUTTONS.split(",").map((btn) => btn.trim()); const mediaLibButtonIndex = buttons.findIndex((btn) => btn === STRAPI_MEDIA_BUTTON_NAME); if (mediaLibButtonIndex !== -1) { buttons[mediaLibButtonIndex] = mediaLibButton; } const removeButtons = options.removeButtons ? options.removeButtons.split(",").map((btn) => btn.trim()) : []; const showToolbar = options.toolbar !== false; const fonts = options.fonts ? options.fonts.split("\n").reduce((acc, font) => { acc[`${font.trim()}`] = font.split(",")[0].trim(); return acc; }, {}) : {}; const webpEnabled = options.webp !== "" ? options.webp?.split(",") : []; const handleFileUpload = useCallback(async (file) => { try { const formData = new FormData(); formData.append("files", file); const response = await post("/upload", formData, { headers: { "Content-Type": "multipart/form-data" } }); if (response.data && response.data.length > 0) { const uploadedFile = response.data[0]; console.log("Jodit file uploaded successfully:", uploadedFile); return prefixFileUrlWithBackendUrl(uploadedFile.url); } return null; } catch (error2) { console.error("Jodit file upload error:", error2); return null; } }, [post]); const config = useMemo(() => ({ readonly: disabled || options.readonly || false, height, toolbar: showToolbar, placeholder: formatMessage({ id: placeholder || "jodit-editor.placeholder", defaultMessage: "Start typing..." }), style: { fontFamily: "Helvetica", fontSize: "14px", h1: { fontSize: "24px", fontWeight: "bold" } }, // Toolbar configuration buttons, removeButtons, controls: { font: { list: Object.keys(fonts).length > 0 ? fonts : {} } }, // Event handlers events: { afterInit: function(jodit) { console.log("📎 Jodit: Editor initialized, storing instance:", jodit); }, beforeOpen: () => { console.log("📎 Jodit: Editor opened"); }, // Handle paste events for images, videos, and audio paste: async (e) => { const items = e.clipboardData?.items; const jodit = editorRef.current; jodit?.selection.insertHTML(cursorPlaceholderContent); if (items) { for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.type.startsWith("image/") || item.type.startsWith("video/") || item.type.startsWith("audio/")) { e.preventDefault(); const file = item.getAsFile(); if (file) { const mediaObject = await fileToMediaObject(file, handleFileUpload, webpEnabled); const mediaHtml = generateMediaHtml(mediaObject); const jodit2 = editorRef.current; const nodeToSelect = jodit2?.editor.querySelector(cursorPlaceholder); if (nodeToSelect) { jodit2?.selection?.setCursorBefore(nodeToSelect); jodit2?.selection.removeNode(nodeToSelect); } jodit2?.selection.insertHTML(mediaHtml); const newContent = jodit2?.value || ""; onChange({ target: { name, value: newContent.split(cursorPlaceholderContent).join("").trim() } }); } break; } } } }, // Handle drag and drop for images, videos, and audio drop: async (e) => { const jodit = editorRef.current; jodit?.selection.insertHTML(cursorPlaceholderContent); const files = e.dataTransfer?.files; if (files && files.length > 0) { e.preventDefault(); for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.type.startsWith("image/") || file.type.startsWith("video/") || file.type.startsWith("audio/")) { const mediaObject = await fileToMediaObject(file, handleFileUpload, webpEnabled); const mediaHtml = generateMediaHtml(mediaObject); const jodit2 = editorRef.current; const nodeToSelect = jodit2?.editor.querySelector(cursorPlaceholder); if (nodeToSelect) { jodit2?.selection?.setCursorBefore(nodeToSelect); jodit2?.selection.removeNode(nodeToSelect); } jodit2?.selection.insertHTML(mediaHtml); const newContent = jodit2?.value || ""; onChange({ target: { name, value: newContent.split(cursorPlaceholderContent).join("").trim() } }); } } } } }, // Language configuration language: "en", // Theme theme: "default", // Additional Strapi-specific settings beautifyHTML: true, allowTabNavigation: true, askBeforePasteHTML: false, askBeforePasteFromWord: false, cleanHTML: { removeUnknownDOMElements: false } }), [ disabled, height, showToolbar, removeButtons, placeholder, formatMessage, toggleMediaLib, handleFileUpload ]); const displayLabel = label || fieldSchema?.displayName || metadatas?.label || formatMessage(intlLabel); const displayDescription = description || fieldSchema?.description || metadatas?.description; const displayHint = hint; return /* @__PURE__ */ jsx(JoditContainer, { children: /* @__PURE__ */ jsxs( Field.Root, { name, id: name, required, error, hint: displayHint, style: { position: "relative" }, children: [ /* @__PURE__ */ jsx(Field.Label, { children: displayLabel }), /* @__PURE__ */ jsx("div", { style: { position: "relative" }, children: /* @__PURE__ */ jsx( JoditEditor, { value: initialValue, ref: editorRef, editorRef: (editor) => { editorRef.current = editor; }, config, onBlur: (newContent) => { console.log("📎 Jodit: Content changed", newContent?.length || 0, "characters"); const jodit = editorRef.current; jodit?.selection.save(); onChange({ target: { name, value: newContent.split(cursorPlaceholderContent).join("").trim() } }); }, onChange: (newContent) => { console.log("📎 Jodit: Content changed", newContent?.length || 0, "characters"); const jodit = editorRef.current; jodit?.selection.save(); onChange({ target: { name, value: newContent.split(cursorPlaceholderContent).join("").trim() } }); } } ) }), displayDescription ? /* @__PURE__ */ jsx(Field.Hint, { children: displayDescription }) : null, /* @__PURE__ */ jsx(Field.Hint, {}), /* @__PURE__ */ jsx(Field.Error, {}), /* @__PURE__ */ jsx( MediaLib, { isOpen: mediaLibVisible, onChange: handleMediaLibChange, onToggle: toggleMediaLib } ), isLoading ? /* @__PURE__ */ jsx( "div", { style: { position: "absolute", top: 0, right: 0, width: "100%", height: "100%", background: "rgba(255,255,255,0.5)" }, children: /* @__PURE__ */ jsx( "div", { style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(255,255,255,0.5)" }, children: /* @__PURE__ */ jsx(Loader, {}) } ) } ) : null ] } ) }); }; const JoditInput$1 = memo(JoditInput, (prevProps, nextProps) => { return prevProps.name === nextProps.name && prevProps.required === nextProps.required && prevProps.disabled === nextProps.disabled && prevProps.error === nextProps.error; }); const JoditInput$2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: JoditInput$1 }, Symbol.toStringTag, { value: "Module" })); const IconBox = styled(Flex)` padding: 6px; background-color: #f0f0ff; /* primary100 */ border: 1px solid #d9d8ff; /* primary200 */ svg > path { fill: #4945ff; /* primary600 */ } `; const PluginIcon = () => /* @__PURE__ */ jsx(IconBox, { justifyContent: "center", alignItems: "center", hasRadius: true, children: /* @__PURE__ */ jsx(Pencil, {}) }); const index = { register(app) { console.log("🎯 Jodit Editor plugin - ADMIN REGISTER function called!"); app.customFields.register({ name: "jodit", pluginId: PLUGIN_ID, type: "richtext", icon: PluginIcon, intlLabel: { id: `${PLUGIN_ID}.jodit.label`, defaultMessage: "Jodit Editor" }, intlDescription: { id: `${PLUGIN_ID}.jodit.description`, defaultMessage: "Rich text editor powered by Jodit with advanced formatting options" }, components: { Input: async () => Promise.resolve().then(() => JoditInput$2) }, options: { advanced: [ { sectionTitle: { id: `${PLUGIN_ID}.jodit.options.advanced.settings`, defaultMessage: "Editor Settings" }, items: [ { name: "options.height", type: "number", intlLabel: { id: `${PLUGIN_ID}.jodit.options.height.label`, defaultMessage: "Editor Height (px)" }, description: { id: `${PLUGIN_ID}.jodit.options.height.description`, defaultMessage: "Set the height of the editor in pixels" }, defaultValue: 400 }, { name: "options.readonly", type: "checkbox", intlLabel: { id: `${PLUGIN_ID}.jodit.options.readonly.label`, defaultMessage: "Read Only" }, description: { id: `${PLUGIN_ID}.jodit.options.readonly.description`, defaultMessage: "Make the editor read-only" }, defaultValue: false }, { name: "options.buttons", type: "textarea", intlLabel: { id: `${PLUGIN_ID}.jodit.options.buttons.label`, defaultMessage: "Toolbar Buttons (comma-separated)" }, description: { id: `${PLUGIN_ID}.jodit.options.buttons.description`, defaultMessage: `Specify which buttons to include in the toolbar. Default: ${DEFAULT_BUTTONS} (${STRAPI_MEDIA_BUTTON_NAME} for Strapi's media library)` }, defaultValue: DEFAULT_BUTTONS }, { name: "options.removeButtons", type: "textarea", intlLabel: { id: `${PLUGIN_ID}.jodit.options.removeButtons.label`, defaultMessage: "Remove Buttons (comma-separated)" }, description: { id: `${PLUGIN_ID}.jodit.options.removeButtons.description`, defaultMessage: "Specify which buttons to remove from the toolbar. Example: bold,italic,underline" } }, { name: "options.toolbar", type: "checkbox", intlLabel: { id: `${PLUGIN_ID}.jodit.options.toolbar.label`, defaultMessage: "Show Toolbar" }, description: { id: `${PLUGIN_ID}.jodit.options.toolbar.description`, defaultMessage: "Whether to show the editor toolbar" }, defaultValue: true }, { name: "options.fonts", type: "textarea", intlLabel: { id: `${PLUGIN_ID}.jodit.options.fonts.label`, defaultMessage: "Custom Fonts (one line per value)" }, description: { id: `${PLUGIN_ID}.jodit.options.fonts.description`, defaultMessage: "Set the available fonts for the editor. Example: Arial, Helvetica, sans-serif" }, defaultValue: "" }, { name: "options.webp", type: "textarea", intlLabel: { id: `${PLUGIN_ID}.jodit.options.webp.label`, defaultMessage: "WebP Conversion Settings" }, description: { id: `${PLUGIN_ID}.jodit.options.webp.description`, defaultMessage: "Set Mime Types separated by commas for WebP conversion. Example: image/jpeg,image/jpg,image/png,image/bmp" }, defaultValue: "" } ] } ] } }); console.log("🎯 Jodit Editor custom field registered successfully!"); }, async registerTrads({ locales }) { return Promise.all( locales.map(async (locale) => { try { const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-eJbSYHI0.mjs") }), `./translations/${locale}.json`, 3); return { data, locale }; } catch { return { data: {}, locale }; } }) ); } }; export { index as default };