Skip to content

Commit

Permalink
Merge pull request #12886 from ckeditor/ck/12885-enterblock-public
Browse files Browse the repository at this point in the history
Other (enter): Made `enterBlock()` helper publicly accessible through `EnterCommand#enterBlock()`. Closes #12885.
  • Loading branch information
arkflpc authored Nov 21, 2022
2 parents 20359d1 + e34c24f commit 03b105a
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 58 deletions.
20 changes: 20 additions & 0 deletions packages/ckeditor5-enter/_src/entercommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ export default class EnterCommand extends Command {
this.fire( 'afterExecute', { writer } );
} );
}

/**
* Splits a block where the document selection is placed, in the way how the <kbd>Enter</kbd> key is expected to work:
*
* <p>Foo[]bar</p> -> <p>Foo</p><p>[]bar</p>
* <p>Foobar[]</p> -> <p>Foobar</p><p>[]</p>
* <p>Fo[ob]ar</p> -> <p>Fo</p><p>[]ar</p>
*
* In some cases, the split will not happen:
*
* // The selection parent is a limit element:
* <figcaption>A[bc]d</figcaption> -> <figcaption>A[]d</figcaption>
*
* // The selection spans over multiple elements:
* <h>x[x</h><p>y]y<p> -> <h>x</h><p>[]y</p>
*
* @method #enterBlock
* @param {module:engine/model/writer~Writer} writer Writer to use when performing the enter action.
* @returns {Boolean} `true` if a block was split, `false` otherwise.
*/
}

