diff --git a/packages/ckeditor5-code-block/docs/features/code-blocks.md b/packages/ckeditor5-code-block/docs/features/code-blocks.md index 292384cb0ac..3c6226f7ab5 100644 --- a/packages/ckeditor5-code-block/docs/features/code-blocks.md +++ b/packages/ckeditor5-code-block/docs/features/code-blocks.md @@ -16,7 +16,7 @@ Code blocks is a perfect feature to present programming- or software-related iss ## Demo -Use the code block toolbar button {@icon @ckeditor/ckeditor5-code-block/theme/icons/codeblock.svg Insert code block} and the type dropdown to insert a desired code block. Alternatively, start the line with `` ``` `` to format it as a code block thanks to the {@link features/autoformat autoformatting feature}. To add a paragraph underneath a code block, just use the double caret function (press Enter twice). +Use the code block toolbar button {@icon @ckeditor/ckeditor5-code-block/theme/icons/codeblock.svg Insert code block} and the type dropdown to insert a desired code block. Alternatively, start the line with `` ``` `` to format it as a code block thanks to the {@link features/autoformat autoformatting feature}. To add a paragraph underneath a code block, just press Enter three times. {@snippet features/code-block} @@ -100,7 +100,7 @@ There could be situations when there is no obvious way to set the caret before o {@img assets/img/code-blocks-typing-before.gif 770 The animation shows typing before the code blocks in CKEditor 5 rich text editor.} -* To type **after the code block**: Put the selection at the end of the last line of the code block and press Enter twice. A new paragraph that you can type in will be created after the code block. +* To type **after the code block**: Put the selection at the end of the last line of the code block and press Enter three times. A new paragraph that you can type in will be created after the code block. {@img assets/img/code-blocks-typing-after.gif 770 The animation shows typing after the code blocks in CKEditor 5 rich text editor.} diff --git a/packages/ckeditor5-code-block/src/codeblockediting.js b/packages/ckeditor5-code-block/src/codeblockediting.js index 39e80f3edc0..a64cc28a4c1 100644 --- a/packages/ckeditor5-code-block/src/codeblockediting.js +++ b/packages/ckeditor5-code-block/src/codeblockediting.js @@ -219,7 +219,7 @@ export default class CodeBlockEditing extends Plugin { // Customize the response to the Enter and Shift+Enter // key press when the selection is in the code block. Upon enter key press we can either - // leave the block if it's "two enters" in a row or create a new code block line, preserving + // leave the block if it's "two or three enters" in a row or create a new code block line, preserving // previous line's indentation. this.listenTo( editor.editing.view.document, 'enter', ( evt, data ) => { const positionParent = editor.model.document.selection.getLastPosition().parent; @@ -301,7 +301,7 @@ function leaveBlockStartOnEnter( editor, isSoftEnter ) { return false; } - if ( !nodeAfter || !nodeAfter.is( 'element', 'softBreak' ) ) { + if ( !isSoftBreakNode( nodeAfter ) ) { return false; } @@ -352,42 +352,61 @@ function leaveBlockEndOnEnter( editor, isSoftEnter ) { let emptyLineRangeToRemoveOnEnter; - if ( isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtEnd || !nodeBefore ) { + if ( isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtEnd || !nodeBefore || !nodeBefore.previousSibling ) { return false; } - // When the position is directly preceded by a soft break + // When the position is directly preceded by two soft breaks // - // foo[] + // foo[] // // it creates the following range that will be cleaned up before leaving: // - // foo[] + // foo[] // - if ( nodeBefore.is( 'element', 'softBreak' ) ) { - emptyLineRangeToRemoveOnEnter = model.createRangeOn( nodeBefore ); + if ( isSoftBreakNode( nodeBefore ) && isSoftBreakNode( nodeBefore.previousSibling ) ) { + emptyLineRangeToRemoveOnEnter = model.createRange( + model.createPositionBefore( nodeBefore.previousSibling ), model.createPositionAfter( nodeBefore ) + ); } - // When there's some text before the position made purely of white–space characters + // When there's some text before the position that is + // preceded by two soft breaks and made purely of white–space characters + // + // foo [] // - // foo [] + // it creates the following range to clean up before leaving: // - // but NOT when it's the first one of the kind + // foo[ ] // - // [] + else if ( + isEmptyishTextNode( nodeBefore ) && + isSoftBreakNode( nodeBefore.previousSibling ) && + isSoftBreakNode( nodeBefore.previousSibling.previousSibling ) + ) { + emptyLineRangeToRemoveOnEnter = model.createRange( + model.createPositionBefore( nodeBefore.previousSibling.previousSibling ), model.createPositionAfter( nodeBefore ) + ); + } + + // When there's some text before the position that is made purely of white–space characters + // and is preceded by some other text made purely of white–space characters + // + // foo [] // // it creates the following range to clean up before leaving: // - // foo[ ] + // foo[ ] // else if ( - nodeBefore.is( '$text' ) && - !nodeBefore.data.match( /\S/ ) && - nodeBefore.previousSibling && - nodeBefore.previousSibling.is( 'element', 'softBreak' ) + isEmptyishTextNode( nodeBefore ) && + isSoftBreakNode( nodeBefore.previousSibling ) && + isEmptyishTextNode( nodeBefore.previousSibling.previousSibling ) && + isSoftBreakNode( nodeBefore.previousSibling.previousSibling.previousSibling ) ) { emptyLineRangeToRemoveOnEnter = model.createRange( - model.createPositionBefore( nodeBefore.previousSibling ), model.createPositionAfter( nodeBefore ) + model.createPositionBefore( nodeBefore.previousSibling.previousSibling.previousSibling ), + model.createPositionAfter( nodeBefore ) ); } @@ -395,8 +414,9 @@ function leaveBlockEndOnEnter( editor, isSoftEnter ) { // // [] // a [] - // foobar[] - // foo a [] + // foo[] + // foobar[] + // foo a [] // else { return false; @@ -404,7 +424,7 @@ function leaveBlockEndOnEnter( editor, isSoftEnter ) { // We're doing everything in a single change block to have a single undo step. editor.model.change( writer => { - // Remove the last and all white space characters that followed it. + // Remove the last s and all white space characters that followed them. writer.remove( emptyLineRangeToRemoveOnEnter ); // "Clone" the in the standard way. @@ -422,3 +442,11 @@ function leaveBlockEndOnEnter( editor, isSoftEnter ) { return true; } + +function isEmptyishTextNode( node ) { + return node && node.is( '$text' ) && !node.data.match( /\S/ ); +} + +function isSoftBreakNode( node ) { + return node && node.is( 'element', 'softBreak' ); +} diff --git a/packages/ckeditor5-code-block/tests/codeblockediting.js b/packages/ckeditor5-code-block/tests/codeblockediting.js index 0ecebfffa56..d87d45257bf 100644 --- a/packages/ckeditor5-code-block/tests/codeblockediting.js +++ b/packages/ckeditor5-code-block/tests/codeblockediting.js @@ -394,7 +394,7 @@ describe( 'CodeBlockEditing', () => { describe( 'leaving block using the enter key', () => { describe( 'leaving the block end', () => { - it( 'should leave the block when pressed twice at the end', () => { + it( 'should leave the block when pressed three times at the end', () => { const spy = sinon.spy( editor.editing.view, 'scrollToTheSelection' ); setModelData( model, 'foo[]' ); @@ -406,6 +406,11 @@ describe( 'CodeBlockEditing', () => { viewDoc.fire( 'enter', getEvent() ); + expect( getModelData( model ) ).to.equal( + 'foo[]' ); + + viewDoc.fire( 'enter', getEvent() ); + expect( getModelData( model ) ).to.equal( 'foo' + '[]' @@ -415,14 +420,17 @@ describe( 'CodeBlockEditing', () => { editor.execute( 'undo' ); expect( getModelData( model ) ).to.equal( - 'foo[]' ); + 'foo[]' ); + + editor.execute( 'undo' ); + expect( getModelData( model ) ).to.equal( 'foo[]' ); editor.execute( 'undo' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); } ); it( 'should not leave the block when the selection is not collapsed', () => { - setModelData( model, 'f[oo]' ); + setModelData( model, 'f[oo]' ); viewDoc.fire( 'enter', getEvent() ); @@ -430,7 +438,7 @@ describe( 'CodeBlockEditing', () => { 'f[]' ); } ); - it( 'should not leave the block when pressed twice when in the middle of the code', () => { + it( 'should not leave the block when pressed three times when in the middle of the code', () => { setModelData( model, 'fo[]o' ); viewDoc.fire( 'enter', getEvent() ); @@ -442,9 +450,16 @@ describe( 'CodeBlockEditing', () => { expect( getModelData( model ) ).to.equal( 'fo[]o' ); + + viewDoc.fire( 'enter', getEvent() ); + + expect( getModelData( model ) ).to.equal( + '' + + 'fo[]o' + + '' ); } ); - it( 'should not leave the block when pressed twice at the beginning of the code', () => { + it( 'should not leave the block when pressed three times at the beginning of the code', () => { setModelData( model, '[]foo' ); viewDoc.fire( 'enter', getEvent() ); @@ -456,23 +471,49 @@ describe( 'CodeBlockEditing', () => { expect( getModelData( model ) ).to.equal( '[]foo' ); + + viewDoc.fire( 'enter', getEvent() ); + + expect( getModelData( model ) ).to.equal( + '' + + '[]foo' + + '' ); } ); - it( 'should not leave the block when pressed shift+enter twice at the end of the code', () => { - setModelData( model, 'foo[]' ); + it( 'should not leave the block when pressed shift+enter three times at the end of the code', () => { + setModelData( model, 'foo[]' ); viewDoc.fire( 'enter', getEvent( { isSoft: true } ) ); expect( getModelData( model ) ).to.equal( - 'foo[]' ); + '' + + 'foo[]' + + '' ); + } ); + + it( 'should clean up the last two lines if the last one has white-space characters only', () => { + setModelData( model, 'foo[]' ); + + model.change( writer => { + // foo [] + writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 5 ); + } ); + + viewDoc.fire( 'enter', getEvent() ); + + expect( getModelData( model ) ).to.equal( + 'foo[]' ); } ); - it( 'should clean up the last line if has white–space characters only', () => { - setModelData( model, 'foo[]' ); + it( 'should clean up the last two lines if both have white-space characters only', () => { + setModelData( model, 'foo[]' ); model.change( writer => { - // foo [] + // foo [] writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 4 ); + + // foo [] + writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 7 ); } ); viewDoc.fire( 'enter', getEvent() ); diff --git a/packages/ckeditor5-code-block/tests/manual/codeblock.md b/packages/ckeditor5-code-block/tests/manual/codeblock.md index 3b781ac34f7..a9459815a5a 100644 --- a/packages/ckeditor5-code-block/tests/manual/codeblock.md +++ b/packages/ckeditor5-code-block/tests/manual/codeblock.md @@ -14,9 +14,9 @@ ### Block end -- Create an empty line at the end of the block and put the selection there. +- Create two empty lines at the end of the block and put the selection there. - Press Enter again. -- The new line created in the code block should no longer be there. +- The new lines created in the code block should no longer be there. - A new empty paragraph should be created after the code block. - The selection should be in that paragraph.