diff --git a/src/utils/bindtwostepcarettoattribute.js b/src/utils/bindtwostepcarettoattribute.js
index 601c208ae..e921305d8 100644
--- a/src/utils/bindtwostepcarettoattribute.js
+++ b/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 (→) and left (←) 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:
@@ -78,13 +81,15 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
*
* <$text a="true">ba{}r$text>b{}az
*
- * @param {module:engine/view/view~View} view View controller instance.
- * @param {module:engine/model/model~Model} model Data model instance.
- * @param {module:utils/dom/emittermixin~Emitter} emitter The emitter to which this behavior should be added
+ * @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} attribute Attribute for which this behavior will be added.
+ * @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 ) {
+export default function bindTwoStepCaretToAttribute( { view, model, emitter, attribute, locale } ) {
const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
const modelSelection = model.document.selection;
@@ -120,15 +125,16 @@ export default function bindTwoStepCaretToAttribute( view, model, emitter, attri
}
const position = modelSelection.getFirstPosition();
+ const contentDirection = locale.contentLanguageDirection;
let isMovementHandled;
- if ( arrowRightPressed ) {
+ if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
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 +143,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.
*
diff --git a/tests/manual/tickets/1301/1.js b/tests/manual/tickets/1301/1.js
index 7fb2da070..65434e236 100644
--- a/tests/manual/tickets/1301/1.js
+++ b/tests/manual/tickets/1301/1.js
@@ -21,7 +21,13 @@ ClassicEditor
.then( editor => {
const bold = editor.plugins.get( Bold );
- bindTwoStepCaretToAttribute( editor.editing.view, editor.model, bold, 'bold' );
+ bindTwoStepCaretToAttribute( {
+ view: editor.editing.view,
+ model: editor.model,
+ emitter: bold,
+ attribute: 'bold',
+ locale: editor.locale
+ } );
} )
.catch( err => {
console.error( err.stack );
diff --git a/tests/manual/two-step-caret.html b/tests/manual/two-step-caret.html
index 46f8315e9..1af285f20 100644
--- a/tests/manual/two-step-caret.html
+++ b/tests/manual/two-step-caret.html
@@ -1,5 +1,25 @@
-
+
Left-to–right content
+
+
Foo bar biz
Foo barbiz buz?
Foo bar biz
+
+
Right–to–left content
+
+
+
היא תכונה של
+
זהה לזהשיוצג בתוצאה
+
וכדומה. תכונה זו מאפיינת
+
+
+
diff --git a/tests/manual/two-step-caret.js b/tests/manual/two-step-caret.js
index 5fef68471..0c351b379 100644
--- a/tests/manual/two-step-caret.js
+++ b/tests/manual/two-step-caret.js
@@ -15,7 +15,7 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
ClassicEditor
- .create( document.querySelector( '#editor' ), {
+ .create( document.querySelector( '#editor-ltr' ), {
plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
} )
@@ -23,8 +23,51 @@ ClassicEditor
const bold = editor.plugins.get( Italic );
const underline = editor.plugins.get( Underline );
- bindTwoStepCaretToAttribute( editor.editing.view, editor.model, bold, 'italic' );
- bindTwoStepCaretToAttribute( editor.editing.view, editor.model, underline, 'underline' );
+ 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
+ } );
+ } )
+ .catch( err => {
+ console.error( err.stack );
+ } );
+
+ClassicEditor
+ .create( document.querySelector( '#editor-rtl' ), {
+ language: {
+ content: 'he'
+ },
+ plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
+ toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
+ } )
+ .then( editor => {
+ const bold = editor.plugins.get( Italic );
+ const underline = editor.plugins.get( Underline );
+
+ 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
+ } );
} )
.catch( err => {
console.error( err.stack );
diff --git a/tests/manual/two-step-caret.md b/tests/manual/two-step-caret.md
index a2c79fcea..ef8fa44c5 100644
--- a/tests/manual/two-step-caret.md
+++ b/tests/manual/two-step-caret.md
@@ -47,3 +47,10 @@
### Not bounded attribute
Just make sure that two-steps caret movement is disabled for bold text from the third paragraph.
+
+### Right–to–left content
+
+**Tip**: Change the system keyboard to Hebrew before testing.
+
+Two-steps caret movement should also work when the content is right–to–left. Repeat all previous steps keeping in mind that the flow of the text is "reversed".
+
diff --git a/tests/utils/bindtwostepcarettoattribute.js b/tests/utils/bindtwostepcarettoattribute.js
index f2bdd2576..55b7f66b1 100644
--- a/tests/utils/bindtwostepcarettoattribute.js
+++ b/tests/utils/bindtwostepcarettoattribute.js
@@ -9,15 +9,18 @@ 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';
describe( 'bindTwoStepCaretToAttribute()', () => {
- let editor, model, emitter, selection, view;
+ let editor, model, emitter, selection, view, locale;
let preventDefaultSpy, evtStopSpy;
+ testUtils.createSinonSandbox();
+
beforeEach( () => {
emitter = Object.create( DomEmitterMixin );
@@ -26,6 +29,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
model = editor.model;
selection = model.document.selection;
view = editor.editing.view;
+ locale = editor.locale;
preventDefaultSpy = sinon.spy();
evtStopSpy = sinon.spy();
@@ -41,7 +45,13 @@ 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( {
+ view: editor.editing.view,
+ model: editor.model,
+ emitter,
+ attribute: 'a',
+ locale
+ } );
} );
} );
@@ -550,7 +560,13 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
describe( 'multiple attributes', () => {
beforeEach( () => {
- bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'c' );
+ bindTwoStepCaretToAttribute( {
+ view: editor.editing.view,
+ model: editor.model,
+ emitter,
+ attribute: 'c',
+ locale
+ } );
} );
it( 'should work with the two-step caret movement (moving right)', () => {
@@ -743,6 +759,93 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
expect( getSelectionAttributesArray( selection ) ).to.have.members( [] );
} );
+ describe( '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( {
+ language: {
+ content: 'ar'
+ }
+ } )
+ .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( {
+ view: newEditor.editing.view,
+ model: newEditor.model,
+ emitter,
+ attribute: 'a',
+ locale: newEditor.locale
+ } );
+
+ 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'