浏览代码

Change the bindTwoStepCaretToAttribute util to the TwoStepCaretMovement plugin. Closes #7444.

Tomek Wytrębowicz 5 年之前
父节点
当前提交
80a872929b

+ 10 - 9
packages/ckeditor5-link/src/linkediting.js

@@ -9,7 +9,7 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
-import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-typing/src/twostepcaretmovement';
+import TwoStepCaretMovement from '@ckeditor/ckeditor5-typing/src/twostepcaretmovement';
 import LinkCommand from './linkcommand';
 import UnlinkCommand from './unlinkcommand';
 import AutomaticDecorators from './utils/automaticdecorators';
@@ -43,6 +43,13 @@ export default class LinkEditing extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
+	static get requires() {
+		return [ TwoStepCaretMovement ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	constructor( editor ) {
 		super( editor );
 
@@ -56,7 +63,6 @@ export default class LinkEditing extends Plugin {
 	 */
 	init() {
 		const editor = this.editor;
-		const locale = editor.locale;
 
 		// Allow link attribute on all inline nodes.
 		editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
@@ -93,13 +99,8 @@ export default class LinkEditing extends Plugin {
 		this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) );
 
 		// Enable two-step caret movement for `linkHref` attribute.
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: this,
-			attribute: 'linkHref',
-			locale
-		} );
+		const twoStepCaretMovementPlugin = editor.plugins.get( TwoStepCaretMovement );
+		twoStepCaretMovementPlugin.registerAttribute( 'linkHref' );
 
 		// Setup highlight over selected link.
 		this._setupLinkHighlight();

+ 1 - 1
packages/ckeditor5-link/tests/linkediting.js

@@ -75,7 +75,7 @@ describe( 'LinkEditing', () => {
 		expect( model.schema.checkAttribute( [ '$block' ], 'linkHref' ) ).to.be.false;
 	} );
 
-	// Let's check only the minimum to not duplicate `bindTwoStepCaretToAttribute()` tests.
+	// Let's check only the minimum to not duplicate `TwoStepCaretMovement` tests.
 	// Testing minimum is better than testing using spies that might give false positive results.
 	describe( 'two-step caret movement', () => {
 		it( 'should be bound to the `linkHref` attribute (LTR)', () => {

+ 106 - 57
packages/ckeditor5-typing/src/twostepcaretmovement.js

@@ -7,24 +7,27 @@
  * @module typing/twostepcaretmovement
  */
 
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 
 /**
- * This helper enables the two-step caret (phantom) movement behavior for the given {@link module:engine/model/model~Model}
- * attribute on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
+ * This plugin enables the two-step caret (phantom) movement behavior for
+ * {@link module:typing/twostepcaretmovement~TwoStepCaretMovement#registerAttribute registered attributes}
+ * on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
  *
  * Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
  * beginning/end of an attribute.
  *
- * **Note:** This helper support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
+ * **Note:** This class support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
  * but for the sake of simplicity examples showcase only left–to–right use–cases.
  *
  * # Forward movement
  *
  * ## "Entering" an attribute:
  *
- * When this behavior is enabled for the `a` attribute and the selection is right before it
+ * When this plugin is enabled and registered for the `a` attribute and the selection is right before it
  * (at the attribute boundary), pressing the right arrow key will not move the selection but update its
  * attributes accordingly:
  *
@@ -80,67 +83,113 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
  *   <kbd>←</kbd>
  *
  *   		<$text a="true">ba{}r</$text>b{}az
- *
- * @param {Object} options Helper options.
- * @param {module:engine/view/view~View} options.view View controller instance.
- * @param {module:engine/model/model~Model} options.model Data model instance.
- * @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior should be added
- * (e.g. a plugin instance).
- * @param {String} options.attribute Attribute for which this behavior will be added.
- * @param {module:utils/locale~Locale} options.locale The {@link module:core/editor/editor~Editor#locale} instance.
  */
-export default function bindTwoStepCaretToAttribute( { view, model, emitter, attribute, locale } ) {
-	const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
-	const modelSelection = model.document.selection;
-
-	// Listen to keyboard events and handle the caret movement according to the 2-step caret logic.
-	//
-	// Note: This listener has the "high+1" priority:
-	// * "high" because of the filler logic implemented in the renderer which also engages on #keydown.
-	// When the gravity is overridden the attributes of the (model) selection attributes are reset.
-	// It may end up with the filler kicking in and breaking the selection.
-	// * "+1" because we would like to avoid collisions with other features (like Widgets), which
-	// take over the keydown events with the "high" priority. Two-step caret movement takes precedence
-	// over Widgets in that matter.
-	//
-	// Find out more in https://github.com/ckeditor/ckeditor5-engine/issues/1301.
-	emitter.listenTo( view.document, 'keydown', ( evt, data ) => {
-		// This implementation works only for collapsed selection.
-		if ( !modelSelection.isCollapsed ) {
-			return;
-		}
+export class TwoStepCaretMovement extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TwoStepCaretMovement';
+	}
 
-		// When user tries to expand the selection or jump over the whole word or to the beginning/end then
-		// two-steps movement is not necessary.
-		if ( data.shiftKey || data.altKey || data.ctrlKey ) {
-			return;
-		}
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
 
-		const arrowRightPressed = data.keyCode == keyCodes.arrowright;
-		const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
+		/**
+		 * A map of handlers for each attribute.
+		 *
+		 * @protected
+		 * @property {module:typing/twostepcaretmovement~TwoStepCaretMovement}
+		 */
+		this._handlers = new Map();
+	}
 
-		// When neither left or right arrow has been pressed then do noting.
-		if ( !arrowRightPressed && !arrowLeftPressed ) {
-			return;
-		}
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const model = editor.model;
+		const view = editor.editing.view;
+		const locale = editor.locale;
 
-		const position = modelSelection.getFirstPosition();
-		const contentDirection = locale.contentLanguageDirection;
-		let isMovementHandled;
+		// const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
+		const modelSelection = model.document.selection;
 
-		if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
-			isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
-		} else {
-			isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
-		}
+		// Listen to keyboard events and handle the caret movement according to the 2-step caret logic.
+		//
+		// Note: This listener has the "high+1" priority:
+		// * "high" because of the filler logic implemented in the renderer which also engages on #keydown.
+		// When the gravity is overridden the attributes of the (model) selection attributes are reset.
+		// It may end up with the filler kicking in and breaking the selection.
+		// * "+1" because we would like to avoid collisions with other features (like Widgets), which
+		// take over the keydown events with the "high" priority. Two-step caret movement takes precedence
+		// over Widgets in that matter.
+		//
+		// Find out more in https://github.com/ckeditor/ckeditor5-engine/issues/1301.
+		this.listenTo( view.document, 'keydown', ( evt, data ) => {
+			// Do nothing if the plugin is disabled.
+			// if ( !this.isEnabled ) {
+			// 	return;
+			// }
+
+			// This implementation works only for collapsed selection.
+			if ( !modelSelection.isCollapsed ) {
+				return;
+			}
 
-		// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
-		// with other features which may also take over the caret movement (e.g. Widget).
-		if ( isMovementHandled ) {
-			evt.stop();
-		}
-	}, { priority: priorities.get( 'high' ) + 1 } );
+			// When user tries to expand the selection or jump over the whole word or to the beginning/end then
+			// two-steps movement is not necessary.
+			if ( data.shiftKey || data.altKey || data.ctrlKey ) {
+				return;
+			}
+
+			const arrowRightPressed = data.keyCode == keyCodes.arrowright;
+			const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
+
+			// When neither left or right arrow has been pressed then do noting.
+			if ( !arrowRightPressed && !arrowLeftPressed ) {
+				return;
+			}
+
+			const position = modelSelection.getFirstPosition();
+			const contentDirection = locale.contentLanguageDirection;
+			let isMovementHandled = false;
+
+			if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
+				for ( const [ , handler ] of this._handlers ) {
+					isMovementHandled = isMovementHandled || handler.handleForwardMovement( position, data );
+				}
+			} else {
+				for ( const [ , handler ] of this._handlers ) {
+					isMovementHandled = isMovementHandled || handler.handleBackwardMovement( position, data );
+				}
+			}
+
+			// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
+			// with other features which may also take over the caret movement (e.g. Widget).
+			if ( isMovementHandled ) {
+				evt.stop();
+			}
+		}, { priority: priorities.get( 'high' ) + 1 } );
+	}
+
+	/**
+	 * Registers a given attribute for the two-step caret movement.
+	 *
+	 * @param {String} attribute Name of the attribute to handle.
+	 */
+	registerAttribute( attribute ) {
+		this._handlers.set(
+			attribute,
+			new TwoStepCaretHandler( this.editor.model, this, attribute )
+		);
+	}
 }
+export default TwoStepCaretMovement;
 
 /**
  * This is a protected helper–class for {@link module:typing/twostepcaretmovement}.

+ 4 - 10
packages/ckeditor5-typing/tests/manual/1301/1.js

@@ -11,23 +11,17 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 
-import bindTwoStepCaretToAttribute from '../../src/twostepcaretmovement';
+import TwoStepCaretMovement from '../../../src/twostepcaretmovement';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ Essentials, Paragraph, Bold, Italic ],
+		plugins: [ Essentials, Paragraph, Bold, Italic, TwoStepCaretMovement ],
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ]
 	} )
 	.then( editor => {
-		const bold = editor.plugins.get( Bold );
+		const twoStepCaretMovement = editor.plugins.get( TwoStepCaretMovement );
 
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: bold,
-			attribute: 'bold',
-			locale: editor.locale
-		} );
+		twoStepCaretMovement.registerAttribute( 'bold' );
 	} )
 	.catch( err => {
 		console.error( err.stack );

+ 9 - 35
packages/ckeditor5-typing/tests/manual/two-step-caret.js

@@ -12,31 +12,18 @@ import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 
-import bindTwoStepCaretToAttribute from '../../src/twostepcaretmovement';
+import TwoStepCaretMovement from '../../src/twostepcaretmovement';
 
 ClassicEditor
 	.create( document.querySelector( '#editor-ltr' ), {
-		plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
+		plugins: [ Essentials, Paragraph, Underline, Bold, Italic, TwoStepCaretMovement ],
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
 	} )
 	.then( editor => {
-		const bold = editor.plugins.get( Italic );
-		const underline = editor.plugins.get( Underline );
+		const twoStepCaretMovement = editor.plugins.get( TwoStepCaretMovement );
 
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: bold,
-			attribute: 'italic',
-			locale: editor.locale
-		} );
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: underline,
-			attribute: 'underline',
-			locale: editor.locale
-		} );
+		twoStepCaretMovement.registerAttribute( 'italic' );
+		twoStepCaretMovement.registerAttribute( 'underline' );
 	} )
 	.catch( err => {
 		console.error( err.stack );
@@ -47,27 +34,14 @@ ClassicEditor
 		language: {
 			content: 'he'
 		},
-		plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
+		plugins: [ Essentials, Paragraph, Underline, Bold, Italic, TwoStepCaretMovement ],
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
 	} )
 	.then( editor => {
-		const bold = editor.plugins.get( Italic );
-		const underline = editor.plugins.get( Underline );
+		const twoStepCaretMovement = editor.plugins.get( TwoStepCaretMovement );
 
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: bold,
-			attribute: 'italic',
-			locale: editor.locale
-		} );
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: underline,
-			attribute: 'underline',
-			locale: editor.locale
-		} );
+		twoStepCaretMovement.registerAttribute( 'italic' );
+		twoStepCaretMovement.registerAttribute( 'underline' );
 	} )
 	.catch( err => {
 		console.error( err.stack );

+ 9 - 27
packages/ckeditor5-typing/tests/twostepcaretmovement.js

@@ -9,14 +9,14 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
-import bindTwoStepCaretToAttribute, { TwoStepCaretHandler } from '../src/twostepcaretmovement';
+import { TwoStepCaretMovement, TwoStepCaretHandler } from '../src/twostepcaretmovement';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-describe( 'bindTwoStepCaretToAttribute()', () => {
-	let editor, model, emitter, selection, view, locale;
+describe( 'TwoStepCaretMovement()', () => {
+	let editor, model, emitter, selection, view, plugin;
 	let preventDefaultSpy, evtStopSpy;
 
 	testUtils.createSinonSandbox();
@@ -24,12 +24,12 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 	beforeEach( () => {
 		emitter = Object.create( DomEmitterMixin );
 
-		return VirtualTestEditor.create().then( newEditor => {
+		return VirtualTestEditor.create( { plugins: [ TwoStepCaretMovement ] } ).then( newEditor => {
 			editor = newEditor;
 			model = editor.model;
 			selection = model.document.selection;
 			view = editor.editing.view;
-			locale = editor.locale;
+			plugin = editor.plugins.get( TwoStepCaretMovement );
 
 			preventDefaultSpy = sinon.spy();
 			evtStopSpy = sinon.spy();
@@ -45,13 +45,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 			editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
-			bindTwoStepCaretToAttribute( {
-				view: editor.editing.view,
-				model: editor.model,
-				emitter,
-				attribute: 'a',
-				locale
-			} );
+			plugin.registerAttribute( 'a' );
 		} );
 	} );
 
@@ -560,13 +554,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 
 	describe( 'multiple attributes', () => {
 		beforeEach( () => {
-			bindTwoStepCaretToAttribute( {
-				view: editor.editing.view,
-				model: editor.model,
-				emitter,
-				attribute: 'c',
-				locale
-			} );
+			plugin.registerAttribute( 'c' );
 		} );
 
 		it( 'should work with the two-step caret movement (moving right)', () => {
@@ -786,12 +774,12 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		it( 'should use the opposite helper methods (RTL content direction)', () => {
 			const forwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleForwardMovement' );
 			const backwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleBackwardMovement' );
-			const emitter = Object.create( DomEmitterMixin );
 
 			let model;
 
 			return VirtualTestEditor
 				.create( {
+					plugins: [ TwoStepCaretMovement ],
 					language: {
 						content: 'ar'
 					}
@@ -812,13 +800,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 					newEditor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
-					bindTwoStepCaretToAttribute( {
-						view: newEditor.editing.view,
-						model: newEditor.model,
-						emitter,
-						attribute: 'a',
-						locale: newEditor.locale
-					} );
+					newEditor.plugins.get( TwoStepCaretMovement ).registerAttribute( 'a' );
 
 					return newEditor;
 				} )