瀏覽代碼

Initial implementation.

Piotrek Koszuliński 9 年之前
父節點
當前提交
65c7446331

+ 2 - 2
packages/ckeditor5-typing/src/changebuffer.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-import utils from '../utils/utils.js';
+import count from '../utils/count.js';
 
 /**
  * Change buffer allows to group atomic changes (like characters that have been typed) into
@@ -125,7 +125,7 @@ export default class ChangeBuffer {
 	 */
 	_onBatch( batch ) {
 		// 1 operation means a newly created batch.
-		if ( batch !== this._batch && utils.count( batch.getOperations() ) <= 1 ) {
+		if ( batch !== this._batch && count( batch.getOperations() ) <= 1 ) {
 			this._batch = null;
 		}
 	}

+ 136 - 0
packages/ckeditor5-typing/src/typing.js

@@ -0,0 +1,136 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Feature from '../feature.js';
+import ChangeBuffer from './changebuffer.js';
+import MutationObserver from '../engine/view/observer/mutationobserver.js';
+import KeyObserver from '../engine/view/observer/keyobserver.js';
+import ModelPosition from '../engine/model/position.js';
+import ModelRange from '../engine/model/range.js';
+import ViewPosition from '../engine/view/position.js';
+import diff from '../utils/diff.js';
+import diffToChanges from '../utils/difftochanges.js';
+import { getCode } from '../utils/keyboard.js';
+import { getData } from '/tests/engine/_utils/model.js';
+
+/**
+ * The typing feature. Handles... typing.
+ *
+ * @memberOf typing
+ * @extends ckeditor5.Feature
+ */
+export default class Typing extends Feature {
+	constructor( editor ) {
+		super( editor );
+	}
+
+	init() {
+		const editor = this.editor;
+		const doc = editor.document;
+		const editingView = editor.editing.view;
+
+		this.buffer = new ChangeBuffer( doc, 5 );
+
+		editingView.addObserver( MutationObserver );
+		editingView.addObserver( KeyObserver );
+
+		this.listenTo( editingView, 'keydown', ( evt, data ) => {
+			if ( isSafeKeystroke( data ) || doc.selection.isCollapsed ) {
+				return;
+			}
+
+			doc.enqueueChanges( () => {
+				doc.composer.deleteContents( this.buffer.batch, doc.selection );
+			} );
+
+			data.preventDefault();
+		}, null, 9999 ); // LOWEST
+
+		this.listenTo( editingView, 'mutations', ( evt, mutations ) => {
+			mutations.forEach( mutation => this._handleMutation( mutation ) );
+		} );
+	}
+
+	destroy() {
+		super.destroy();
+
+		this.buffer.destroy();
+		this.buffer = null;
+	}
+
+	_handleMutation( mutation ) {
+		if ( mutation.type == 'children' ) {
+			this._handleChildrenMutation( mutation );
+		} else {
+			this._handleTextMutation( mutation );
+		}
+	}
+
+	_handleChildrenMutation( mutation ) {
+		// TODO Currently it's a bit tricky to implement this piece because quirks are not filtered out
+		// so typing inside <p>^<br></p> will give us "remove <br>, add 'x'".
+
+		console.log( 'Children mutation', mutation ); // jshint ignore:line
+	}
+
+	_handleTextMutation( mutation ) {
+		const doc = this.editor.document;
+		const changes = diffToChanges( diff( mutation.oldText, mutation.newText ), mutation.newText );
+		let lastPos;
+		let insertedCharacters = 0;
+
+		console.log( 'Text mutation', JSON.stringify( changes ) ); // jshint ignore:line
+		console.log( mutation ); // jshint ignore:line
+
+		doc.enqueueChanges( () => {
+			changes
+				.forEach( change => {
+					const viewPos = new ViewPosition( mutation.node, change.index );
+					const modelPos = this.editor.editing.mapper.toModelPosition( viewPos );
+
+					if ( change.type == 'INSERT' ) {
+						const insertedText = change.values.join( '' );
+
+						this.buffer.batch.weakInsert( modelPos, insertedText );
+
+						lastPos = ModelPosition.createAt( modelPos.parent, modelPos.offset + insertedText.length );
+						insertedCharacters += insertedText.length;
+					} else /* if ( change.type == 'DELETE' ) */ {
+						this.buffer.batch.remove( new ModelRange( modelPos, modelPos.getShiftedBy( change.howMany ) ) );
+
+						lastPos = modelPos;
+					}
+				} );
+
+			// Placing the selection right after the last change will work for many cases, but not
+			// for ones like autocorrection or spellchecking. The caret should be placed after the whole piece
+			// which was corrected (e.g. a word), not after the letter that was replaced.
+			doc.selection.collapse( lastPos );
+
+			this.buffer.input( insertedCharacters );
+
+			console.log( getData( doc, { rootName: 'editor' } ) ); // jshint ignore:line
+		} );
+	}
+}
+
+// This is absolutely lame, but it's enough for now.
+
+const safeKeystrokes = [
+	getCode( 'ctrl' ),
+	getCode( 'cmd' ),
+	getCode( 'shift' ),
+	getCode( 'alt' ),
+	getCode( 'arrowUp' ),
+	getCode( 'arrowRight' ),
+	getCode( 'arrowDown' ),
+	getCode( 'arrowLeft' )
+];
+
+function isSafeKeystroke( keyData ) {
+	return safeKeystrokes.indexOf( keyData.keyCode );
+}

+ 3 - 3
packages/ckeditor5-typing/tests/changebuffer.js

@@ -6,9 +6,9 @@
 'use strict';
 
 import ChangeBuffer from '/ckeditor5/typing/changebuffer.js';
-import Document from '/ckeditor5/engine/treemodel/document.js';
-import Batch from '/ckeditor5/engine/treemodel/batch.js';
-import Position from '/ckeditor5/engine/treemodel/position.js';
+import Document from '/ckeditor5/engine/model/document.js';
+import Batch from '/ckeditor5/engine/model/batch.js';
+import Position from '/ckeditor5/engine/model/position.js';
 
 describe( 'ChangeBuffer', () => {
 	const CHANGE_LIMIT = 3;

+ 5 - 0
packages/ckeditor5-typing/tests/manual/base.html

@@ -0,0 +1,5 @@
+<div id="editor">
+	<h1>Hello world!</h1>
+
+	<p>I'm an instance of <a href="http://ckeditor.com">CKEditor</a>.</p>
+</div>

+ 52 - 0
packages/ckeditor5-typing/tests/manual/base.js

@@ -0,0 +1,52 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import CKEDITOR from '/ckeditor.js';
+import ClassicCreator from '/ckeditor5/creator-classic/classiccreator.js';
+import ModelElement from '/ckeditor5/engine/model/element.js';
+import ViewContainerElement from '/ckeditor5/engine/view/containerelement.js';
+// import Text from '/ckeditor5/engine/view/text.js';
+import ModelPosition from '/ckeditor5/engine/model/position.js';
+// import { getData } from '/tests/engine/_utils/model.js';
+import { insertElement } from '/ckeditor5/engine/conversion/model-to-view-converters.js';
+
+CKEDITOR.create( '#editor', {
+	creator: ClassicCreator,
+	features: [ 'typing', 'delete', 'enter' ],
+	toolbar: []
+} )
+.then( editor => {
+	const doc = editor.document;
+	const editingView = editor.editing.view;
+
+	editor.editing.toView.on( 'insert:paragraph', insertElement( new ViewContainerElement( 'p' ) ) );
+
+	doc.batch().insert(
+		new ModelPosition( doc.getRoot( 'editor' ), [ 0 ] ),
+		[
+			new ModelElement( 'paragraph', null, 'foobar' ),
+			new ModelElement( 'paragraph', null, 'barfoo' )
+		]
+	);
+
+	editingView.render();
+
+	// console.log( getData( doc, { rootName: 'editor' } ) );
+
+	// editingView.viewRoots.get( 'editor' ).appendChildren( [
+	// 	new Element( 'h1', null, [ new Text( 'Hello world!' ) ] ),
+	// 	new Element( 'p', null, [ new Element( 'br' ) ] ),
+	// 	new Element( 'p', null, [ new Text( 'I\'m an instance of CKEditor.' ) ] )
+	// ] );
+
+	// editingView.render();
+
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );  // jshint ignore:line
+} );

+ 3 - 0
packages/ckeditor5-typing/tests/manual/base.md

@@ -0,0 +1,3 @@
+@bender-ui: collapsed
+
+TODO