diff --git a/docs/builds/guides/migration/migration-to-28.0.0.md b/docs/builds/guides/migration/migration-to-28.0.0.md index 154dd7668cd..db8d10eeb20 100644 --- a/docs/builds/guides/migration/migration-to-28.0.0.md +++ b/docs/builds/guides/migration/migration-to-28.0.0.md @@ -14,4 +14,10 @@ Listed below are the most important changes that require your attention when upg Prior to version 28.0.0 inserting a table into another table was not allowed. -If you wish to bring back this restriction see {@link features/table#disallowing-nesting-tables "Disallowing nesting tables"}. \ No newline at end of file +If you wish to bring back this restriction see {@link features/table#disallowing-nesting-tables "Disallowing nesting tables"}. + +## Disallowing nesting block quotes + +Prior to version 28.0.0 inserting block quote into a block quote was not allowed. + +If you wish to bring back the old behavior see {@link features/block-quote#disallowing-nesting-block-quotes "Disallowing nesting block quotes"}. diff --git a/packages/ckeditor5-block-quote/docs/features/block-quote.md b/packages/ckeditor5-block-quote/docs/features/block-quote.md index 967b9818cb9..22b885a0f78 100644 --- a/packages/ckeditor5-block-quote/docs/features/block-quote.md +++ b/packages/ckeditor5-block-quote/docs/features/block-quote.md @@ -48,6 +48,33 @@ ClassicEditor Read more about {@link builds/guides/integration/installing-plugins installing plugins}. +## Disallowing nesting block quotes + +By default, the editor supports a block quote inserted into another block quote. + +In order to disallow nesting block quotes you need to register an additional schema rule. It needs to be added before the data gets loaded into the editor, hence it is best to implement it as a plugin: + +```js +function DisallowNestingBlockQuotes( editor ) { + editor.model.schema.addChildCheck( ( context, childDefinition ) => { + if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'blockQuote' ) { + return false; + } + } ); +} + +// Pass it via config.extraPlugins or config.plugins: + +ClassicEditor + .create( document.querySelector( '#editor' ), { + extraPlugins: [ DisallowNestingBlockQuotes ], + + // The rest of the config. + } ) + .then( ... ) + .catch( ... ); +``` + ## Common API The {@link module:block-quote/blockquote~BlockQuote} plugin registers: diff --git a/packages/ckeditor5-block-quote/src/blockquoteediting.js b/packages/ckeditor5-block-quote/src/blockquoteediting.js index fea393d9b23..eb74d7f6941 100644 --- a/packages/ckeditor5-block-quote/src/blockquoteediting.js +++ b/packages/ckeditor5-block-quote/src/blockquoteediting.js @@ -49,13 +49,6 @@ export default class BlockQuoteEditing extends Plugin { allowContentOf: '$root' } ); - // Disallow blockQuote in blockQuote. - schema.addChildCheck( ( ctx, childDef ) => { - if ( ctx.endsWith( 'blockQuote' ) && childDef.name == 'blockQuote' ) { - return false; - } - } ); - editor.conversion.elementToElement( { model: 'blockQuote', view: 'blockquote' } ); // Postfixer which cleans incorrect model states connected with block quotes. @@ -77,13 +70,12 @@ export default class BlockQuoteEditing extends Plugin { return true; } else if ( element.is( 'element', 'blockQuote' ) && !schema.checkChild( entry.position, element ) ) { - // Added a blockQuote in incorrect place - most likely inside another blockQuote. Unwrap it - // so the content inside is not lost. + // Added a blockQuote in incorrect place. Unwrap it so the content inside is not lost. writer.unwrap( element ); return true; } else if ( element.is( 'element' ) ) { - // Just added an element. Check its children to see if there are no nested blockQuotes somewhere inside. + // Just added an element. Check that all children meet the scheme rules. const range = writer.createRangeIn( element ); for ( const child of range.getItems() ) { diff --git a/packages/ckeditor5-block-quote/tests/blockquoteediting.js b/packages/ckeditor5-block-quote/tests/blockquoteediting.js index 68531660bde..2697bb2f654 100644 --- a/packages/ckeditor5-block-quote/tests/blockquoteediting.js +++ b/packages/ckeditor5-block-quote/tests/blockquoteediting.js @@ -48,8 +48,8 @@ describe( 'BlockQuoteEditing', () => { expect( model.schema.checkChild( [ '$root', 'blockQuote' ], 'paragraph' ) ).to.be.true; } ); - it( 'does not allow for blockQuote in blockQuote', () => { - expect( model.schema.checkChild( [ '$root', 'blockQuote' ], 'blockQuote' ) ).to.be.false; + it( 'allows for blockQuote in blockQuote', () => { + expect( model.schema.checkChild( [ '$root', 'blockQuote' ], 'blockQuote' ) ).to.be.true; } ); it( 'does not break when checking an unregisterd item', () => { @@ -102,7 +102,7 @@ describe( 'BlockQuoteEditing', () => { expect( editor.getData( { trim: 'none' } ) ).to.equal( '

 

' ); // Autoparagraphed. } ); - it( 'should unwrap a blockQuote if it was inserted into another blockQuote', () => { + it( 'should not unwrap a blockQuote if it was inserted into another blockQuote', () => { setModelData( model, '
Foo
' ); model.change( writer => { @@ -115,10 +115,10 @@ describe( 'BlockQuoteEditing', () => { writer.insert( bq, root.getChild( 0 ), 1 ); // Insert after

Foo

. } ); - expect( editor.getData() ).to.equal( '

Foo

Bar

' ); + expect( editor.getData() ).to.equal( '

Foo

Bar

' ); } ); - it( 'should unwrap nested blockQuote if it was wrapped into another blockQuote', () => { + it( 'should not unwrap nested blockQuote if it was wrapped into another blockQuote', () => { setModelData( model, '
Foo
Bar' ); model.change( writer => { @@ -127,7 +127,7 @@ describe( 'BlockQuoteEditing', () => { writer.wrap( writer.createRangeIn( root ), 'blockQuote' ); } ); - expect( editor.getData() ).to.equal( '

Foo

Bar

' ); + expect( editor.getData() ).to.equal( '

Foo

Bar

' ); } ); it( 'postfixer should do nothing on attribute change', () => { @@ -143,4 +143,44 @@ describe( 'BlockQuoteEditing', () => { expect( editor.getData() ).to.equal( '

Foo

' ); } ); + + describe( 'nested blockQuote forbidden by custom rule', () => { + // Nested block quotes are supported since https://github.com/ckeditor/ckeditor5/issues/9210, so let's check + // if the editor will not blow up in case nested block quotes are forbidden by custom scheme rule. + beforeEach( () => { + model.schema.addChildCheck( ( ctx, childDef ) => { + if ( ctx.endsWith( 'blockQuote' ) && childDef.name == 'blockQuote' ) { + return false; + } + } ); + } ); + + it( 'should unwrap a blockQuote if it was inserted into another blockQuote', () => { + setModelData( model, '
Foo
' ); + + model.change( writer => { + const root = model.document.getRoot(); + const bq = writer.createElement( 'blockQuote' ); + const p = writer.createElement( 'paragraph' ); + + writer.insertText( 'Bar', p, 0 ); //

Bar

. + writer.insert( p, bq, 0 ); //

Bar

. + writer.insert( bq, root.getChild( 0 ), 1 ); // Insert after

Foo

. + } ); + + expect( editor.getData() ).to.equal( '

Foo

Bar

' ); + } ); + + it( 'should unwrap nested blockQuote if it was wrapped into another blockQuote', () => { + setModelData( model, '
Foo
Bar' ); + + model.change( writer => { + const root = model.document.getRoot(); + + writer.wrap( writer.createRangeIn( root ), 'blockQuote' ); + } ); + + expect( editor.getData() ).to.equal( '

Foo

Bar

' ); + } ); + } ); } ); diff --git a/packages/ckeditor5-block-quote/tests/manual/blockquote.html b/packages/ckeditor5-block-quote/tests/manual/blockquote.html index 5bde4c10c2e..b8ea9f6b252 100644 --- a/packages/ckeditor5-block-quote/tests/manual/blockquote.html +++ b/packages/ckeditor5-block-quote/tests/manual/blockquote.html @@ -15,4 +15,11 @@

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+

Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris.

+
+

Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

diff --git a/packages/ckeditor5-block-quote/tests/manual/blockquote.md b/packages/ckeditor5-block-quote/tests/manual/blockquote.md index d36ddc8cf97..13392bf0adf 100644 --- a/packages/ckeditor5-block-quote/tests/manual/blockquote.md +++ b/packages/ckeditor5-block-quote/tests/manual/blockquote.md @@ -8,4 +8,5 @@ Check block quote related behaviors: * Backspace, * undo/redo, * applying headings and lists, -* stability when used with nested lists. +* stability when used with nested lists, +* stability when used with nested block quotes. diff --git a/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.html b/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.html new file mode 100644 index 00000000000..573ba51c487 --- /dev/null +++ b/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.html @@ -0,0 +1,20 @@ + + +
+

Nested block quotes (in the data):

+ +
+

Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris.

+
+

Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+

Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+
+
+
diff --git a/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.js b/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.js new file mode 100644 index 00000000000..3ab094e5bbf --- /dev/null +++ b/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.js @@ -0,0 +1,35 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; + +function DisallowNestingBlockQuotes( editor ) { + editor.model.schema.addChildCheck( ( context, childDefinition ) => { + if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'blockQuote' ) { + return false; + } + } ); +} + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet, DisallowNestingBlockQuotes ], + toolbar: [ + 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + ], + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], + tableToolbar: [ 'bold', 'italic' ] + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.md b/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.md new file mode 100644 index 00000000000..f6113c6bbd7 --- /dev/null +++ b/packages/ckeditor5-block-quote/tests/manual/blockquotenonesting.md @@ -0,0 +1,2 @@ +* The nested block quote present in the data should be "unnested" ("unwrapped") on data load. All block quotes (the top level block quote and all nested ones) should be converted to siblings in editor's root. +* It should not be possible to insert a block quote into another block quote as a direct child in any way (UI, paste, d&d, etc.). It is allowed to have nested block quotes indirectly, i.e. a block quote inside a table, which is inside another block quote. \ No newline at end of file diff --git a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md index 2a7109e7879..01c38f4ceef 100644 --- a/packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md +++ b/packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md @@ -431,7 +431,7 @@ The {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} m These listeners can be added either by listening directly to the {@link module:engine/model/schema~Schema#event:checkChild} event or by using the handy {@link module:engine/model/schema~Schema#addChildCheck `Schema#addChildCheck()`} method. -For instance, the block quote feature defines such a listener to disallow nested `
` structures: +For instance, to disallow nested `
` structures, you can define such a listener: ```js schema.addChildCheck( ( context, childDefinition ) => { diff --git a/packages/ckeditor5-engine/tests/model/operation/transform/unwrap.js b/packages/ckeditor5-engine/tests/model/operation/transform/unwrap.js index f994115f07d..40a1828c680 100644 --- a/packages/ckeditor5-engine/tests/model/operation/transform/unwrap.js +++ b/packages/ckeditor5-engine/tests/model/operation/transform/unwrap.js @@ -335,7 +335,9 @@ describe( 'transform', () => { expectClients( '
' + 'A' + - 'B' + + '
' + + 'B' + + '
' + 'C' + '
' );