From 4dd2637d24d4f816621beda9a19ef846e162b730 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 5 Jan 2022 17:14:32 +0100 Subject: [PATCH 01/10] Bootstrapped enter support (and split command). --- .../src/documentlist/documentlistediting.js | 13 + .../documentlist/documentlistsplitcommand.js | 94 +++++++ .../tests/documentlist/documentlistediting.js | 93 ++++++- .../documentlist/documentlistsplitcommand.js | 231 ++++++++++++++++++ 4 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js create mode 100644 packages/ckeditor5-list/tests/documentlist/documentlistsplitcommand.js diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index a9d0b144fc5..c2bfa8efde3 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -84,6 +84,19 @@ export default class DocumentListEditing extends Plugin { editor.commands.add( 'indentList', new DocumentListIndentCommand( editor, 'forward' ) ); editor.commands.add( 'outdentList', new DocumentListIndentCommand( editor, 'backward' ) ); + + // Overwrite default Enter key behavior. + this.listenTo( editor.editing.view.document, 'enter', ( evt, data ) => { + const doc = model.document; + const positionParent = doc.selection.getLastPosition().parent; + + if ( doc.selection.isCollapsed && positionParent.hasAttribute( 'listItemId' ) && positionParent.isEmpty ) { + editor.execute( 'outdentList' ); + + data.preventDefault(); + evt.stop(); + } + }, { context: 'li' } ); } /** diff --git a/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js new file mode 100644 index 00000000000..05d37011a37 --- /dev/null +++ b/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js @@ -0,0 +1,94 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module TODO + */ + +import { Command } from 'ckeditor5/src/core'; +import { + sortBlocks +} from './utils/model'; + +/** + * The list split command. It is used by the {@link module:list/documentlist~DocumentList document list feature}. + * + * @extends module:core/command~Command + */ +export default class DocumentListSplitCommand extends Command { + /** + * @inheritDoc + */ + refresh() { + this.isEnabled = this._checkEnabled(); + } + + /** + * TODO + * + * @fires execute + * @fires afterExecute + */ + execute() { + // const editor = this.editor; + // const model = editor.model; + // const selection = model.document.selection; + // const firstPosition = selection.getFirstPosition(); + + // if ( selection.isCollapsed ) { + // } + + // if ( empty list item ) { + // outdent list item + + // return; + // } + + // if ( in first block ) { + + // } + + // if ( single block in item ) { + // if ( at end ) { + // create list item after + // } else { + // split list item + // } + // } else if ( multiple blocks in item ) { + // split block + // } + } + + /** + * Fires the `afterExecute` event. + * + * @private + * @param {Array.} changedBlocks The changed list elements. + */ + _fireAfterExecute( changedBlocks ) { + /** + * Event fired by the {@link #execute} method. + * + * It allows to execute an action after executing the {@link ~TODO#execute} method, + * for example adjusting attributes of changed list items. + * + * @protected + * @event afterExecute + */ + this.fire( 'afterExecute', sortBlocks( new Set( changedBlocks ) ) ); + } + + /** + * Checks whether the command can be enabled in the current context. + * + * @private + * @returns {Boolean} Whether the command should be enabled. + */ + _checkEnabled() { + // TODO + + return true; + } +} diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js index 9f18ed0f59b..3fe8963e552 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js @@ -23,7 +23,7 @@ import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/vie import ListEditing from '../../src/list/listediting'; import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; import stubUid from './_utils/uid'; -import { prepareTest } from './_utils/utils'; +import { prepareTest, modelList } from './_utils/utils'; describe( 'DocumentListEditing', () => { let editor, model, modelDoc, modelRoot, view; @@ -1148,4 +1148,95 @@ describe( 'DocumentListEditing', () => { } ); } ); } ); + + describe( 'enter key handling', () => { + let domEvtDataStub; + + beforeEach( () => { + domEvtDataStub = { preventDefault() {} }; + } ); + + // TODO: Negative cases for all these tests. + // * non-collapsed selection, + // * non-last item, + // * non-empty item. + + it( 'should outdent if the collapsed selection is anchored in an empty, last sub-list item', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' # []' + ] ) ); + + editor.editing.view.document.fire( 'enter', domEvtDataStub ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + '* []' + ] ) ); + } ); + + it( 'should outdent if the collapsed selection is anchored in an empty, last sub-list item (bigger indentation)', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' * c', + ' * []' + ] ) ); + + editor.editing.view.document.fire( 'enter', domEvtDataStub ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + ' * c', + ' # []' + ] ) ); + } ); + + it( 'should outdent if the collapsed selection is anchored in an empty, only sub-list item', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' * []', + ' #' + ] ) ); + + editor.editing.view.document.fire( 'enter', domEvtDataStub ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + ' # []', + ' #' + ] ) ); + } ); + + it( 'should convert the only list item which is empty into a paragraph and thus turn of the list', () => { + setModelData( model, modelList( [ + '* []' + ] ) ); + + editor.editing.view.document.fire( 'enter', domEvtDataStub ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + } ); + + it( 'should convert an empty list item at the end of the list into a paragraph', () => { + setModelData( model, modelList( [ + '* a', + '* []' + ] ) ); + + editor.editing.view.document.fire( 'enter', domEvtDataStub ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '[]' + ] ) ); + } ); + } ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistsplitcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistsplitcommand.js new file mode 100644 index 00000000000..659f22abdab --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/documentlistsplitcommand.js @@ -0,0 +1,231 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import DocumentListSplitCommand from '../../src/documentlist/documentlistsplitcommand'; +import stubUid from './_utils/uid'; +import { modelList } from './_utils/utils'; + +import Editor from '@ckeditor/ckeditor5-core/src/editor/editor'; +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; + +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'DocumentListSplitCommand', () => { + let editor, command, model, doc; + // let changedBlocks; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + editor = new Editor(); + editor.model = new Model(); + + model = editor.model; + doc = model.document; + + doc.createRoot(); + + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + model.schema.register( 'blockQuote', { inheritAllFrom: '$container' } ); + model.schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); + + command = new DocumentListSplitCommand( editor ); + // command.on( 'afterExecute', ( evt, data ) => { + // changedBlocks = data; + // } ); + + stubUid(); + } ); + + afterEach( () => { + command.destroy(); + } ); + + describe( 'isEnabled', () => { + // TODO + } ); + + describe( 'execute()', () => { + it( 'should use parent batch', () => { + setData( model, '[0]' ); + + model.change( writer => { + expect( writer.batch.operations.length, 'before' ).to.equal( 0 ); + + command.execute(); + + expect( writer.batch.operations.length, 'after' ).to.be.above( 0 ); + } ); + } ); + + it( '0', () => { + setData( model, modelList( [ + '* a[]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* []' + ] ) ); + + // TODO changed blocks + } ); + + it( '1.-1', () => { + setData( model, modelList( [ + '* a[]', + ' b', + ' c' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' []', + ' b', + ' c' + ] ) ); + + // TODO changed blocks + } ); + + it( '1.0', () => { + setData( model, modelList( [ + '* []a', + ' b', + ' c' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* ', + '* []a', + ' b', + ' c' + ] ) ); + + // TODO changed blocks + } ); + + it( '1.1', () => { + setData( model, modelList( [ + '* a', + ' b', + ' []c' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + ' ', + ' []c' + ] ) ); + + // TODO + // expect( changedBlocks.length ).to.equal( ... ); + // expect( changedBlocks ).to.deep.equal( [ + // ] ); + } ); + + it( '1.2', () => { + setData( model, modelList( [ + '* a', + ' b', + ' c[]' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + ' c', + ' []' + ] ) ); + + // TODO changed blocks + } ); + + it( '3', () => { + setData( model, modelList( [ + '* a', + ' []', + ' c' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' ', + ' []', + ' c' + ] ) ); + + // TODO changed blocks + } ); + + it( '4', () => { + setData( model, modelList( [ + '* a', + ' []' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* []' + ] ) ); + + // TODO changed blocks + } ); + + it( '5', () => { + setData( model, modelList( [ + '* a', + ' b', + ' []' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + '* []' + ] ) ); + + // TODO changed blocks + } ); + + it( '6', () => { + setData( model, modelList( [ + '* a', + ' b', + ' []', + '*' + ] ) ); + + command.execute(); + + expect( getData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + '* []', + '*' + ] ) ); + + // TODO changed blocks + } ); + } ); +} ); From 87aae67716f8ac42ead55535b02cf1c17d22065f Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 12 Jan 2022 11:57:12 +0100 Subject: [PATCH 02/10] DocumentListSplitCommand implementation + tests. --- .../src/documentlist/documentlistediting.js | 63 +- .../documentlist/documentlistsplitcommand.js | 52 +- .../tests/documentlist/documentlistediting.js | 1207 ++++++++++++++++- .../documentlist/documentlistsplitcommand.js | 195 +-- 4 files changed, 1324 insertions(+), 193 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index c2bfa8efde3..340513742c5 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -14,6 +14,7 @@ import { CKEditorError } from 'ckeditor5/src/utils'; import DocumentListIndentCommand from './documentlistindentcommand'; import DocumentListCommand from './documentlistcommand'; +import DocumentListSplitCommand from './documentlistsplitcommand'; import { listItemDowncastConverter, listItemParagraphDowncastConverter, @@ -27,6 +28,11 @@ import { fixListIndents, fixListItemIds } from './utils/postfixers'; +import { + getAllListItemBlocks, + isFirstBlockOfListItem, + isLastBlockOfListItem +} from './utils/model'; import { iterateSiblingListBlocks } from './utils/listwalker'; import '../../theme/documentlist.css'; @@ -85,16 +91,30 @@ export default class DocumentListEditing extends Plugin { editor.commands.add( 'indentList', new DocumentListIndentCommand( editor, 'forward' ) ); editor.commands.add( 'outdentList', new DocumentListIndentCommand( editor, 'backward' ) ); - // Overwrite default Enter key behavior. + editor.commands.add( 'splitListItem', new DocumentListSplitCommand( editor ) ); + + // Overwrite default Enter key behavior and outdent the list. this.listenTo( editor.editing.view.document, 'enter', ( evt, data ) => { const doc = model.document; - const positionParent = doc.selection.getLastPosition().parent; + const positionParent = doc.selection.getFirstPosition().parent; if ( doc.selection.isCollapsed && positionParent.hasAttribute( 'listItemId' ) && positionParent.isEmpty ) { - editor.execute( 'outdentList' ); + // * a → * a + // * [] → [] + if ( isFirstBlockOfListItem( positionParent ) ) { + editor.execute( 'outdentList' ); + + data.preventDefault(); + evt.stop(); + } + // * a → * a + // [] → * [] + else if ( isLastBlockOfListItem( positionParent ) ) { + editor.execute( 'splitListItem' ); - data.preventDefault(); - evt.stop(); + data.preventDefault(); + evt.stop(); + } } }, { context: 'li' } ); } @@ -103,10 +123,12 @@ export default class DocumentListEditing extends Plugin { * @inheritDoc */ afterInit() { - const commands = this.editor.commands; + const editor = this.editor; + const commands = editor.commands; const indent = commands.get( 'indent' ); const outdent = commands.get( 'outdent' ); + const enterCommand = commands.get( 'enter' ); if ( indent ) { indent.registerChildCommand( commands.get( 'indentList' ) ); @@ -115,6 +137,35 @@ export default class DocumentListEditing extends Plugin { if ( outdent ) { outdent.registerChildCommand( commands.get( 'outdentList' ) ); } + + if ( enterCommand ) { + // In some cases, the integration with the enter key is done after the default handler in EnterCommand. + this.listenTo( enterCommand, 'afterExecute', () => { + const splitCommand = editor.commands.get( 'splitListItem' ); + + // The commands has not refreshed because the change block related to EnterCommand#execute() is not over yet. + // Let's keep it up to date and take advantage of DocumentListSplitCommand#isEnabled. + splitCommand.refresh(); + + if ( !splitCommand.isEnabled ) { + return; + } + + const doc = editor.model.document; + const positionParent = doc.selection.getLastPosition().parent; + const listItemBlocks = getAllListItemBlocks( positionParent ); + + // Keep in mind this split happens after the default enter handler was executed. For instance: + // + // │ Initial state │ After default enter │ Here in #afterExecute │ + // ├───────────────────────────┼───────────────────────────┼───────────────────────────┤ + // │ * a[] │ * a │ * a │ + // │ │ [] │ * [] │ + if ( listItemBlocks.length === 2 ) { + editor.execute( 'splitListItem' ); + } + } ); + } } /** diff --git a/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js index 05d37011a37..3babb5b0075 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js @@ -4,16 +4,20 @@ */ /** - * @module TODO + * @module list/documentlist/documentlistsplitcommand */ import { Command } from 'ckeditor5/src/core'; import { - sortBlocks + isFirstBlockOfListItem, + sortBlocks, + splitListItemBefore } from './utils/model'; /** - * The list split command. It is used by the {@link module:list/documentlist~DocumentList document list feature}. + * The document list split command that splits the list item at the first position of the selection. + * + * It is used by the {@link module:list/documentlist~DocumentList document list feature}. * * @extends module:core/command~Command */ @@ -26,39 +30,20 @@ export default class DocumentListSplitCommand extends Command { } /** - * TODO + * Splits the list item at the first position of the selection. * * @fires execute * @fires afterExecute */ execute() { - // const editor = this.editor; - // const model = editor.model; - // const selection = model.document.selection; - // const firstPosition = selection.getFirstPosition(); - - // if ( selection.isCollapsed ) { - // } - - // if ( empty list item ) { - // outdent list item - - // return; - // } - - // if ( in first block ) { + const editor = this.editor; - // } + editor.model.change( writer => { + const positionParent = editor.model.document.selection.getFirstPosition().parent; + const changedBlocks = splitListItemBefore( positionParent, writer ); - // if ( single block in item ) { - // if ( at end ) { - // create list item after - // } else { - // split list item - // } - // } else if ( multiple blocks in item ) { - // split block - // } + this._fireAfterExecute( changedBlocks ); + } ); } /** @@ -71,7 +56,7 @@ export default class DocumentListSplitCommand extends Command { /** * Event fired by the {@link #execute} method. * - * It allows to execute an action after executing the {@link ~TODO#execute} method, + * It allows to execute an action after executing the {@link ~DocumentListSplitCommand#execute} method, * for example adjusting attributes of changed list items. * * @protected @@ -87,8 +72,11 @@ export default class DocumentListSplitCommand extends Command { * @returns {Boolean} Whether the command should be enabled. */ _checkEnabled() { - // TODO + const doc = this.editor.model.document; + const positionParent = doc.selection.getFirstPosition().parent; - return true; + return doc.selection.isCollapsed && + positionParent.hasAttribute( 'listItemId' ) && + !isFirstBlockOfListItem( positionParent ); } } diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js index 3fe8963e552..228a79d6109 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js @@ -22,8 +22,12 @@ import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/vie import ListEditing from '../../src/list/listediting'; import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; +import DocumentListSplitCommand from '../../src/documentlist/documentlistsplitcommand'; + import stubUid from './_utils/uid'; import { prepareTest, modelList } from './_utils/utils'; +import { DomEventData } from '@ckeditor/ckeditor5-engine'; +import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; describe( 'DocumentListEditing', () => { let editor, model, modelDoc, modelRoot, view; @@ -117,6 +121,12 @@ describe( 'DocumentListEditing', () => { expect( command ).to.be.instanceOf( DocumentListIndentCommand ); } ); + it( 'should register the splitListItem command', () => { + const command = editor.commands.get( 'splitListItem' ); + + expect( command ).to.be.instanceOf( DocumentListSplitCommand ); + } ); + it( 'should add indent list command to indent command', async () => { const editor = await VirtualTestEditor.create( { plugins: [ Paragraph, IndentEditing, DocumentListEditing ] @@ -1150,93 +1160,1142 @@ describe( 'DocumentListEditing', () => { } ); describe( 'enter key handling', () => { - let domEvtDataStub; + const changedBlocks = []; + let domEventData, splitCommand, indentCommand, eventInfo, splitCommandExecuteSpy, outdentCommandExecuteSpy; beforeEach( () => { - domEvtDataStub = { preventDefault() {} }; - } ); + eventInfo = new EventInfo( view.document, 'enter' ); + domEventData = new DomEventData( view.document, { + preventDefault: sinon.spy() + } ); - // TODO: Negative cases for all these tests. - // * non-collapsed selection, - // * non-last item, - // * non-empty item. - - it( 'should outdent if the collapsed selection is anchored in an empty, last sub-list item', () => { - setModelData( model, modelList( [ - '* a', - ' # b', - ' # []' - ] ) ); - - editor.editing.view.document.fire( 'enter', domEvtDataStub ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' # b', - '* []' - ] ) ); - } ); + splitCommand = editor.commands.get( 'splitListItem' ); + indentCommand = editor.commands.get( 'outdentList' ); - it( 'should outdent if the collapsed selection is anchored in an empty, last sub-list item (bigger indentation)', () => { - setModelData( model, modelList( [ - '* a', - ' # b', - ' * c', - ' * []' - ] ) ); - - editor.editing.view.document.fire( 'enter', domEvtDataStub ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' # b', - ' * c', - ' # []' - ] ) ); - } ); + splitCommandExecuteSpy = sinon.spy( splitCommand, 'execute' ); + outdentCommandExecuteSpy = sinon.spy( indentCommand, 'execute' ); + + changedBlocks.length = 0; + + splitCommand.on( 'afterExecute', ( evt, data ) => { + changedBlocks.push( ...data ); + } ); - it( 'should outdent if the collapsed selection is anchored in an empty, only sub-list item', () => { - setModelData( model, modelList( [ - '* a', - ' # b', - ' * []', - ' #' - ] ) ); - - editor.editing.view.document.fire( 'enter', domEvtDataStub ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' # b', - ' # []', - ' #' - ] ) ); + indentCommand.on( 'afterExecute', ( evt, data ) => { + changedBlocks.push( ...data ); + } ); } ); - it( 'should convert the only list item which is empty into a paragraph and thus turn of the list', () => { - setModelData( model, modelList( [ - '* []' - ] ) ); + describe( 'collapsed selection', () => { + describe( 'with just one block per list item', () => { + it( 'should outdent if the slection in the only empty list item (convert into paragraph and turn of the list)', () => { + setModelData( model, modelList( [ + '* []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 0 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should outdent if the slection in the last empty list item (convert the item into paragraph)', () => { + setModelData( model, modelList( [ + '* a', + '* []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should create another list item when the selection in a non-empty only list item', () => { + setModelData( model, modelList( [ + '* a[]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should outdent if the selection in an empty, last sub-list item', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' * c', + ' * []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + ' * c', + ' # []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 3 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + } ); + + describe( 'with multiple blocks in a list item', () => { + it( 'should outdent if the selection is anchored in an empty, last item block', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' # []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 2 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should outdent if the selection is anchored in an empty, only sub-item block', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' * []', + ' #' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + ' # []', + ' #' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 2 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should create another block when the selection at the start of a non-empty first block', () => { + setModelData( model, modelList( [ + '* a[]', + ' b', + ' c' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' []', + ' b', + ' c' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another block when the selection at the end of a non-empty first block', () => { + setModelData( model, modelList( [ + '* []a', + ' b', + ' c' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ', + ' []a', + ' b', + ' c' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another block when the selection at the start of a non-empty last block', () => { + setModelData( model, modelList( [ + '* a', + ' b', + ' []c' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + ' ', + ' []c' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another block when the selection at the end of a non-empty last block', () => { + setModelData( model, modelList( [ + '* a', + ' b', + ' c[]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + ' c', + ' []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); - editor.editing.view.document.fire( 'enter', domEvtDataStub ); + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '[]' - ] ) ); + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another block when the selection in an empty middle block', () => { + setModelData( model, modelList( [ + '* a', + ' []', + ' c' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' ', + ' []', + ' c' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another list item when the selection in an empty last block (two blocks in total)', () => { + setModelData( model, modelList( [ + '* a', + ' []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should create another list item when the selection in an empty last block (three blocks in total)', () => { + setModelData( model, modelList( [ + '* a', + ' b', + ' []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 2 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should create another list item when the selection in an empty last block (followed by a list item)', () => { + setModelData( model, modelList( [ + '* a', + ' b', + ' []', + '* ' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + '* [] {id:a00}', + '* ' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 2 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + } ); } ); - it( 'should convert an empty list item at the end of the list into a paragraph', () => { - setModelData( model, modelList( [ - '* a', - '* []' - ] ) ); + describe( 'non-collapsed selection', () => { + describe( 'with just one block per list item', () => { + it( 'should create another list item if the selection contains some content at the end of the list item', () => { + setModelData( model, modelList( [ + '* a[b]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another list item if the selection contains some content at the start of the list item', () => { + setModelData( model, modelList( [ + '* [a]b' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ', + '* []b {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content and turn off the list if slection contains all content at the zero indent level', () => { + setModelData( model, modelList( [ + '* [a', + '* b]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( splitCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content and move the selection when it contains some content at the zero indent level', () => { + setModelData( model, modelList( [ + '* a[b', + '* b]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( splitCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content when the selection contains all content at a deeper indent level', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' * [c', + ' * d]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + ' * []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + describe( 'cross-indent level selection', () => { + it( 'should clean the content and remove list across different indentation levels (list the only content)', () => { + setModelData( model, modelList( [ + '* [ab', + ' # cd]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (one level, entire blocks)', () => { + setModelData( model, modelList( [ + 'foo', + '* [ab', + ' # cd]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (one level, subset of blocks)', () => { + setModelData( model, modelList( [ + 'foo', + '* a[b', + ' # c]d' + ] ) ); - editor.editing.view.document.fire( 'enter', domEvtDataStub ); + view.document.fire( eventInfo, domEventData ); - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - '[]' - ] ) ); + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* a', + ' # []d' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (two levels, entire blocks)', () => { + setModelData( model, modelList( [ + '* [ab', + ' # cd', + ' * ef]', + ' * gh' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* []', + ' * gh {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (two levels, subset of blocks)', () => { + setModelData( model, modelList( [ + '* a[b', + ' # cd', + ' * e]f', + ' * gh' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' * []f {id:002}', + ' * gh {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (three levels, entire blocks)', () => { + setModelData( model, modelList( [ + 'foo', + '* [ab', + ' # cd', + ' * ef', + ' * gh]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content and remove list across different indentation levels ' + + '(three levels, list the only content)', () => { + setModelData( model, modelList( [ + '* [ab', + ' # cd', + ' * ef', + ' * gh]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (three levels, subset of blocks)', () => { + setModelData( model, modelList( [ + '* a[b', + ' # cd', + ' * ef', + ' # g]h', + ' * ij' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # []h {id:003}', + '* ij {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (one level, start at first, entire blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' # [cd', + ' * ef', + ' * gh]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (one level, start at first, part of blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' # c[d', + ' * ef', + ' * g]h' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # c', + ' * []h {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (level up then down, subset of blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' # c[d', + ' * ef', + ' # g]h' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # c', + ' # []h {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (level up then down, entire of blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' # [cd', + ' * ef', + ' # gh]', + '* ij' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # []', + '* ij {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (level up then down, preceded by an item)', () => { + setModelData( model, modelList( [ + '* ab', + ' # cd', + ' # [ef', + ' * gh', + ' # ij]', + '* kl' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # cd', + ' # []', + '* kl {id:005}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + } ); + } ); + + describe( 'with multiple blocks in a list item', () => { + it( 'should clean the selected content (partial blocks)', () => { + setModelData( model, modelList( [ + '* a[b', + ' c]d' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* []d {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks)', () => { + setModelData( model, modelList( [ + 'foo', + '* [ab', + ' cd]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire block, middle one)', () => { + setModelData( model, modelList( [ + '* ab', + ' [cd]', + ' ef' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' []', + ' ef' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks, starting from the second)', () => { + setModelData( model, modelList( [ + '* ab', + ' [cd', + ' ef]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + // Generally speaking, we'd rather expect something like this: + // * ab + // [] + // But there is no easy way to tell what the original selection looked like when it came to EnterCommand#afterExecute. + // Enter deletes all the content first [cd, ef] and in #afterExecute it looks like the original selection was: + // * ab + // [] + // and the algorithm falls back to splitting in this case. There's even a test for this kind of selection. + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (partial blocks, starting from the second)', () => { + setModelData( model, modelList( [ + '* ab', + ' c[d', + ' e]f' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' c', + ' []f' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks, three blocks in total)', () => { + setModelData( model, modelList( [ + '* [ab', + ' cd', + ' ef]', + '* gh' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* []', + '* gh {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks, across list items)', () => { + setModelData( model, modelList( [ + 'foo', + '* [ab', + ' cd', + ' ef', + '* gh]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks + a partial block, across list items)', () => { + setModelData( model, modelList( [ + '* [ab', + ' cd', + ' ef', + '* g]h' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ', + '* []h {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (partial blocks, across list items)', () => { + setModelData( model, modelList( [ + '* ab', + ' cd', + ' e[f', + '* g]h' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' cd', + ' e', + '* []h' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + describe( 'cross-indent level selection', () => { + it( 'should clean the selected content (partial blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' * cd', + ' e[f', + ' gh', + ' * i]j' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' * cd', + ' e', + ' * []j {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (partial blocks + entire block)', () => { + setModelData( model, modelList( [ + '* ab', + ' * cd', + ' e[f', + ' gh', + ' * ij]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' * cd', + ' e', + ' * [] {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (across two middle levels)', () => { + setModelData( model, modelList( [ + '* ab', + ' c[d', + ' * ef', + ' g]h', + ' * ij' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' c', + ' * []h', + ' * ij {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + } ); + } ); } ); } ); } ); diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistsplitcommand.js b/packages/ckeditor5-list/tests/documentlist/documentlistsplitcommand.js index 659f22abdab..25b2d41c836 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistsplitcommand.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistsplitcommand.js @@ -14,8 +14,8 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'DocumentListSplitCommand', () => { - let editor, command, model, doc; - // let changedBlocks; + let editor, command, model, doc, root; + let changedBlocks; testUtils.createSinonSandbox(); @@ -25,17 +25,17 @@ describe( 'DocumentListSplitCommand', () => { model = editor.model; doc = model.document; - - doc.createRoot(); + root = doc.createRoot(); model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); model.schema.register( 'blockQuote', { inheritAllFrom: '$container' } ); model.schema.extend( '$container', { allowAttributes: [ 'listType', 'listIndent', 'listItemId' ] } ); command = new DocumentListSplitCommand( editor ); - // command.on( 'afterExecute', ( evt, data ) => { - // changedBlocks = data; - // } ); + + command.on( 'afterExecute', ( evt, data ) => { + changedBlocks = data; + } ); stubUid(); } ); @@ -45,102 +45,109 @@ describe( 'DocumentListSplitCommand', () => { } ); describe( 'isEnabled', () => { - // TODO - } ); - - describe( 'execute()', () => { - it( 'should use parent batch', () => { - setData( model, '[0]' ); + it( 'should be false if selection is not in a list item', () => { + setData( model, modelList( [ + '[]' + ] ) ); - model.change( writer => { - expect( writer.batch.operations.length, 'before' ).to.equal( 0 ); + expect( command.isEnabled ).to.be.false; + } ); - command.execute(); + it( 'should be false if selection is not collapsed in a list item', () => { + setData( model, modelList( [ + '* a', + ' [b]' + ] ) ); - expect( writer.batch.operations.length, 'after' ).to.be.above( 0 ); - } ); + expect( command.isEnabled ).to.be.false; } ); - it( '0', () => { + it( 'should be false if selection is in the first block of a list item', () => { setData( model, modelList( [ '* a[]' ] ) ); - command.execute(); + expect( command.isEnabled ).to.be.false; + } ); - expect( getData( model ) ).to.equalMarkup( modelList( [ + it( 'should be true if selection is collapsed in a non-first block of a list item', () => { + setData( model, modelList( [ '* a', - '* []' + ' []' ] ) ); - // TODO changed blocks - } ); + expect( command.isEnabled ).to.be.true; - it( '1.-1', () => { setData( model, modelList( [ - '* a[]', - ' b', - ' c' + '* a', + ' b[]' ] ) ); - command.execute(); + expect( command.isEnabled ).to.be.true; - expect( getData( model ) ).to.equalMarkup( modelList( [ + setData( model, modelList( [ '* a', - ' []', - ' b', - ' c' + ' []b' ] ) ); - // TODO changed blocks - } ); + expect( command.isEnabled ).to.be.true; - it( '1.0', () => { setData( model, modelList( [ - '* []a', - ' b', - ' c' + '* a', + ' b[]c' ] ) ); - command.execute(); + expect( command.isEnabled ).to.be.true; - expect( getData( model ) ).to.equalMarkup( modelList( [ - '* ', - '* []a', - ' b', - ' c' + setData( model, modelList( [ + '* a', + ' b[]c', + ' d' ] ) ); - // TODO changed blocks + expect( command.isEnabled ).to.be.true; } ); + } ); - it( '1.1', () => { + describe( 'execute()', () => { + it( 'should use parent batch', () => { setData( model, modelList( [ '* a', - ' b', - ' []c' + ' []' + ] ) ); + + model.change( writer => { + expect( writer.batch.operations.length, 'before' ).to.equal( 0 ); + + command.execute(); + + expect( writer.batch.operations.length, 'after' ).to.be.above( 0 ); + } ); + } ); + + it( 'should create another list item when the selection in an empty last block (two blocks in total)', () => { + setData( model, modelList( [ + '* a', + ' []' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* a', - ' b', - ' ', - ' []c' + '* [] {id:a00}' ] ) ); - // TODO - // expect( changedBlocks.length ).to.equal( ... ); - // expect( changedBlocks ).to.deep.equal( [ - // ] ); + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 1 ) + ] ); } ); - it( '1.2', () => { + it( 'should create another list item when the selection in an empty last block (three blocks in total)', () => { setData( model, modelList( [ '* a', ' b', - ' c[]' + ' []' ] ) ); command.execute(); @@ -148,53 +155,65 @@ describe( 'DocumentListSplitCommand', () => { expect( getData( model ) ).to.equalMarkup( modelList( [ '* a', ' b', - ' c', - ' []' + '* [] {id:a00}' ] ) ); - // TODO changed blocks + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 2 ) + ] ); } ); - it( '3', () => { + it( 'should create another list item when the selection in an empty last block (followed by a list item)', () => { setData( model, modelList( [ '* a', + ' b', ' []', - ' c' + '* ' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* a', - ' ', - ' []', - ' c' + ' b', + '* [] {id:a00}', + '* ' ] ) ); - // TODO changed blocks + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 2 ) + ] ); } ); - it( '4', () => { + it( 'should create another list item in a nested structure (last block of the list item)', () => { setData( model, modelList( [ '* a', - ' []' + ' b', + ' * c', + ' []' ] ) ); command.execute(); expect( getData( model ) ).to.equalMarkup( modelList( [ '* a', - '* []' + ' b', + ' * c', + ' * [] {id:a00}' ] ) ); - // TODO changed blocks + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 3 ) + ] ); } ); - it( '5', () => { + it( 'should create another list item in a nested structure (middle block of the list item)', () => { setData( model, modelList( [ '* a', ' b', - ' []' + ' * c', + ' d[]', + ' e' ] ) ); command.execute(); @@ -202,18 +221,26 @@ describe( 'DocumentListSplitCommand', () => { expect( getData( model ) ).to.equalMarkup( modelList( [ '* a', ' b', - '* []' + ' * c', + ' * d[] {id:a00}', + ' e' ] ) ); - // TODO changed blocks + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 3 ), + root.getChild( 4 ) + ] ); } ); - it( '6', () => { + it( 'should create another list item in a nested structure (middle block of the list item, followed by list items)', () => { setData( model, modelList( [ '* a', ' b', - ' []', - '*' + ' * c', + ' d[]', + ' e', + ' * f', + '* g' ] ) ); command.execute(); @@ -221,11 +248,17 @@ describe( 'DocumentListSplitCommand', () => { expect( getData( model ) ).to.equalMarkup( modelList( [ '* a', ' b', - '* []', - '*' + ' * c', + ' * d[] {id:a00}', + ' e', + ' * f', + '* g' ] ) ); - // TODO changed blocks + expect( changedBlocks ).to.deep.equal( [ + root.getChild( 3 ), + root.getChild( 4 ) + ] ); } ); } ); } ); From 3500d839e899ad86a8ea07d4aeb15ee75affd48f Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 12 Jan 2022 12:03:32 +0100 Subject: [PATCH 03/10] Let\'s assume EnterCommand will always be present when DocumentListEditing is loaded. --- .../src/documentlist/documentlistediting.js | 49 +++++++++---------- .../tests/documentlist/documentlistediting.js | 4 +- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index 340513742c5..6297c4424ed 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -129,6 +129,7 @@ export default class DocumentListEditing extends Plugin { const indent = commands.get( 'indent' ); const outdent = commands.get( 'outdent' ); const enterCommand = commands.get( 'enter' ); + const splitCommand = commands.get( 'splitListItem' ); if ( indent ) { indent.registerChildCommand( commands.get( 'indentList' ) ); @@ -138,34 +139,30 @@ export default class DocumentListEditing extends Plugin { outdent.registerChildCommand( commands.get( 'outdentList' ) ); } - if ( enterCommand ) { - // In some cases, the integration with the enter key is done after the default handler in EnterCommand. - this.listenTo( enterCommand, 'afterExecute', () => { - const splitCommand = editor.commands.get( 'splitListItem' ); + // In some cases, the integration with the enter key is done after the default handler in EnterCommand. + this.listenTo( enterCommand, 'afterExecute', () => { + // The command has not refreshed because the change block related to EnterCommand#execute() is not over yet. + // Let's keep it up to date and take advantage of DocumentListSplitCommand#isEnabled. + splitCommand.refresh(); - // The commands has not refreshed because the change block related to EnterCommand#execute() is not over yet. - // Let's keep it up to date and take advantage of DocumentListSplitCommand#isEnabled. - splitCommand.refresh(); - - if ( !splitCommand.isEnabled ) { - return; - } + if ( !splitCommand.isEnabled ) { + return; + } - const doc = editor.model.document; - const positionParent = doc.selection.getLastPosition().parent; - const listItemBlocks = getAllListItemBlocks( positionParent ); - - // Keep in mind this split happens after the default enter handler was executed. For instance: - // - // │ Initial state │ After default enter │ Here in #afterExecute │ - // ├───────────────────────────┼───────────────────────────┼───────────────────────────┤ - // │ * a[] │ * a │ * a │ - // │ │ [] │ * [] │ - if ( listItemBlocks.length === 2 ) { - editor.execute( 'splitListItem' ); - } - } ); - } + const doc = editor.model.document; + const positionParent = doc.selection.getLastPosition().parent; + const listItemBlocks = getAllListItemBlocks( positionParent ); + + // Keep in mind this split happens after the default enter handler was executed. For instance: + // + // │ Initial state │ After default enter │ Here in #afterExecute │ + // ├───────────────────────────┼───────────────────────────┼───────────────────────────┤ + // │ * a[] │ * a │ * a │ + // │ │ [] │ * [] │ + if ( listItemBlocks.length === 2 ) { + editor.execute( 'splitListItem' ); + } + } ); } /** diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js index 228a79d6109..b64fb296923 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js @@ -15,10 +15,12 @@ import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import { getData as getModelData, parse as parseModel, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { DomEventData } from '@ckeditor/ckeditor5-engine'; import ListEditing from '../../src/list/listediting'; import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; @@ -26,8 +28,6 @@ import DocumentListSplitCommand from '../../src/documentlist/documentlistsplitco import stubUid from './_utils/uid'; import { prepareTest, modelList } from './_utils/utils'; -import { DomEventData } from '@ckeditor/ckeditor5-engine'; -import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; describe( 'DocumentListEditing', () => { let editor, model, modelDoc, modelRoot, view; From d6604f9fac6f061f2f4b8dc797fc500ed724c795 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 12 Jan 2022 12:08:58 +0100 Subject: [PATCH 04/10] Docs. --- .../src/documentlist/documentlistsplitcommand.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js b/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js index 3babb5b0075..1d6e9fe1184 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistsplitcommand.js @@ -15,7 +15,7 @@ import { } from './utils/model'; /** - * The document list split command that splits the list item at the first position of the selection. + * The document list split command that splits the list item at the selection. * * It is used by the {@link module:list/documentlist~DocumentList document list feature}. * @@ -30,7 +30,7 @@ export default class DocumentListSplitCommand extends Command { } /** - * Splits the list item at the first position of the selection. + * Splits the list item at the selection. * * @fires execute * @fires afterExecute From c5cc8dbf5fe9a3d364d3a963df81682d737b9592 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 12 Jan 2022 12:28:14 +0100 Subject: [PATCH 05/10] Tests: Moved clipboard and enter integration tests of DocumentListEditing to another file. --- .../documentlistediting-integrations.js | 1648 +++++++++++++++++ .../tests/documentlist/documentlistediting.js | 1589 +--------------- 2 files changed, 1649 insertions(+), 1588 deletions(-) create mode 100644 packages/ckeditor5-list/tests/documentlist/documentlistediting-integrations.js diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting-integrations.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting-integrations.js new file mode 100644 index 00000000000..bcbc1982642 --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting-integrations.js @@ -0,0 +1,1648 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import DocumentListEditing from '../../src/documentlist/documentlistediting'; + +import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting'; +import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; +import ClipboardPipeline from '@ckeditor/ckeditor5-clipboard/src/clipboardpipeline'; +import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; +import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting'; +import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { getData as getModelData, parse as parseModel, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { DomEventData } from '@ckeditor/ckeditor5-engine'; + +import stubUid from './_utils/uid'; +import { modelList } from './_utils/utils'; + +describe( 'DocumentListEditing integrations', () => { + let editor, model, modelDoc, modelRoot, view; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, ClipboardPipeline, BoldEditing, DocumentListEditing, UndoEditing, + BlockQuoteEditing, TableEditing, HeadingEditing ] + } ); + + model = editor.model; + modelDoc = model.document; + modelRoot = modelDoc.getRoot(); + + view = editor.editing.view; + + model.schema.extend( 'paragraph', { + allowAttributes: 'foo' + } ); + + model.schema.register( 'nonListable', { + allowWhere: '$block', + allowContentOf: '$block', + inheritTypesFrom: '$block', + allowAttributes: 'foo' + } ); + + editor.conversion.elementToElement( { model: 'nonListable', view: 'div' } ); + + // Stub `view.scrollToTheSelection` as it will fail on VirtualTestEditor without DOM. + sinon.stub( view, 'scrollToTheSelection' ).callsFake( () => {} ); + stubUid(); + } ); + + afterEach( async () => { + await editor.destroy(); + } ); + + describe( 'clipboard integration', () => { + describe( 'copy and getSelectedContent', () => { + it( 'should be able to downcast part of a nested list', () => { + setModelData( model, + 'A' + + '[B1' + + 'B2' + + 'C1]' + + 'C2' + ); + + const modelFragment = model.getSelectedContent( model.document.selection ); + const viewFragment = editor.data.toView( modelFragment ); + const data = editor.data.htmlProcessor.toData( viewFragment ); + + expect( data ).to.equal( + '
    ' + + '
  • ' + + '

    B1

    ' + + '

    B2

    ' + + '
      ' + + '
    • C1
    • ' + + '
    ' + + '
  • ' + + '
' + ); + } ); + + it( 'should be able to downcast part of a deep nested list', () => { + setModelData( model, + 'A' + + 'B1' + + 'B2' + + '[C1' + + 'C2]' + ); + + const modelFragment = model.getSelectedContent( model.document.selection ); + const viewFragment = editor.data.toView( modelFragment ); + const data = editor.data.htmlProcessor.toData( viewFragment ); + + expect( data ).to.equal( + '
    ' + + '
  • ' + + '

    C1

    ' + + '

    C2

    ' + + '
  • ' + + '
' + ); + } ); + } ); + + describe( 'paste and insertContent integration', () => { + it( 'should be triggered on DataController#insertContent()', () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + editor.model.insertContent( + parseModel( + 'X' + + 'Y', + model.schema + ) + ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'BX' + + 'Y[]' + + 'C' + ); + } ); + + it( 'should be triggered when selectable is passed', () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + model.insertContent( + parseModel( + 'X' + + 'Y', + model.schema + ), + model.createRange( + model.createPositionFromPath( modelRoot, [ 1, 1 ] ), + model.createPositionFromPath( modelRoot, [ 1, 1 ] ) + ) + ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'B[]X' + + 'Y' + + 'C' + ); + } ); + + // Just checking that it doesn't crash. #69 + it( 'should work if an element is passed to DataController#insertContent()', () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + model.change( writer => { + const paragraph = writer.createElement( 'paragraph', { listType: 'bulleted', listItemId: 'x', listIndent: '0' } ); + writer.insertText( 'X', paragraph ); + + model.insertContent( paragraph ); + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'BX[]' + + 'C' + ); + } ); + + // Just checking that it doesn't crash. #69 + it( 'should work if an element is passed to DataController#insertContent() - case #69', () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + model.change( writer => { + model.insertContent( writer.createText( 'X' ) ); + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'BX[]' + + 'C' + ); + } ); + + it( 'should fix indents of pasted list items', () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
  • X
    • Y
' ) + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'BX' + + 'Y[]' + + 'C' + ); + } ); + + it( 'should not fix indents of list items that are separated by non-list element', () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
  • W
    • X

Y

  • Z
' ) + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'BW' + + 'X' + + 'Y' + + 'Z[]' + + 'C' + ); + } ); + + it( 'should co-work correctly with post fixer', () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '

X

  • Y
' ) + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'BX' + + 'Y[]' + + 'C' + ); + } ); + + it( 'should work if items are pasted between paragraph elements', () => { + // Wrap all changes in one block to avoid post-fixing the selection + // (which may be incorret) in the meantime. + model.change( () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
  • X
    • Y
' ) + } ); + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'B' + + 'X' + + 'Y[]' + + 'C' + ); + } ); + + it( 'should create correct model when list items are pasted in top-level list', () => { + setModelData( model, + 'A[]' + + 'B' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
  • X
    • Y
' ) + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'AX' + + 'Y[]' + + 'B' + ); + } ); + + it( 'should create correct model when list items are pasted in non-list context', () => { + setModelData( model, + 'A[]' + + 'B' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
  • X
    • Y
' ) + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'AX' + + 'Y[]' + + 'B' + ); + } ); + + it( 'should not crash when "empty content" is inserted', () => { + setModelData( model, '[]' ); + + expect( () => { + model.change( writer => { + editor.model.insertContent( writer.createDocumentFragment() ); + } ); + } ).not.to.throw(); + } ); + + it( 'should correctly handle item that is pasted without its parent', () => { + // Wrap all changes in one block to avoid post-fixing the selection + // (which may be incorret) in the meantime. + model.change( () => { + setModelData( model, + 'Foo' + + 'A' + + 'B' + + '[]' + + 'Bar' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
  • X
  • ' ) + } ); + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'Foo' + + 'A' + + 'B' + + 'X[]' + + 'Bar' + ); + } ); + + it( 'should correctly handle item that is pasted without its parent #2', () => { + // Wrap all changes in one block to avoid post-fixing the selection + // (which may be incorret) in the meantime. + model.change( () => { + setModelData( model, + 'Foo' + + 'A' + + 'B' + + '[]' + + 'Bar' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
  • X
    • Y
  • ' ) + } ); + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'Foo' + + 'A' + + 'B' + + 'X' + + 'Y[]' + + 'Bar' + ); + } ); + + it( 'should handle block elements inside pasted list #1', () => { + setModelData( model, + 'A' + + 'B[]' + + 'C' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
    • W
      • X

        Y

        Z
    ' ) + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'A' + + 'BW' + + 'X' + + 'Y' + + 'Z[]' + + 'C' + ); + } ); + + it( 'should handle block elements inside pasted list #2', () => { + setModelData( model, + 'A[]' + + 'B' + + 'C' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
    • W
      • X

        Y

        Z
    ' ) + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'AW' + + 'X' + + 'Y' + + 'Z[]' + + 'B' + + 'C' + ); + } ); + + it( 'should handle block elements inside pasted list #3', () => { + setModelData( model, + 'A[]' + + 'B' + + 'C' + ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
    • W

      X

      Y

    • Z
    ' ) + } ); + + expect( getModelData( model ) ).to.equalMarkup( + 'AW' + + 'X' + + 'Y' + + 'Z[]' + + 'B' + + 'C' + ); + } ); + + it( 'should properly handle split of list items with non-standard converters', () => { + setModelData( model, + 'A[]' + + 'B' + + 'C' + ); + + editor.model.schema.register( 'splitBlock', { allowWhere: '$block' } ); + + editor.conversion.for( 'downcast' ).elementToElement( { model: 'splitBlock', view: 'splitBlock' } ); + editor.conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:splitBlock', ( evt, data, conversionApi ) => { + const splitBlock = conversionApi.writer.createElement( 'splitBlock' ); + + conversionApi.consumable.consume( data.viewItem, { name: true } ); + conversionApi.safeInsert( splitBlock, data.modelCursor ); + conversionApi.updateConversionResult( splitBlock, data ); + } ) ); + + const clipboard = editor.plugins.get( 'ClipboardPipeline' ); + + clipboard.fire( 'inputTransformation', { + content: parseView( '
    • ab
    ' ) + } ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( + 'Aa' + + '' + + 'b' + + 'B' + + 'C' + ); + } ); + } ); + } ); + + describe( 'enter key handling', () => { + const changedBlocks = []; + let domEventData, splitCommand, indentCommand, eventInfo, splitCommandExecuteSpy, outdentCommandExecuteSpy; + + beforeEach( () => { + eventInfo = new EventInfo( view.document, 'enter' ); + domEventData = new DomEventData( view.document, { + preventDefault: sinon.spy() + } ); + + splitCommand = editor.commands.get( 'splitListItem' ); + indentCommand = editor.commands.get( 'outdentList' ); + + splitCommandExecuteSpy = sinon.spy( splitCommand, 'execute' ); + outdentCommandExecuteSpy = sinon.spy( indentCommand, 'execute' ); + + changedBlocks.length = 0; + + splitCommand.on( 'afterExecute', ( evt, data ) => { + changedBlocks.push( ...data ); + } ); + + indentCommand.on( 'afterExecute', ( evt, data ) => { + changedBlocks.push( ...data ); + } ); + } ); + + describe( 'collapsed selection', () => { + describe( 'with just one block per list item', () => { + it( 'should outdent if the slection in the only empty list item (convert into paragraph and turn of the list)', () => { + setModelData( model, modelList( [ + '* []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 0 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should outdent if the slection in the last empty list item (convert the item into paragraph)', () => { + setModelData( model, modelList( [ + '* a', + '* []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should create another list item when the selection in a non-empty only list item', () => { + setModelData( model, modelList( [ + '* a[]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should outdent if the selection in an empty, last sub-list item', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' * c', + ' * []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + ' * c', + ' # []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 3 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + } ); + + describe( 'with multiple blocks in a list item', () => { + it( 'should outdent if the selection is anchored in an empty, last item block', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' # []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 2 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should outdent if the selection is anchored in an empty, only sub-item block', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' * []', + ' #' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + ' # []', + ' #' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 2 ) + ] ); + + sinon.assert.calledOnce( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should create another block when the selection at the start of a non-empty first block', () => { + setModelData( model, modelList( [ + '* a[]', + ' b', + ' c' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' []', + ' b', + ' c' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another block when the selection at the end of a non-empty first block', () => { + setModelData( model, modelList( [ + '* []a', + ' b', + ' c' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ', + ' []a', + ' b', + ' c' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another block when the selection at the start of a non-empty last block', () => { + setModelData( model, modelList( [ + '* a', + ' b', + ' []c' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + ' ', + ' []c' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another block when the selection at the end of a non-empty last block', () => { + setModelData( model, modelList( [ + '* a', + ' b', + ' c[]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + ' c', + ' []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another block when the selection in an empty middle block', () => { + setModelData( model, modelList( [ + '* a', + ' []', + ' c' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' ', + ' []', + ' c' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another list item when the selection in an empty last block (two blocks in total)', () => { + setModelData( model, modelList( [ + '* a', + ' []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should create another list item when the selection in an empty last block (three blocks in total)', () => { + setModelData( model, modelList( [ + '* a', + ' b', + ' []' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 2 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + + it( 'should create another list item when the selection in an empty last block (followed by a list item)', () => { + setModelData( model, modelList( [ + '* a', + ' b', + ' []', + '* ' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' b', + '* [] {id:a00}', + '* ' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 2 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.true; + } ); + } ); + } ); + + describe( 'non-collapsed selection', () => { + describe( 'with just one block per list item', () => { + it( 'should create another list item if the selection contains some content at the end of the list item', () => { + setModelData( model, modelList( [ + '* a[b]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should create another list item if the selection contains some content at the start of the list item', () => { + setModelData( model, modelList( [ + '* [a]b' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ', + '* []b {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content and turn off the list if slection contains all content at the zero indent level', () => { + setModelData( model, modelList( [ + '* [a', + '* b]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( splitCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content and move the selection when it contains some content at the zero indent level', () => { + setModelData( model, modelList( [ + '* a[b', + '* b]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( splitCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content when the selection contains all content at a deeper indent level', () => { + setModelData( model, modelList( [ + '* a', + ' # b', + ' * [c', + ' * d]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # b', + ' * []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + describe( 'cross-indent level selection', () => { + it( 'should clean the content and remove list across different indentation levels (list the only content)', () => { + setModelData( model, modelList( [ + '* [ab', + ' # cd]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (one level, entire blocks)', () => { + setModelData( model, modelList( [ + 'foo', + '* [ab', + ' # cd]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (one level, subset of blocks)', () => { + setModelData( model, modelList( [ + 'foo', + '* a[b', + ' # c]d' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* a', + ' # []d' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (two levels, entire blocks)', () => { + setModelData( model, modelList( [ + '* [ab', + ' # cd', + ' * ef]', + ' * gh' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* []', + ' * gh {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (two levels, subset of blocks)', () => { + setModelData( model, modelList( [ + '* a[b', + ' # cd', + ' * e]f', + ' * gh' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' * []f {id:002}', + ' * gh {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (three levels, entire blocks)', () => { + setModelData( model, modelList( [ + 'foo', + '* [ab', + ' # cd', + ' * ef', + ' * gh]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content and remove list across different indentation levels ' + + '(three levels, list the only content)', () => { + setModelData( model, modelList( [ + '* [ab', + ' # cd', + ' * ef', + ' * gh]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '[]' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (three levels, subset of blocks)', () => { + setModelData( model, modelList( [ + '* a[b', + ' # cd', + ' * ef', + ' # g]h', + ' * ij' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + ' # []h {id:003}', + '* ij {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (one level, start at first, entire blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' # [cd', + ' * ef', + ' * gh]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (one level, start at first, part of blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' # c[d', + ' * ef', + ' * g]h' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # c', + ' * []h {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (level up then down, subset of blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' # c[d', + ' * ef', + ' # g]h' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # c', + ' # []h {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (level up then down, entire of blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' # [cd', + ' * ef', + ' # gh]', + '* ij' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # []', + '* ij {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the content across different indentation levels (level up then down, preceded by an item)', () => { + setModelData( model, modelList( [ + '* ab', + ' # cd', + ' # [ef', + ' * gh', + ' # ij]', + '* kl' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' # cd', + ' # []', + '* kl {id:005}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + } ); + } ); + + describe( 'with multiple blocks in a list item', () => { + it( 'should clean the selected content (partial blocks)', () => { + setModelData( model, modelList( [ + '* a[b', + ' c]d' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* a', + '* []d {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks)', () => { + setModelData( model, modelList( [ + 'foo', + '* [ab', + ' cd]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire block, middle one)', () => { + setModelData( model, modelList( [ + '* ab', + ' [cd]', + ' ef' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' []', + ' ef' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks, starting from the second)', () => { + setModelData( model, modelList( [ + '* ab', + ' [cd', + ' ef]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + // Generally speaking, we'd rather expect something like this: + // * ab + // [] + // But there is no easy way to tell what the original selection looked like when it came to EnterCommand#afterExecute. + // Enter deletes all the content first [cd, ef] and in #afterExecute it looks like the original selection was: + // * ab + // [] + // and the algorithm falls back to splitting in this case. There's even a test for this kind of selection. + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + '* [] {id:a00}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [ + modelRoot.getChild( 1 ) + ] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.calledOnce( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (partial blocks, starting from the second)', () => { + setModelData( model, modelList( [ + '* ab', + ' c[d', + ' e]f' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' c', + ' []f' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks, three blocks in total)', () => { + setModelData( model, modelList( [ + '* [ab', + ' cd', + ' ef]', + '* gh' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* []', + '* gh {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks, across list items)', () => { + setModelData( model, modelList( [ + 'foo', + '* [ab', + ' cd', + ' ef', + '* gh]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + 'foo', + '* []' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (entire blocks + a partial block, across list items)', () => { + setModelData( model, modelList( [ + '* [ab', + ' cd', + ' ef', + '* g]h' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ', + '* []h {id:003}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (partial blocks, across list items)', () => { + setModelData( model, modelList( [ + '* ab', + ' cd', + ' e[f', + '* g]h' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' cd', + ' e', + '* []h' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + describe( 'cross-indent level selection', () => { + it( 'should clean the selected content (partial blocks)', () => { + setModelData( model, modelList( [ + '* ab', + ' * cd', + ' e[f', + ' gh', + ' * i]j' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' * cd', + ' e', + ' * []j {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (partial blocks + entire block)', () => { + setModelData( model, modelList( [ + '* ab', + ' * cd', + ' e[f', + ' gh', + ' * ij]' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' * cd', + ' e', + ' * [] {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + + it( 'should clean the selected content (across two middle levels)', () => { + setModelData( model, modelList( [ + '* ab', + ' c[d', + ' * ef', + ' g]h', + ' * ij' + ] ) ); + + view.document.fire( eventInfo, domEventData ); + + expect( getModelData( model ) ).to.equalMarkup( modelList( [ + '* ab', + ' c', + ' * []h', + ' * ij {id:004}' + ] ) ); + + expect( changedBlocks ).to.deep.equal( [] ); + + sinon.assert.notCalled( outdentCommandExecuteSpy ); + sinon.assert.notCalled( splitCommandExecuteSpy ); + + sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); + expect( eventInfo.stop.called ).to.be.undefined; + } ); + } ); + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js index b64fb296923..af10603e76f 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting.js @@ -15,19 +15,16 @@ import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import { getData as getModelData, parse as parseModel, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -import { DomEventData } from '@ckeditor/ckeditor5-engine'; import ListEditing from '../../src/list/listediting'; import DocumentListIndentCommand from '../../src/documentlist/documentlistindentcommand'; import DocumentListSplitCommand from '../../src/documentlist/documentlistsplitcommand'; import stubUid from './_utils/uid'; -import { prepareTest, modelList } from './_utils/utils'; +import { prepareTest } from './_utils/utils'; describe( 'DocumentListEditing', () => { let editor, model, modelDoc, modelRoot, view; @@ -714,1588 +711,4 @@ describe( 'DocumentListEditing', () => { } ); } ); } ); - - describe( 'clipboard integration', () => { - describe( 'copy and getSelectedContent', () => { - it( 'should be able to downcast part of a nested list', () => { - setModelData( model, - 'A' + - '[B1' + - 'B2' + - 'C1]' + - 'C2' - ); - - const modelFragment = model.getSelectedContent( model.document.selection ); - const viewFragment = editor.data.toView( modelFragment ); - const data = editor.data.htmlProcessor.toData( viewFragment ); - - expect( data ).to.equal( - '
      ' + - '
    • ' + - '

      B1

      ' + - '

      B2

      ' + - '
        ' + - '
      • C1
      • ' + - '
      ' + - '
    • ' + - '
    ' - ); - } ); - - it( 'should be able to downcast part of a deep nested list', () => { - setModelData( model, - 'A' + - 'B1' + - 'B2' + - '[C1' + - 'C2]' - ); - - const modelFragment = model.getSelectedContent( model.document.selection ); - const viewFragment = editor.data.toView( modelFragment ); - const data = editor.data.htmlProcessor.toData( viewFragment ); - - expect( data ).to.equal( - '
      ' + - '
    • ' + - '

      C1

      ' + - '

      C2

      ' + - '
    • ' + - '
    ' - ); - } ); - } ); - - describe( 'paste and insertContent integration', () => { - it( 'should be triggered on DataController#insertContent()', () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - editor.model.insertContent( - parseModel( - 'X' + - 'Y', - model.schema - ) - ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'BX' + - 'Y[]' + - 'C' - ); - } ); - - it( 'should be triggered when selectable is passed', () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - model.insertContent( - parseModel( - 'X' + - 'Y', - model.schema - ), - model.createRange( - model.createPositionFromPath( modelRoot, [ 1, 1 ] ), - model.createPositionFromPath( modelRoot, [ 1, 1 ] ) - ) - ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'B[]X' + - 'Y' + - 'C' - ); - } ); - - // Just checking that it doesn't crash. #69 - it( 'should work if an element is passed to DataController#insertContent()', () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - model.change( writer => { - const paragraph = writer.createElement( 'paragraph', { listType: 'bulleted', listItemId: 'x', listIndent: '0' } ); - writer.insertText( 'X', paragraph ); - - model.insertContent( paragraph ); - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'BX[]' + - 'C' - ); - } ); - - // Just checking that it doesn't crash. #69 - it( 'should work if an element is passed to DataController#insertContent() - case #69', () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - model.change( writer => { - model.insertContent( writer.createText( 'X' ) ); - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'BX[]' + - 'C' - ); - } ); - - it( 'should fix indents of pasted list items', () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • X
      • Y
    ' ) - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'BX' + - 'Y[]' + - 'C' - ); - } ); - - it( 'should not fix indents of list items that are separated by non-list element', () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • W
      • X

    Y

    • Z
    ' ) - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'BW' + - 'X' + - 'Y' + - 'Z[]' + - 'C' - ); - } ); - - it( 'should co-work correctly with post fixer', () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '

    X

    • Y
    ' ) - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'BX' + - 'Y[]' + - 'C' - ); - } ); - - it( 'should work if items are pasted between paragraph elements', () => { - // Wrap all changes in one block to avoid post-fixing the selection - // (which may be incorret) in the meantime. - model.change( () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • X
      • Y
    ' ) - } ); - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'B' + - 'X' + - 'Y[]' + - 'C' - ); - } ); - - it( 'should create correct model when list items are pasted in top-level list', () => { - setModelData( model, - 'A[]' + - 'B' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • X
      • Y
    ' ) - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'AX' + - 'Y[]' + - 'B' - ); - } ); - - it( 'should create correct model when list items are pasted in non-list context', () => { - setModelData( model, - 'A[]' + - 'B' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • X
      • Y
    ' ) - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'AX' + - 'Y[]' + - 'B' - ); - } ); - - it( 'should not crash when "empty content" is inserted', () => { - setModelData( model, '[]' ); - - expect( () => { - model.change( writer => { - editor.model.insertContent( writer.createDocumentFragment() ); - } ); - } ).not.to.throw(); - } ); - - it( 'should correctly handle item that is pasted without its parent', () => { - // Wrap all changes in one block to avoid post-fixing the selection - // (which may be incorret) in the meantime. - model.change( () => { - setModelData( model, - 'Foo' + - 'A' + - 'B' + - '[]' + - 'Bar' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
  • X
  • ' ) - } ); - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'Foo' + - 'A' + - 'B' + - 'X[]' + - 'Bar' - ); - } ); - - it( 'should correctly handle item that is pasted without its parent #2', () => { - // Wrap all changes in one block to avoid post-fixing the selection - // (which may be incorret) in the meantime. - model.change( () => { - setModelData( model, - 'Foo' + - 'A' + - 'B' + - '[]' + - 'Bar' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
  • X
    • Y
  • ' ) - } ); - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'Foo' + - 'A' + - 'B' + - 'X' + - 'Y[]' + - 'Bar' - ); - } ); - - it( 'should handle block elements inside pasted list #1', () => { - setModelData( model, - 'A' + - 'B[]' + - 'C' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • W
      • X

        Y

        Z
    ' ) - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'A' + - 'BW' + - 'X' + - 'Y' + - 'Z[]' + - 'C' - ); - } ); - - it( 'should handle block elements inside pasted list #2', () => { - setModelData( model, - 'A[]' + - 'B' + - 'C' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • W
      • X

        Y

        Z
    ' ) - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'AW' + - 'X' + - 'Y' + - 'Z[]' + - 'B' + - 'C' - ); - } ); - - it( 'should handle block elements inside pasted list #3', () => { - setModelData( model, - 'A[]' + - 'B' + - 'C' - ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • W

      X

      Y

    • Z
    ' ) - } ); - - expect( getModelData( model ) ).to.equalMarkup( - 'AW' + - 'X' + - 'Y' + - 'Z[]' + - 'B' + - 'C' - ); - } ); - - it( 'should properly handle split of list items with non-standard converters', () => { - setModelData( model, - 'A[]' + - 'B' + - 'C' - ); - - editor.model.schema.register( 'splitBlock', { allowWhere: '$block' } ); - - editor.conversion.for( 'downcast' ).elementToElement( { model: 'splitBlock', view: 'splitBlock' } ); - editor.conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:splitBlock', ( evt, data, conversionApi ) => { - const splitBlock = conversionApi.writer.createElement( 'splitBlock' ); - - conversionApi.consumable.consume( data.viewItem, { name: true } ); - conversionApi.safeInsert( splitBlock, data.modelCursor ); - conversionApi.updateConversionResult( splitBlock, data ); - } ) ); - - const clipboard = editor.plugins.get( 'ClipboardPipeline' ); - - clipboard.fire( 'inputTransformation', { - content: parseView( '
    • ab
    ' ) - } ); - - expect( getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( - 'Aa' + - '' + - 'b' + - 'B' + - 'C' - ); - } ); - } ); - } ); - - describe( 'enter key handling', () => { - const changedBlocks = []; - let domEventData, splitCommand, indentCommand, eventInfo, splitCommandExecuteSpy, outdentCommandExecuteSpy; - - beforeEach( () => { - eventInfo = new EventInfo( view.document, 'enter' ); - domEventData = new DomEventData( view.document, { - preventDefault: sinon.spy() - } ); - - splitCommand = editor.commands.get( 'splitListItem' ); - indentCommand = editor.commands.get( 'outdentList' ); - - splitCommandExecuteSpy = sinon.spy( splitCommand, 'execute' ); - outdentCommandExecuteSpy = sinon.spy( indentCommand, 'execute' ); - - changedBlocks.length = 0; - - splitCommand.on( 'afterExecute', ( evt, data ) => { - changedBlocks.push( ...data ); - } ); - - indentCommand.on( 'afterExecute', ( evt, data ) => { - changedBlocks.push( ...data ); - } ); - } ); - - describe( 'collapsed selection', () => { - describe( 'with just one block per list item', () => { - it( 'should outdent if the slection in the only empty list item (convert into paragraph and turn of the list)', () => { - setModelData( model, modelList( [ - '* []' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '[]' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 0 ) - ] ); - - sinon.assert.calledOnce( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.true; - } ); - - it( 'should outdent if the slection in the last empty list item (convert the item into paragraph)', () => { - setModelData( model, modelList( [ - '* a', - '* []' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - '[]' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 1 ) - ] ); - - sinon.assert.calledOnce( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.true; - } ); - - it( 'should create another list item when the selection in a non-empty only list item', () => { - setModelData( model, modelList( [ - '* a[]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - '* [] {id:a00}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 1 ) - ] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.calledOnce( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should outdent if the selection in an empty, last sub-list item', () => { - setModelData( model, modelList( [ - '* a', - ' # b', - ' * c', - ' * []' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' # b', - ' * c', - ' # []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 3 ) - ] ); - - sinon.assert.calledOnce( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.true; - } ); - } ); - - describe( 'with multiple blocks in a list item', () => { - it( 'should outdent if the selection is anchored in an empty, last item block', () => { - setModelData( model, modelList( [ - '* a', - ' # b', - ' # []' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' # b', - '* []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 2 ) - ] ); - - sinon.assert.calledOnce( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.true; - } ); - - it( 'should outdent if the selection is anchored in an empty, only sub-item block', () => { - setModelData( model, modelList( [ - '* a', - ' # b', - ' * []', - ' #' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' # b', - ' # []', - ' #' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 2 ) - ] ); - - sinon.assert.calledOnce( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.true; - } ); - - it( 'should create another block when the selection at the start of a non-empty first block', () => { - setModelData( model, modelList( [ - '* a[]', - ' b', - ' c' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' []', - ' b', - ' c' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should create another block when the selection at the end of a non-empty first block', () => { - setModelData( model, modelList( [ - '* []a', - ' b', - ' c' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ', - ' []a', - ' b', - ' c' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should create another block when the selection at the start of a non-empty last block', () => { - setModelData( model, modelList( [ - '* a', - ' b', - ' []c' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' b', - ' ', - ' []c' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should create another block when the selection at the end of a non-empty last block', () => { - setModelData( model, modelList( [ - '* a', - ' b', - ' c[]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' b', - ' c', - ' []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should create another block when the selection in an empty middle block', () => { - setModelData( model, modelList( [ - '* a', - ' []', - ' c' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' ', - ' []', - ' c' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should create another list item when the selection in an empty last block (two blocks in total)', () => { - setModelData( model, modelList( [ - '* a', - ' []' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - '* [] {id:a00}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 1 ) - ] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.calledOnce( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.true; - } ); - - it( 'should create another list item when the selection in an empty last block (three blocks in total)', () => { - setModelData( model, modelList( [ - '* a', - ' b', - ' []' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' b', - '* [] {id:a00}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 2 ) - ] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.calledOnce( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.true; - } ); - - it( 'should create another list item when the selection in an empty last block (followed by a list item)', () => { - setModelData( model, modelList( [ - '* a', - ' b', - ' []', - '* ' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' b', - '* [] {id:a00}', - '* ' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 2 ) - ] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.calledOnce( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.true; - } ); - } ); - } ); - - describe( 'non-collapsed selection', () => { - describe( 'with just one block per list item', () => { - it( 'should create another list item if the selection contains some content at the end of the list item', () => { - setModelData( model, modelList( [ - '* a[b]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - '* [] {id:a00}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 1 ) - ] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.calledOnce( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should create another list item if the selection contains some content at the start of the list item', () => { - setModelData( model, modelList( [ - '* [a]b' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ', - '* []b {id:a00}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 1 ) - ] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.calledOnce( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content and turn off the list if slection contains all content at the zero indent level', () => { - setModelData( model, modelList( [ - '* [a', - '* b]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '[]' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( splitCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content and move the selection when it contains some content at the zero indent level', () => { - setModelData( model, modelList( [ - '* a[b', - '* b]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - '* []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( splitCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content when the selection contains all content at a deeper indent level', () => { - setModelData( model, modelList( [ - '* a', - ' # b', - ' * [c', - ' * d]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' # b', - ' * []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - describe( 'cross-indent level selection', () => { - it( 'should clean the content and remove list across different indentation levels (list the only content)', () => { - setModelData( model, modelList( [ - '* [ab', - ' # cd]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '[]' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (one level, entire blocks)', () => { - setModelData( model, modelList( [ - 'foo', - '* [ab', - ' # cd]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - 'foo', - '* []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (one level, subset of blocks)', () => { - setModelData( model, modelList( [ - 'foo', - '* a[b', - ' # c]d' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - 'foo', - '* a', - ' # []d' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (two levels, entire blocks)', () => { - setModelData( model, modelList( [ - '* [ab', - ' # cd', - ' * ef]', - ' * gh' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* []', - ' * gh {id:003}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (two levels, subset of blocks)', () => { - setModelData( model, modelList( [ - '* a[b', - ' # cd', - ' * e]f', - ' * gh' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' * []f {id:002}', - ' * gh {id:003}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (three levels, entire blocks)', () => { - setModelData( model, modelList( [ - 'foo', - '* [ab', - ' # cd', - ' * ef', - ' * gh]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - 'foo', - '* []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content and remove list across different indentation levels ' + - '(three levels, list the only content)', () => { - setModelData( model, modelList( [ - '* [ab', - ' # cd', - ' * ef', - ' * gh]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '[]' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (three levels, subset of blocks)', () => { - setModelData( model, modelList( [ - '* a[b', - ' # cd', - ' * ef', - ' # g]h', - ' * ij' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - ' # []h {id:003}', - '* ij {id:004}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (one level, start at first, entire blocks)', () => { - setModelData( model, modelList( [ - '* ab', - ' # [cd', - ' * ef', - ' * gh]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' # []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (one level, start at first, part of blocks)', () => { - setModelData( model, modelList( [ - '* ab', - ' # c[d', - ' * ef', - ' * g]h' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' # c', - ' * []h {id:003}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (level up then down, subset of blocks)', () => { - setModelData( model, modelList( [ - '* ab', - ' # c[d', - ' * ef', - ' # g]h' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' # c', - ' # []h {id:003}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (level up then down, entire of blocks)', () => { - setModelData( model, modelList( [ - '* ab', - ' # [cd', - ' * ef', - ' # gh]', - '* ij' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' # []', - '* ij {id:004}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the content across different indentation levels (level up then down, preceded by an item)', () => { - setModelData( model, modelList( [ - '* ab', - ' # cd', - ' # [ef', - ' * gh', - ' # ij]', - '* kl' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' # cd', - ' # []', - '* kl {id:005}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - } ); - } ); - - describe( 'with multiple blocks in a list item', () => { - it( 'should clean the selected content (partial blocks)', () => { - setModelData( model, modelList( [ - '* a[b', - ' c]d' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* a', - '* []d {id:a00}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 1 ) - ] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.calledOnce( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (entire blocks)', () => { - setModelData( model, modelList( [ - 'foo', - '* [ab', - ' cd]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - 'foo', - '* []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (entire block, middle one)', () => { - setModelData( model, modelList( [ - '* ab', - ' [cd]', - ' ef' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' []', - ' ef' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (entire blocks, starting from the second)', () => { - setModelData( model, modelList( [ - '* ab', - ' [cd', - ' ef]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - // Generally speaking, we'd rather expect something like this: - // * ab - // [] - // But there is no easy way to tell what the original selection looked like when it came to EnterCommand#afterExecute. - // Enter deletes all the content first [cd, ef] and in #afterExecute it looks like the original selection was: - // * ab - // [] - // and the algorithm falls back to splitting in this case. There's even a test for this kind of selection. - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - '* [] {id:a00}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [ - modelRoot.getChild( 1 ) - ] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.calledOnce( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (partial blocks, starting from the second)', () => { - setModelData( model, modelList( [ - '* ab', - ' c[d', - ' e]f' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' c', - ' []f' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (entire blocks, three blocks in total)', () => { - setModelData( model, modelList( [ - '* [ab', - ' cd', - ' ef]', - '* gh' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* []', - '* gh {id:003}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (entire blocks, across list items)', () => { - setModelData( model, modelList( [ - 'foo', - '* [ab', - ' cd', - ' ef', - '* gh]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - 'foo', - '* []' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (entire blocks + a partial block, across list items)', () => { - setModelData( model, modelList( [ - '* [ab', - ' cd', - ' ef', - '* g]h' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ', - '* []h {id:003}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (partial blocks, across list items)', () => { - setModelData( model, modelList( [ - '* ab', - ' cd', - ' e[f', - '* g]h' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' cd', - ' e', - '* []h' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - describe( 'cross-indent level selection', () => { - it( 'should clean the selected content (partial blocks)', () => { - setModelData( model, modelList( [ - '* ab', - ' * cd', - ' e[f', - ' gh', - ' * i]j' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' * cd', - ' e', - ' * []j {id:004}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (partial blocks + entire block)', () => { - setModelData( model, modelList( [ - '* ab', - ' * cd', - ' e[f', - ' gh', - ' * ij]' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' * cd', - ' e', - ' * [] {id:004}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - - it( 'should clean the selected content (across two middle levels)', () => { - setModelData( model, modelList( [ - '* ab', - ' c[d', - ' * ef', - ' g]h', - ' * ij' - ] ) ); - - view.document.fire( eventInfo, domEventData ); - - expect( getModelData( model ) ).to.equalMarkup( modelList( [ - '* ab', - ' c', - ' * []h', - ' * ij {id:004}' - ] ) ); - - expect( changedBlocks ).to.deep.equal( [] ); - - sinon.assert.notCalled( outdentCommandExecuteSpy ); - sinon.assert.notCalled( splitCommandExecuteSpy ); - - sinon.assert.calledOnce( domEventData.domEvent.preventDefault ); - expect( eventInfo.stop.called ).to.be.undefined; - } ); - } ); - } ); - } ); - } ); } ); From 83eb4573b014e5d1e5fd6011a3b9688fb9e3f9e7 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 12 Jan 2022 12:37:41 +0100 Subject: [PATCH 06/10] Docs. --- packages/ckeditor5-list/src/documentlist/documentlistediting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index 6297c4424ed..e1db92743cf 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -93,7 +93,7 @@ export default class DocumentListEditing extends Plugin { editor.commands.add( 'splitListItem', new DocumentListSplitCommand( editor ) ); - // Overwrite default Enter key behavior and outdent the list. + // Overwrite the default Enter key behavior: outdent or split the list in certain cases. this.listenTo( editor.editing.view.document, 'enter', ( evt, data ) => { const doc = model.document; const positionParent = doc.selection.getFirstPosition().parent; From df97642d7aed6f09d9faed36012d6d91625d2927 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 12 Jan 2022 12:50:51 +0100 Subject: [PATCH 07/10] Docs: Moved the EnterCommand#afterExecute listener in DocumentListEditing to init() because Enter is required by DocumentListEditing. --- .../src/documentlist/documentlistediting.js | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index e1db92743cf..7d4a4412db2 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -63,6 +63,8 @@ export default class DocumentListEditing extends Plugin { init() { const editor = this.editor; const model = editor.model; + const commands = editor.commands; + const enterCommand = commands.get( 'enter' ); if ( editor.plugins.has( 'ListEditing' ) ) { /** @@ -117,30 +119,11 @@ export default class DocumentListEditing extends Plugin { } } }, { context: 'li' } ); - } - - /** - * @inheritDoc - */ - afterInit() { - const editor = this.editor; - const commands = editor.commands; - - const indent = commands.get( 'indent' ); - const outdent = commands.get( 'outdent' ); - const enterCommand = commands.get( 'enter' ); - const splitCommand = commands.get( 'splitListItem' ); - - if ( indent ) { - indent.registerChildCommand( commands.get( 'indentList' ) ); - } - - if ( outdent ) { - outdent.registerChildCommand( commands.get( 'outdentList' ) ); - } // In some cases, the integration with the enter key is done after the default handler in EnterCommand. this.listenTo( enterCommand, 'afterExecute', () => { + const splitCommand = commands.get( 'splitListItem' ); + // The command has not refreshed because the change block related to EnterCommand#execute() is not over yet. // Let's keep it up to date and take advantage of DocumentListSplitCommand#isEnabled. splitCommand.refresh(); @@ -165,6 +148,24 @@ export default class DocumentListEditing extends Plugin { } ); } + /** + * @inheritDoc + */ + afterInit() { + const editor = this.editor; + const commands = editor.commands; + const indent = commands.get( 'indent' ); + const outdent = commands.get( 'outdent' ); + + if ( indent ) { + indent.registerChildCommand( commands.get( 'indentList' ) ); + } + + if ( outdent ) { + outdent.registerChildCommand( commands.get( 'outdentList' ) ); + } + } + /** * Registers the conversion helpers for the document-list feature. * @private From d3c20e6e1f36f6b66defe45b3e434581e8b6b9fe Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Wed, 12 Jan 2022 13:41:31 +0100 Subject: [PATCH 08/10] Apply review comment. --- packages/ckeditor5-list/src/documentlist/documentlistediting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index 7d4a4412db2..78424b3461a 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -120,7 +120,7 @@ export default class DocumentListEditing extends Plugin { } }, { context: 'li' } ); - // In some cases, the integration with the enter key is done after the default handler in EnterCommand. + // In some cases, after the default block splitting, we want to modify the new block to become a new list item instead of an additional block in the same list item. this.listenTo( enterCommand, 'afterExecute', () => { const splitCommand = commands.get( 'splitListItem' ); From 7e627b26c017c0fb30dc422ae5c3115bc99ffd10 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Wed, 12 Jan 2022 13:41:45 +0100 Subject: [PATCH 09/10] Apply review comment. --- .../tests/documentlist/documentlistediting-integrations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/tests/documentlist/documentlistediting-integrations.js b/packages/ckeditor5-list/tests/documentlist/documentlistediting-integrations.js index bcbc1982642..a1fca787ed2 100644 --- a/packages/ckeditor5-list/tests/documentlist/documentlistediting-integrations.js +++ b/packages/ckeditor5-list/tests/documentlist/documentlistediting-integrations.js @@ -535,7 +535,7 @@ describe( 'DocumentListEditing integrations', () => { describe( 'collapsed selection', () => { describe( 'with just one block per list item', () => { - it( 'should outdent if the slection in the only empty list item (convert into paragraph and turn of the list)', () => { + it( 'should outdent if the slection in the only empty list item (convert into paragraph and turn off the list)', () => { setModelData( model, modelList( [ '* []' ] ) ); From f46fcefae63031613f1c345bb644637088e215a2 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Wed, 12 Jan 2022 13:44:22 +0100 Subject: [PATCH 10/10] Docs. --- .../ckeditor5-list/src/documentlist/documentlistediting.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-list/src/documentlist/documentlistediting.js b/packages/ckeditor5-list/src/documentlist/documentlistediting.js index 78424b3461a..106338d2715 100644 --- a/packages/ckeditor5-list/src/documentlist/documentlistediting.js +++ b/packages/ckeditor5-list/src/documentlist/documentlistediting.js @@ -120,7 +120,8 @@ export default class DocumentListEditing extends Plugin { } }, { context: 'li' } ); - // In some cases, after the default block splitting, we want to modify the new block to become a new list item instead of an additional block in the same list item. + // In some cases, after the default block splitting, we want to modify the new block to become a new list item + // instead of an additional block in the same list item. this.listenTo( enterCommand, 'afterExecute', () => { const splitCommand = commands.get( 'splitListItem' );