// Creates a new block in the way that the <kbd>Enter</kbd> key is expected to work.
Expand Down
123 changes: 70 additions & 53 deletions packages/ckeditor5-enter/src/entercommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,75 +29,92 @@ export default class EnterCommand extends Command {
* @inheritDoc
*/
public override execute(): void {
const model = this.editor.model;
const doc = model.document;

model.change( writer => {
enterBlock( this.editor.model, writer, doc.selection, model.schema );
this.editor.model.change( writer => {
this.enterBlock( writer );
this.fire<EnterCommandAfterExecuteEvent>( 'afterExecute', { writer } );
} );
}
}

export type EnterCommandAfterExecuteEvent = {
name: 'afterExecute';
args: [ { writer: Writer } ];
};
/**
* Splits a block where the document selection is placed, in the way how the <kbd>Enter</kbd> key is expected to work:
*
* <p>Foo[]bar</p> -> <p>Foo</p><p>[]bar</p>
* <p>Foobar[]</p> -> <p>Foobar</p><p>[]</p>
* <p>Fo[ob]ar</p> -> <p>Fo</p><p>[]ar</p>
*
* In some cases, the split will not happen:
*
* // The selection parent is a limit element:
* <figcaption>A[bc]d</figcaption> -> <figcaption>A[]d</figcaption>
*
* // The selection spans over multiple elements:
* <h>x[x</h><p>y]y<p> -> <h>x</h><p>[]y</p>
*
* @param writer Writer to use when performing the enter action.
* @returns `true` if a block was split, `false` otherwise.
*/
public enterBlock( writer: Writer ): boolean {
const model = this.editor.model;
const selection = model.document.selection;
const schema = model.schema;
const isSelectionEmpty = selection.isCollapsed;
const range = selection.getFirstRange()!;
const startElement = range.start.parent as Element;
const endElement = range.end.parent as Element;

// Creates a new block in the way that the <kbd>Enter</kbd> key is expected to work.
//
// @param {module:engine/model~Model} model
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
// Selection on which the action should be performed.
// @param {module:engine/model/schema~Schema} schema
function enterBlock( model: Model, writer: Writer, selection: DocumentSelection, schema: Schema ): void {
const isSelectionEmpty = selection.isCollapsed;
const range = selection.getFirstRange()!;
const startElement = range.start.parent as Element;
const endElement = range.end.parent as Element;

// Don't touch the roots and other limit elements.
if ( schema.isLimit( startElement ) || schema.isLimit( endElement ) ) {
// Delete the selected content but only if inside a single limit element.
// Abort, when crossing limit elements boundary (e.g. <limit1>x[x</limit1>donttouchme<limit2>y]y</limit2>).
// This is an edge case and it's hard to tell what should actually happen because such a selection
// is not entirely valid.
if ( !isSelectionEmpty && startElement == endElement ) {
model.deleteContent( selection );
// Don't touch the roots and other limit elements.
if ( schema.isLimit( startElement ) || schema.isLimit( endElement ) ) {
// Delete the selected content but only if inside a single limit element.
// Abort, when crossing limit elements boundary (e.g. <limit1>x[x</limit1>donttouchme<limit2>y]y</limit2>).
// This is an edge case and it's hard to tell what should actually happen because such a selection
// is not entirely valid.
if ( !isSelectionEmpty && startElement == endElement ) {
model.deleteContent( selection );
}

return false;
}

return;
}
if ( isSelectionEmpty ) {
const attributesToCopy = getCopyOnEnterAttributes( writer.model.schema, selection.getAttributes() );

if ( isSelectionEmpty ) {
const attributesToCopy = getCopyOnEnterAttributes( writer.model.schema, selection.getAttributes() );
splitBlock( writer, range.start );
writer.setSelectionAttribute( attributesToCopy );

splitBlock( writer, range.start );
writer.setSelectionAttribute( attributesToCopy );
} else {
const leaveUnmerged = !( range.start.isAtStart && range.end.isAtEnd );
const isContainedWithinOneElement = ( startElement == endElement );
return true;
} else {
const leaveUnmerged = !( range.start.isAtStart && range.end.isAtEnd );
const isContainedWithinOneElement = ( startElement == endElement );

model.deleteContent( selection, { leaveUnmerged } );
model.deleteContent( selection, { leaveUnmerged } );

if ( leaveUnmerged ) {
// Partially selected elements.
//
// <h>x[xx]x</h> -> <h>x^x</h> -> <h>x</h><h>^x</h>
if ( isContainedWithinOneElement ) {
splitBlock( writer, selection.focus! );
}
// Selection over multiple elements.
//
// <h>x[x</h><p>y]y<p> -> <h>x^</h><p>y</p> -> <h>x</h><p>^y</p>
else {
writer.setSelection( endElement, 0 );
if ( leaveUnmerged ) {
// Partially selected elements.
//
// <h>x[xx]x</h> -> <h>x^x</h> -> <h>x</h><h>^x</h>
if ( isContainedWithinOneElement ) {
splitBlock( writer, selection.focus! );

return true;
}
// Selection over multiple elements.
//
// <h>x[x</h><p>y]y<p> -> <h>x^</h><p>y</p> -> <h>x</h><p>^y</p>
else {
writer.setSelection( endElement, 0 );
}
}
}

return false;
}
}

export type EnterCommandAfterExecuteEvent = {
name: 'afterExecute';
args: [ { writer: Writer } ];
};

function splitBlock( writer: Writer, splitPos: Position ): void {
writer.split( splitPos );
writer.setSelection( splitPos.parent.nextSibling, 0 );
Expand Down
45 changes: 41 additions & 4 deletions packages/ckeditor5-enter/tests/entercommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,35 @@ describe( 'EnterCommand', () => {
} );

describe( 'execute()', () => {
it( 'uses enterBlock()', () => {
setData( model, '<p>foo[]bar</p>' );

sinon.spy( command, 'enterBlock' );

editor.execute( 'enter' );

expect( command.enterBlock.called ).to.be.true;
} );

it( 'fires afterExecute() event with the current writer as a parameter', done => {
setData( model, '<p>foo[]bar</p>' );

let currentWriter;

command.on( 'afterExecute', ( evt, { writer } ) => {
expect( writer ).to.equal( currentWriter );

done();
} );

model.change( writer => {
currentWriter = writer;
editor.execute( 'enter' );
} );
} );
} );

describe( 'enterBlock()', () => {
describe( 'collapsed selection', () => {
test(
'does nothing in the root',
Expand Down Expand Up @@ -162,7 +191,9 @@ describe( 'EnterCommand', () => {
model.change( () => {
setData( model, '<p><inlineLimit>ba[r</inlineLimit></p><p>f]oo</p>' );

command.execute();
model.change( writer => {
command.enterBlock( writer );
} );

expect( getData( model ) ).to.equal( '<p><inlineLimit>ba[r</inlineLimit></p><p>f]oo</p>' );
} );
Expand All @@ -179,7 +210,9 @@ describe( 'EnterCommand', () => {
// @TODO: Add option for setting selection direction to model utils.
doc.selection._lastRangeBackward = true;

command.execute();
model.change( writer => {
command.enterBlock( writer );
} );

expect( getData( model ) ).to.equal( '<p>[]</p>' );
} );
Expand All @@ -191,7 +224,9 @@ describe( 'EnterCommand', () => {

setData( model, '<p>[x]</p>' );

command.execute();
model.change( writer => {
command.enterBlock( writer );
} );

expect( spy.calledOnce ).to.be.true;
} );
Expand All @@ -201,7 +236,9 @@ describe( 'EnterCommand', () => {
it( title, () => {
setData( model, input );

command.execute();
model.change( writer => {
command.enterBlock( writer );
} );

expect( getData( model ) ).to.equal( output );
} );
Expand Down
4 changes: 3 additions & 1 deletion packages/ckeditor5-paragraph/src/paragraphbuttonui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import { Plugin, icons } from '@ckeditor/ckeditor5-core';
import { ButtonView } from '@ckeditor/ckeditor5-ui';

import type ParagraphCommand from './paragraphcommand';

const icon = icons.paragraph;

/**
Expand All @@ -36,7 +38,7 @@ export default class ParagraphButtonUI extends Plugin {

editor.ui.componentFactory.add( 'paragraph', locale => {
const view = new ButtonView( locale );
const command = editor.commands.get( 'paragraph' )!;
const command: ParagraphCommand = editor.commands.get( 'paragraph' )!;

view.label = t( 'Paragraph' );
view.icon = icon;
Expand Down

0 comments on commit 03b105a

Please sign in to comment.