Skip to content

Commit

Permalink
feat: new plugin configuration method (#191)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: ckeditor.js|ts file is no longer supported for plugin configuration. The plugin now requires using the setPluginConfig method for configuration. Existing setups must be updated to call this method before the admin panel's bootstrap lifecycle. dontMergePresets and dontMergeTheme options have been removed. User-provided configuration objects now completely overwrite the default ones.
  • Loading branch information
nshenderov committed Dec 12, 2024
1 parent 5a189c4 commit 6625acc
Show file tree
Hide file tree
Showing 27 changed files with 262 additions and 336 deletions.
3 changes: 3 additions & 0 deletions admin/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ declare global {
strapi: {
backendURL: string;
};
SH_CKE: {
IS_UPLOAD_RESPONSIVE: boolean;
};
}
}
7 changes: 2 additions & 5 deletions admin/src/components/CKEReact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CKEditor } from '@ckeditor/ckeditor5-react';
import 'ckeditor5/ckeditor5.css';

import { useEditorContext } from './EditorProvider';
import { getStoredToken } from '../utils';
import { getStoredToken, prefixFileUrlWithBackendUrl } from '../utils';
import { MediaLib } from './MediaLib';
import type {
StrapiMediaLibPlugin,
Expand Down Expand Up @@ -130,12 +130,9 @@ export function CKEReact() {
'StrapiUploadAdapter'
) as StrapiUploadAdapterPlugin;
const token = getStoredToken();
const { backendURL } = window.strapi;
const config: StrapiUploadAdapterConfig = {
uploadUrl: `${backendURL}/upload`,
backendUrl: backendURL,
uploadUrl: prefixFileUrlWithBackendUrl('/upload'),
headers: { Authorization: `Bearer ${token}` },
responsive: window.SH_CKE_UPLOAD_ADAPTER_IS_RESPONSIVE,
};

StrapiUploadAdapterPlugin.initAdapter(config);
Expand Down
4 changes: 2 additions & 2 deletions admin/src/components/EditorLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Box, Flex, IconButton, FocusTrap, Portal } from '@strapi/design-system'
import { Expand, Collapse } from '@strapi/icons';
import { css, styled } from 'styled-components';

import type { Styles } from 'src/config';
import type { CSS } from '../config';
import { useEditorContext } from './EditorProvider';

