diff --git a/.changelog/20260403084854_ck_20017.md b/.changelog/20260403084854_ck_20017.md new file mode 100644 index 00000000000..2656fe0f32b --- /dev/null +++ b/.changelog/20260403084854_ck_20017.md @@ -0,0 +1,9 @@ +--- +type: Fix +scope: + - ckeditor5-editor-classic +closes: + - https://github.com/ckeditor/ckeditor5/issues/20017 +--- + +The classic editor no longer throws an unclear error when initialized with a source element that is not attached to the DOM. A dedicated `editor-source-element-not-attached` error is thrown instead. diff --git a/packages/ckeditor5-editor-classic/src/classiceditor.ts b/packages/ckeditor5-editor-classic/src/classiceditor.ts index 5651344ab18..eece0e329a0 100644 --- a/packages/ckeditor5-editor-classic/src/classiceditor.ts +++ b/packages/ckeditor5-editor-classic/src/classiceditor.ts @@ -7,6 +7,7 @@ * @module editor-classic/classiceditor */ +import { CKEditorError } from '@ckeditor/ckeditor5-utils'; import { ClassicEditorUI } from './classiceditorui.js'; import { ClassicEditorUIView } from './classiceditoruiview.js'; @@ -84,6 +85,15 @@ export class ClassicEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) { this.config.define( 'menuBar.isVisible', false ); if ( isElement( sourceElement ) ) { + if ( !sourceElement.isConnected ) { + /** + * Cannot initialize the editor because the provided source element is not attached to the DOM and cannot be replaced. + * + * @error editor-source-element-not-attached + */ + throw new CKEditorError( 'editor-source-element-not-attached', null ); + } + this.sourceElement = sourceElement; } diff --git a/packages/ckeditor5-editor-classic/tests/classiceditor.js b/packages/ckeditor5-editor-classic/tests/classiceditor.js index ac8e35aa4be..5ab9e635ca1 100644 --- a/packages/ckeditor5-editor-classic/tests/classiceditor.js +++ b/packages/ckeditor5-editor-classic/tests/classiceditor.js @@ -106,7 +106,7 @@ describe( 'ClassicEditor', () => { describe( 'automatic toolbar items groupping', () => { it( 'should be on by default', async () => { - const editorElement = document.createElement( 'div' ); + const editorElement = document.body.appendChild( document.createElement( 'div' ) ); const editor = new ClassicEditor( editorElement ); expect( editor.ui.view.toolbar.options.shouldGroupWhenFull ).to.be.true; @@ -118,7 +118,7 @@ describe( 'ClassicEditor', () => { } ); it( 'can be disabled using config.toolbar.shouldNotGroupWhenFull', async () => { - const editorElement = document.createElement( 'div' ); + const editorElement = document.body.appendChild( document.createElement( 'div' ) ); const editor = new ClassicEditor( editorElement, { toolbar: { shouldNotGroupWhenFull: true @@ -136,18 +136,25 @@ describe( 'ClassicEditor', () => { } ); describe( 'config.roots.main.initialData', () => { - it( 'if not set, is set using DOM element data', async () => { - const editorElement = document.createElement( 'div' ); + let editorElement; + + beforeEach( () => { + editorElement = document.createElement( 'div' ); editorElement.innerHTML = '

Foo

'; + document.body.appendChild( editorElement ); + } ); + afterEach( () => { + editorElement.remove(); + } ); + + it( 'if not set, is set using DOM element data', async () => { const editor = new ClassicEditor( editorElement ); expect( editor.config.get( 'roots.main.initialData' ) ).to.equal( '

Foo

' ); editor.fire( 'ready' ); await editor.destroy(); - - editorElement.remove(); } ); it( 'if not set, is set using data passed in constructor', async () => { @@ -160,9 +167,6 @@ describe( 'ClassicEditor', () => { } ); it( 'if set, is not overwritten with DOM element data (legacy config.initialData)', async () => { - const editorElement = document.createElement( 'div' ); - editorElement.innerHTML = '

Foo

'; - const editor = new ClassicEditor( editorElement, { initialData: '

Bar

' } ); expect( editor.config.get( 'roots.main.initialData' ) ).to.equal( '

Bar

' ); @@ -193,9 +197,6 @@ describe( 'ClassicEditor', () => { } ); it( 'it should throw if config.root and config.roots.main is set', () => { - const editorElement = document.createElement( 'div' ); - editorElement.innerHTML = '

Foo

'; - expect( () => { // eslint-disable-next-line no-new new ClassicEditor( editorElement, { @@ -206,9 +207,6 @@ describe( 'ClassicEditor', () => { } ); it( 'it should throw if legacy config.initialData and config.root.initialData is set', () => { - const editorElement = document.createElement( 'div' ); - editorElement.innerHTML = '

Foo

'; - expect( () => { // eslint-disable-next-line no-new new ClassicEditor( editorElement, { @@ -219,9 +217,6 @@ describe( 'ClassicEditor', () => { } ); it( 'it should throw if legacy config.initialData and config.roots.main.initialData is set', () => { - const editorElement = document.createElement( 'div' ); - editorElement.innerHTML = '

Foo

'; - expect( () => { // eslint-disable-next-line no-new new ClassicEditor( editorElement, { @@ -232,14 +227,11 @@ describe( 'ClassicEditor', () => { } ); it( 'it should throw if source element and config.attachTo are both set', () => { - const sourceElement = document.createElement( 'div' ); - sourceElement.innerHTML = '

Foo

'; - const attachToElement = document.createElement( 'div' ); expect( () => { // eslint-disable-next-line no-new - new ClassicEditor( sourceElement, { attachTo: attachToElement } ); + new ClassicEditor( editorElement, { attachTo: attachToElement } ); } ).to.throw( CKEditorError, 'editor-create-attachto-overspecified' ); } ); } ); @@ -295,7 +287,7 @@ describe( 'ClassicEditor', () => { } ); it( 'should create editor with config.attachTo and use data from it', async () => { - const el = document.createElement( 'div' ); + const el = document.body.appendChild( document.createElement( 'div' ) ); el.innerHTML = '

Bar

'; const editor = new ClassicEditor( { @@ -307,10 +299,12 @@ describe( 'ClassicEditor', () => { editor.fire( 'ready' ); await editor.destroy(); + + el.remove(); } ); it( 'should create editor with config.attachTo and use root.initialData', async () => { - const el = document.createElement( 'div' ); + const el = document.body.appendChild( document.createElement( 'div' ) ); el.innerHTML = '

Bar

'; const editor = new ClassicEditor( { @@ -325,6 +319,8 @@ describe( 'ClassicEditor', () => { editor.fire( 'ready' ); await editor.destroy(); + + el.remove(); } ); it( 'should log warning when config.root.element is set', async () => { @@ -503,6 +499,19 @@ describe( 'ClassicEditor', () => { } ); } ); + it( 'should raise exception when editor is being attached to not attached DOM element', async () => { + const editorElement = document.createElement( 'div' ); + + try { + await ClassicEditor.create( { attachTo: editorElement } ); + expect.fail( 'Promise should have been rejected' ); + } catch ( err ) { + expect( err ).to.be.instanceof( CKEditorError ); + expect( err.context ).to.be.null; // avoid watchdog restart + expect( err.message ).to.contain( 'editor-source-element-not-attached' ); + } + } ); + describe( 'ui', () => { it( 'inserts editor UI next to editor element', () => { expect( editor.ui.view.element.previousSibling ).to.equal( editorElement );