Skip to content

Commit cdb2af9

Browse files
authored
Merge pull request #255 from marco-ms/ckeditor_attach_to_container
Add `container` option to attach() for mounting in multi-document environments
2 parents fbea39b + 685a91f commit cdb2af9

5 files changed

Lines changed: 142 additions & 2 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
type: Fix
3+
see:
4+
- ckeditor/ckeditor5-inspector#39
5+
- ckeditor/ckeditor5-inspector#100
6+
communityCredits:
7+
- marco-ms
8+
---
9+
10+
Added a `container` option to `CKEditorInspector.attach()` allowing the inspector to be mounted into a custom DOM element instead of `document.body`.
11+
Useful in multi-window environments (Electron, WebView2, iframes) where the global `document` is not the document where the editor lives.
12+
The wrapper element is created in `container.ownerDocument` so ReactDOM event delegation works in the target document.

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,27 @@ CKEditorInspector.attach( { 'editor-name': editor }, {
110110
} );
111111
```
112112

113+
#### `container`
114+
115+
To mount the inspector into a specific DOM element instead of the default `document.body`, use the `options.container` option.
116+
117+
This is useful in multi-window / multi-document environments (Electron, WebView2, iframes) where the global document of the realm that loaded the inspector script is not the document where the editor lives. Without this option, the inspector would be appended to the wrong document's `<body>` and remain invisible.
118+
119+
The inspector wrapper element is created in `container.ownerDocument`, so ReactDOM event delegation works correctly in the target document.
120+
121+
**Note**: This option works when `CKEditorInspector.attach()` is called for the first time only.
122+
123+
```js
124+
const editorIframe = document.getElementById( 'editor-frame' );
125+
const editorDocument = editorIframe.contentDocument;
126+
127+
CKEditorInspector.attach( editor, {
128+
// Mount the inspector into the iframe document where the editor lives,
129+
// instead of the default `document.body` of the outer window.
130+
container: editorDocument.body
131+
} );
132+
```
133+
113134
## Development
114135

115136
> [!NOTE]

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ minimumReleaseAgeExclude:
1414
shellEmulator: true
1515
shamefullyHoist: true
1616
preferFrozenLockfile: true
17+
allowBuilds:
18+
esbuild: true

src/ckeditorinspector.jsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ export default class CKEditorInspector {
6565
* 'footer-editor': editor2
6666
* }, { option: 'value', ... } );
6767
*
68+
* **Note:** In multi-window / multi-document environments (Electron, WebView2, iframes),
69+
* the inspector can be mounted into a specific container instead of the default `document.body`:
70+
*
71+
* CKEditorInspector.attach( editor, { container: someElement } );
72+
*
73+
* This is useful when the global `document` of the realm where the inspector module was
74+
* evaluated is not the document where the editor lives — for example, when the editor runs
75+
* inside an iframe but the inspector bundle is loaded in the outer window. The inspector
76+
* wrapper element is created in `container.ownerDocument` so ReactDOM event delegation
77+
* works correctly in the target document.
78+
*
6879
* @param {Editor|Object} editorOrEditors If an editor instance is passed, the inspect will attach to the editor
6980
* with an auto–generated name. It is possible to pass an object with `name: instance` pairs to attach to
7081
* multiple editors at a time with unique names.
@@ -197,12 +208,15 @@ export default class CKEditorInspector {
197208
return;
198209
}
199210

200-
const container = CKEditorInspector._wrapper = document.createElement( 'div' );
211+
const mountTarget = options.container || document.body;
212+
const ownerDocument = mountTarget.ownerDocument || document;
213+
const container = CKEditorInspector._wrapper = ownerDocument.createElement( 'div' );
214+
201215
let previousEditor;
202216
let wasUICollapsed;
203217

204218
container.className = 'ck-inspector-wrapper';
205-
document.body.appendChild( container );
219+
mountTarget.appendChild( container );
206220

207221
// Create a listener that will trigger the store action when the model
208222
// is changing or the view is being rendered.

tests/inspector/ckeditorinspector.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,97 @@ describe( 'CKEditorInspector', () => {
485485
expect( getStoreState().ui.isCollapsed ).toBe( false );
486486
} );
487487
} );
488+
489+
describe( '#container', () => {
490+
let customContainer;
491+
492+
beforeEach( () => {
493+
customContainer = document.createElement( 'div' );
494+
document.body.appendChild( customContainer );
495+
} );
496+
497+
afterEach( () => {
498+
customContainer.remove();
499+
} );
500+
501+
it( 'should mount the inspector wrapper inside the provided container instead of document.body', () => {
502+
CKEditorInspector.attach( editor, { container: customContainer } );
503+
504+
const wrapper = CKEditorInspector._wrapper;
505+
506+
expect( wrapper ).toBeInstanceOf( HTMLElement );
507+
expect( wrapper.parentNode ).toBe( customContainer );
508+
expect( wrapper.parentNode ).not.toBe( document.body );
509+
} );
510+
511+
it( 'should render the inspector UI inside the provided container', () => {
512+
CKEditorInspector.attach( editor, { container: customContainer } );
513+
514+
expect( customContainer.querySelector( '.ck-inspector' ) ).not.toBeNull();
515+
expect( CKEditorInspector._wrapper.parentNode ).not.toBe( document.body );
516+
} );
517+
518+
it( 'should create the wrapper element using ownerDocument of the provided container', () => {
519+
CKEditorInspector.attach( editor, { container: customContainer } );
520+
521+
const wrapper = CKEditorInspector._wrapper;
522+
523+
expect( wrapper.ownerDocument ).toBe( customContainer.ownerDocument );
524+
} );
525+
526+
it( 'should work the same as default when container is document.body', () => {
527+
CKEditorInspector.attach( editor, { container: document.body } );
528+
529+
const wrapper = CKEditorInspector._wrapper;
530+
531+
expect( wrapper.parentNode ).toBe( document.body );
532+
expect( wrapper.firstChild.classList.contains( 'ck-inspector' ) ).toBe( true );
533+
} );
534+
535+
it( 'should not mount a second wrapper when attaching a second editor with a different container', async () => {
536+
const anotherContainer = document.createElement( 'div' );
537+
document.body.appendChild( anotherContainer );
538+
539+
CKEditorInspector.attach( editor, { container: customContainer } );
540+
541+
const firstWrapper = CKEditorInspector._wrapper;
542+
const anotherEditor = await TestEditor.create( element );
543+
544+
CKEditorInspector.attach( anotherEditor, { container: anotherContainer } );
545+
546+
expect( CKEditorInspector._wrapper ).toBe( firstWrapper );
547+
expect( document.querySelectorAll( '.ck-inspector-wrapper' ) ).toHaveLength( 1 );
548+
549+
anotherContainer.remove();
550+
551+
await anotherEditor.destroy();
552+
} );
553+
554+
it( 'should still function correctly (attach/detach/destroy) when using a custom container', () => {
555+
CKEditorInspector.attach( { foo: editor }, { container: customContainer } );
556+
557+
let state = getStoreState();
558+
559+
expect( state.editors.get( 'foo' ) ).toBe( editor );
560+
561+
CKEditorInspector.detach( 'foo' );
562+
563+
state = getStoreState();
564+
565+
expect( state.editors.size ).toBe( 0 );
566+
567+
CKEditorInspector.destroy();
568+
569+
expect( CKEditorInspector._wrapper ).toBeNull();
570+
expect( customContainer.querySelector( '.ck-inspector-wrapper' ) ).toBeNull();
571+
} );
572+
573+
it( 'should accept the container option passed with named editors', () => {
574+
CKEditorInspector.attach( { foo: editor }, { container: customContainer } );
575+
576+
expect( CKEditorInspector._wrapper.parentNode ).toBe( customContainer );
577+
} );
578+
} );
488579
} );
489580
} );
490581

0 commit comments

Comments
 (0)