export function EditorLayout({ children }: { children: ReactNode }) {
Expand Down Expand Up @@ -85,7 +85,7 @@ export function EditorLayout({ children }: { children: ReactNode }) {
const EditorWrapper = styled('div')<{
$isExpanded: boolean;
$hasError: boolean;
$presetStyles?: Styles;
$presetStyles?: CSS;
}>`
position: relative;
width: 100%;
Expand Down
24 changes: 21 additions & 3 deletions admin/src/components/EditorProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import type { InputProps } from '@strapi/strapi/admin';

import { type Preset, getClonedPreset, setUpLanguage } from '../config';
import { type Preset, setUpLanguage, getPluginConfig } from '../config';
import type { WordCountPluginStats } from './CKEReact';

type EditorProviderBaseProps = Pick<
Expand Down Expand Up @@ -54,8 +54,8 @@ export function EditorProvider({

useEffect(() => {
(async () => {
const currentPreset = getClonedPreset(presetName);

const { presets } = getPluginConfig();
const currentPreset = clonePreset(presets[presetName]);
await setUpLanguage(currentPreset.editorConfig, isFieldLocalized);

if (placeholder) {
Expand Down Expand Up @@ -132,3 +132,21 @@ export function EditorProvider({

return <EditorContext.Provider value={EditorContextValue}>{children}</EditorContext.Provider>;
}

function clonePreset(preset: Preset): Preset {
const clonedPreset = {
...preset,
editorConfig: {
...preset.editorConfig,
},
};

Object.entries(clonedPreset.editorConfig).forEach(([key, val]) => {
if (val && typeof val === 'object' && Object.getPrototypeOf(val) === Object.prototype) {
// @ts-ignore
clonedPreset.editorConfig[key] = { ...val };
}
});

return clonedPreset;
}
2 changes: 1 addition & 1 deletion admin/src/components/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { type InputProps, type FieldValue } from '@strapi/strapi/admin';
import type { InputProps, FieldValue } from '@strapi/strapi/admin';

import { Editor } from './Editor';
import { EditorProvider } from './EditorProvider';
Expand Down
9 changes: 3 additions & 6 deletions admin/src/components/GlobalStyling.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React from 'react';
import { createGlobalStyle, css } from 'styled-components';

import { defaultTheme } from '../theme';
import { getProfileTheme } from '../utils';
import type { Theme, Styles } from '../config';
import { type Theme, type CSS, getPluginConfig } from '../config';

const GlobalStyle = createGlobalStyle<{
$editortTheme?: Theme;
$variant: 'light' | 'dark';
$presetStyles?: Styles;
$presetStyles?: CSS;
}>`
${({ $editortTheme, $variant }) =>
$editortTheme &&
Expand All @@ -23,11 +22,9 @@ const getSystemColorScheme = (): 'light' | 'dark' =>
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';

function GlobalStyling() {
const { theme: userTheme, dontMergeTheme } = window.SH_CKE_CONFIG || {};

const { theme } = getPluginConfig();
const profileTheme = getProfileTheme();
const variant = profileTheme && profileTheme !== 'system' ? profileTheme : getSystemColorScheme();
const theme = dontMergeTheme ? userTheme : { ...defaultTheme, ...userTheme };

return <GlobalStyle $editortTheme={theme} $variant={variant} />;
}
Expand Down
8 changes: 4 additions & 4 deletions admin/src/components/MediaLib.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { useStrapiApp } from '@strapi/strapi/admin';

import { prefixFileUrlWithBackendUrl } from '../utils';
import { prefixFileUrlWithBackendUrl, isImageResponsive } from '../utils';

type MediaLibProps = {
isOpen: boolean;
Expand Down Expand Up @@ -34,8 +34,8 @@ function MediaLib({ isOpen = false, toggle, handleChangeAssets }: MediaLibProps)

assets.forEach(({ name, url, alt, formats, mime, width, height }: any) => {
if (mime.includes('image')) {
if (formats && window.SH_CKE_UPLOAD_ADAPTER_IS_RESPONSIVE) {
const set = formSet(formats);
if (formats && 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}" />`;
Expand All @@ -53,7 +53,7 @@ function MediaLib({ isOpen = false, toggle, handleChangeAssets }: MediaLibProps)
return newElems;
}

function formSet(formats: any): string {
function formSrcSet(formats: any): string {
let set = '';
const keys = Object.keys(formats).sort((a, b) => formats[a].width - formats[b].width);
keys.forEach(k => {
Expand Down
12 changes: 2 additions & 10 deletions admin/src/config/defaultPreset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,15 +355,7 @@ const defaultEditorConfig: EditorConfig = {
};

export const defaultPreset: Preset = {
field: {
key: 'default',
value: 'default',
metadatas: {
intlLabel: {
id: 'ckeditor.preset.default.label',
defaultMessage: 'default',
},
},
},
name: 'default',
description: 'default',
editorConfig: defaultEditorConfig,
};
19 changes: 0 additions & 19 deletions admin/src/config/expToGlobal.ts

This file was deleted.

2 changes: 0 additions & 2 deletions admin/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ export * from './language';
export * from './pluginConfig';
export * from './colors';
export * from './defaultPreset';
export * from './presets';
export * from './expToGlobal';
67 changes: 48 additions & 19 deletions admin/src/config/pluginConfig.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,56 @@
import type { PluginConfig } from './types';
import { PLUGIN_ID } from '../utils';
import { defaultTheme } from '../theme';
import { defaultPreset } from './defaultPreset';
import type { PluginConfig, UserPluginConfig } from './types';

export async function getPluginConfig(): Promise<PluginConfig | null> {
const config = await loadConfig().catch(error => console.error('CKEditor: ', error));
const PLUGIN_CONFIG: PluginConfig = {
presets: {
default: defaultPreset,
},
theme: defaultTheme,
};

return config || null;
}
/**
* Sets a configuration for the plugin.
*
* @remarks
*
* - The function must be invoked before the admin panel's bootstrap lifecycle function.
*
* - Provided objects will overwrite the default configuration values.
*
* - The provided configuration will be frozen after the first invocation, preventing further modifications.
*
* @param userConfig - The configuration object provided by the user.
*
* @public
*/
export function setPluginConfig(userConfig: UserPluginConfig): void {
const { presets, theme } = userConfig || {};

async function loadConfig(): Promise<PluginConfig | null> {
return new Promise((resolve, reject) => {
const { backendURL } = window.strapi;
const url =
backendURL !== '/'
? `${backendURL}/${PLUGIN_ID}/config/ckeditor`
: `/${PLUGIN_ID}/config/ckeditor`;
if (presets) {
presets.forEach(preset => {
PLUGIN_CONFIG.presets[preset.name] = preset;
});
}

const script = document.createElement('script');
script.id = 'ckeditor-config';
script.src = url;
if (theme) {
PLUGIN_CONFIG.theme = theme;
}

script.onload = () => resolve(window.SH_CKE_CONFIG);
script.onerror = () => reject(new Error('Failed loading config script'));
deepFreeze(PLUGIN_CONFIG);
}

document.body.appendChild(script);
export function getPluginConfig(): PluginConfig {
if (!Object.isFrozen(PLUGIN_CONFIG)) deepFreeze(PLUGIN_CONFIG);
return PLUGIN_CONFIG;
}

function deepFreeze(obj: Object): Readonly<Object> {
(Object.keys(obj) as (keyof typeof obj)[]).forEach(p => {
if (typeof obj[p] === 'object' && obj[p] !== null && !Object.isFrozen(obj[p])) {
deepFreeze(obj[p]);
}
});

return Object.freeze(obj);
}
57 changes: 0 additions & 57 deletions admin/src/config/presets.ts

This file was deleted.

Loading

0 comments on commit 6625acc

Please sign in to comment.