Skip to content

Commit

Permalink
Merge pull request #10316 from marcellofuschi/i/6682
Browse files Browse the repository at this point in the history
Other (code-block): Makes three Enter clicks necessary at the end of a code block to escape it. Closes #6682.
  • Loading branch information
arkflpc authored Oct 5, 2021
2 parents 5cd38c0 + dec00b9 commit ba7dedb
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 36 deletions.
4 changes: 2 additions & 2 deletions packages/ckeditor5-code-block/docs/features/code-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kbd>Enter</kbd> 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 <kbd>Enter</kbd> three times.

{@snippet features/code-block}

Expand Down Expand Up @@ -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 <kbd>Enter</kbd> 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 <kbd>Enter</kbd> 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.}

Expand Down
70 changes: 49 additions & 21 deletions packages/ckeditor5-code-block/src/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export default class CodeBlockEditing extends Plugin {

// Customize the response to the <kbd>Enter</kbd> and <kbd>Shift</kbd>+<kbd>Enter</kbd>
// 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;
Expand Down Expand Up @@ -301,7 +301,7 @@ function leaveBlockStartOnEnter( editor, isSoftEnter ) {
return false;
}

if ( !nodeAfter || !nodeAfter.is( 'element', 'softBreak' ) ) {
if ( !isSoftBreakNode( nodeAfter ) ) {
return false;
}

Expand Down Expand Up @@ -352,59 +352,79 @@ 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
//
// <codeBlock>foo<softBreak></softBreak>[]</codeBlock>
// <codeBlock>foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>
//
// it creates the following range that will be cleaned up before leaving:
//
// <codeBlock>foo[<softBreak></softBreak>]</codeBlock>
// <codeBlock>foo[<softBreak></softBreak><softBreak></softBreak>]</codeBlock>
//
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
//
// <codeBlock>foo<softBreak></softBreak><softBreak></softBreak> []</codeBlock>
//
// <codeBlock>foo<softBreak></softBreak> []</codeBlock>
// it creates the following range to clean up before leaving:
//
// but NOT when it's the first one of the kind
// <codeBlock>foo[<softBreak></softBreak><softBreak></softBreak> ]</codeBlock>
//
// <codeBlock> []</codeBlock>
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
//
// <codeBlock>foo<softBreak></softBreak> <softBreak></softBreak> []</codeBlock>
//
// it creates the following range to clean up before leaving:
//
// <codeBlock>foo[<softBreak></softBreak> ]</codeBlock>
// <codeBlock>foo[<softBreak></softBreak> <softBreak></softBreak> ]</codeBlock>
//
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 )
);
}

// Not leaving the block in the following cases:
//
// <codeBlock> []</codeBlock>
// <codeBlock> a []</codeBlock>
// <codeBlock>foo<softBreak></softBreak>bar[]</codeBlock>
// <codeBlock>foo<softBreak></softBreak> a []</codeBlock>
// <codeBlock>foo<softBreak></softBreak>[]</codeBlock>
// <codeBlock>foo<softBreak></softBreak><softBreak></softBreak>bar[]</codeBlock>
// <codeBlock>foo<softBreak></softBreak><softBreak></softBreak> a []</codeBlock>
//
else {
return false;
}

// We're doing everything in a single change block to have a single undo step.
editor.model.change( writer => {
// Remove the last <softBreak> and all white space characters that followed it.
// Remove the last <softBreak>s and all white space characters that followed them.
writer.remove( emptyLineRangeToRemoveOnEnter );

// "Clone" the <codeBlock> in the standard way.
Expand All @@ -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' );
}
63 changes: 52 additions & 11 deletions packages/ckeditor5-code-block/tests/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '<codeBlock language="css">foo[]</codeBlock>' );
Expand All @@ -406,6 +406,11 @@ describe( 'CodeBlockEditing', () => {

viewDoc.fire( 'enter', getEvent() );

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>' );

viewDoc.fire( 'enter', getEvent() );

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">foo</codeBlock>' +
'<paragraph>[]</paragraph>'
Expand All @@ -415,22 +420,25 @@ describe( 'CodeBlockEditing', () => {

editor.execute( 'undo' );
expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
'<codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>' );

editor.execute( 'undo' );
expect( getModelData( model ) ).to.equal( '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );

editor.execute( 'undo' );
expect( getModelData( model ) ).to.equal( '<codeBlock language="css">foo[]</codeBlock>' );
} );

it( 'should not leave the block when the selection is not collapsed', () => {
setModelData( model, '<codeBlock language="css">f[oo<softBreak></softBreak>]</codeBlock>' );
setModelData( model, '<codeBlock language="css">f[oo<softBreak></softBreak><softBreak></softBreak>]</codeBlock>' );

viewDoc.fire( 'enter', getEvent() );

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">f<softBreak></softBreak>[]</codeBlock>' );
} );

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, '<codeBlock language="css">fo[]o</codeBlock>' );

viewDoc.fire( 'enter', getEvent() );
Expand All @@ -442,9 +450,16 @@ describe( 'CodeBlockEditing', () => {

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">fo<softBreak></softBreak><softBreak></softBreak>[]o</codeBlock>' );

viewDoc.fire( 'enter', getEvent() );

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">' +
'fo<softBreak></softBreak><softBreak></softBreak><softBreak></softBreak>[]o' +
'</codeBlock>' );
} );

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, '<codeBlock language="css">[]foo</codeBlock>' );

viewDoc.fire( 'enter', getEvent() );
Expand All @@ -456,23 +471,49 @@ describe( 'CodeBlockEditing', () => {

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css"><softBreak></softBreak><softBreak></softBreak>[]foo</codeBlock>' );

viewDoc.fire( 'enter', getEvent() );

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">' +
'<softBreak></softBreak><softBreak></softBreak><softBreak></softBreak>[]foo' +
'</codeBlock>' );
} );

it( 'should not leave the block when pressed shift+enter twice at the end of the code', () => {
setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
it( 'should not leave the block when pressed shift+enter three times at the end of the code', () => {
setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>' );

viewDoc.fire( 'enter', getEvent( { isSoft: true } ) );

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>' );
'<codeBlock language="css">' +
'foo<softBreak></softBreak><softBreak></softBreak><softBreak></softBreak>[]' +
'</codeBlock>' );
} );

it( 'should clean up the last two lines if the last one has white-space characters only', () => {
setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>' );

model.change( writer => {
// <codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak> []</codeBlock>
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 5 );
} );

viewDoc.fire( 'enter', getEvent() );

expect( getModelData( model ) ).to.equal(
'<codeBlock language="css">foo</codeBlock><paragraph>[]</paragraph>' );
} );

it( 'should clean up the last line if has whitespace characters only', () => {
setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak>[]</codeBlock>' );
it( 'should clean up the last two lines if both have white-space characters only', () => {
setModelData( model, '<codeBlock language="css">foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>' );

model.change( writer => {
// <codeBlock language="css">foo<softBreak></softBreak> []</codeBlock>
// <codeBlock language="css">foo<softBreak></softBreak> <softBreak></softBreak>[]</codeBlock>
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 4 );

// <codeBlock language="css">foo<softBreak></softBreak> <softBreak></softBreak> []</codeBlock>
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 7 );
} );

viewDoc.fire( 'enter', getEvent() );
Expand Down
4 changes: 2 additions & 2 deletions packages/ckeditor5-code-block/tests/manual/codeblock.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kbd>Enter</kbd> 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.

Expand Down

0 comments on commit ba7dedb

Please sign in to comment.