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

Commit

Permalink
Merge pull request #6 from ckeditor/t/1
Browse files Browse the repository at this point in the history
Initial implementation
  • Loading branch information
Piotr Jasiun committed May 13, 2016
2 parents 69c69ef + 22a244e commit 7ff0543
Show file tree
Hide file tree
Showing 4 changed files with 452 additions and 0 deletions.
50 changes: 50 additions & 0 deletions src/enter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

'use strict';

import Feature from '../feature.js';
import DomEventData from '../engine/treeview/observer/domeventdata.js';
import KeyObserver from '../engine/treeview/observer/keyobserver.js';
import EnterCommand from './entercommand.js';
import { keyCodes } from '../utils/keyboard.js';

/**
* The enter feature. Handles the <kbd>Enter</kbd> and <kbd>Shift + Enter</kbd> keys in the editor.
*
* @memberOf enter
* @extends ckeditor5.Feature
*/
export default class Enter extends Feature {
init() {
const editor = this.editor;
const editingView = editor.editing.view;

editingView.addObserver( KeyObserver );

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

this.listenTo( editingView, 'keydown', ( evt, data ) => {
if ( data.keyCode == keyCodes.enter ) {
editingView.fire( 'enter', new DomEventData( editingView, data.domEvent ) );
}
} );

// TODO We may use keystroke handler for that.
this.listenTo( editingView, 'enter', ( evt, data ) => {
editor.execute( 'enter' );
data.preventDefault();
} );
}
}

/**
* Event fired when the user presses <kbd>Enter</kbd>.
*
* Note: This event is fired by the {@link enter.Enter enter feature}.
*
* @event engine.treeView.TreeView#enter
* @param {engine.treeView.observer.DomEventData} data
*/
124 changes: 124 additions & 0 deletions src/entercommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

'use strict';

import Command from '../command/command.js';
import Element from '../engine/treemodel/element.js';
import LivePosition from '../engine/treemodel/liveposition.js';
import Position from '../engine/treemodel/position.js';

/**
* Enter command. Used by the {@link enter.Enter enter feature} to handle the <kbd>Enter</kbd> key.
*
* @member enter
* @extends ckeditor5.command.Command
*/
export default class EnterCommand extends Command {
_doExecute() {
const doc = this.editor.document;

doc.enqueueChanges( () => {
enterBlock( doc.batch(), doc.selection, { defaultBlock: 'paragraph' } );
} );
}
}

/**
* Enters new block in the way how <kbd>Enter</kbd> is expected to work.
*
* @param {engine.treeModel.Batch} batch Batch to which the deltas will be added.
* @param {engine.treeModel.Selection} selection
* @param {Object} options
* @param {Boolean} options.defaultBlockName Name of the block which should be created when enter leaves
* another block. Usuall set to `'paragraph'`. E.g. when entering block in `<heading>foo^</heading>` the result will be
* `<heading>foo</heading><paragraph>^</paragraph>`.
*/
export function enterBlock( batch, selection, options = {} ) {
const defaultBlockName = options.defaultBlockName;
const doc = batch.doc;
const isSelectionEmpty = selection.isCollapsed;
const range = selection.getFirstRange();
const startElement = range.start.parent;
const endElement = range.end.parent;

// Don't touch the root.
if ( startElement.root == startElement ) {
if ( !isSelectionEmpty ) {
doc.composer.deleteContents( batch, selection );
}

return;
}

if ( isSelectionEmpty ) {
splitBlock( batch, selection, range.start, defaultBlockName );
} else {
const shouldMerge = range.start.isAtStart() && range.end.isAtEnd();
const isContainedWithinOneElement = ( startElement == endElement );

doc.composer.deleteContents( batch, selection, { merge: shouldMerge } );

// Fully selected elements.
//
// <h>[xx</h><p>yy]<p> -> <h>^</h> -> <p>^</p>
// <h>[xxyy]</h> -> <h>^</h> -> <p>^</p>
if ( shouldMerge ) {
// We'll lose the ref to the renamed element, so let's keep a position inside it
// (offsets won't change, so it will stay in place). See ckeditor5-engine#367.
const pos = Position.createFromPosition( selection.focus );
const newBlockName = getNewBlockName( doc, startElement, defaultBlockName );

if ( startElement.name != newBlockName ) {
batch.rename( newBlockName, startElement );
}

selection.collapse( pos );
}
// Partially selected elements.
//
// <h>x[xx]x</h> -> <h>x^x</h> -> <h>x</h><h>^x</h>
else if ( isContainedWithinOneElement ) {
splitBlock( batch, selection, selection.focus, defaultBlockName );
}
// Selection over multilpe elements.
//
// <h>x[x</h><p>y]y<p> -> <h>x^</h><p>y</p> -> <h>x</h><p>^y</p>
else {
selection.collapse( endElement );
}
}
}

