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

T/48 #71

Merged
merged 17 commits into from
Mar 3, 2017
Merged

T/48 #71

Show file tree
Hide file tree
Changes from all commits
Commits
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
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