Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions demos/_internal/ShadowRootHost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import React, { useCallback, useState, type ReactNode } from 'react';
import { createPortal } from 'react-dom';

/**
* Renders its children inside a shadow root.
*/
export function ShadowRootHost( {
mode = 'open',
adoptedStyleSheets = () => [],
children
}: ShadowRootHostProps ): ReactNode {
const [ mount, setMount ] = useState<Mount | null>( null );

const hostRef = useCallback( ( host: HTMLDivElement | null ) => {
if ( host ) {
setMount( attachShadowRoot( host, mode, adoptedStyleSheets() ) );
}
}, [ mode, adoptedStyleSheets ] );

return (
<div ref={ hostRef }>
{ mount && createPortal(
typeof children === 'function' ? children( mount.shadowRoot ) : children,
mount.container
) }
</div>
);
}

type ShadowRootHostProps = {

/**
* The mode of the shadow root.
*
* @default 'open'
*/
mode?: ShadowRootMode;

/**
* Stylesheets adopted by the shadow root before the children are rendered, so there is no
* unstyled frame. Called once, when the root is attached.
*/
adoptedStyleSheets?: () => Array<CSSStyleSheet>;

/**
* What to render inside the shadow root. Pass a function to get hold of the root itself.
*/
children: ReactNode | ( ( shadowRoot: ShadowRoot ) => ReactNode );
};

const MOUNT_SYMBOL = Symbol.for( 'ckeditor-demo-shadow-root-mount' );

type HostElement = HTMLDivElement & { [ MOUNT_SYMBOL ]?: Mount };

/**
* Attaches a shadow root to the host, or returns the one attached earlier.
*/
function attachShadowRoot(
host: HostElement,
mode: ShadowRootMode,
adoptedStyleSheets: Array<CSSStyleSheet>
): Mount {
if ( !host[ MOUNT_SYMBOL ] ) {
const shadowRoot = host.attachShadow( { mode } );

shadowRoot.adoptedStyleSheets = adoptedStyleSheets;

host[ MOUNT_SYMBOL ] = {
shadowRoot,
container: shadowRoot.appendChild( document.createElement( 'div' ) )
};
}

return host[ MOUNT_SYMBOL ];
}

type Mount = {
shadowRoot: ShadowRoot;
container: HTMLElement;
};
18 changes: 18 additions & 0 deletions demos/_internal/getCKEditorStyleSheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import { once } from '@ckeditor/ckeditor5-integrations-common';
import ckeditorStyles from 'ckeditor5/ckeditor5.css?inline';

/**
* Returns the editor styles as a constructed stylesheet, built once and shared by everything that adopts it.
*/
export const getCKEditorStyleSheet = once( (): CSSStyleSheet => {
const styleSheet = new CSSStyleSheet();

styleSheet.replaceSync( ckeditorStyles );

return styleSheet;
} );
26 changes: 26 additions & 0 deletions demos/_internal/renderReactRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import React, { type ComponentType } from 'react';

export async function renderReactRoot( Component: ComponentType ): Promise<void> {
const element = document.getElementById( 'root' ) as HTMLDivElement;

if ( __REACT_VERSION__ <= 17 ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const ReactDOM = await import( 'react-dom' );

ReactDOM.render( React.createElement( Component ), element );
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { createRoot } = await import( 'react-dom/client' );

createRoot( element ).render( <Component /> );
}

console.log( `%cVersion of React used: ${ React.version }`, 'color:red;font-weight:bold;' );
}
20 changes: 2 additions & 18 deletions demos/cdn-multiroot-react/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,7 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import React from 'react';
import { renderReactRoot } from '../_internal/renderReactRoot.js';
import App from './App.js';

const element = document.getElementById( 'root' ) as HTMLDivElement;

if ( __REACT_VERSION__ <= 17 ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const ReactDOM = await import( 'react-dom' );

ReactDOM.render( React.createElement( App ), element );
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { createRoot } = await import( 'react-dom/client' );

createRoot( element ).render( <App /> );
}

console.log( `%cVersion of React used: ${ React.version }`, 'color:red;font-weight:bold;' );
renderReactRoot( App );
20 changes: 2 additions & 18 deletions demos/cdn-react/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,7 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import React from 'react';
import { renderReactRoot } from '../_internal/renderReactRoot.js';
import { App } from './App.js';

const element = document.getElementById( 'root' ) as HTMLDivElement;

if ( __REACT_VERSION__ <= 17 ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const ReactDOM = await import( 'react-dom' );

ReactDOM.render( React.createElement( App ), element );
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { createRoot } = await import( 'react-dom/client' );

createRoot( element ).render( <App /> );
}

console.log( `%cVersion of React used: ${ React.version }`, 'color:red;font-weight:bold;' );
renderReactRoot( App );
42 changes: 42 additions & 0 deletions demos/cdn-shadow-root-react/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import React, { useState, type ReactNode } from 'react';

import { CKEditorShadowRootCloudDemo } from './CKEditorShadowRootCloudDemo.js';

