UNPKG

@_sh/strapi-plugin-ckeditor

Version:

Integrates CKEditor 5 into your Strapi project as a fully customizable custom field. (Community Edition)

614 lines (610 loc) 22.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const jsxRuntime = require("react/jsx-runtime"); const React = require("react"); const designSystem = require("@strapi/design-system"); const styledComponents = require("styled-components"); const admin = require("@strapi/strapi/admin"); const ckeditor5 = require("ckeditor5"); const ckeditor5React = require("@ckeditor/ckeditor5-react"); require("ckeditor5/ckeditor5.css"); const index = require("./index-CgWKes-C.js"); require("sanitize-html"); const icons = require("@strapi/icons"); const _interopDefault = (e) => e && e.__esModule ? e : { default: e }; const React__default = /* @__PURE__ */ _interopDefault(React); const STORAGE_KEYS = { TOKEN: "jwtToken", PREFERED_LANGUAGE: "strapi-admin-language", PROFILE_THEME: "STRAPI_THEME" }; function getCookieValue(name) { let result = null; const cookieArray = document.cookie.split(";"); cookieArray.forEach((cookie) => { const [key, value] = cookie.split("=").map((item) => item.trim()); if (key === name) { result = decodeURIComponent(value); } }); return result; } function getStoredToken() { const tokenFromStorage = localStorage.getItem(STORAGE_KEYS.TOKEN) ?? sessionStorage.getItem(STORAGE_KEYS.TOKEN); if (tokenFromStorage) { return JSON.parse(tokenFromStorage); } const tokenFromCookie = getCookieValue(STORAGE_KEYS.TOKEN); return tokenFromCookie; } function getPreferedLanguage() { const language = localStorage.getItem(STORAGE_KEYS.PREFERED_LANGUAGE)?.replace(/^"|"$/g, "") || "en"; return language; } function getProfileTheme() { const theme = localStorage.getItem(STORAGE_KEYS.PROFILE_THEME); return theme; } const TRANSLATIONS = {}; async function setUpLanguage(config, isFieldLocalized) { if (typeof config.language !== "object") { config.language = { ui: config.language, content: config.language, textPartLanguage: void 0 }; } config.language.ui ||= getPreferedLanguage(); if (isFieldLocalized) { config.language.content = detecti18n(); } if (config.language.ui !== "en") { await importLang(config, config.language.ui); } } async function importLang(config, language) { if (TRANSLATIONS[language]) { config.translations = TRANSLATIONS[language]; } else if (translationImports[language]) { const translation = await translationImports[language](); TRANSLATIONS[language] = translation.default; config.translations = translation.default; } else { console.warn(`No CKEditor translation found for language: ${language}`); } } function detecti18n() { const urlSearchParams = new URLSearchParams(window.location.search); const params = Object.fromEntries(urlSearchParams.entries()); const i18n = params["plugins[i18n][locale]"]; return i18n && i18n.split("-")[0]; } const translationImports = { af: () => import("ckeditor5/translations/af.js"), ar: () => import("ckeditor5/translations/ar.js"), ast: () => import("ckeditor5/translations/ast.js"), az: () => import("ckeditor5/translations/az.js"), bg: () => import("ckeditor5/translations/bg.js"), bn: () => import("ckeditor5/translations/bn.js"), bs: () => import("ckeditor5/translations/bs.js"), ca: () => import("ckeditor5/translations/ca.js"), cs: () => import("ckeditor5/translations/cs.js"), da: () => import("ckeditor5/translations/da.js"), "de-ch": () => import("ckeditor5/translations/de-ch.js"), de: () => import("ckeditor5/translations/de.js"), el: () => import("ckeditor5/translations/el.js"), "en-au": () => import("ckeditor5/translations/en-au.js"), "en-gb": () => import("ckeditor5/translations/en-gb.js"), en: () => import("ckeditor5/translations/en.js"), eo: () => import("ckeditor5/translations/eo.js"), "es-co": () => import("ckeditor5/translations/es-co.js"), es: () => import("ckeditor5/translations/es.js"), et: () => import("ckeditor5/translations/et.js"), eu: () => import("ckeditor5/translations/eu.js"), fa: () => import("ckeditor5/translations/fa.js"), fi: () => import("ckeditor5/translations/fi.js"), fr: () => import("ckeditor5/translations/fr.js"), gl: () => import("ckeditor5/translations/gl.js"), gu: () => import("ckeditor5/translations/gu.js"), he: () => import("ckeditor5/translations/he.js"), hi: () => import("ckeditor5/translations/hi.js"), hr: () => import("ckeditor5/translations/hr.js"), hu: () => import("ckeditor5/translations/hu.js"), hy: () => import("ckeditor5/translations/hy.js"), id: () => import("ckeditor5/translations/id.js"), it: () => import("ckeditor5/translations/it.js"), ja: () => import("ckeditor5/translations/ja.js"), jv: () => import("ckeditor5/translations/jv.js"), kk: () => import("ckeditor5/translations/kk.js"), km: () => import("ckeditor5/translations/km.js"), kn: () => import("ckeditor5/translations/kn.js"), ko: () => import("ckeditor5/translations/ko.js"), ku: () => import("ckeditor5/translations/ku.js"), lt: () => import("ckeditor5/translations/lt.js"), lv: () => import("ckeditor5/translations/lv.js"), ms: () => import("ckeditor5/translations/ms.js"), nb: () => import("ckeditor5/translations/nb.js"), ne: () => import("ckeditor5/translations/ne.js"), nl: () => import("ckeditor5/translations/nl.js"), no: () => import("ckeditor5/translations/no.js"), oc: () => import("ckeditor5/translations/oc.js"), pl: () => import("ckeditor5/translations/pl.js"), "pt-br": () => import("ckeditor5/translations/pt-br.js"), pt: () => import("ckeditor5/translations/pt.js"), ro: () => import("ckeditor5/translations/ro.js"), ru: () => import("ckeditor5/translations/ru.js"), si: () => import("ckeditor5/translations/si.js"), sk: () => import("ckeditor5/translations/sk.js"), sl: () => import("ckeditor5/translations/sl.js"), sq: () => import("ckeditor5/translations/sq.js"), sr: () => import("ckeditor5/translations/sr.js"), "sr-latn": () => import("ckeditor5/translations/sr-latn.js"), sv: () => import("ckeditor5/translations/sv.js"), th: () => import("ckeditor5/translations/th.js"), ti: () => import("ckeditor5/translations/ti.js"), tk: () => import("ckeditor5/translations/tk.js"), tr: () => import("ckeditor5/translations/tr.js"), tt: () => import("ckeditor5/translations/tt.js"), ug: () => import("ckeditor5/translations/ug.js"), uk: () => import("ckeditor5/translations/uk.js"), ur: () => import("ckeditor5/translations/ur.js"), uz: () => import("ckeditor5/translations/uz.js"), vi: () => import("ckeditor5/translations/vi.js"), "zh-cn": () => import("ckeditor5/translations/zh-cn.js"), zh: () => import("ckeditor5/translations/zh.js") }; const EditorContext = React.createContext(null); function useEditorContext() { const context = React.useContext(EditorContext); if (!context) { throw new Error("The useEditorContext hook must be used within the EditorProvider."); } return context; } function EditorProvider({ name, disabled, error, placeholder, hint, label, labelAction, required, presetName, wordsLimit, charsLimit, children, isFieldLocalized }) { const [preset, setPreset] = React.useState(null); React.useEffect(() => { (async () => { const { presets } = index.getPluginConfig(); const currentPreset = clonePreset(presets[presetName]); await setUpLanguage(currentPreset.editorConfig, isFieldLocalized); if (placeholder) { currentPreset.editorConfig.placeholder = placeholder; } setPreset(currentPreset); })(); }, [presetName, placeholder, isFieldLocalized]); const EditorContextValue = React.useMemo( () => ({ name, disabled, placeholder, hint, label, labelAction, required, presetName, preset, error, wordsLimit, charsLimit, isFieldLocalized }), [ name, disabled, placeholder, hint, label, labelAction, required, presetName, wordsLimit, charsLimit, preset, error, isFieldLocalized ] ); return /* @__PURE__ */ jsxRuntime.jsx(EditorContext.Provider, { value: EditorContextValue, children }); } function clonePreset(preset) { const clonedPreset = { ...preset, editorConfig: { ...preset.editorConfig } }; Object.entries(clonedPreset.editorConfig).forEach(([key, val]) => { if (val && typeof val === "object" && Object.getPrototypeOf(val) === Object.prototype) { clonedPreset.editorConfig[key] = { ...val }; } }); return clonedPreset; } function MediaLib({ isOpen = false, toggle, handleChangeAssets }) { const components = admin.useStrapiApp("MediaLib", (state) => state.components); const MediaLibraryDialog = components["media-library"]; function handleSelectAssets(files) { const formattedFiles = files.map((f) => ({ name: f.name, alt: f.alternativeText || f.name, url: index.prefixFileUrlWithBackendUrl(f.url), mime: f.mime, formats: f.formats, width: f.width, height: f.height })); const newElems = getNewElems(formattedFiles); handleChangeAssets(newElems); } function getNewElems(assets) { let newElems = ""; assets.forEach(({ name, url, alt, formats, mime, width, height }) => { if (mime.includes("image")) { if (formats && index.isImageResponsive(formats)) { const set = formSrcSet(formats); newElems += `<img src="${url}" alt="${alt}" width="${width}" height="${height}" srcset="${set}" />`; } else { newElems += `<img src="${url}" alt="${alt}" width="${width}" height="${height}" />`; } } else if (mime.includes("video")) { newElems += ` <video class="video" controls width="500px"> <source src="${url}" type="${mime}" /> </video>`; } else { newElems += `<a href="${url}">${name || "Open document"}</a>`; } }); return newElems; } function formSrcSet(formats) { let set = ""; const keys = Object.keys(formats).sort((a, b) => formats[a].width - formats[b].width); keys.forEach((k) => { set += `${index.prefixFileUrlWithBackendUrl(formats[k].url)} ${formats[k].width}w,`; }); return set; } if (!isOpen) { return null; } return /* @__PURE__ */ jsxRuntime.jsx(MediaLibraryDialog, { onClose: toggle, onSelectAssets: handleSelectAssets }); } const MemoizedMediaLib = React__default.default.memo(MediaLib); function CKEReact() { const [mediaLibVisible, setMediaLibVisible] = React.useState(false); const [editorInstance, setEditorInstance] = React.useState(null); const [isWordsMax, setIsWordsMax] = React.useState(false); const [isCharsMax, setIsCharsMax] = React.useState(false); const { name, disabled, preset, wordsLimit, charsLimit } = useEditorContext(); const { onChange: fieldOnChange, value: fieldValue } = admin.useField(name); const wordCounterRef = React.useRef(null); const onEditorReady = (editor) => { setUpPlugins(editor); setEditorInstance(editor); }; const onEditorChange = (_e, editor) => { const data = editor.getData(); fieldOnChange(name, data); }; const toggleMediaLib = React.useCallback(() => setMediaLibVisible((prev) => !prev), [setMediaLibVisible]); const handleChangeAssets = React.useCallback( (newElems) => { if (!editorInstance) { throw new Error("The editor instance has not been initialized."); } const viewFragment = editorInstance.data.processor.toView(newElems); const modelFragment = editorInstance.data.toModel(viewFragment); editorInstance?.model.insertContent(modelFragment); toggleMediaLib(); }, [toggleMediaLib, editorInstance] ); if (!preset) { return null; } return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx( ckeditor5React.CKEditor, { editor: ckeditor5.ClassicEditor, config: preset.editorConfig, disabled, data: fieldValue ?? "", onReady: onEditorReady, onChange: onEditorChange } ), /* @__PURE__ */ jsxRuntime.jsx(WordCounter, { ref: wordCounterRef, $isWordsMax: isWordsMax, $isCharsMax: isCharsMax }), /* @__PURE__ */ jsxRuntime.jsx( MemoizedMediaLib, { isOpen: mediaLibVisible, toggle: toggleMediaLib, handleChangeAssets } ) ] }); function setUpPlugins(editor) { const pluginsToSetup = { WordCount: setUpWordCount, ImageUploadEditing: setUpImageUploadEditing, StrapiMediaLib: setUpStrapiMediaLib, StrapiUploadAdapter: setUpStrapiUploadAdapter }; Object.entries(pluginsToSetup).forEach(([pluginName, setUpFn]) => { if (editor.plugins.has(pluginName)) { try { setUpFn(editor); } catch (err) { console.error(`Failed to set up the ${pluginName} plugin `, err); } } }); } function setUpWordCount(editor) { const wordCountPlugin = editor.plugins.get("WordCount"); if (wordsLimit || charsLimit) { wordCountPlugin.on("update", (_e, stats) => validateInputLength(stats)); const { words, characters } = wordCountPlugin; validateInputLength({ words, characters }); } wordCounterRef.current?.appendChild(wordCountPlugin.wordCountContainer); } function setUpImageUploadEditing(editor) { const imageUploadEditingPlugin = editor.plugins.get("ImageUploadEditing"); const setAltAttribute = (_e, { data, imageElement }) => { editor.model.change((writer) => { writer.setAttribute("alt", data.alt, imageElement); }); }; imageUploadEditingPlugin.on("uploadComplete", setAltAttribute); } function setUpStrapiMediaLib(editor) { const strapiMediaLibPlugin = editor.plugins.get("StrapiMediaLib"); strapiMediaLibPlugin.connect(toggleMediaLib); } function setUpStrapiUploadAdapter(editor) { const StrapiUploadAdapterPlugin = editor.plugins.get( "StrapiUploadAdapter" ); const token = getStoredToken(); const config = { uploadUrl: index.prefixFileUrlWithBackendUrl("/upload"), headers: { Authorization: `Bearer ${token}` } }; StrapiUploadAdapterPlugin.initAdapter(config); } function validateInputLength(stats) { if (wordsLimit) { setIsWordsMax(stats.words > wordsLimit); } if (charsLimit) { setIsCharsMax(stats.characters > charsLimit); } } } const WordCounter = styledComponents.styled(designSystem.Flex)` ${({ theme, $isWordsMax, $isCharsMax }) => styledComponents.css` .ck-word-count__words { color: ${$isWordsMax ? theme.colors.danger600 : theme.colors.neutral400}; } .ck-word-count__characters { color: ${$isCharsMax ? theme.colors.danger600 : theme.colors.neutral400}; } `} `; function EditorLayout({ children }) { const { error, preset } = useEditorContext(); const [isExpandedMode, handleToggleExpand] = React.useReducer((prev) => !prev, false); React.useEffect(() => { if (isExpandedMode) { document.body.classList.add("lock-body-scroll"); } return () => { document.body.classList.remove("lock-body-scroll"); }; }, [isExpandedMode]); if (isExpandedMode) { return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Portal, { role: "dialog", "aria-modal": false, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.FocusTrap, { onEscape: handleToggleExpand, children: /* @__PURE__ */ jsxRuntime.jsx( Backdrop, { position: "fixed", top: 0, left: 0, right: 0, bottom: 0, zIndex: 4, justifyContent: "center", onClick: handleToggleExpand, children: /* @__PURE__ */ jsxRuntime.jsx( FullScreenBox, { background: "neutral100", hasRadius: true, shadow: "popupShadow", overflow: "hidden", width: "90%", height: "90%", onClick: (e) => e.stopPropagation(), position: "relative", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { height: "100%", alignItems: "flex-start", direction: "column", children: /* @__PURE__ */ jsxRuntime.jsxs( EditorWrapper, { $presetStyles: preset?.styles, $isExpanded: isExpandedMode, $hasError: Boolean(error), className: "ck-editor__expanded", children: [ children, /* @__PURE__ */ jsxRuntime.jsx(CollapseButton, { label: "Collapse", onClick: handleToggleExpand, children: /* @__PURE__ */ jsxRuntime.jsx(icons.Collapse, {}) }) ] } ) }) } ) } ) }) }); } return /* @__PURE__ */ jsxRuntime.jsxs( EditorWrapper, { $presetStyles: preset?.styles, $isExpanded: isExpandedMode, $hasError: Boolean(error), children: [ children, /* @__PURE__ */ jsxRuntime.jsx(ExpandButton, { label: "Expand", onClick: handleToggleExpand, children: /* @__PURE__ */ jsxRuntime.jsx(icons.Expand, {}) }) ] } ); } const EditorWrapper = styledComponents.styled("div")` position: relative; width: 100%; ${({ $presetStyles, theme, $hasError = false, $isExpanded }) => styledComponents.css` height: ${$isExpanded ? "100%" : "auto"}; border-radius: ${theme.borderRadius}; outline: none; box-shadow: 0; transition-property: border-color, box-shadow, fill; transition-duration: 0.2s; border: 1px solid ${$hasError ? theme.colors.danger600 : theme.colors.neutral200}; border-radius: ${theme.borderRadius}; &:focus-within { border: 1px solid ${$isExpanded ? theme.colors.neutral200 : theme.colors.primary600}; border-color: ${$hasError && theme.colors.danger600}; box-shadow: ${$hasError ? theme.colors.danger600 : theme.colors.primary600} 0px 0px 0px 2px; } ${$presetStyles} `} `; const ExpandButton = styledComponents.styled(designSystem.IconButton)` position: absolute; bottom: 1.4rem; right: 1.2rem; z-index: 2; box-shadow: ${({ theme }) => theme.shadows.filterShadow}; `; const CollapseButton = styledComponents.styled(designSystem.IconButton)` position: absolute; bottom: 2.5rem; right: 1.2rem; z-index: 2; box-shadow: ${({ theme }) => theme.shadows.filterShadow}; `; const FullScreenBox = styledComponents.styled(designSystem.Box)` max-width: var(--ck-editor-full-screen-box-max-width); `; const Backdrop = styledComponents.styled(designSystem.Flex)` background: ${({ theme }) => `${theme.colors.neutral800}1F`}; `; const GlobalStyle = styledComponents.createGlobalStyle` ${({ $editortTheme, $variant }) => $editortTheme && styledComponents.css` ${$editortTheme.common} ${$editortTheme[$variant]} ${$editortTheme.additional} `} `; const getSystemColorScheme = () => window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; function GlobalStyling() { const { theme } = index.getPluginConfig(); const profileTheme = getProfileTheme(); const variant = profileTheme && profileTheme !== "system" ? profileTheme : getSystemColorScheme(); return /* @__PURE__ */ jsxRuntime.jsx(GlobalStyle, { $editortTheme: theme, $variant: variant }); } const MemoizedGlobalStyling = React__default.default.memo(GlobalStyling); function Editor() { const { name, hint, required, labelAction, label, error, preset } = useEditorContext(); return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Root, { id: name, name, error, hint, required, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }), preset ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx(MemoizedGlobalStyling, {}), /* @__PURE__ */ jsxRuntime.jsx(EditorLayout, { children: /* @__PURE__ */ jsxRuntime.jsx(CKEReact, {}) }) ] }) : /* @__PURE__ */ jsxRuntime.jsx(LoaderBox, { hasRadius: true, background: "neutral100", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading..." }) }), /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}), /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {}) ] }) }); } const LoaderBox = styledComponents.styled(designSystem.Box)` display: flex; justify-content: center; align-items: center; height: 200px; width: 100%; `; function Field({ name, hint, error, placeholder, label, attribute, labelAction = null, disabled = false, required = false }) { const { preset, maxLengthWords, maxLengthCharacters } = attribute.options; const isFieldLocalized = attribute?.pluginOptions?.i18n?.localized ?? false; return /* @__PURE__ */ jsxRuntime.jsx( EditorProvider, { name, error, disabled, required, placeholder, hint, label, labelAction, presetName: preset, wordsLimit: maxLengthWords, charsLimit: maxLengthCharacters, isFieldLocalized, children: /* @__PURE__ */ jsxRuntime.jsx(Editor, {}) } ); } function compare(oldProps, newProps) { return oldProps.error === newProps.error && oldProps.labelAction === newProps.labelAction; } const MemoizedField = React__default.default.memo(Field, compare); exports.Field = MemoizedField;