-
Notifications
You must be signed in to change notification settings - Fork 7
T/48 #71
Changes from 5 commits
0da72ce
19f7f83
42eb5c1
c755227
cd737ae
0d64eeb
59885b8
3b2e801
69e4f0b
4367f57
4dbf13b
966fb08
e2c4ad2
07fdf0a
7ce5362
5690acf
8b04412
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,13 +8,13 @@ | |
*/ | ||
|
||
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. | ||
|
@@ -28,36 +28,19 @@ export default class Input extends Plugin { | |
init() { | ||
const editor = this.editor; | ||
const editingView = editor.editing.view; | ||
const inputCommand = new InputCommand( editor ); | ||
|
||
/** | ||
* 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 ); | ||
|
||
// 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 | ||
|
@@ -72,29 +55,28 @@ export default class Input extends Plugin { | |
* | ||
* @private | ||
* @param {module:engine/view/observer/keyobserver~KeyEventData} evtData | ||
* @param {typing.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 | ||
* @param {Array.<module:engine/view/document~MutatatedText|module:engine/view/document~MutatatedChildren>} mutations | ||
* @param {module:engine/controller/editingcontroller~EditingController} 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 ); | ||
} | ||
} | ||
|
||
|
@@ -107,45 +89,36 @@ 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 | ||
* @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/document~MutatatedText|module:engine/view/document~MutatatedChildren>} mutations | ||
*/ | ||
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 ) { | ||
|
@@ -211,20 +184,11 @@ class MutationHandler { | |
const viewPos = new ViewPosition( mutation.node, firstChangeAt ); | ||
const modelPos = this.editing.mapper.toModelPosition( viewPos ); | ||
|
||
// 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 = mutation.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: mutation.newText.substr( firstChangeAt, insertions ), | ||
range: deletions > 0 ? ModelRange.createFromPositionAndShift( modelPos, deletions ) : null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move the consts out of this object literal. Those variables can make the code more readable (and easier to debug) thanks to their names. |
||
selectionAnchor: modelSelectionPosition | ||
} ); | ||
} | ||
|
||
_handleTextNodeInsertion( mutation ) { | ||
|
@@ -255,29 +219,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 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 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 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 in case there are some. | ||
text: insertedText.replace( /\u00A0/g, ' ' ), | ||
range: new ModelRange( modelPos ) | ||
} ); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/** | ||
* @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 core.command.Command | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Old notation. Now should be |
||
*/ | ||
export default class InputCommand extends Command { | ||
/** | ||
* Creates an instance of the command. | ||
* | ||
* @param {module:core/editor/editor~Editor} editor | ||
*/ | ||
constructor( editor ) { | ||
super( editor ); | ||
|
||
/** | ||
* Typing's change buffer used to group subsequent changes into batches. | ||
* | ||
* @protected | ||
* @member {typing.ChangeBuffer} #_buffer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Old type notation as well. |
||
*/ | ||
this._buffer = new ChangeBuffer( editor.document, editor.config.get( 'typing.undoStep' ) || 20 ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be given to the command as an option, not taken by it from the editor. A class like a command should depend on the world in as little details as possible. |
||
|
||
// TODO The above default configuration value should be defined using editor.config.define() once it's fixed. | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
destroy() { | ||
super.destroy(); | ||
|
||
this._buffer.destroy(); | ||
this._buffer = null; | ||
} | ||
|
||
/** | ||
* Returns the current buffer. | ||
* | ||
* @type {typing.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=null] Range in which the text is inserted. Defaults | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that the range could default to first selection's range if not passed. Will be pretty useful. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does, it is stated in the comment: Range in which the text is inserted. Defaults to first range in the current selection. However, it is no stated in the param specs, should it be like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes perfect sense, I'll go with first one. |
||
* to first range in the current selection. | ||
* @param {module:engine/model/position~Position} [options.selectionAnchor] Selection anchor which will be used | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This command should not need to accept the selection anchor (I don't even know how it's used). It should calculate the result selection position based on the given target range to remove and text which it's inserting. At least... I hope that it can be done this way ;> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For simple case it works as you described. But there are cases like e.g. spell checking and probably it was the reason that modelSelectionPosition was introduced at the first place. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you replace I understand that this may not be fully intuitive at first, but think about this from OT perspective. If you're fixing the word's "athat" spelling you're replacing it with "that". The whole word, not just a single letter. This may be crucial when resolving some conflicting changes and, in general, is more semantical. Hence, try to get rid of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I was relying too much on what information we already have calculated based on mutations. For case described above the mutation provided by mutation observer is:
I assume based only on this information it is not possible to reliable calculate the range and text which will be passed to The other spellchecking cases to consider are: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, forgot about |
||
* to set selection on a data model. | ||
*/ | ||
_doExecute( options = {} ) { | ||
const doc = this.editor.document; | ||
const range = options.range || doc.selection.getFirstRange(); | ||
const text = options.text || ''; | ||
const selectionAnchor = options.selectionAnchor; | ||
let textInsertions = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it needed? |
||
|
||
if ( range ) { | ||
doc.enqueueChanges( () => { | ||
const isCollapsedRange = range.isCollapsed; | ||
|
||
if ( !isCollapsedRange ) { | ||
this._buffer.batch.remove( range ); | ||
} | ||
|
||
if ( text ) { | ||
textInsertions = text.length; | ||
this._buffer.batch.weakInsert( range.start, text ); | ||
} | ||
|
||
if ( selectionAnchor ) { | ||
this.editor.data.model.selection.collapse( selectionAnchor ); | ||
} 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 ); | ||
} ); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong type.