diff --git a/packages/ckeditor5-block-quote/src/blockquotecommand.ts b/packages/ckeditor5-block-quote/src/blockquotecommand.ts index bdbd318c60b..ce3d9a928e2 100644 --- a/packages/ckeditor5-block-quote/src/blockquotecommand.ts +++ b/packages/ckeditor5-block-quote/src/blockquotecommand.ts @@ -214,9 +214,17 @@ function getRangesOfBlockGroups( writer: ModelWriter, blocks: Array can wrap the block. */ function checkCanBeQuoted( schema: ModelSchema, block: ModelElement ): boolean { - // TMP will be replaced with schema.checkWrap(). - const isBQAllowed = schema.checkChild( block.parent as ModelElement, 'blockQuote' ); - const isBlockAllowedInBQ = schema.checkChild( [ '$root', 'blockQuote' ], block ); + const parentContext = schema.createContext( block.parent as ModelElement ); - return isBQAllowed && isBlockAllowedInBQ; + // Is block-quote allowed in parent of block. + if ( !schema.checkChild( parentContext, 'blockQuote' ) ) { + return false; + } + + // Is block allowed inside block-quote. + if ( !schema.checkChild( parentContext.push( 'blockQuote' ), block ) ) { + return false; + } + + return true; } diff --git a/packages/ckeditor5-core/src/editor/editorconfig.ts b/packages/ckeditor5-core/src/editor/editorconfig.ts index d0bcd90e4ba..86af1f9c28f 100644 --- a/packages/ckeditor5-core/src/editor/editorconfig.ts +++ b/packages/ckeditor5-core/src/editor/editorconfig.ts @@ -112,6 +112,16 @@ export interface EditorConfig extends EngineConfig { */ initialData?: string | Record; + /** + * TODO + */ + modelRootElementName?: string | Record; + + /** + * TODO + */ + viewRootElementName?: string | Record; + /** * The language of the editor UI and its content. * diff --git a/packages/ckeditor5-editor-balloon/src/ballooneditor.ts b/packages/ckeditor5-editor-balloon/src/ballooneditor.ts index 59d8e66b7c4..cc24332bcf0 100644 --- a/packages/ckeditor5-editor-balloon/src/ballooneditor.ts +++ b/packages/ckeditor5-editor-balloon/src/ballooneditor.ts @@ -82,9 +82,14 @@ export class BalloonEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) { this.config.define( 'balloonToolbar', this.config.get( 'toolbar' ) ); - this.model.document.createRoot(); - - const view = new BalloonEditorUIView( this.locale, this.editing.view, this.sourceElement, this.config.get( 'label' ) ); + this.model.document.createRoot( this.config.get( 'modelRootElementName' ) as string || '$root' ); + + const view = new BalloonEditorUIView( + this.locale, + this.editing.view, + this.sourceElement || this.config.get( 'viewRootElementName' ) as string, + this.config.get( 'label' ) + ); this.ui = new BalloonEditorUI( this, view ); attachToForm( this ); diff --git a/packages/ckeditor5-editor-balloon/src/ballooneditoruiview.ts b/packages/ckeditor5-editor-balloon/src/ballooneditoruiview.ts index 0b8c33c79af..d0ead4825b9 100644 --- a/packages/ckeditor5-editor-balloon/src/ballooneditoruiview.ts +++ b/packages/ckeditor5-editor-balloon/src/ballooneditoruiview.ts @@ -38,7 +38,7 @@ export class BalloonEditorUIView extends EditorUIView { constructor( locale: Locale, editingView: EditingView, - editableElement?: HTMLElement, + editableElement?: HTMLElement | string, // TODO string => create DOM element name label?: string | Record ) { super( locale ); diff --git a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.html b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.html new file mode 100644 index 00000000000..905781e0d9a --- /dev/null +++ b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.html @@ -0,0 +1,12 @@ +
+ + diff --git a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.js b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.js new file mode 100644 index 00000000000..bf53b2535ac --- /dev/null +++ b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.js @@ -0,0 +1,24 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { BalloonEditor } from '../../src/ballooneditor.js'; +import { ArticlePluginSet } from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; + +const container = document.querySelector( '.container' ); + +BalloonEditor + .create( '

Editor

This is an editor instance.

', { + plugins: [ ArticlePluginSet ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], + modelRootElementName: '$inlineRoot', + viewRootElementName: 'p' + } ) + .then( editor => { + window.editor = editor; + container.appendChild( editor.ui.element ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.md b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.md new file mode 100644 index 00000000000..1effe928104 --- /dev/null +++ b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline-data.md @@ -0,0 +1,3 @@ +1. Click "Init editor". +2. New editor instance should be appended to the document with initial data in it. You can create more than one editor. +3. After clicking "Destroy editors" all editors should be removed from the document. diff --git a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.html b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.html new file mode 100644 index 00000000000..5bd4f6b130a --- /dev/null +++ b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.html @@ -0,0 +1,14 @@ +
+

This is editor

+
+ + diff --git a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.js b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.js new file mode 100644 index 00000000000..9c3b811b804 --- /dev/null +++ b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.js @@ -0,0 +1,21 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { BalloonEditor } from '../../src/ballooneditor.js'; +import { ArticlePluginSet } from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; + +BalloonEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], + modelRootElementName: '$inlineRoot', + viewRootElementName: 'p' // TODO this is ignored as original element is reused + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.md b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.md new file mode 100644 index 00000000000..1effe928104 --- /dev/null +++ b/packages/ckeditor5-editor-balloon/tests/manual/ballooneditor-inline.md @@ -0,0 +1,3 @@ +1. Click "Init editor". +2. New editor instance should be appended to the document with initial data in it. You can create more than one editor. +3. After clicking "Destroy editors" all editors should be removed from the document. diff --git a/packages/ckeditor5-editor-classic/src/classiceditor.ts b/packages/ckeditor5-editor-classic/src/classiceditor.ts index c442e740c67..ceb7d1ca436 100644 --- a/packages/ckeditor5-editor-classic/src/classiceditor.ts +++ b/packages/ckeditor5-editor-classic/src/classiceditor.ts @@ -72,7 +72,7 @@ export class ClassicEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) { this.sourceElement = sourceElementOrData; } - this.model.document.createRoot(); + this.model.document.createRoot( this.config.get( 'modelRootElementName' ) as string || '$root' ); const shouldToolbarGroupWhenFull = !this.config.get( 'toolbar.shouldNotGroupWhenFull' ); @@ -80,6 +80,7 @@ export class ClassicEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) { const view = new ClassicEditorUIView( this.locale, this.editing.view, { shouldToolbarGroupWhenFull, + editableElementName: this.config.get( 'viewRootElementName' ) as string | undefined, useMenuBar: menuBarConfig.isVisible, label: this.config.get( 'label' ) } ); diff --git a/packages/ckeditor5-editor-classic/src/classiceditoruiview.ts b/packages/ckeditor5-editor-classic/src/classiceditoruiview.ts index bf0e43ea562..bc26b091060 100644 --- a/packages/ckeditor5-editor-classic/src/classiceditoruiview.ts +++ b/packages/ckeditor5-editor-classic/src/classiceditoruiview.ts @@ -51,6 +51,7 @@ export class ClassicEditorUIView extends BoxedEditorUIView { editingView: EditingView, options: { shouldToolbarGroupWhenFull?: boolean; + editableElementName?: string; // TODO docs useMenuBar?: boolean; label?: string | Record; } = {} @@ -67,7 +68,7 @@ export class ClassicEditorUIView extends BoxedEditorUIView { this.menuBarView = new MenuBarView( locale ); } - this.editable = new InlineEditableUIView( locale, editingView, undefined, { + this.editable = new InlineEditableUIView( locale, editingView, options.editableElementName, { label: options.label } ); } diff --git a/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.html b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.html new file mode 100644 index 00000000000..8653adc7b4e --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.html @@ -0,0 +1,12 @@ +
+ + diff --git a/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.js b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.js new file mode 100644 index 00000000000..ccaa10e3f41 --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.js @@ -0,0 +1,29 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { ClassicEditor } from '../../src/classiceditor.js'; +import { Enter } from '@ckeditor/ckeditor5-enter'; +import { Typing } from '@ckeditor/ckeditor5-typing'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { Undo } from '@ckeditor/ckeditor5-undo'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; + +const container = document.querySelector( '.container' ); + +ClassicEditor + .create( '

Hello world!

This is an editor instance.

', { + plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ], + modelRootElementName: '$inlineRoot', + viewRootElementName: 'p' + } ) + .then( editor => { + window.editor = editor; + container.appendChild( editor.ui.element ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.md b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.md new file mode 100644 index 00000000000..b7c1b956206 --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline-data.md @@ -0,0 +1,3 @@ +1. Click "Init editor". +2. New editor instance should be appended to the document with initial data in it. You can create more than one editor. +3. After clicking "Destroy editor" all editors should be removed from the document. diff --git a/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.html b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.html new file mode 100644 index 00000000000..e973fd5512b --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.html @@ -0,0 +1,17 @@ +
+

Hello world!

+

This is an editor instance.

+
+ + diff --git a/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.js b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.js new file mode 100644 index 00000000000..0caaaf4201b --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.js @@ -0,0 +1,30 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { ClassicEditor } from '../../src/classiceditor.js'; +import { Enter } from '@ckeditor/ckeditor5-enter'; +import { Typing } from '@ckeditor/ckeditor5-typing'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { Undo } from '@ckeditor/ckeditor5-undo'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ], + menuBar: { isVisible: true }, + modelRootElementName: '$inlineRoot', + viewRootElementName: 'h2' + } ) + .then( newEditor => { + console.log( 'Editor was initialized', newEditor ); + console.log( 'You can now play with it using global `editor` and `editable` variables.' ); + + window.editor = newEditor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.md b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.md new file mode 100644 index 00000000000..7716551295e --- /dev/null +++ b/packages/ckeditor5-editor-classic/tests/manual/classiceditor-inline.md @@ -0,0 +1,18 @@ +1. Click "Init editor". +2. Expected: + * Framed editor should be created. + * Source element should disappear. + * There should be a toolbar with "Bold", "Italic", "Undo" and "Redo" buttons. +3. Click "Destroy editor". +4. Expected: + * Editor should be destroyed. + * Source element should be visible. + * The element should contain its data (updated). + * The 'ck-body region' should be removed. + +## Notes: + +* You can play with: + * `editable.isReadOnly`, +* Changes to `editable.isFocused` should be logged to the console. +* Features should work. diff --git a/packages/ckeditor5-editor-decoupled/src/decouplededitor.ts b/packages/ckeditor5-editor-decoupled/src/decouplededitor.ts index b76555cdb90..03b9dbb6de5 100644 --- a/packages/ckeditor5-editor-decoupled/src/decouplededitor.ts +++ b/packages/ckeditor5-editor-decoupled/src/decouplededitor.ts @@ -83,11 +83,11 @@ export class DecoupledEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) { secureSourceElement( this, sourceElementOrData ); } - this.model.document.createRoot(); + this.model.document.createRoot( this.config.get( 'modelRootElementName' ) as string || '$root' ); const shouldToolbarGroupWhenFull = !this.config.get( 'toolbar.shouldNotGroupWhenFull' ); const view = new DecoupledEditorUIView( this.locale, this.editing.view, { - editableElement: this.sourceElement, + editableElement: this.sourceElement || this.config.get( 'viewRootElementName' ) as string, shouldToolbarGroupWhenFull, label: this.config.get( 'label' ) } ); diff --git a/packages/ckeditor5-editor-decoupled/src/decouplededitoruiview.ts b/packages/ckeditor5-editor-decoupled/src/decouplededitoruiview.ts index 877df1088ee..875ab24b905 100644 --- a/packages/ckeditor5-editor-decoupled/src/decouplededitoruiview.ts +++ b/packages/ckeditor5-editor-decoupled/src/decouplededitoruiview.ts @@ -55,7 +55,7 @@ export class DecoupledEditorUIView extends EditorUIView { locale: Locale, editingView: EditingView, options: { - editableElement?: HTMLElement; + editableElement?: HTMLElement | string; // TODO string => create DOM element name shouldToolbarGroupWhenFull?: boolean; label?: string | Record; } = {} diff --git a/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.html b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.html new file mode 100644 index 00000000000..8dfc898045a --- /dev/null +++ b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.html @@ -0,0 +1,48 @@ +

The toolbar

+
+ +

The editable

+
+
+

This element becomes the editable

+

It has the initial editor data. It should keep it after the editor is destroyed too.

+
+
+ + diff --git a/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.js b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.js new file mode 100644 index 00000000000..75226df0f21 --- /dev/null +++ b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.js @@ -0,0 +1,31 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { DecoupledEditor } from '../../src/decouplededitor.js'; +import { Enter } from '@ckeditor/ckeditor5-enter'; +import { Typing } from '@ckeditor/ckeditor5-typing'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { Undo } from '@ckeditor/ckeditor5-undo'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; + +DecoupledEditor + .create( document.querySelector( '.editor__editable' ), { + plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ], + modelRootElementName: '$inlineRoot', + viewModelRootElementName: 'p' // TODO this is ignored as original element is reused + } ) + .then( newEditor => { + console.log( 'Editor was initialized', newEditor ); + console.log( 'You can now play with it using global `editor` and `editable` variables.' ); + + document.querySelector( '.toolbar-container' ).appendChild( newEditor.ui.view.toolbar.element ); + + window.editor = newEditor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.md b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.md new file mode 100644 index 00000000000..de48bb69340 --- /dev/null +++ b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-editable-inline.md @@ -0,0 +1,20 @@ +1. Click "Init editor". +2. Expected: + * The toolbar container should get the toolbar. + * The toolbar should appear with "Heading", "Bold", "Italic", "Undo" and "Redo" buttons. + * **The yellow element should become an editable**. +3. Do some editing and formatting. +4. Click "Destroy editor". +5. Expected: + * Editor should be destroyed. + * The toolbar should disappear from the container. + * **The editable must remain**. + * **The editable must retain the editor data**. + * The `.ck-body` region should be removed. + +## Notes: + +* You can play with: + * `editable.isReadOnly`, +* Changes to `editable.isFocused` should be logged to the console. +* Features should work. diff --git a/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.html b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.html new file mode 100644 index 00000000000..673ee635b56 --- /dev/null +++ b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.html @@ -0,0 +1,44 @@ + +

The toolbar

+
+ +

The editable

+
+ + diff --git a/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.js b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.js new file mode 100644 index 00000000000..ff2ff9c578c --- /dev/null +++ b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.js @@ -0,0 +1,34 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { DecoupledEditor } from '../../src/decouplededitor.js'; +import { Enter } from '@ckeditor/ckeditor5-enter'; +import { Typing } from '@ckeditor/ckeditor5-typing'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { Undo } from '@ckeditor/ckeditor5-undo'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; + +const editorData = '

Hello world

This is the decoupled editor.

'; + +DecoupledEditor + .create( editorData, { + plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ], + modelRootElementName: '$inlineRoot', + viewRootElementName: 'h2' + } ) + .then( newEditor => { + console.log( 'Editor was initialized', newEditor ); + console.log( 'You can now play with it using global `editor` and `editable` variables.' ); + + document.querySelector( '.toolbar-container' ).appendChild( newEditor.ui.view.toolbar.element ); + document.querySelector( '.editable-container' ).appendChild( newEditor.ui.view.editable.element ); + + window.editor = newEditor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.md b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.md new file mode 100644 index 00000000000..06f5ce55edd --- /dev/null +++ b/packages/ckeditor5-editor-decoupled/tests/manual/decouplededitor-inline.md @@ -0,0 +1,16 @@ +1. Click "Init editor". +2. Expected: + * The containers should fill up with the respective editor UI. + * There should be a toolbar with "Heading", "Bold", "Italic", "Undo" and "Redo" buttons. +3. Click "Destroy editor". +4. Expected: + * Editor should be destroyed. + * **The editor UI should remain in the containers**. + * The `.ck-body` region should be removed. + +## Notes: + +* You can play with: + * `editable.isReadOnly`, +* Changes to `editable.isFocused` should be logged to the console. +* Features should work. diff --git a/packages/ckeditor5-editor-inline/src/inlineeditor.ts b/packages/ckeditor5-editor-inline/src/inlineeditor.ts index 31415619730..7e3b740310f 100644 --- a/packages/ckeditor5-editor-inline/src/inlineeditor.ts +++ b/packages/ckeditor5-editor-inline/src/inlineeditor.ts @@ -69,7 +69,7 @@ export class InlineEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) { this.config.set( 'initialData', getInitialData( sourceElementOrData ) ); } - this.model.document.createRoot(); + this.model.document.createRoot( this.config.get( 'modelRootElementName' ) as string || '$root' ); if ( isElement( sourceElementOrData ) ) { this.sourceElement = sourceElementOrData; @@ -80,11 +80,16 @@ export class InlineEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) { const menuBarConfig = this.config.get( 'menuBar' )!; - const view = new InlineEditorUIView( this.locale, this.editing.view, this.sourceElement, { - shouldToolbarGroupWhenFull, - useMenuBar: menuBarConfig.isVisible, - label: this.config.get( 'label' ) - } ); + const view = new InlineEditorUIView( + this.locale, + this.editing.view, + this.sourceElement || this.config.get( 'viewRootElementName' ) as string, + { + shouldToolbarGroupWhenFull, + useMenuBar: menuBarConfig.isVisible, + label: this.config.get( 'label' ) + } + ); this.ui = new InlineEditorUI( this, view ); attachToForm( this ); diff --git a/packages/ckeditor5-editor-inline/src/inlineeditoruiview.ts b/packages/ckeditor5-editor-inline/src/inlineeditoruiview.ts index c6d7566b4db..2b8411a1c12 100644 --- a/packages/ckeditor5-editor-inline/src/inlineeditoruiview.ts +++ b/packages/ckeditor5-editor-inline/src/inlineeditoruiview.ts @@ -140,7 +140,7 @@ export class InlineEditorUIView extends EditorUIView { constructor( locale: Locale, editingView: EditingView, - editableElement?: HTMLElement, + editableElement?: HTMLElement | string, // TODO string => create DOM element name options: { shouldToolbarGroupWhenFull?: boolean; useMenuBar?: boolean; diff --git a/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.html b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.html new file mode 100644 index 00000000000..59f42d3f14d --- /dev/null +++ b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.html @@ -0,0 +1,8 @@ +
+ + diff --git a/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.js b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.js new file mode 100644 index 00000000000..044791abb20 --- /dev/null +++ b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.js @@ -0,0 +1,25 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { InlineEditor } from '../../src/inlineeditor.js'; +import { ArticlePluginSet } from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; + +const container = document.querySelector( '.container' ); + +InlineEditor + .create( '

Editor

This is an editor instance.

', { + image: { toolbar: [ 'toggleImageCaption', 'imageTextAlternative' ] }, + plugins: [ ArticlePluginSet ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], + modelRootElementName: '$inlineRoot', + viewRootElementName: 'h3' + } ) + .then( editor => { + window.editor = editor; + container.appendChild( editor.ui.element ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.md b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.md new file mode 100644 index 00000000000..b7c1b956206 --- /dev/null +++ b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline-data.md @@ -0,0 +1,3 @@ +1. Click "Init editor". +2. New editor instance should be appended to the document with initial data in it. You can create more than one editor. +3. After clicking "Destroy editor" all editors should be removed from the document. diff --git a/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.html b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.html new file mode 100644 index 00000000000..12eef8e7435 --- /dev/null +++ b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.html @@ -0,0 +1,13 @@ +
+

Editor

+

This is an editor instance.

+
+ + diff --git a/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.js b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.js new file mode 100644 index 00000000000..08befc658a1 --- /dev/null +++ b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.js @@ -0,0 +1,25 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { InlineEditor } from '../../src/inlineeditor.js'; +import { ArticlePluginSet } from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; + +InlineEditor + .create( document.querySelector( '#editor' ), { + image: { toolbar: [ 'toggleImageCaption', 'imageTextAlternative' ] }, + plugins: [ ArticlePluginSet ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], + modelRootElementName: '$inlineRoot', + viewRootElementName: 'h2' // TODO this is ignored as original element is reused + } ) + .then( editor => { + console.log( 'Editor has been initialized', editor ); + console.log( 'It has been added to global `editors` and `editables`.' ); + + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.md b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.md new file mode 100644 index 00000000000..623bf0f9d91 --- /dev/null +++ b/packages/ckeditor5-editor-inline/tests/manual/inlineeditor-inline.md @@ -0,0 +1,27 @@ +1. Click "Init editors". +2. Expected: + * Two inline editor should be created. + * Elements used as editables should remain visible. + * They should preserve `.custom-class` and `custom-attr="foo"`. + * There should be floating toolbars with "Bold", "Italic", "Undo", "Redo", "Link" and "Unlink" buttons. +3. Scroll the webpage. +4. Expected: + * Focused editor's toolbar should float around but always stick to editable. + * Focused editor's toolbar should stick to the bottom of the editable if there's not enough space above. +5. Press Alt+F10 when focusing the editor. +6. Expected: + * Toolbar should gain focus. Editable should keep its styling. +7. Click "Destroy editors". +8. Expected: + * Editors should be destroyed. + * Element used as editables should remain visible. + * They should preserve `.custom-class` and `custom-attr="foo"`. + * Elements should contain its data (updated). + * `.ck-body` regions should be removed from ``. + +## Notes: + +* You can play with: + * `window.editables[ N ].isReadOnly`, +* Changes to `window.editors[ name ].focusTracker.isFocused` should be logged to the console. +* Features should work. diff --git a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts index ee4601f1620..14fa0e8874b 100644 --- a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts +++ b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts @@ -148,13 +148,15 @@ export class MultiRootEditor extends Editor { } ); for ( const rootName of rootNames ) { + const rootElementName = ( this.config.get( 'modelRootElementName' ) as Record )?.[ rootName ] || '$root'; + // Create root and `UIView` element for each editable container. - this.model.document.createRoot( '$root', rootName ); + this.model.document.createRoot( rootElementName, rootName ); } if ( this.config.get( 'lazyRoots' ) ) { for ( const rootName of this.config.get( 'lazyRoots' )! ) { - const root = this.model.document.createRoot( '$root', rootName ); + const root = this.model.document.createRoot( '$root', rootName ); // TODO $inlineRoot root._isLoaded = false; } @@ -198,7 +200,9 @@ export class MultiRootEditor extends Editor { const options = { shouldToolbarGroupWhenFull: !this.config.get( 'toolbar.shouldNotGroupWhenFull' ), - editableElements: sourceIsData ? undefined : sourceElementsOrData as Record, + editableElements: ( + sourceIsData ? this.config.get( 'viewRootElementName' ) : sourceElementsOrData + ) as Record, label: this.config.get( 'label' ) }; @@ -503,6 +507,14 @@ export class MultiRootEditor extends Editor { } } + public createEditable( root: ModelRootElement, placeholder?: string, label?: string ): HTMLElement; + + public createEditable( root: ModelRootElement, options: { + placeholder?: string; + label?: string; + editableElementName?: string; + } ): HTMLElement; + /** * Creates and returns a new DOM editable element for the given root element. * @@ -514,8 +526,25 @@ export class MultiRootEditor extends Editor { * @param label The accessible label text describing the editable to the assistive technologies. * @returns The created DOM element. Append it in a desired place in your application. */ - public createEditable( root: ModelRootElement, placeholder?: string, label?: string ): HTMLElement { - const editable = this.ui.view.createEditable( root.rootName, undefined, label ); + public createEditable( + root: ModelRootElement, + optionsOrPlaceholder?: string | { + placeholder?: string; + label?: string; + editableElementName?: string; + }, + label?: string + ): HTMLElement { + let placeholder: string | undefined; + let editableElementName: string | undefined; + + if ( optionsOrPlaceholder && typeof optionsOrPlaceholder == 'object' ) { + placeholder = optionsOrPlaceholder.placeholder; + label = optionsOrPlaceholder.label; + editableElementName = optionsOrPlaceholder.editableElementName; + } + + const editable = this.ui.view.createEditable( root.rootName, editableElementName, label ); this.ui.addEditable( editable, placeholder ); @@ -677,7 +706,7 @@ export class MultiRootEditor extends Editor { } this._registeredRootsAttributesKeys.add( key ); - this.editing.model.schema.extend( '$root', { allowAttributes: key } ); + this.editing.model.schema.extend( '$root', { allowAttributes: key } ); // TODO $inlineRoot } /** diff --git a/packages/ckeditor5-editor-multi-root/src/multirooteditoruiview.ts b/packages/ckeditor5-editor-multi-root/src/multirooteditoruiview.ts index c58687ebf69..ca86d6b96f7 100644 --- a/packages/ckeditor5-editor-multi-root/src/multirooteditoruiview.ts +++ b/packages/ckeditor5-editor-multi-root/src/multirooteditoruiview.ts @@ -65,7 +65,7 @@ export class MultiRootEditorUIView extends EditorUIView { editingView: EditingView, editableNames: Array, options: { - editableElements?: Record; + editableElements?: Record; // TODO docs shouldToolbarGroupWhenFull?: boolean; label?: string | Record; } = {} @@ -132,7 +132,8 @@ export class MultiRootEditorUIView extends EditorUIView { * @param label The accessible editable label used by assistive technologies. * @returns The created editable instance. */ - public createEditable( editableName: string, editableElement?: HTMLElement, label?: string ): InlineEditableUIView { + public createEditable( editableName: string, editableElement?: HTMLElement | string, label?: string ): InlineEditableUIView { + // TODO $inlineRoot editableElement: string => create DOM element name const editable = new InlineEditableUIView( this.locale, this._editingView, editableElement, { label } ); diff --git a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-detached.js b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-detached.js index 0a6147da24b..1dc45cebd63 100644 --- a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-detached.js +++ b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-detached.js @@ -12,9 +12,10 @@ import { Undo } from '@ckeditor/ckeditor5-undo'; import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; const editorData = { - intro: '

Exciting intro text to an article.

', + intro: 'Exciting intro text to an article.', content: '

Exciting news!

Lorem ipsum dolor sit amet.

', - outro: '

Closing text.

' + outro: '

Closing text.

', + signature: 'John Doe' }; let editor; @@ -22,7 +23,17 @@ function initEditor() { MultiRootEditor .create( editorData, { plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ] + toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ], + modelRootElementName: { + intro: '$inlineRoot', + outro: '$root', + signature: '$inlineRoot' + }, + viewRootElementName: { + intro: 'h2', + outro: 'blockquote', + signature: 'em' + } } ) .then( newEditor => { console.log( 'Editor was initialized', newEditor ); @@ -31,6 +42,7 @@ function initEditor() { document.querySelector( '.editable-container' ).appendChild( newEditor.ui.getEditableElement( 'intro' ) ); document.querySelector( '.editable-container' ).appendChild( newEditor.ui.getEditableElement( 'content' ) ); document.querySelector( '.editable-container' ).appendChild( newEditor.ui.getEditableElement( 'outro' ) ); + document.querySelector( '.editable-container' ).appendChild( newEditor.ui.getEditableElement( 'signature' ) ); window.editor = editor = newEditor; } ) diff --git a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.html b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.html new file mode 100644 index 00000000000..8e9551bb420 --- /dev/null +++ b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.html @@ -0,0 +1,94 @@ + + + + + + +

+ + +

+ +

The menubar

+ + +

The toolbar

+
+ +

The editable

+
+

Exciting intro text to an article.

+
+ + + + + +
First cellSecond cell
+ +
+
+ +

Hello World

+
+

Closing text.

+
Signature: John Doe
+
+ + diff --git a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.js b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.js new file mode 100644 index 00000000000..aaca6253edb --- /dev/null +++ b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.js @@ -0,0 +1,90 @@ +/** + * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { MultiRootEditor } from '../../src/multirooteditor.js'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles'; +import { Image, AutoImage, ImageInsert } from '@ckeditor/ckeditor5-image'; +import { LinkImage } from '@ckeditor/ckeditor5-link'; +import { ArticlePluginSet } from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js'; +import { CKFinderUploadAdapter } from '@ckeditor/ckeditor5-adapter-ckfinder'; +import { CKFinder } from '@ckeditor/ckeditor5-ckfinder'; + +const editorElements = { + intro: document.querySelector( '#editor-intro' ), + content: document.querySelector( '#editor-content' ), + outro: document.querySelector( '#editor-outro' ), + signature: document.querySelector( '#editor-signature' ) +}; + +let editor; + +function initEditor() { + MultiRootEditor + .create( {}, { + plugins: [ + Paragraph, Heading, Bold, Italic, + Image, ImageInsert, AutoImage, LinkImage, + ArticlePluginSet, CKFinderUploadAdapter, CKFinder + ], + toolbar: [ + 'heading', '|', 'bold', 'italic', 'undo', 'redo', '|', + 'insertImage', 'insertTable', 'blockQuote' + ], + image: { + toolbar: [ + 'imageStyle:inline', 'imageStyle:block', + 'imageStyle:wrapText', '|', 'toggleImageCaption', + 'imageTextAlternative' + ] + }, + ckfinder: { + // eslint-disable-next-line @stylistic/max-len + uploadUrl: 'https://ckeditor.com/apps/ckfinder/3.5.0/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json' + } + } ) + .then( newEditor => { + console.log( 'Editor was initialized', newEditor ); + + newEditor.on( 'addRoot', ( evt, root ) => { + editorElements[ root.rootName ].replaceWith( newEditor.createEditable( root, { + editableElementName: editorElements[ root.rootName ].tagName.toLowerCase() + } ) ); + } ); + + newEditor.addRoot( 'intro', { data: editorElements.intro.innerHTML, elementName: '$inlineRoot' } ); + newEditor.addRoot( 'content', { data: editorElements.content.innerHTML } ); + newEditor.addRoot( 'outro', { data: editorElements.outro.innerHTML } ); + newEditor.addRoot( 'signature', { data: editorElements.signature.innerHTML, elementName: '$inlineRoot' } ); + + document.querySelector( '.toolbar-container' ).appendChild( newEditor.ui.view.toolbar.element ); + document.querySelector( '.menubar-container' ).appendChild( newEditor.ui.view.menuBarView.element ); + + window.editor = editor = newEditor; + window.editables = newEditor.ui.view.editables; + } ) + .catch( err => { + console.error( err.stack ); + } ); +} + +function destroyEditor() { + editor.destroy() + .then( () => { + editor.ui.view.toolbar.element.remove(); + editor.ui.view.menuBarView.element.remove(); + + window.editor = editor = null; + window.editables = null; + + console.log( 'Editor was destroyed' ); + } ); +} + +document.getElementById( 'initEditor' ).addEventListener( 'click', initEditor ); +document.getElementById( 'destroyEditor' ).addEventListener( 'click', destroyEditor ); + +initEditor(); diff --git a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.md b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.md new file mode 100644 index 00000000000..c36ccd4fc06 --- /dev/null +++ b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor-dynamic.md @@ -0,0 +1,13 @@ +This test checks whether multi-root editor initializes correctly when DOM elements are passed in `.create()`. + +1. Click "Init editor". +2. Expected: +* Toolbar should be added in the first container. +* Three editable areas should be added in the second container. +3. Click "Destroy editor". +4. Expected: +* Editor should be destroyed. +* Toolbar should be removed. +* Editable areas should not be editable -- should be simple `
`s. +* The `.ck-body` region should be removed. + diff --git a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.html b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.html index 2a896841058..8e9551bb420 100644 --- a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.html +++ b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.html @@ -17,7 +17,7 @@

The toolbar

The editable

-

Exciting intro text to an article.

+

Exciting intro text to an article.

@@ -32,6 +32,7 @@

The editable

Hello World

Closing text.

+
Signature: John Doe
diff --git a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.js b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.js index 93fca3884be..80a5fe16556 100644 --- a/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.js +++ b/packages/ckeditor5-editor-multi-root/tests/manual/multirooteditor.js @@ -16,7 +16,8 @@ import { CKFinder } from '@ckeditor/ckeditor5-ckfinder'; const editorData = { intro: document.querySelector( '#editor-intro' ), content: document.querySelector( '#editor-content' ), - outro: document.querySelector( '#editor-outro' ) + outro: document.querySelector( '#editor-outro' ), + signature: document.querySelector( '#editor-signature' ) }; let editor; @@ -33,6 +34,16 @@ function initEditor() { 'heading', '|', 'bold', 'italic', 'undo', 'redo', '|', 'insertImage', 'insertTable', 'blockQuote' ], + modelRootElementName: { + intro: '$inlineRoot', + outro: '$root', + signature: '$inlineRoot' + }, + viewRootElementName: { // TODO this is ignored as original element is reused + intro: 'h2', + outro: 'blockquote', + signature: 'em' + }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.ts b/packages/ckeditor5-engine/src/controller/datacontroller.ts index bf28e73cc9b..7d103708795 100644 --- a/packages/ckeditor5-engine/src/controller/datacontroller.ts +++ b/packages/ckeditor5-engine/src/controller/datacontroller.ts @@ -465,6 +465,8 @@ export class DataController extends /* #__PURE__ */ EmitterMixin() { * @returns Parsed data. */ public parse( data: string, context: ModelSchemaContextDefinition = '$root' ): ModelDocumentFragment { + // TODO make sure correct context is passed in all calls. + // data -> view const viewDocumentFragment = this.processor.toView( data ); @@ -490,6 +492,8 @@ export class DataController extends /* #__PURE__ */ EmitterMixin() { viewElementOrFragment: ViewElement | ViewDocumentFragment, context: ModelSchemaContextDefinition = '$root' ): ModelDocumentFragment { + // TODO make sure correct context is passed in all calls. + return this.model.change( writer => { return this.upcastDispatcher.convert( viewElementOrFragment, writer, context ); } ); diff --git a/packages/ckeditor5-engine/src/model/model.ts b/packages/ckeditor5-engine/src/model/model.ts index 4a2532a8c5f..845be6208d3 100644 --- a/packages/ckeditor5-engine/src/model/model.ts +++ b/packages/ckeditor5-engine/src/model/model.ts @@ -112,6 +112,11 @@ export class Model extends /* #__PURE__ */ ObservableMixin() { isLimit: true } ); + this.schema.register( '$inlineRoot', { + allowContentOf: '$block', + isLimit: true + } ); + this.schema.register( '$container', { allowIn: [ '$root', '$container' ] } ); diff --git a/packages/ckeditor5-engine/src/model/writer.ts b/packages/ckeditor5-engine/src/model/writer.ts index c41974fe59f..b1e27c22f94 100644 --- a/packages/ckeditor5-engine/src/model/writer.ts +++ b/packages/ckeditor5-engine/src/model/writer.ts @@ -1341,6 +1341,8 @@ export class ModelWriter { * @returns The added root element. */ public addRoot( rootName: string, elementName = '$root' ): ModelRootElement { + // TODO make sure correct elementName is passed in all calls. + this._assertWriterUsedCorrectly(); const root = this.model.document.getRoot( rootName ); diff --git a/packages/ckeditor5-heading/src/title.ts b/packages/ckeditor5-heading/src/title.ts index 8a57e437732..6268302d49a 100644 --- a/packages/ckeditor5-heading/src/title.ts +++ b/packages/ckeditor5-heading/src/title.ts @@ -169,6 +169,12 @@ export class Title extends Plugin { const model = editor.model; const rootName = options.rootName ? options.rootName as string : undefined; const root = editor.model.document.getRoot( rootName )!; + + // Ignore $inlineRoot. + if ( !model.schema.checkChild( root, 'title' ) ) { + return ''; // TODO what should be returned? Maybe an exception should be thrown? + } + const view = editor.editing.view; const viewWriter = new ViewDowncastWriter( view.document ); @@ -205,7 +211,13 @@ export class Title extends Plugin { * Returns the `title` element when it is in the document. Returns `undefined` otherwise. */ private _getTitleElement( rootName?: string ): ModelElement | undefined { - const root = this.editor.model.document.getRoot( rootName )!; + const model = this.editor.model; + const root = model.document.getRoot( rootName )!; + + // Ignore $inlineRoot. + if ( !model.schema.checkChild( root, 'title' ) ) { + return; + } for ( const child of root.getChildren() as IterableIterator ) { if ( isTitle( child ) ) { @@ -255,6 +267,11 @@ export class Title extends Plugin { const model = this.editor.model; for ( const modelRoot of this.editor.model.document.getRoots() ) { + // Ignore inline roots. + if ( !model.schema.checkChild( modelRoot, 'title' ) ) { + continue; + } + const titleElements = Array.from( modelRoot.getChildren() as IterableIterator ).filter( isTitle ); const firstTitleElement = titleElements[ 0 ]; const firstRootChild = modelRoot.getChild( 0 ) as ModelElement; @@ -304,12 +321,13 @@ export class Title extends Plugin { * when it is needed for the placeholder purposes. */ private _fixBodyElement( writer: ModelWriter ) { + const schema = writer.model.schema; let changed = false; for ( const rootName of this.editor.model.document.getRootNames() ) { const modelRoot = this.editor.model.document.getRoot( rootName )!; - if ( modelRoot.childCount < 2 ) { + if ( modelRoot.childCount < 2 && schema.checkChild( modelRoot, 'paragraph' ) ) { const placeholder = writer.createElement( 'paragraph' ); writer.insert( placeholder, modelRoot, 1 ); @@ -385,6 +403,13 @@ export class Title extends Plugin { continue; } + // Ignore $inlineRoot roots. + const modelRoot = editor.editing.mapper.toModelElement( viewRoot )!; + + if ( !editor.model.schema.checkChild( modelRoot, 'title' ) ) { + continue; + } + // If `viewRoot` is not empty, then we can expect at least two elements in it. const body = viewRoot!.getChild( 1 ) as ViewElement; const oldBody = bodyViewElements.get( viewRoot.rootName ); @@ -453,6 +478,11 @@ export class Title extends Plugin { const selectionPosition = selection.getFirstPosition()!; const root = editor.model.document.getRoot( selectionPosition.root.rootName! )!; + // Ignore $inlineRoot. + if ( !model.schema.checkChild( root, 'title' ) ) { + return; + } + const title = root.getChild( 0 ) as ModelElement; const body = root.getChild( 1 ); diff --git a/packages/ckeditor5-horizontal-line/src/horizontallinecommand.ts b/packages/ckeditor5-horizontal-line/src/horizontallinecommand.ts index 9ada887dfcc..e70d5ab7f5a 100644 --- a/packages/ckeditor5-horizontal-line/src/horizontallinecommand.ts +++ b/packages/ckeditor5-horizontal-line/src/horizontallinecommand.ts @@ -71,7 +71,7 @@ function getInsertHorizontalLineParent( selection: ModelSelection | ModelDocumen const insertionRange = findOptimalInsertionRange( selection, model ); const parent = insertionRange.start.parent; - if ( parent.isEmpty && !parent.is( 'element', '$root' ) ) { + if ( parent.isEmpty && !parent.is( 'rootElement' ) ) { return parent.parent! as ModelElement; } diff --git a/packages/ckeditor5-image/src/imageutils.ts b/packages/ckeditor5-image/src/imageutils.ts index e86fcb2f0d9..8c7ed1a9adc 100644 --- a/packages/ckeditor5-image/src/imageutils.ts +++ b/packages/ckeditor5-image/src/imageutils.ts @@ -351,7 +351,7 @@ function getInsertImageParent( selection: ModelSelection | ModelDocumentSelectio const insertionRange = findOptimalInsertionRange( selection, model ); const parent = insertionRange.start.parent; - if ( parent.isEmpty && !parent.is( 'element', '$root' ) ) { + if ( parent.isEmpty && !parent.is( 'rootElement' ) ) { return parent.parent!; } diff --git a/packages/ckeditor5-page-break/src/pagebreakcommand.ts b/packages/ckeditor5-page-break/src/pagebreakcommand.ts index bc3a99ad9af..7b991d6409f 100644 --- a/packages/ckeditor5-page-break/src/pagebreakcommand.ts +++ b/packages/ckeditor5-page-break/src/pagebreakcommand.ts @@ -66,7 +66,7 @@ function getInsertPageBreakParent( selection: ModelDocumentSelection, model: Mod const insertionRange = findOptimalInsertionRange( selection, model ); const parent = insertionRange.start.parent; - if ( parent.isEmpty && !parent.is( 'element', '$root' ) ) { + if ( parent.isEmpty && !parent.is( 'rootElement' ) ) { return parent.parent as ModelElement; } diff --git a/packages/ckeditor5-ui/src/editableui/editableuiview.ts b/packages/ckeditor5-ui/src/editableui/editableuiview.ts index 5d2d1140d84..81211194a76 100644 --- a/packages/ckeditor5-ui/src/editableui/editableuiview.ts +++ b/packages/ckeditor5-ui/src/editableui/editableuiview.ts @@ -60,12 +60,12 @@ export class EditableUIView extends View { constructor( locale: Locale, editingView: EditingView, - editableElement?: HTMLElement + editableElement?: HTMLElement | string // TODO string => create DOM element name ) { super( locale ); this.setTemplate( { - tag: 'div', + tag: typeof editableElement == 'string' ? editableElement : 'div', attributes: { class: [ 'ck', @@ -80,7 +80,7 @@ export class EditableUIView extends View { this.set( 'isFocused', false ); - this._editableElement = editableElement; + this._editableElement = typeof editableElement == 'string' ? undefined : editableElement; this._hasExternalElement = !!this._editableElement; this._editingView = editingView; } diff --git a/packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.ts b/packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.ts index 3639d0e930f..c057b130033 100644 --- a/packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.ts +++ b/packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.ts @@ -36,7 +36,7 @@ export class InlineEditableUIView extends EditableUIView { constructor( locale: Locale, editingView: EditingView, - editableElement?: HTMLElement, + editableElement?: HTMLElement | string, // TODO string => create DOM element name options: InlineEditableUIViewOptions = {} ) { super( locale, editingView, editableElement );