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

Commit d6c7f55

Browse files
authored
Merge pull request #520 from ckeditor/t/ckeditor5/1151
Feature: Brought support for right–to–left (RTL) languages to various UI components. See ckeditor/ckeditor5#1151.
2 parents e89ad60 + 31881a8 commit d6c7f55

File tree

13 files changed

+291
-104
lines changed

13 files changed

+291
-104
lines changed

docs/features/blocktoolbar.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ To adjust the position of the block toolbar button to match the styles of your w
3131
}
3232
```
3333

34+
If you plan to run the editor in a right–to–left (RTL) language, keep in mind the button will be attached to the **right** boundary of the editable area. In that case, make sure the CSS position adjustment works properly by adding the following styles:
35+
36+
```css
37+
.ck[dir="rtl"] .ck-block-toolbar-button {
38+
transform: translateX( 10px );
39+
}
40+
```
41+
3442
## Installation
3543

3644
<info-box hint>

src/dropdown/dropdownview.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,18 +253,11 @@ export default class DropdownView extends View {
253253
// If "auto", find the best position of the panel to fit into the viewport.
254254
// Otherwise, simply assign the static position.
255255
if ( this.panelPosition === 'auto' ) {
256-
const defaultPanelPositions = DropdownView.defaultPanelPositions;
257-
258-
this.panelView.position = getOptimalPosition( {
256+
this.panelView.position = DropdownView._getOptimalPosition( {
259257
element: this.panelView.element,
260258
target: this.buttonView.element,
261259
fitInViewport: true,
262-
positions: [
263-
defaultPanelPositions.southEast,
264-
defaultPanelPositions.southWest,
265-
defaultPanelPositions.northEast,
266-
defaultPanelPositions.northWest
267-
]
260+
positions: this._panelPositions
268261
} ).name;
269262
} else {
270263
this.panelView.position = this.panelPosition;
@@ -312,6 +305,24 @@ export default class DropdownView extends View {
312305
focus() {
313306
this.buttonView.focus();
314307
}
308+
309+
/**
310+
* Returns {@link #panelView panel} positions to be used by the
311+
* {@link module:utils/dom/position~getOptimalPosition `getOptimalPosition()`}
312+
* utility considering the direction of the language the UI of the editor is displayed in.
313+
*
314+
* @type {module:utils/dom/position~Options#positions}
315+
* @private
316+
*/
317+
get _panelPositions() {
318+
const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
319+
320+
if ( this.locale.uiLanguageDirection === 'ltr' ) {
321+
return [ southEast, southWest, northEast, northWest ];
322+
} else {
323+
return [ southWest, southEast, northWest, northEast ];
324+
}
325+
}
315326
}
316327

317328
/**
@@ -392,3 +403,11 @@ DropdownView.defaultPanelPositions = {
392403
};
393404
}
394405
};
406+
407+
/**
408+
* A function used to calculate the optimal position for the dropdown panel.
409+
*
410+
* @protected
411+
* @member {Function} module:ui/dropdown/dropdownview~DropdownView._getOptimalPosition
412+
*/
413+
DropdownView._getOptimalPosition = getOptimalPosition;

src/editableui/editableuiview.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export default class EditableUIView extends View {
3434
'ck-content',
3535
'ck-editor__editable',
3636
'ck-rounded-corners'
37-
]
37+
],
38+
lang: locale.contentLanguage,
39+
dir: locale.contentLanguageDirection
3840
}
3941
} );
4042

src/editorui/boxed/boxededitoruiview.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ export default class BoxedEditorUIView extends EditorUIView {
6666
'ck-rounded-corners'
6767
],
6868
role: 'application',
69-
dir: 'ltr',
70-
lang: locale.language,
69+
dir: locale.uiLanguageDirection,
70+
lang: locale.uiLanguage,
7171
'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`
7272
},
7373

