Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit d57ff5a

Browse files
authored
Merge pull request #1769 from ckeditor/t/ckeditor5/1151
Feature: Brought support for RTL content in the `bindTwoStepCaretToAttribute()` helper. See ckeditor/ckeditor5#1151. BREAKING CHANGE: The `bindTwoStepCaretToAttribute()` helper arguments syntax has changed (replaced by an object). Please refer to the helper documentation to learn more.
2 parents 941c000 + ded4d58 commit d57ff5a

File tree

6 files changed

+205
-20
lines changed

6 files changed

+205
-20
lines changed

src/utils/bindtwostepcarettoattribute.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
1111
import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
1212

1313
/**
14-
* This helper enabled the two-step caret (phantom) movement behavior for the given {@link module:engine/model/model~Model}
14+
* This helper enables the two-step caret (phantom) movement behavior for the given {@link module:engine/model/model~Model}
1515
* attribute on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
1616
*
1717
* Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
1818
* beginning/end of an attribute.
1919
*
20+
* **Note:** This helper support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
21+
* but for the sake of simplicity examples showcase only left–to–right use–cases.
22+
*
2023
* # Forward movement
2124
*
2225
* ## "Entering" an attribute:
@@ -78,13 +81,15 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
7881
*
7982
* <$text a="true">ba{}r</$text>b{}az
8083
*
81-
* @param {module:engine/view/view~View} view View controller instance.
82-
* @param {module:engine/model/model~Model} model Data model instance.
83-
* @param {module:utils/dom/emittermixin~Emitter} emitter The emitter to which this behavior should be added
84+
* @param {Object} options Helper options.
85+
* @param {module:engine/view/view~View} options.view View controller instance.
86+
* @param {module:engine/model/model~Model} options.model Data model instance.
87+
* @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior should be added
8488
* (e.g. a plugin instance).
85-
* @param {String} attribute Attribute for which this behavior will be added.
89+
* @param {String} options.attribute Attribute for which this behavior will be added.
90+
* @param {module:utils/locale~Locale} options.locale The {@link module:core/editor/editor~Editor#locale} instance.
8691
*/
87-
export default function bindTwoStepCaretToAttribute( view, model, emitter, attribute ) {
92+
export default function bindTwoStepCaretToAttribute( { view, model, emitter, attribute, locale } ) {
8893
const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
8994
const modelSelection = model.document.selection;
9095

@@ -120,15 +125,16 @@ export default function bindTwoStepCaretToAttribute( view, model, emitter, attri
120125
}
121126

122127
const position = modelSelection.getFirstPosition();
128+
const contentDirection = locale.contentLanguageDirection;
123129
let isMovementHandled;
124130

125-
if ( arrowRightPressed ) {
131+
if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
126132
isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
127133
} else {
128134
isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
129135
}
130136

131-
// Stop the keydown event if the two-step arent movement handled it. Avoid collisions
137+
// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
132138
// with other features which may also take over the caret movement (e.g. Widget).
133139
if ( isMovementHandled ) {
134140
evt.stop();
@@ -137,13 +143,13 @@ export default function bindTwoStepCaretToAttribute( view, model, emitter, attri
137143
}
138144

139145
/**
140-
* This is a private helper–class for {@link module:engine/utils/bindtwostepcarettoattribute}.
146+
* This is a protected helper–class for {@link module:engine/utils/bindtwostepcarettoattribute}.
141147
* It handles the state of the 2-step caret movement for a single {@link module:engine/model/model~Model}
142148
* attribute upon the `keypress` in the {@link module:engine/view/view~View}.
143149
*
144-
* @private
150+
* @protected
145151
*/
146-
class TwoStepCaretHandler {
152+
export class TwoStepCaretHandler {
147153
/*
148154
* Creates two step handler instance.
149155
*

tests/manual/tickets/1301/1.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ ClassicEditor
2121
.then( editor => {
2222
const bold = editor.plugins.get( Bold );
2323

24-
bindTwoStepCaretToAttribute( editor.editing.view, editor.model, bold, 'bold' );
24+
bindTwoStepCaretToAttribute( {
25+
view: editor.editing.view,
26+
model: editor.model,
27+
emitter: bold,
28+
attribute: 'bold',
29+
locale: editor.locale
30+
} );
2531
} )
2632
.catch( err => {
2733
console.error( err.stack );

tests/manual/two-step-caret.html

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
1-
<div id="editor">
1+
<h2>Left-to–right content</h2>
2+
3+
<div id="editor-ltr">
24
<p>Foo <u>bar</u> biz</p>
35
<p>Foo <u>bar</u><i>biz</i> buz?</p>
46
<p>Foo <b>bar</b> biz</p>
57
</div>
8+
9+
<h2>Right–to–left content</h2>
10+
11+
<div id="editor-rtl">
12+
<p>&nbsp;היא <u>תכונה</u> של</p>
13+
<p>&nbsp;זהה <i>לזה</i><u>שיוצג</u> בתוצאה</p>
14+
<p>וכדומה. <strong>תכונה</strong> זו מאפיינת</p>
15+
</div>
16+
17+
<style>
18+
u {
19+
background: rgba(255,0,0,.3);
20+
}
21+
22+
i {
23+
background: rgba(0,255,0,.3);
24+
}
25+
</style>

tests/manual/two-step-caret.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,59 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
1515
import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
1616

1717
ClassicEditor
18-
.create( document.querySelector( '#editor' ), {
18+
.create( document.querySelector( '#editor-ltr' ), {
1919
plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
2020
toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
2121
} )
2222
.then( editor => {
2323
const bold = editor.plugins.get( Italic );
2424
const underline = editor.plugins.get( Underline );
2525

26-
bindTwoStepCaretToAttribute( editor.editing.view, editor.model, bold, 'italic' );
27-
bindTwoStepCaretToAttribute( editor.editing.view, editor.model, underline, 'underline' );
26+
bindTwoStepCaretToAttribute( {
27+
view: editor.editing.view,
28+
model: editor.model,
29+
emitter: bold,
30+
attribute: 'italic',
31+
locale: editor.locale
32+
} );
33+
bindTwoStepCaretToAttribute( {
34+
view: editor.editing.view,
35+
model: editor.model,
36+
emitter: underline,
37+
attribute: 'underline',
38+
locale: editor.locale
39+
} );
40+
} )
41+
.catch( err => {
42+
console.error( err.stack );
43+
} );
44+
45+
ClassicEditor
46+
.create( document.querySelector( '#editor-rtl' ), {
47+
language: {
48+
content: 'he'
49+
},
50+
plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
51+
toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
52+
} )
53+
.then( editor => {
54+
const bold = editor.plugins.get( Italic );
55+
const underline = editor.plugins.get( Underline );
56+
57+
bindTwoStepCaretToAttribute( {
58+
view: editor.editing.view,
59+
model: editor.model,
60+
emitter: bold,
61+
attribute: 'italic',
62+
locale: editor.locale
63+
} );
64+
bindTwoStepCaretToAttribute( {
65+
view: editor.editing.view,
66+
model: editor.model,
67+
emitter: underline,
68+
attribute: 'underline',
69+
locale: editor.locale
70+
} );
2871
} )
2972
.catch( err => {
3073
console.error( err.stack );

tests/manual/two-step-caret.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,10 @@
4747

4848
### Not bounded attribute
4949
Just make sure that two-steps caret movement is disabled for bold text from the third paragraph.
50+
51+
### Right–to–left content
52+
53+
**Tip**: Change the system keyboard to Hebrew before testing.
54+
55+
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".
56+

tests/utils/bindtwostepcarettoattribute.js

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
99
import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
1010
import DomEventData from '../../src/view/observer/domeventdata';
1111
import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
12-
import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
12+
import bindTwoStepCaretToAttribute, { TwoStepCaretHandler } from '../../src/utils/bindtwostepcarettoattribute';
1313
import Position from '../../src/model/position';
14+
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
1415
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
1516
import { setData } from '../../src/dev-utils/model';
1617

1718
describe( 'bindTwoStepCaretToAttribute()', () => {
18-
let editor, model, emitter, selection, view;
19+
let editor, model, emitter, selection, view, locale;
1920
let preventDefaultSpy, evtStopSpy;
2021

22+
testUtils.createSinonSandbox();
23+
2124
beforeEach( () => {
2225
emitter = Object.create( DomEmitterMixin );
2326

@@ -26,6 +29,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
2629
model = editor.model;
2730
selection = model.document.selection;
2831
view = editor.editing.view;
32+
locale = editor.locale;
2933

3034
preventDefaultSpy = sinon.spy();
3135
evtStopSpy = sinon.spy();
@@ -41,7 +45,13 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
4145
editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
4246
editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
4347

44-
bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'a' );
48+
bindTwoStepCaretToAttribute( {
49+
view: editor.editing.view,
50+
model: editor.model,
51+
emitter,
52+
attribute: 'a',
53+
locale
54+
} );
4555
} );
4656
} );
4757

@@ -550,7 +560,13 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
550560

551561
describe( 'multiple attributes', () => {
552562
beforeEach( () => {
553-
bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'c' );
563+
bindTwoStepCaretToAttribute( {
564+
view: editor.editing.view,
565+
model: editor.model,
566+
emitter,
567+
attribute: 'c',
568+
locale
569+
} );
554570
} );
555571

556572
it( 'should work with the two-step caret movement (moving right)', () => {
@@ -743,6 +759,93 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
743759
expect( getSelectionAttributesArray( selection ) ).to.have.members( [] );
744760
} );
745761

762+
describe( 'left–to–right and right–to–left content', () => {
763+
it( 'should call methods associated with the keys (LTR content direction)', () => {
764+
const forwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleForwardMovement' );
765+
const backwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleBackwardMovement' );
766+
767+
setData( model, '<$text>foo[]</$text><$text a="true">bar</$text>' );
768+
769+
fireKeyDownEvent( {
770+
keyCode: keyCodes.arrowright
771+
} );
772+
773+
sinon.assert.calledOnce( forwardStub );
774+
sinon.assert.notCalled( backwardStub );
775+
776+
setData( model, '<$text>foo</$text><$text a="true">[]bar</$text>' );
777+
778+
fireKeyDownEvent( {
779+
keyCode: keyCodes.arrowleft
780+
} );
781+
782+
sinon.assert.calledOnce( backwardStub );
783+
sinon.assert.calledOnce( forwardStub );
784+
} );
785+
786+
it( 'should use the opposite helper methods (RTL content direction)', () => {
787+
const forwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleForwardMovement' );
788+
const backwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleBackwardMovement' );
789+
const emitter = Object.create( DomEmitterMixin );
790+
791+
let model;
792+
793+
return VirtualTestEditor
794+
.create( {
795+
language: {
796+
content: 'ar'
797+
}
798+
} )
799+
.then( newEditor => {
800+
model = newEditor.model;
801+
selection = model.document.selection;
802+
view = newEditor.editing.view;
803+
804+
newEditor.model.schema.extend( '$text', {
805+
allowAttributes: [ 'a', 'b', 'c' ],
806+
allowIn: '$root'
807+
} );
808+
809+
model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
810+
newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: 'a' } );
811+
newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'b', model: 'b' } );
812+
newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
813+
newEditor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
814+
815+
bindTwoStepCaretToAttribute( {
816+
view: newEditor.editing.view,
817+
model: newEditor.model,
818+
emitter,
819+
attribute: 'a',
820+
locale: newEditor.locale
821+
} );
822+
823+
return newEditor;
824+
} )
825+
.then( newEditor => {
826+
setData( model, '<$text>foo[]</$text><$text a="true">bar</$text>' );
827+
828+
fireKeyDownEvent( {
829+
keyCode: keyCodes.arrowleft
830+
} );
831+
832+
sinon.assert.calledOnce( forwardStub );
833+
sinon.assert.notCalled( backwardStub );
834+
835+
setData( model, '<$text>foo</$text><$text a="true">[]bar</$text>' );
836+
837+
fireKeyDownEvent( {
838+
keyCode: keyCodes.arrowright
839+
} );
840+
841+
sinon.assert.calledOnce( backwardStub );
842+
sinon.assert.calledOnce( forwardStub );
843+
844+
return newEditor.destroy();
845+
} );
846+
} );
847+
} );
848+
746849
const keyMap = {
747850
'→': 'arrowright',
748851
'←': 'arrowleft'

0 commit comments

Comments
 (0)