const EDITOR_CONTENT = `
<h2>Sample</h2>
<p>This editor is rendered inside a shadow root, and so are its stylesheets.</p>
<p>Switch the mode above to check that both an open and a closed shadow root work the same.</p>
`;

export const App = (): ReactNode => {
const [ mode, setMode ] = useState<ShadowRootMode>( 'open' );

return (
<React.StrictMode>
<h1>CKEditor 5 – React Component – shadow root CDN demo</h1>

<p>
Shadow root mode{ ' ' }
<select
value={ mode }
onChange={ event => setMode( event.target.value as ShadowRootMode ) }
>
{ [ 'open', 'closed' ].map( item => (
<option key={ item } value={ item }>{ item }</option>
) ) }
</select>
</p>

<CKEditorShadowRootCloudDemo
key={ mode }
mode={ mode }
content={ EDITOR_CONTENT }
/>
</React.StrictMode>
);
};
63 changes: 63 additions & 0 deletions demos/cdn-shadow-root-react/CKEditorShadowRootCloudDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import React, { type ReactNode } from 'react';

import { getCKCdnClassicEditor } from './getCKCdnClassicEditor.js';
import { ShadowRootHost } from '../_internal/ShadowRootHost.js';
import { CKEditor, useCKEditorCloud } from '../../src/index.js';

type CKEditorShadowRootCloudDemoProps = {
content: string;
mode: ShadowRootMode;
};

/**
* Renders the editor inside a shadow root and asks the loader to inject the editor stylesheets there
* instead of `document.head`.
*/
export const CKEditorShadowRootCloudDemo = ( { content, mode }: CKEditorShadowRootCloudDemoProps ): ReactNode => (
<ShadowRootHost key={ mode } mode={ mode }>
{ shadowRoot => <ShadowRootEditor shadowRoot={ shadowRoot } content={ content } /> }
</ShadowRootHost>
);

/**
* Loads the bundles into the given shadow root and creates the editor in it.
*/
function ShadowRootEditor( { shadowRoot, content }: { shadowRoot: ShadowRoot; content: string } ): ReactNode {
const cloud = useCKEditorCloud( {
version: 'nightly',
injectedStylesheetsLocation: {
targetNode: shadowRoot,
placement: 'end'
}
} );

if ( cloud.status === 'error' ) {
console.error( cloud );

return <div>Error!</div>;
}

if ( cloud.status !== 'success' ) {
return <div>Loading...</div>;
}

const CKEditorClassic = getCKCdnClassicEditor( {
cloud,
overrideConfig: {
licenseKey: import.meta.env.CKEDITOR_LICENSE_KEY ?? 'GPL'
}
} );

return (
<CKEditor
editor={ CKEditorClassic }
data={ content }
disableWatchdog
/>
);
}
104 changes: 104 additions & 0 deletions demos/cdn-shadow-root-react/getCKCdnClassicEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import type { ClassicEditor, Plugin, ContextPlugin, EditorConfig } from 'https://cdn.ckeditor.com/typings/ckeditor5.d.ts';
import type { CKEditorCloudConfig, CKEditorCloudResult } from '../../src/index.js';

type ClassicEditorCreatorConfig = {
cloud: CKEditorCloudResult<CKEditorCloudConfig>;
additionalPlugins?: Array<typeof Plugin | typeof ContextPlugin>;
overrideConfig?: EditorConfig;
};

export const getCKCdnClassicEditor = ( {
cloud, additionalPlugins, overrideConfig
}: ClassicEditorCreatorConfig ): typeof ClassicEditor => {
const {
ClassicEditor: ClassicEditorBase,
Essentials,
Autoformat,
Bold,
Italic,
BlockQuote,
CloudServices,
Heading,
Image,
ImageCaption,
ImageStyle,
ImageToolbar,
ImageUpload,
Indent,
Link,
List,
MediaEmbed,
Paragraph,
PasteFromOffice,
PictureEditing,
Table,
TableToolbar,
TextTransformation
} = cloud.CKEditor;

class CustomEditor extends ClassicEditorBase {
public static builtinPlugins = [
Essentials,
Autoformat,
Bold,
Italic,
BlockQuote,
Heading,
Image,
ImageCaption,
ImageStyle,
ImageToolbar,
ImageUpload,
Indent,
Link,
List,
MediaEmbed,
Paragraph,
PasteFromOffice,
PictureEditing,
Table,
TableToolbar,
TextTransformation,
CloudServices,
...additionalPlugins || []
];

public static defaultConfig = {
toolbar: {
items: [
'undo', 'redo',
'|', 'heading',
'|', 'bold', 'italic',
'|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed',
'|', 'bulletedList', 'numberedList', 'outdent', 'indent'
]
},
image: {
toolbar: [
'imageStyle:inline',
'imageStyle:block',
'imageStyle:side',
'|',
'toggleImageCaption',
'imageTextAlternative'
]
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells'
]
},
language: 'en',
...overrideConfig
};
}

return CustomEditor;
};
Loading