function splitBlock( batch, selection, splitPos, defaultBlockName ) {
const doc = batch.doc;
const parent = splitPos.parent;

if ( splitPos.isAtEnd() ) {
const newElement = new Element( getNewBlockName( doc, parent, defaultBlockName ) );

batch.insert( Position.createAfter( parent ), newElement );

selection.collapse( newElement );
} else {
// TODO After ckeditor5-engine#340 is fixed we'll be able to base on splitPos's location.
const endPos = LivePosition.createFromPosition( splitPos );
endPos.stickiness = 'STICKS_TO_NEXT';

batch.split( splitPos );

selection.collapse( endPos );

endPos.detach();
}
}

function getNewBlockName( doc, startElement, defaultBlockName ) {
if ( doc.schema.check( { name: defaultBlockName, inside: startElement.parent.name } ) ) {
return defaultBlockName;
}

return startElement.name;
}
80 changes: 80 additions & 0 deletions tests/enter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

'use strict';

import Editor from '/ckeditor5/editor.js';
import StandardCreator from '/ckeditor5/creator/standardcreator.js';
import Enter from '/ckeditor5/enter/enter.js';
import EnterCommand from '/ckeditor5/enter/entercommand.js';
import DomEventData from '/ckeditor5/engine/treeview/observer/domeventdata.js';
import { getCode } from '/ckeditor5/utils/keyboard.js';

describe( 'Enter feature', () => {
let editor, editingView;

beforeEach( () => {
editor = new Editor( null, {
creator: StandardCreator,
features: [ Enter ]
} );

return editor.init()
.then( () => {
editor.document.createRoot( 'main' );
editingView = editor.editing.view;
} );
} );

it( 'creates the commands', () => {
expect( editor.commands.get( 'enter' ) ).to.be.instanceof( EnterCommand );
} );

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

view.fire( 'enter', new DomEventData( editingView, domEvt ) );

expect( spy.calledOnce ).to.be.true;
expect( spy.calledWithExactly( 'enter' ) ).to.be.true;

expect( domEvt.preventDefault.calledOnce ).to.be.true;
} );

describe( 'enter event', () => {
it( 'is fired on keydown', () => {
const view = editor.editing.view;
const spy = sinon.spy();

view.on( 'enter', spy );

view.fire( 'keydown', new DomEventData( editingView, getDomEvent(), {
keyCode: getCode( 'enter' )
} ) );

expect( spy.calledOnce ).to.be.true;
} );
it( 'is not fired on keydown when keyCode does not match enter', () => {
const view = editor.editing.view;
const spy = sinon.spy();

view.on( 'enter', spy );

view.fire( 'keydown', new DomEventData( editingView, getDomEvent(), {
keyCode: 1
} ) );

expect( spy.calledOnce ).to.be.false;
} );
} );

function getDomEvent() {
return {
preventDefault: sinon.spy()
};
}
} );
Loading

0 comments on commit 7ff0543

Please sign in to comment.