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 #71 from ckeditor/t/48
Browse files Browse the repository at this point in the history
Feature: Introduced `InputCommand` which can be used to simulate typing. Closes #48.
  • Loading branch information
Reinmar committed Mar 3, 2017
2 parents b8dfcdf + 8b04412 commit cdb7fdf
Show file tree
Hide file tree
Showing 4 changed files with 414 additions and 124 deletions.
127 changes: 43 additions & 84 deletions src/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ChangeBuffer from './changebuffer';
import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
import diff from '@ckeditor/ckeditor5-utils/src/diff';
import diffToChanges from '@ckeditor/ckeditor5-utils/src/difftochanges';
import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
import InputCommand from './inputcommand';

/**
* Handles text input coming from the keyboard or other input methods.
*
* @extends core.Plugin
* @extends module:core/plugin~Plugin
*/
export default class Input extends Plugin {
/**
Expand All @@ -28,36 +28,21 @@ export default class Input extends Plugin {
init() {
const editor = this.editor;
const editingView = editor.editing.view;

/**
* Typing's change buffer used to group subsequent changes into batches.
*
* @protected
* @member {typing.ChangeBuffer} #_buffer
*/
this._buffer = new ChangeBuffer( editor.document, editor.config.get( 'typing.undoStep' ) || 20 );
const inputCommand = new InputCommand( editor, editor.config.get( 'typing.undoStep' ) || 20 );

// TODO The above default configuration value should be defined using editor.config.define() once it's fixed.

editor.commands.set( 'input', inputCommand );

this.listenTo( editingView, 'keydown', ( evt, data ) => {
this._handleKeydown( data );
this._handleKeydown( data, inputCommand.buffer );
}, { priority: 'lowest' } );

this.listenTo( editingView, 'mutations', ( evt, mutations, viewSelection ) => {
this._handleMutations( mutations, viewSelection );
} );
}

/**
* @inheritDoc
*/
destroy() {
super.destroy();

this._buffer.destroy();
this._buffer = null;
}

/**
* Handles the keydown event. We need to guess whether such keystroke is going to result
* in typing. If so, then before character insertion happens, any selected content needs
Expand All @@ -72,29 +57,29 @@ export default class Input extends Plugin {
*
* @private
* @param {module:engine/view/observer/keyobserver~KeyEventData} evtData
* @param {module:typing/changebuffer~ChangeBuffer} buffer
*/
_handleKeydown( evtData ) {
_handleKeydown( evtData, buffer ) {
const doc = this.editor.document;

if ( isSafeKeystroke( evtData ) || doc.selection.isCollapsed ) {
return;
}

doc.enqueueChanges( () => {
this.editor.data.deleteContent( doc.selection, this._buffer.batch );
this.editor.data.deleteContent( doc.selection, buffer.batch );
} );
}

/**
* Handles DOM mutations.
*
* @param {Array.<engine.view.Document~MutatatedText|engine.view.Document~MutatatedChildren>} mutations
* @private
* @param {Array.<module:engine/view/document~MutatatedText|module:engine/view/document~MutatatedChildren>} mutations
* @param {module:engine/view/selection~Selection|null} viewSelection
*/
_handleMutations( mutations, viewSelection ) {
const doc = this.editor.document;
const handler = new MutationHandler( this.editor.editing, this._buffer );

doc.enqueueChanges( () => handler.handle( mutations, viewSelection ) );
new MutationHandler( this.editor ).handle( mutations, viewSelection );
}
}

