Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Soft Break support #54

Merged
merged 24 commits into from
Jun 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3ac04cc
Implementation of soft breaks into enter plugin. Usage is optional.
alexeckermann Apr 24, 2018
148c5d5
README update. eslint changes not included in last commit.
alexeckermann Apr 24, 2018
b328298
Merge branch 'master' into soft-breaks
alexeckermann Apr 26, 2018
3b32206
Fix: EnterObserver shouldnt respond to soft break key events.
alexeckermann Apr 26, 2018
6a77cc2
Fix: Soft break schema must be defined as a block
alexeckermann Apr 26, 2018
fb1d320
EnterObserver passes "isSoft" option in the event object.
May 15, 2018
a21afcc
Introduced the ShiftEnter plugin.
May 15, 2018
deb0ba2
Introduced the Essentials plugin.
May 15, 2018
33b6af3
Updated the MT.
May 15, 2018
f899536
Code style.
May 15, 2018
9b2ad57
Added missing devDependencies in package.json.
May 15, 2018
2782422
Improved the docs. [skip ci]
May 15, 2018
5f3ef34
Fixed invalid plugin name.
pomek May 18, 2018
75f2d7b
Docs and comment improvements.
Reinmar May 30, 2018
9c42c9b
The Essentials plugin is defined in ckeditor5-essentials package and …
Reinmar May 30, 2018
645a128
Use more meaningful <softBreak> instand of <break>.
Reinmar May 30, 2018
8d1e951
Minor API docs polishing.
Reinmar Jun 4, 2018
74cc5b2
Merge branch 'master' into soft-breaks
Jun 4, 2018
7c2afe9
Added missing tests.
Jun 8, 2018
2eb8708
Code style.
Jun 11, 2018
f49dc6f
Removed an unnecessary helper function.
Reinmar Jun 11, 2018
bc0d221
Wording.
Reinmar Jun 11, 2018
089fc14
Explained one decision and clarified the code.
Reinmar Jun 11, 2018
cec771c
Docs: Fixed link.
Reinmar Jun 11, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CKEditor 5 Enter feature
CKEditor 5 enter feature
========================================

