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
9 changes: 9 additions & 0 deletions .changelog/20260403084854_ck_20017.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions packages/ckeditor5-editor-classic/src/classiceditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @module editor-classic/classiceditor
*/

import { CKEditorError } from '@ckeditor/ckeditor5-utils';
import { ClassicEditorUI } from './classiceditorui.js';
import { ClassicEditorUIView } from './classiceditoruiview.js';

Expand Down Expand Up @@ -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;
}

Expand Down
57 changes: 33 additions & 24 deletions packages/ckeditor5-editor-classic/tests/classiceditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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 = '<p>Foo</p>';
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( '<p>Foo</p>' );

editor.fire( 'ready' );
await editor.destroy();

editorElement.remove();
} );

it( 'if not set, is set using data passed in constructor', async () => {
Expand All @@ -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 = '<p>Foo</p>';

const editor = new ClassicEditor( editorElement, { initialData: '<p>Bar</p>' } );

expect( editor.config.get( 'roots.main.initialData' ) ).to.equal( '<p>Bar</p>' );
Expand Down Expand Up @@ -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 = '<p>Foo</p>';

expect( () => {
// eslint-disable-next-line no-new
new ClassicEditor( editorElement, {
Expand All @@ -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 = '<p>Foo</p>';

expect( () => {
// eslint-disable-next-line no-new
new ClassicEditor( editorElement, {
Expand All @@ -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 = '<p>Foo</p>';

expect( () => {
// eslint-disable-next-line no-new
new ClassicEditor( editorElement, {
Expand All @@ -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 = '<p>Foo</p>';

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' );
} );
} );
Expand Down Expand Up @@ -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 = '<p>Bar</p>';

const editor = new ClassicEditor( {
Expand All @@ -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 = '<p>Bar</p>';

const editor = new ClassicEditor( {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 );
Expand Down