Expand All @@ -107,45 +92,39 @@ class MutationHandler {
/**
* Creates an instance of the mutation handler.
*
* @param {module:engine/controller/editingcontroller~EditingController} editing
* @param {module:typing/changebuffer~ChangeBuffer} buffer
* @param {module:core/editor/editor~Editor} editor
*/
constructor( editing, buffer ) {
constructor( editor ) {
/**
* The editing controller.
* Editor instance for which mutations are handled.
*
* @member {engine.controller.EditingController} #editing
* @readonly
* @member {module:core/editor/editor~Editor} #editor
*/
this.editing = editing;
this.editor = editor;

/**
* The change buffer.
*
* @member {engine.controller.EditingController} #buffer
*/
this.buffer = buffer;

/**
* The number of inserted characters which need to be fed to the {@link #buffer change buffer}.
* The editing controller.
*
* @member {Number} #insertedCharacterCount
* @readonly
* @member {module:engine/controller/editingcontroller~EditingController} #editing
*/
this.insertedCharacterCount = 0;
this.editing = this.editor.editing;
}

/**
* Handles given mutations.
*
* @param {Array.<engine.view.Document~MutatatedText|engine.view.Document~MutatatedChildren>} mutations
* @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
* module:engine/view/observer/mutationobserver~MutatatedChildren>} mutations
* @param {module:engine/view/selection~Selection|null} viewSelection
*/
handle( mutations, viewSelection ) {
for ( let mutation of mutations ) {
// Fortunately it will never be both.
this._handleTextMutation( mutation, viewSelection );
this._handleTextNodeInsertion( mutation );
}

this.buffer.input( Math.max( this.insertedCharacterCount, 0 ) );
}

_handleTextMutation( mutation, viewSelection ) {
Expand Down Expand Up @@ -210,21 +189,14 @@ class MutationHandler {
// Get the position in view and model where the changes will happen.
const viewPos = new ViewPosition( mutation.node, firstChangeAt );
const modelPos = this.editing.mapper.toModelPosition( viewPos );
const removeRange = ModelRange.createFromPositionAndShift( modelPos, deletions );
const insertText = newText.substr( firstChangeAt, insertions );

// Remove appropriate number of characters from the model text node.
if ( deletions > 0 ) {
const removeRange = ModelRange.createFromPositionAndShift( modelPos, deletions );
this._remove( removeRange, deletions );
}

// Insert appropriate characters basing on `mutation.text`.
const insertedText = newText.substr( firstChangeAt, insertions );
this._insert( modelPos, insertedText );

// If there was `viewSelection` and it got correctly mapped, collapse selection at found model position.
if ( modelSelectionPosition ) {
this.editing.model.selection.collapse( modelSelectionPosition );
}
this.editor.execute( 'input', {
text: insertText,
range: removeRange,
resultPosition: modelSelectionPosition
} );
}

_handleTextNodeInsertion( mutation ) {
Expand Down Expand Up @@ -255,29 +227,16 @@ class MutationHandler {

const viewPos = new ViewPosition( mutation.node, change.index );
const modelPos = this.editing.mapper.toModelPosition( viewPos );
let insertedText = change.values[ 0 ].data;

// Replace &nbsp; inserted by the browser with normal space.
// See comment in `_handleTextMutation`.
// In this case we don't need to do this before `diff` because we diff whole nodes.
// Just change &nbsp; in case there are some.
insertedText = insertedText.replace( /\u00A0/g, ' ' );

this._insert( modelPos, insertedText );

this.editing.model.selection.collapse( modelPos.getShiftedBy( insertedText.length ) );
}

_insert( position, text ) {
this.buffer.batch.weakInsert( position, text );

this.insertedCharacterCount += text.length;
}

_remove( range, length ) {
this.buffer.batch.remove( range );

this.insertedCharacterCount -= length;
const insertedText = change.values[ 0 ].data;

this.editor.execute( 'input', {
// Replace &nbsp; inserted by the browser with normal space.
// See comment in `_handleTextMutation`.
// In this case we don't need to do this before `diff` because we diff whole nodes.
// Just change &nbsp; in case there are some.
text: insertedText.replace( /\u00A0/g, ' ' ),
range: new ModelRange( modelPos )
} );
}
}

Expand Down
97 changes: 97 additions & 0 deletions src/inputcommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module typing/inputcommand
*/

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

/**
* The input command. Used by the {@link module:typing/input~Input input feature} to handle typing.
*
* @extends module:core/command/command~Command
*/
export default class InputCommand extends Command {
/**
* Creates an instance of the command.
*
* @param {module:core/editor/editor~Editor} editor
* @param {Number} undoStepSize The maximum number of atomic changes
* which can be contained in one batch in the command buffer.
*/
constructor( editor, undoStepSize ) {
super( editor );

/**
* Typing's change buffer used to group subsequent changes into batches.
*
* @readonly
* @private
* @member {module:typing/changebuffer~ChangeBuffer} #_buffer
*/
this._buffer = new ChangeBuffer( editor.document, undoStepSize );
}

/**
* @inheritDoc
*/
destroy() {
super.destroy();

this._buffer.destroy();
this._buffer = null;
}

/**
* The current change buffer.
*
* @type {module:typing/changebuffer~ChangeBuffer}
*/
get buffer() {
return this._buffer;
}

/**
* Executes the input command. It replaces the content within the given range with the given text.
* Replacing is a two step process, first content within the range is removed and then new text is inserted
* on the beginning of the range (which after removal is a collapsed range).
*
* @param {Object} [options] The command options.
* @param {String} [options.text=''] Text to be inserted.
* @param {module:engine/model/range~Range} [options.range] Range in which the text is inserted. Defaults
* to the first range in the current selection.
* @param {module:engine/model/position~Position} [options.resultPosition] Position at which the selection
* should be placed after the insertion. If not specified, the selection will be placed right after
* the inserted text.
*/
_doExecute( options = {} ) {
const doc = this.editor.document;
const text = options.text || '';
const textInsertions = text.length;
const range = options.range || doc.selection.getFirstRange();
const resultPosition = options.resultPosition;

doc.enqueueChanges( () => {
const isCollapsedRange = range.isCollapsed;

if ( !isCollapsedRange ) {
this._buffer.batch.remove( range );
}

this._buffer.batch.weakInsert( range.start, text );

if ( resultPosition ) {
this.editor.data.model.selection.collapse( resultPosition );
} else if ( isCollapsedRange ) {
// If range was collapsed just shift the selection by the number of inserted characters.
this.editor.data.model.selection.collapse( range.start.getShiftedBy( textInsertions ) );
}

this._buffer.input( textInsertions );
} );
}
}
Loading

0 comments on commit cdb7fdf

Please sign in to comment.