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.

464 lines (463 loc) 15.6 kB
import { jsxs, jsx } from "react/jsx-runtime"; import { memo, useRef, useState, useCallback, useMemo } from "react"; import styled from "styled-components"; import JoditEditorImport from "jodit-react"; import { useIntl } from "react-intl"; import { Field, Loader } from "@strapi/design-system"; import { useFetchClient, useStrapiApp } from "@strapi/strapi/admin"; import { D as DEFAULT_BUTTONS, S as STRAPI_MEDIA_BUTTON_NAME } from "./index-CgnzvvLc.mjs"; const JoditEditor = JoditEditorImport.default || JoditEditorImport; const cursorPlaceholder = `current_cursor_placeholder`; const cursorPlaceholderContent = `<${cursorPlaceholder}></${cursorPlaceholder}>`; const sanitizeEditorContent = (content = "") => content.split(cursorPlaceholderContent).join("").trim(); const saveWysiwygSelection = (jodit) => { if (jodit?.isEditorMode()) { jodit.selection.save(); } }; 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 = () => { } }) => { const components = useStrapiApp("ImageDialog", (state) => state.components); if (!components || !isOpen) return null; const ImageDialog = components?.["media-library"] ?? null; 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 || !ImageDialog) { return null; } const ComponentToRender = ImageDialog?.default || ImageDialog; return /* @__PURE__ */ jsx( ComponentToRender, { 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 { 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 emitContentChange = useCallback((content = "") => { onChange({ target: { name, value: sanitizeEditorContent(content) } }); }, [name, onChange]); 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; emitContentChange(newContent); toggleMediaLib(); } }; 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: intlLabel?.defaultMessage || "Start typing here..." }), 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 || ""; emitContentChange(newContent); } 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 || ""; emitContentChange(newContent); } } } } }, // Language configuration language: "en", // Theme theme: "default", // Additional Strapi-specific settings beautifyHTML: true, allowTabNavigation: true, askBeforePasteHTML: false, askBeforePasteFromWord: false, cleanHTML: { disableCleanFilter: "true" } }), [ disabled, height, showToolbar, removeButtons, placeholder, formatMessage, toggleMediaLib, handleFileUpload, emitContentChange ]); const displayLabel = label || fieldSchema?.displayName || metadatas?.label || formatMessage(intlLabel); const displayDescription = description || fieldSchema?.description || metadatas?.description; const displayHint = hint; return /* @__PURE__ */ jsxs( Field.Root, { name, id: name, required, error, hint: displayHint, style: { position: "relative" }, children: [ /* @__PURE__ */ jsx(Field.Label, { children: displayLabel }), /* @__PURE__ */ jsx(JoditContainer, { 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; saveWysiwygSelection(jodit); emitContentChange(newContent); }, onChange: (newContent) => { console.log("📎 Jodit: Content changed", newContent?.length || 0, "characters"); const jodit = editorRef.current; saveWysiwygSelection(jodit); emitContentChange(newContent); } } ) }), displayDescription ? /* @__PURE__ */ jsx(Field.Hint, { children: displayDescription }) : null, error ? /* @__PURE__ */ jsx(Field.Error, { children: error }) : null, /* @__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_default = memo(JoditInput, (prevProps, nextProps) => { return prevProps.name === nextProps.name && prevProps.required === nextProps.required && prevProps.disabled === nextProps.disabled && prevProps.error === nextProps.error; }); export { JoditInput_default as default };