8
0
Просмотр исходного кода

Brought support for RTL content in the bindTwoStepCaretToAttribute() helper.

Aleksander Nowodzinski 6 лет назад
Родитель
Сommit
fdab118fe2

+ 22 - 9
packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js

@@ -11,12 +11,15 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 
 /**
- * This helper enabled the two-step caret (phantom) movement behavior for the given {@link module:engine/model/model~Model}
+ * 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.
  *
  * 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
+ * but for the sake of simplicity examples showcase only left–to–right use–cases.
+ *
  * # Forward movement
  *
  * ## "Entering" an attribute:
@@ -83,8 +86,10 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
  * @param {module:utils/dom/emittermixin~Emitter} emitter The emitter to which this behavior should be added
  * (e.g. a plugin instance).
  * @param {String} attribute Attribute for which this behavior will be added.
+ * @param {String} contentDirection Either "ltr" or "rtl" depending on the editor content direction.
+ * Please refer to the {@link module:utils/locale~Locale editor locale} class to learn more.
  */
-export default function bindTwoStepCaretToAttribute( view, model, emitter, attribute ) {
+export default function bindTwoStepCaretToAttribute( view, model, emitter, attribute, contentDirection ) {
 	const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
 	const modelSelection = model.document.selection;
 
@@ -122,13 +127,21 @@ export default function bindTwoStepCaretToAttribute( view, model, emitter, attri
 		const position = modelSelection.getFirstPosition();
 		let isMovementHandled;
 
-		if ( arrowRightPressed ) {
-			isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
+		if ( contentDirection === 'rtl' ) {
+			if ( arrowRightPressed ) {
+				isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
+			} else {
+				isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
+			}
 		} else {
-			isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
+			if ( arrowRightPressed ) {
+				isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
+			} else {
+				isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
+			}
 		}
 
-		// Stop the keydown event if the two-step arent movement handled it. Avoid collisions
+		// 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();
@@ -137,13 +150,13 @@ export default function bindTwoStepCaretToAttribute( view, model, emitter, attri
 }
 
 /**
- * This is a private helper–class for {@link module:engine/utils/bindtwostepcarettoattribute}.
+ * This is a protected helper–class for {@link module:engine/utils/bindtwostepcarettoattribute}.
  * It handles the state of the 2-step caret movement for a single {@link module:engine/model/model~Model}
  * attribute upon the `keypress` in the {@link module:engine/view/view~View}.
  *
- * @private
+ * @protected
  */
-class TwoStepCaretHandler {
+export class TwoStepCaretHandler {
 	/*
 	 * Creates two step handler instance.
 	 *

+ 82 - 3
packages/ckeditor5-engine/tests/utils/bindtwostepcarettoattribute.js

@@ -9,8 +9,9 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
 import DomEventData from '../../src/view/observer/domeventdata';
 import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
-import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
+import bindTwoStepCaretToAttribute, { TwoStepCaretHandler } from '../../src/utils/bindtwostepcarettoattribute';
 import Position from '../../src/model/position';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { setData } from '../../src/dev-utils/model';
 
@@ -18,6 +19,8 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 	let editor, model, emitter, selection, view;
 	let preventDefaultSpy, evtStopSpy;
 
+	testUtils.createSinonSandbox();
+
 	beforeEach( () => {
 		emitter = Object.create( DomEmitterMixin );
 
@@ -41,7 +44,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 			editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
-			bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'a' );
+			bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'a', 'ltr' );
 		} );
 	} );
 
@@ -550,7 +553,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 
 	describe( 'multiple attributes', () => {
 		beforeEach( () => {
-			bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'c' );
+			bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'c', 'ltr' );
 		} );
 
 		it( 'should work with the two-step caret movement (moving right)', () => {
@@ -743,6 +746,82 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		expect( getSelectionAttributesArray( selection ) ).to.have.members( [] );
 	} );
 
+	describe.only( 'left–to–right and right–to–left content', () => {
+		it( 'should call methods associated with the keys (LTR content direction)', () => {
+			const forwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleForwardMovement' );
+			const backwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleBackwardMovement' );
+
+			setData( model, '<$text>foo[]</$text><$text a="true">bar</$text>' );
+
+			fireKeyDownEvent( {
+				keyCode: keyCodes.arrowright
+			} );
+
+			sinon.assert.calledOnce( forwardStub );
+			sinon.assert.notCalled( backwardStub );
+
+			setData( model, '<$text>foo</$text><$text a="true">[]bar</$text>' );
+
+			fireKeyDownEvent( {
+				keyCode: keyCodes.arrowleft
+			} );
+
+			sinon.assert.calledOnce( backwardStub );
+			sinon.assert.calledOnce( forwardStub );
+		} );
+
+		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()
+				.then( newEditor => {
+					model = newEditor.model;
+					selection = model.document.selection;
+					view = newEditor.editing.view;
+
+					newEditor.model.schema.extend( '$text', {
+						allowAttributes: [ 'a', 'b', 'c' ],
+						allowIn: '$root'
+					} );
+
+					model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: 'a' } );
+					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'b', model: 'b' } );
+					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
+					newEditor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+					bindTwoStepCaretToAttribute( newEditor.editing.view, newEditor.model, emitter, 'a', 'rtl' );
+
+					return newEditor;
+				} )
+				.then( newEditor => {
+					setData( model, '<$text>foo[]</$text><$text a="true">bar</$text>' );
+
+					fireKeyDownEvent( {
+						keyCode: keyCodes.arrowleft
+					} );
+
+					sinon.assert.calledOnce( forwardStub );
+					sinon.assert.notCalled( backwardStub );
+
+					setData( model, '<$text>foo</$text><$text a="true">[]bar</$text>' );
+
+					fireKeyDownEvent( {
+						keyCode: keyCodes.arrowright
+					} );
+
+					sinon.assert.calledOnce( backwardStub );
+					sinon.assert.calledOnce( forwardStub );
+
+					return newEditor.destroy();
+				} );
+		} );
+	} );
+
 	const keyMap = {
 		'→': 'arrowright',
 		'←': 'arrowleft'