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{}rb{}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 a="true">bar' ); + + fireKeyDownEvent( { + keyCode: keyCodes.arrowright + } ); + + sinon.assert.calledOnce( forwardStub ); + sinon.assert.notCalled( backwardStub ); + + setData( model, '<$text>foo<$text a="true">[]bar' ); + + 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 a="true">bar' ); + + fireKeyDownEvent( { + keyCode: keyCodes.arrowleft + } ); + + sinon.assert.calledOnce( forwardStub ); + sinon.assert.notCalled( backwardStub ); + + setData( model, '<$text>foo<$text a="true">[]bar' ); + + fireKeyDownEvent( { + keyCode: keyCodes.arrowright + } ); + + sinon.assert.calledOnce( backwardStub ); + sinon.assert.calledOnce( forwardStub ); + + return newEditor.destroy(); + } ); + } ); + } ); + const keyMap = { '→': 'arrowright', '←': 'arrowleft'