[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Expand All @@ -10,7 +10,7 @@ CKEditor 5 Enter feature
[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-enter/status.svg)](https://david-dm.org/ckeditor/ckeditor5-enter)
[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-enter/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-enter?type=dev)

This package implements the <kbd>Enter</kbd> key support for CKEditor 5.
This package implements the <kbd>Enter</kbd> and <kbd>Shift</kbd>+<kbd>Enter</kbd> (soft break) support for CKEditor 5.

## Documentation

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@ckeditor/ckeditor5-basic-styles": "^10.0.0",
"@ckeditor/ckeditor5-editor-classic": "^10.0.0",
"@ckeditor/ckeditor5-heading": "^10.0.0",
"@ckeditor/ckeditor5-paragraph": "^10.0.0",
"@ckeditor/ckeditor5-typing": "^10.0.0",
"@ckeditor/ckeditor5-undo": "^10.0.0",
"eslint": "^4.15.0",
Expand Down
10 changes: 8 additions & 2 deletions src/enter.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import EnterCommand from './entercommand';
import EnterObserver from './enterobserver';

/**
* The Enter feature. Handles the <kbd>Enter</kbd> and <kbd>Shift + Enter</kbd> keys in the editor.
* This plugin handles the <kbd>Enter</kbd> key (hard line break) in the editor.
*
* See also the {@link module:enter/shiftenter~ShiftEnter} plugin.
*
* @extends module:core/plugin~Plugin
*/
Expand All @@ -33,8 +35,12 @@ export default class Enter extends Plugin {

editor.commands.add( 'enter', new EnterCommand( editor ) );

// TODO We may use the keystroke handler for that.
this.listenTo( viewDocument, 'enter', ( evt, data ) => {
// The soft enter key is handled by the ShiftEnter plugin.
if ( data.isSoft ) {
return;
}

editor.execute( 'enter' );
data.preventDefault();
view.scrollToTheSelection();
Expand Down
14 changes: 9 additions & 5 deletions src/enterobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ export default class EnterObserver extends Observer {
constructor( view ) {
super( view );

const document = this.document;
const doc = this.document;

document.on( 'keydown', ( evt, data ) => {
doc.on( 'keydown', ( evt, data ) => {
if ( this.isEnabled && data.keyCode == keyCodes.enter ) {
// Save the event object to check later if it was stopped or not.
let event;
document.once( 'enter', evt => ( event = evt ), { priority: 'highest' } );
doc.once( 'enter', evt => ( event = evt ), { priority: 'highest' } );

document.fire( 'enter', new DomEventData( document, data.domEvent ) );
doc.fire( 'enter', new DomEventData( doc, data.domEvent, {
isSoft: data.shiftKey
} ) );

// Stop `keydown` event if `enter` event was stopped.
// https://github.com/ckeditor/ckeditor5/issues/753
Expand All @@ -49,8 +51,10 @@ export default class EnterObserver extends Observer {
* Event fired when the user presses the <kbd>Enter</kbd> key.
*
* Note: This event is fired by the {@link module:enter/enterobserver~EnterObserver observer}
* (usually registered by the {@link module:enter/enter~Enter Enter feature}).
* (usually registered by the {@link module:enter/enter~Enter Enter feature} and
* {@link module:enter/shiftenter~ShiftEnter ShiftEnter feature}).
*
* @event module:engine/view/document~Document#event:enter
* @param {module:engine/view/observer/domeventdata~DomEventData} data
* @param {Boolean} data.isSoft Whether it's a soft enter (<kbd>Shift</kbd>+<kbd>Enter</kbd>) or hard enter (<kbd>Enter</kbd>).
*/
71 changes: 71 additions & 0 deletions src/shiftenter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module enter/shiftenter
*/

import ShiftEnterCommand from './shiftentercommand';
import EnterObserver from './enterobserver';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';

/**
* This plugin handles the <kbd>Shift</kbd>+<kbd>Enter</kbd> keystroke (soft line break) in the editor.
*
* See also the {@link module:enter/enter~Enter} plugin.
*
* @extends module:core/plugin~Plugin
*/
export default class ShiftEnter extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'ShiftEnter';
}

init() {
const editor = this.editor;
const schema = editor.model.schema;
const conversion = editor.conversion;
const view = editor.editing.view;
const viewDocument = view.document;

// Configure the schema.
schema.register( 'softBreak', {
allowWhere: '$text'
} );

// Configure converters.
conversion.for( 'upcast' )
.add( upcastElementToElement( {
model: 'softBreak',
view: 'br'
} ) );

conversion.for( 'downcast' )
.add( downcastElementToElement( {
model: 'softBreak',
view: ( modelElement, viewWriter ) => viewWriter.createEmptyElement( 'br' )
} ) );

view.addObserver( EnterObserver );

editor.commands.add( 'shiftEnter', new ShiftEnterCommand( editor ) );

this.listenTo( viewDocument, 'enter', ( evt, data ) => {
// The hard enter key is handled by the Enter plugin.
if ( !data.isSoft ) {
return;
}

editor.execute( 'shiftEnter' );
data.preventDefault();
view.scrollToTheSelection();
}, { priority: 'low' } );
}
}
132 changes: 132 additions & 0 deletions src/shiftentercommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* @module enter/shiftentercommand
*/

import Command from '@ckeditor/ckeditor5-core/src/command';

/**
* ShiftEnter command. It is used by the {@link module:enter/shiftenter~ShiftEnter ShiftEnter feature} to handle
* the <kbd>Shift</kbd>+<kbd>Enter</kbd> keystroke.
*
* @extends module:core/command~Command
*/
export default class ShiftEnterCommand extends Command {
/**
* @inheritDoc
*/
execute() {
const model = this.editor.model;
const doc = model.document;

model.change( writer => {
softBreakAction( model, writer, doc.selection );
this.fire( 'afterExecute', { writer } );
} );
}

refresh() {
const model = this.editor.model;
const doc = model.document;

this.isEnabled = isEnabled( model.schema, doc.selection );
}
}

// Checks whether the shiftEnter command should be enabled in the specified selection.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
function isEnabled( schema, selection ) {
// At this moment it is okay to support single range selections only.
// But in the future we may need to change that.
if ( selection.rangeCount > 1 ) {
return false;
}

const anchorPos = selection.anchor;

// Check whether the break element can be inserted in the current selection anchor.
if ( !anchorPos || !schema.checkChild( anchorPos, 'softBreak' ) ) {
return false;
}

const range = selection.getFirstRange();
const startElement = range.start.parent;
const endElement = range.end.parent;

// Do not modify the content if selection is cross-limit elements.
if ( ( isInsideLimitElement( startElement, schema ) || isInsideLimitElement( endElement, schema ) ) && startElement !== endElement ) {
return false;
}

return true;
}

// Creates a break in the way that the <kbd>Shift+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.
function softBreakAction( model, writer, selection ) {
const isSelectionEmpty = selection.isCollapsed;
const range = selection.getFirstRange();
const startElement = range.start.parent;
const endElement = range.end.parent;
const isContainedWithinOneElement = ( startElement == endElement );

if ( isSelectionEmpty ) {
insertBreak( writer, range.end );
} else {
const leaveUnmerged = !( range.start.isAtStart && range.end.isAtEnd );
model.deleteContent( selection, { leaveUnmerged } );

// Selection within one element:
//
// <h>x[xx]x</h> -> <h>x^x</h> -> <h>x<br>^x</h>
if ( isContainedWithinOneElement ) {
insertBreak( 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>
//
// We chose not to insert a line break in this case because:
//
// * it's not a very common scenario,
// * it actually surprised me when I saw "expected behaviour" in real life.
//
// It's ok if the user will need to be more specific where they want the <br> to be inserted.
else {
// Move the selection to the 2nd element (last step of the example above).
if ( leaveUnmerged ) {
writer.setSelection( endElement, 0 );
}
}
}
}

function insertBreak( writer, position ) {
const breakLineElement = writer.createElement( 'softBreak' );

writer.insert( breakLineElement, position );
writer.setSelection( breakLineElement, 'after' );
}

// Checks whether specified `element` is a children of limit element.
//
// Checking whether the `<p>` element is inside a limit element:
// - <$root><p>Text.</p></$root> => false
// - <$root><limitElement><p>Text</p></limitElement></$root> => true
//
// @param {module:engine/model/element~Element} element
// @param {module:engine/schema~Schema} schema
// @returns {Boolean}
function isInsideLimitElement( element, schema ) {
// `$root` is a limit element but in this case is an invalid element.
if ( element.is( 'rootElement' ) ) {
return false;
}

return schema.isLimit( element ) || isInsideLimitElement( element.parent, schema );
}
18 changes: 17 additions & 1 deletion tests/enter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import Enter from '../src/enter';
import EnterCommand from '../src/entercommand';
import EnterObserver from '../src/enterobserver';
import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';

describe( 'Enter feature', () => {
Expand All @@ -26,11 +27,17 @@ describe( 'Enter feature', () => {
expect( editor.commands.get( 'enter' ) ).to.be.instanceof( EnterCommand );
} );

it( 'registers the EnterObserver', () => {
const observer = editor.editing.view.getObserver( EnterObserver );

expect( observer ).to.be.an.instanceOf( EnterObserver );
} );

it( 'listens to the editing view enter event', () => {
const spy = editor.execute = sinon.spy();
const domEvt = getDomEvent();

viewDocument.fire( 'enter', new DomEventData( viewDocument, domEvt ) );
viewDocument.fire( 'enter', new DomEventData( viewDocument, domEvt, { isSoft: false } ) );

expect( spy.calledOnce ).to.be.true;
expect( spy.calledWithExactly( 'enter' ) ).to.be.true;
Expand All @@ -49,6 +56,15 @@ describe( 'Enter feature', () => {
sinon.assert.callOrder( executeSpy, scrollSpy );
} );

it( 'does not execute the command if soft enter should be used', () => {
const domEvt = getDomEvent();
const commandExecuteSpy = sinon.stub( editor.commands.get( 'enter' ), 'execute' );

viewDocument.fire( 'enter', new DomEventData( viewDocument, domEvt, { isSoft: true } ) );

sinon.assert.notCalled( commandExecuteSpy );
} );

function getDomEvent() {
return {
preventDefault: sinon.spy()
Expand Down
18 changes: 17 additions & 1 deletion tests/enterobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,26 @@ describe( 'EnterObserver', () => {
viewDocument.on( 'enter', spy );

viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'enter' )
keyCode: getCode( 'enter' ),
shiftKey: false
} ) );

expect( spy.calledOnce ).to.be.true;
expect( spy.firstCall.args[ 1 ].isSoft ).to.be.false;
} );

it( 'detects whether shift was pressed along with the "enter" key', () => {
const spy = sinon.spy();

viewDocument.on( 'enter', spy );

viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
keyCode: getCode( 'enter' ),
shiftKey: true
} ) );

expect( spy.calledOnce ).to.be.true;
expect( spy.firstCall.args[ 1 ].isSoft ).to.be.true;
} );

it( 'is not fired on keydown when keyCode does not match enter', () => {
Expand Down
6 changes: 6 additions & 0 deletions tests/manual/enter.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ <h2>Heading 1</h2>
<h3>Heading 2</h3>
<h4>Heading 3</h4>
<p>Paragraph</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br>
Nulla finibus consequat placerat.<br>
Vestibulum id tellus et mauris sagittis tincidunt quis id mauris.<br>
Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.
</p>
</div>
3 changes: 2 additions & 1 deletion tests/manual/enter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '../../src/enter';
import ShiftEnter from '../../src/shiftenter';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
Expand All @@ -15,7 +16,7 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';

ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Heading, Undo, Bold, Italic ],
plugins: [ Enter, ShiftEnter, Typing, Heading, Undo, Bold, Italic ],
toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
Expand Down
2 changes: 1 addition & 1 deletion tests/manual/enter.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Test the <kbd>Enter</kbd> key support.
* Expected behavior:
* At the end of a heading should create a new paragraph.
* In the middle of a heading should split it.
* <kbd>Shift+Enter</kbd> should have <kbd>Enter</kbd> behavior.
* <kbd>Shift+Enter</kbd> should move the text to new line.
* The selection should always be moved to the newly created block.
* Select all + <kbd>Enter</kbd> should leave an empty paragraph.
* Check:
Expand Down
Loading