@_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
JavaScript
;
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;