src/editorui/editoruiview.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default class EditorUIView extends View {
6969
* @private
7070
*/
7171
_renderBodyCollection() {
72+
const locale = this.locale;
7273
const bodyElement = this._bodyCollectionContainer = new Template( {
7374
tag: 'div',
7475
attributes: {
@@ -77,7 +78,8 @@ export default class EditorUIView extends View {
7778
'ck-reset_all',
7879
'ck-body',
7980
'ck-rounded-corners'
80-
]
81+
],
82+
dir: locale.uiLanguageDirection,
8183
},
8284
children: this.body
8385
} ).render();

src/toolbar/block/blocktoolbar.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ import iconPilcrow from '@ckeditor/ckeditor5-core/theme/icons/pilcrow.svg';
5353
* | block of content that the button is
5454
* | attached to.
5555
*
56+
* **Note**: If you plan to run the editor in a right–to–left (RTL) language, keep in mind the button
57+
* will be attached to the **right** boundary of the editable area. In that case, make sure the
58+
* CSS position adjustment works properly by adding the following styles:
59+
*
60+
* .ck[dir="rtl"] .ck-block-toolbar-button {
61+
* transform: translateX( 10px );
62+
* }
63+
*
5664
* @extends module:core/plugin~Plugin
5765
*/
5866
export default class BlockToolbar extends Plugin {
@@ -361,9 +369,17 @@ export default class BlockToolbar extends Plugin {
361369
target: targetElement,
362370
positions: [
363371
( contentRect, buttonRect ) => {
372+
let left;
373+
374+
if ( this.editor.locale.uiLanguageDirection === 'ltr' ) {
375+
left = editableRect.left - buttonRect.width;
376+
} else {
377+
left = editableRect.right;
378+
}
379+
364380
return {
365-
top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
366-
left: editableRect.left - buttonRect.width
381+
top: contentRect.top + contentPaddingTop + ( contentLineHeight - buttonRect.height ) / 2,
382+
left
367383
};
368384
}
369385
]

tests/dropdown/dropdownview.js

Lines changed: 88 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
1010
import ButtonView from '../../src/button/buttonview';
1111
import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
1212
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
13-
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
1413
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
1514

1615
describe( 'DropdownView', () => {
@@ -19,7 +18,10 @@ describe( 'DropdownView', () => {
1918
testUtils.createSinonSandbox();
2019

2120
beforeEach( () => {
22-
locale = { t() {} };
21+
locale = {
22+
uiLanguageDirection: 'ltr',
23+
t() {}
24+
};
2325

2426
buttonView = new ButtonView( locale );
2527
panelView = new DropdownPanelView( locale );
@@ -116,7 +118,7 @@ describe( 'DropdownView', () => {
116118
} );
117119

118120
describe( 'view.panelView#position to view#panelPosition', () => {
119-
it( 'does not update until the dropdown is opened', () => {
121+
it( 'does not update until the dropdown is open', () => {
120122
view.isOpen = false;
121123
view.panelPosition = 'nw';
122124

@@ -128,86 +130,37 @@ describe( 'DropdownView', () => {
128130
} );
129131

130132
describe( 'in "auto" mode', () => {
131-
beforeEach( () => {
132-
// Bloat the panel a little to give the positioning algorithm something to
133-
// work with. If the panel was empty, any smart positioning is pointless.
134-
// Placing an empty element in the viewport isn't that hard, right?
135-
panelView.element.style.width = '200px';
136-
panelView.element.style.height = '200px';
137-
} );
138-
139-
it( 'defaults to "south-east" when there is a plenty of space around', () => {
140-
const windowRect = new Rect( global.window );
141-
142-
// "Put" the dropdown in the middle of the viewport.
143-
stubElementClientRect( view.buttonView.element, {
144-
top: windowRect.height / 2,
145-
left: windowRect.width / 2,
146-
width: 10,
147-
height: 10
148-
} );
149-
150-
view.isOpen = true;
151-
152-
expect( panelView.position ).to.equal( 'se' );
153-
} );
154-
155-
it( 'when the dropdown in the north-west corner of the viewport', () => {
156-
stubElementClientRect( view.buttonView.element, {
157-
top: 0,
158-
left: 0,
159-
width: 100,
160-
height: 10
161-
} );
133+
it( 'uses _getOptimalPosition() and a dedicated set of positions (LTR)', () => {
134+
const spy = testUtils.sinon.spy( DropdownView, '_getOptimalPosition' );
135+
const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
162136

163137
view.isOpen = true;
164138

165-
expect( panelView.position ).to.equal( 'se' );
139+
sinon.assert.calledWithExactly( spy, sinon.match( {
140+
element: panelView.element,
141+
target: buttonView.element,
142+
positions: [
143+
southEast, southWest, northEast, northWest
144+
],
145+
fitInViewport: true
146+
} ) );
166147
} );
167148

168-
it( 'when the dropdown in the north-east corner of the viewport', () => {
169-
const windowRect = new Rect( global.window );
170-
171-
stubElementClientRect( view.buttonView.element, {
172-
top: 0,
173-
left: windowRect.right - 100,
174-
width: 100,
175-
height: 10
176-
} );
149+
it( 'uses _getOptimalPosition() and a dedicated set of positions (RTL)', () => {
150+
const spy = testUtils.sinon.spy( DropdownView, '_getOptimalPosition' );
151+
const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
177152

153+
view.locale.uiLanguageDirection = 'rtl';
178154
view.isOpen = true;
179155

180-
expect( panelView.position ).to.equal( 'sw' );
181-
} );
182-
183-
it( 'when the dropdown in the south-west corner of the viewport', () => {
184-
const windowRect = new Rect( global.window );
185-
186-
stubElementClientRect( view.buttonView.element, {
187-
top: windowRect.bottom - 10,
188-
left: 0,
189-
width: 100,
190-
height: 10
191-
} );
192-
193-
view.isOpen = true;
194-
195-
expect( panelView.position ).to.equal( 'ne' );
196-
} );
197-
198-
it( 'when the dropdown in the south-east corner of the viewport', () => {
199-
const windowRect = new Rect( global.window );
200-
201-
stubElementClientRect( view.buttonView.element, {
202-
top: windowRect.bottom - 10,
203-
left: windowRect.right - 100,
204-
width: 100,
205-
height: 10
206-
} );
207-
208-
view.isOpen = true;
209-
210-
expect( panelView.position ).to.equal( 'nw' );
156+
sinon.assert.calledWithExactly( spy, sinon.match( {
157+
element: panelView.element,
158+
target: buttonView.element,
159+
positions: [
160+
southWest, southEast, northWest, northEast
161+
],
162+
fitInViewport: true
163+
} ) );
211164
} );
212165
} );
213166
} );
@@ -372,13 +325,66 @@ describe( 'DropdownView', () => {
372325
sinon.assert.calledOnce( spy );
373326
} );
374327
} );
375-
} );
376328

377-
function stubElementClientRect( element, data ) {
378-
const clientRect = Object.assign( {}, data );
329+
describe( 'DropdownView.defaultPanelPositions', () => {
330+
let positions, buttonRect, panelRect;
331+
332+
beforeEach( () => {
333+
positions = DropdownView.defaultPanelPositions;
334+
335+
buttonRect = {
336+
top: 100,
337+
bottom: 200,
338+
left: 100,
339+
right: 200,
340+
width: 100,
341+
height: 100
342+
};
343+
344+
panelRect = {
345+
top: 0,
346+
bottom: 0,
347+
left: 0,
348+
right: 0,
349+
width: 50,
350+
height: 50
351+
};
352+
} );
353+
354+
it( 'should have a proper length', () => {
355+
expect( Object.keys( positions ) ).to.have.length( 4 );
356+
} );
357+
358+
it( 'should define the "southEast" position', () => {
359+
expect( positions.southEast( buttonRect, panelRect ) ).to.deep.equal( {
360+
top: 200,
361+
left: 100,
362+
name: 'se'
363+
} );
364+
} );
365+
366+
it( 'should define the "southWest" position', () => {
367+
expect( positions.southWest( buttonRect, panelRect ) ).to.deep.equal( {
368+
top: 200,
369+
left: 150,
370+
name: 'sw'
371+
} );
372+
} );
379373

380-
clientRect.right = clientRect.left + clientRect.width;
381-
clientRect.bottom = clientRect.top + clientRect.height;
374+
it( 'should define the "northEast" position', () => {
375+
expect( positions.northEast( buttonRect, panelRect ) ).to.deep.equal( {
376+
top: 50,
377+
left: 100,
378+
name: 'ne'
379+
} );
380+
} );
382381

383-
testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( clientRect );
384-
}
382+
it( 'should define the "northWest" position', () => {
383+
expect( positions.northWest( buttonRect, panelRect ) ).to.deep.equal( {
384+
top: 150,
385+
left: 150,
386+
name: 'nw'
387+
} );
388+
} );
389+
} );
390+
} );

0 commit comments

Comments
 (0)