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.

622 lines (621 loc) 22.8 kB
"use strict"; const jsxRuntime = require("react/jsx-runtime"); const react = require("react"); const styled = require("styled-components"); const reactIntl = require("react-intl"); const designSystem = require("@strapi/design-system"); const admin = require("@strapi/strapi/admin"); const JoditEditor = require("jodit-react"); const icons = require("@strapi/icons"); const _interopDefault = (e) => e && e.__esModule ? e : { default: e }; const styled__default = /* @__PURE__ */ _interopDefault(styled); const JoditEditor__default = /* @__PURE__ */ _interopDefault(JoditEditor); 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__default.default.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(admin.useStrapiApp("ImageDialog", (state) => state)); const components = admin.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__ */ jsxRuntime.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 } = reactIntl.useIntl(); const { post } = admin.useFetchClient(); const editorRef = react.useRef(null); const [mediaLibVisible, setMediaLibVisible] = react.useState(false); const [initialValue] = react.useState(value || ""); const [isLoading, setIsLoading] = react.useState(false); const toggleMediaLib = react.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 = react.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 = react.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__ */ jsxRuntime.jsx(JoditContainer, { children: /* @__PURE__ */ jsxRuntime.jsxs( designSystem.Field.Root, { name, id: name, required, error, hint: displayHint, style: { position: "relative" }, children: [ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: displayLabel }), /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "relative" }, children: /* @__PURE__ */ jsxRuntime.jsx( JoditEditor__default.default, { 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__ */ jsxRuntime.jsx(designSystem.Field.Hint, { children: displayDescription }) : null, /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}), /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {}), /* @__PURE__ */ jsxRuntime.jsx( MediaLib, { isOpen: mediaLibVisible, onChange: handleMediaLibChange, onToggle: toggleMediaLib } ), isLoading ? /* @__PURE__ */ jsxRuntime.jsx( "div", { style: { position: "absolute", top: 0, right: 0, width: "100%", height: "100%", background: "rgba(255,255,255,0.5)" }, children: /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(designSystem.Loader, {}) } ) } ) : null ] } ) }); }; const JoditInput$1 = react.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__default.default(designSystem.Flex)` padding: 6px; background-color: #f0f0ff; /* primary100 */ border: 1px solid #d9d8ff; /* primary200 */ svg > path { fill: #4945ff; /* primary600 */ } `; const PluginIcon = () => /* @__PURE__ */ jsxRuntime.jsx(IconBox, { justifyContent: "center", alignItems: "center", hasRadius: true, children: /* @__PURE__ */ jsxRuntime.jsx(icons.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": () => Promise.resolve().then(() => require("../_chunks/en-DQ5rLjJ6.js")) }), `./translations/${locale}.json`, 3); return { data, locale }; } catch { return { data: {}, locale }; } }) ); } }; module.exports = index;