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

Merge branch 'master' into t/ckeditor5/1403

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

+ 10 - 2
packages/ckeditor5-ui/docs/features/blocktoolbar.md

@@ -5,12 +5,12 @@ category: features
 
 
 The {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar} plugin provides an additional [configurable](#configuration) toolbar on the left-hand side of the content area (the gutter). The toolbar is represented by a button with a pilcrow (or a paragraph mark — ¶). It is positioned next to the block element that the selection is anchored to (e.g. a paragraph), following the caret as the user edits the content and navigates the document.
 The {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar} plugin provides an additional [configurable](#configuration) toolbar on the left-hand side of the content area (the gutter). The toolbar is represented by a button with a pilcrow (or a paragraph mark — ¶). It is positioned next to the block element that the selection is anchored to (e.g. a paragraph), following the caret as the user edits the content and navigates the document.
 
 
-The block toolbar comes in handy when the main editor toolbar cannot be accessed. It complements the {@link builds/guides/overview#balloon-editor Balloon editor} when it falls short, for example when some content must be inserted (like an image) but the selection is collapsed, leaving the user unable to access the toolbar.
+The block toolbar comes in handy when the main editor toolbar cannot be accessed. It complements the {@link builds/guides/overview#balloon-editor Balloon editor} when it falls short, for example when some content must be inserted (like an image) but the selection is collapsed, leaving the user unable to access the toolbar. You can read more about it in the {@link builds/guides/overview#balloon-block-editor balloon block editor overview}.
 
 
 ## Example
 ## Example
 
 
 <info-box hint>
 <info-box hint>
-	Move the caret around the content with the balloon editor below to see the block toolbar button following the selection. Click the button (&#182;) to use the toolbar, for example to create a header or insert an image.
+	Move the caret around the content with the balloon editor below to see the block toolbar button following the selection. Click the button (&#182;) to use the toolbar, for example, to create a header or insert an image. See the {@link examples/builds/balloon-block-editor balloon block editor example} page, too.
 </info-box>
 </info-box>
 
 
 {@snippet features/blocktoolbar}
 {@snippet features/blocktoolbar}
@@ -31,6 +31,14 @@ To adjust the position of the block toolbar button to match the styles of your w
 }
 }
 ```
 ```
 
 
+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:
+
+```css
+.ck[dir="rtl"] .ck-block-toolbar-button {
+	transform: translateX( 10px );
+}
+```
+
 ## Installation
 ## Installation
 
 
 <info-box hint>
 <info-box hint>

+ 4 - 0
packages/ckeditor5-ui/src/colorgrid/colorgridview.js

@@ -90,6 +90,10 @@ export default class ColorGridView extends View {
 			}
 			}
 		} );
 		} );
 
 
+		this.items.on( 'add', ( evt, colorTile ) => {
+			colorTile.isOn = colorTile.color === this.selectedColor;
+		} );
+
 		colorDefinitions.forEach( item => {
 		colorDefinitions.forEach( item => {
 			const colorTile = new ColorTileView();
 			const colorTile = new ColorTileView();
 
 

+ 28 - 9
packages/ckeditor5-ui/src/dropdown/dropdownview.js

@@ -253,18 +253,11 @@ export default class DropdownView extends View {
 			// If "auto", find the best position of the panel to fit into the viewport.
 			// If "auto", find the best position of the panel to fit into the viewport.
 			// Otherwise, simply assign the static position.
 			// Otherwise, simply assign the static position.
 			if ( this.panelPosition === 'auto' ) {
 			if ( this.panelPosition === 'auto' ) {
-				const defaultPanelPositions = DropdownView.defaultPanelPositions;
-
-				this.panelView.position = getOptimalPosition( {
+				this.panelView.position = DropdownView._getOptimalPosition( {
 					element: this.panelView.element,
 					element: this.panelView.element,
 					target: this.buttonView.element,
 					target: this.buttonView.element,
 					fitInViewport: true,
 					fitInViewport: true,
-					positions: [
-						defaultPanelPositions.southEast,
-						defaultPanelPositions.southWest,
-						defaultPanelPositions.northEast,
-						defaultPanelPositions.northWest
-					]
+					positions: this._panelPositions
 				} ).name;
 				} ).name;
 			} else {
 			} else {
 				this.panelView.position = this.panelPosition;
 				this.panelView.position = this.panelPosition;
@@ -312,6 +305,24 @@ export default class DropdownView extends View {
 	focus() {
 	focus() {
 		this.buttonView.focus();
 		this.buttonView.focus();
 	}
 	}
+
+	/**
+	 * Returns {@link #panelView panel} positions to be used by the
+	 * {@link module:utils/dom/position~getOptimalPosition `getOptimalPosition()`}
+	 * utility considering the direction of the language the UI of the editor is displayed in.
+	 *
+	 * @type {module:utils/dom/position~Options#positions}
+	 * @private
+	 */
+	get _panelPositions() {
+		const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
+
+		if ( this.locale.uiLanguageDirection === 'ltr' ) {
+			return [ southEast, southWest, northEast, northWest ];
+		} else {
+			return [ southWest, southEast, northWest, northEast ];
+		}
+	}
 }
 }
 
 
 /**
 /**
@@ -392,3 +403,11 @@ DropdownView.defaultPanelPositions = {
 		};
 		};
 	}
 	}
 };
 };
+
+/**
+ * A function used to calculate the optimal position for the dropdown panel.
+ *
+ * @protected
+ * @member {Function} module:ui/dropdown/dropdownview~DropdownView._getOptimalPosition
+ */
+DropdownView._getOptimalPosition = getOptimalPosition;

+ 3 - 1
packages/ckeditor5-ui/src/editableui/editableuiview.js

@@ -34,7 +34,9 @@ export default class EditableUIView extends View {
 					'ck-content',
 					'ck-content',
 					'ck-editor__editable',
 					'ck-editor__editable',
 					'ck-rounded-corners'
 					'ck-rounded-corners'
-				]
+				],
+				lang: locale.contentLanguage,
+				dir: locale.contentLanguageDirection
 			}
 			}
 		} );
 		} );
 
 

+ 2 - 2
packages/ckeditor5-ui/src/editorui/boxed/boxededitoruiview.js

@@ -66,8 +66,8 @@ export default class BoxedEditorUIView extends EditorUIView {
 					'ck-rounded-corners'
 					'ck-rounded-corners'
 				],
 				],
 				role: 'application',
 				role: 'application',
-				dir: 'ltr',
-				lang: locale.language,
+				dir: locale.uiLanguageDirection,
+				lang: locale.uiLanguage,
 				'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`
 				'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`
 			},
 			},
 
 

+ 3 - 1
packages/ckeditor5-ui/src/editorui/editoruiview.js

@@ -69,6 +69,7 @@ export default class EditorUIView extends View {
 	 * @private
 	 * @private
 	 */
 	 */
 	_renderBodyCollection() {
 	_renderBodyCollection() {
+		const locale = this.locale;
 		const bodyElement = this._bodyCollectionContainer = new Template( {
 		const bodyElement = this._bodyCollectionContainer = new Template( {
 			tag: 'div',
 			tag: 'div',
 			attributes: {
 			attributes: {
@@ -77,7 +78,8 @@ export default class EditorUIView extends View {
 					'ck-reset_all',
 					'ck-reset_all',
 					'ck-body',
 					'ck-body',
 					'ck-rounded-corners'
 					'ck-rounded-corners'
-				]
+				],
+				dir: locale.uiLanguageDirection,
 			},
 			},
 			children: this.body
 			children: this.body
 		} ).render();
 		} ).render();

+ 1 - 1
packages/ckeditor5-ui/src/template.js

@@ -38,7 +38,7 @@ const xhtmlNs = 'http://www.w3.org/1999/xhtml';
  *			},
  *			},
  *			on: {
  *			on: {
  *				click: bind.to( 'clicked' )
  *				click: bind.to( 'clicked' )
- *			}
+ *			},
  *			children: [
  *			children: [
  *				'A paragraph.'
  *				'A paragraph.'
  *			]
  *			]

+ 18 - 2
packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.js

@@ -53,6 +53,14 @@ import iconPilcrow from '@ckeditor/ckeditor5-core/theme/icons/pilcrow.svg';
  * 		      |  block of content that the button is
  * 		      |  block of content that the button is
  * 		      |  attached to.
  * 		      |  attached to.
  *
  *
+ * **Note**: 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:
+ *
+ * 		.ck[dir="rtl"] .ck-block-toolbar-button {
+ * 			transform: translateX( 10px );
+ * 		}
+ *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
 export default class BlockToolbar extends Plugin {
 export default class BlockToolbar extends Plugin {
@@ -361,9 +369,17 @@ export default class BlockToolbar extends Plugin {
 			target: targetElement,
 			target: targetElement,
 			positions: [
 			positions: [
 				( contentRect, buttonRect ) => {
 				( contentRect, buttonRect ) => {
+					let left;
+
+					if ( this.editor.locale.uiLanguageDirection === 'ltr' ) {
+						left = editableRect.left - buttonRect.width;
+					} else {
+						left = editableRect.right;
+					}
+
 					return {
 					return {
-						top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
-						left: editableRect.left - buttonRect.width
+						top: contentRect.top + contentPaddingTop + ( contentLineHeight - buttonRect.height ) / 2,
+						left
 					};
 					};
 				}
 				}
 			]
 			]

+ 17 - 0
packages/ckeditor5-ui/tests/colorgrid/colorgridview.js

@@ -106,6 +106,23 @@ describe( 'ColorGridView', () => {
 			expect( view.items.get( 2 ).isOn ).to.be.false;
 			expect( view.items.get( 2 ).isOn ).to.be.false;
 		} );
 		} );
 
 
+		it( 'should determine #isOn value when a ColorTileView is added', () => {
+			view.selectedColor = 'gold';
+
+			const tile = new ColorTileView();
+			tile.set( {
+				color: 'gold',
+				label: 'Gold',
+				options: {
+					hasBorder: false
+				}
+			} );
+
+			view.items.add( tile );
+
+			expect( view.items.get( 3 ).isOn ).to.be.true;
+		} );
+
 		describe( 'add colors from definition as child items', () => {
 		describe( 'add colors from definition as child items', () => {
 			it( 'has proper number of elements', () => {
 			it( 'has proper number of elements', () => {
 				expect( view.items.length ).to.equal( 3 );
 				expect( view.items.length ).to.equal( 3 );

+ 88 - 82
packages/ckeditor5-ui/tests/dropdown/dropdownview.js

@@ -10,7 +10,6 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import ButtonView from '../../src/button/buttonview';
 import ButtonView from '../../src/button/buttonview';
 import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
 import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 
 describe( 'DropdownView', () => {
 describe( 'DropdownView', () => {
@@ -19,7 +18,10 @@ describe( 'DropdownView', () => {
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		locale = { t() {} };
+		locale = {
+			uiLanguageDirection: 'ltr',
+			t() {}
+		};
 
 
 		buttonView = new ButtonView( locale );
 		buttonView = new ButtonView( locale );
 		panelView = new DropdownPanelView( locale );
 		panelView = new DropdownPanelView( locale );
@@ -116,7 +118,7 @@ describe( 'DropdownView', () => {
 			} );
 			} );
 
 
 			describe( 'view.panelView#position to view#panelPosition', () => {
 			describe( 'view.panelView#position to view#panelPosition', () => {
-				it( 'does not update until the dropdown is opened', () => {
+				it( 'does not update until the dropdown is open', () => {
 					view.isOpen = false;
 					view.isOpen = false;
 					view.panelPosition = 'nw';
 					view.panelPosition = 'nw';
 
 
@@ -128,86 +130,37 @@ describe( 'DropdownView', () => {
 				} );
 				} );
 
 
 				describe( 'in "auto" mode', () => {
 				describe( 'in "auto" mode', () => {
-					beforeEach( () => {
-						// Bloat the panel a little to give the positioning algorithm something to
-						// work with. If the panel was empty, any smart positioning is pointless.
-						// Placing an empty element in the viewport isn't that hard, right?
-						panelView.element.style.width = '200px';
-						panelView.element.style.height = '200px';
-					} );
-
-					it( 'defaults to "south-east" when there is a plenty of space around', () => {
-						const windowRect = new Rect( global.window );
-
-						// "Put" the dropdown in the middle of the viewport.
-						stubElementClientRect( view.buttonView.element, {
-							top: windowRect.height / 2,
-							left: windowRect.width / 2,
-							width: 10,
-							height: 10
-						} );
-
-						view.isOpen = true;
-
-						expect( panelView.position ).to.equal( 'se' );
-					} );
-
-					it( 'when the dropdown in the north-west corner of the viewport', () => {
-						stubElementClientRect( view.buttonView.element, {
-							top: 0,
-							left: 0,
-							width: 100,
-							height: 10
-						} );
+					it( 'uses _getOptimalPosition() and a dedicated set of positions (LTR)', () => {
+						const spy = testUtils.sinon.spy( DropdownView, '_getOptimalPosition' );
+						const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
 
 
 						view.isOpen = true;
 						view.isOpen = true;
 
 
-						expect( panelView.position ).to.equal( 'se' );
+						sinon.assert.calledWithExactly( spy, sinon.match( {
+							element: panelView.element,
+							target: buttonView.element,
+							positions: [
+								southEast, southWest, northEast, northWest
+							],
+							fitInViewport: true
+						} ) );
 					} );
 					} );
 
 
-					it( 'when the dropdown in the north-east corner of the viewport', () => {
-						const windowRect = new Rect( global.window );
-
-						stubElementClientRect( view.buttonView.element, {
-							top: 0,
-							left: windowRect.right - 100,
-							width: 100,
-							height: 10
-						} );
+					it( 'uses _getOptimalPosition() and a dedicated set of positions (RTL)', () => {
+						const spy = testUtils.sinon.spy( DropdownView, '_getOptimalPosition' );
+						const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
 
 
+						view.locale.uiLanguageDirection = 'rtl';
 						view.isOpen = true;
 						view.isOpen = true;
 
 
-						expect( panelView.position ).to.equal( 'sw' );
-					} );
-
-					it( 'when the dropdown in the south-west corner of the viewport', () => {
-						const windowRect = new Rect( global.window );
-
-						stubElementClientRect( view.buttonView.element, {
-							top: windowRect.bottom - 10,
-							left: 0,
-							width: 100,
-							height: 10
-						} );
-
-						view.isOpen = true;
-
-						expect( panelView.position ).to.equal( 'ne' );
-					} );
-
-					it( 'when the dropdown in the south-east corner of the viewport', () => {
-						const windowRect = new Rect( global.window );
-
-						stubElementClientRect( view.buttonView.element, {
-							top: windowRect.bottom - 10,
-							left: windowRect.right - 100,
-							width: 100,
-							height: 10
-						} );
-
-						view.isOpen = true;
-
-						expect( panelView.position ).to.equal( 'nw' );
+						sinon.assert.calledWithExactly( spy, sinon.match( {
+							element: panelView.element,
+							target: buttonView.element,
+							positions: [
+								southWest, southEast, northWest, northEast
+							],
+							fitInViewport: true
+						} ) );
 					} );
 					} );
 				} );
 				} );
 			} );
 			} );
@@ -372,13 +325,66 @@ describe( 'DropdownView', () => {
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledOnce( spy );
 		} );
 		} );
 	} );
 	} );
-} );
 
 
-function stubElementClientRect( element, data ) {
-	const clientRect = Object.assign( {}, data );
+	describe( 'DropdownView.defaultPanelPositions', () => {
+		let positions, buttonRect, panelRect;
+
+		beforeEach( () => {
+			positions = DropdownView.defaultPanelPositions;
+
+			buttonRect = {
+				top: 100,
+				bottom: 200,
+				left: 100,
+				right: 200,
+				width: 100,
+				height: 100
+			};
+
+			panelRect = {
+				top: 0,
+				bottom: 0,
+				left: 0,
+				right: 0,
+				width: 50,
+				height: 50
+			};
+		} );
+
+		it( 'should have a proper length', () => {
+			expect( Object.keys( positions ) ).to.have.length( 4 );
+		} );
+
+		it( 'should define the "southEast" position', () => {
+			expect( positions.southEast( buttonRect, panelRect ) ).to.deep.equal( {
+				top: 200,
+				left: 100,
+				name: 'se'
+			} );
+		} );
+
+		it( 'should define the "southWest" position', () => {
+			expect( positions.southWest( buttonRect, panelRect ) ).to.deep.equal( {
+				top: 200,
+				left: 150,
+				name: 'sw'
+			} );
+		} );
 
 
-	clientRect.right = clientRect.left + clientRect.width;
-	clientRect.bottom = clientRect.top + clientRect.height;
+		it( 'should define the "northEast" position', () => {
+			expect( positions.northEast( buttonRect, panelRect ) ).to.deep.equal( {
+				top: 50,
+				left: 100,
+				name: 'ne'
+			} );
+		} );
 
 
-	testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( clientRect );
-}
+		it( 'should define the "northWest" position', () => {
+			expect( positions.northWest( buttonRect, panelRect ) ).to.deep.equal( {
+				top: 150,
+				left: 150,
+				name: 'nw'
+			} );
+		} );
+	} );
+} );

+ 58 - 5
packages/ckeditor5-ui/tests/editableui/editableuiview.js

@@ -18,7 +18,7 @@ describe( 'EditableUIView', () => {
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		locale = new Locale( 'en' );
+		locale = new Locale();
 		editableElement = document.createElement( 'div' );
 		editableElement = document.createElement( 'div' );
 
 
 		editingView = new EditingView();
 		editingView = new EditingView();
@@ -31,14 +31,21 @@ describe( 'EditableUIView', () => {
 		view.render();
 		view.render();
 	} );
 	} );
 
 
+	afterEach( () => {
+		view.destroy();
+		editableElement.remove();
+	} );
+
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
 		it( 'sets initial values of attributes', () => {
 		it( 'sets initial values of attributes', () => {
-			view = new EditableUIView( locale, editingView );
+			const view = new EditableUIView( locale, editingView );
 
 
 			expect( view.isFocused ).to.be.false;
 			expect( view.isFocused ).to.be.false;
 			expect( view.name ).to.be.null;
 			expect( view.name ).to.be.null;
 			expect( view._externalElement ).to.be.undefined;
 			expect( view._externalElement ).to.be.undefined;
 			expect( view._editingView ).to.equal( editingView );
 			expect( view._editingView ).to.equal( editingView );
+
+			view.destroy();
 		} );
 		} );
 
 
 		it( 'renders element from template when no editableElement', () => {
 		it( 'renders element from template when no editableElement', () => {
@@ -47,22 +54,58 @@ describe( 'EditableUIView', () => {
 			expect( view.element.classList.contains( 'ck-content' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-content' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
+			expect( view.element.getAttribute( 'lang' ) ).to.equal( 'en' );
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
 			expect( view._externalElement ).to.be.undefined;
 			expect( view._externalElement ).to.be.undefined;
 			expect( view.isRendered ).to.be.true;
 			expect( view.isRendered ).to.be.true;
 		} );
 		} );
 
 
 		it( 'accepts editableElement as an argument', () => {
 		it( 'accepts editableElement as an argument', () => {
-			view = new EditableUIView( locale, editingView, editableElement );
+			const view = new EditableUIView( locale, editingView, editableElement );
 			view.name = editingViewRoot.rootName;
 			view.name = editingViewRoot.rootName;
 
 
 			view.render();
 			view.render();
+
 			expect( view.element ).to.equal( editableElement );
 			expect( view.element ).to.equal( editableElement );
 			expect( view.element ).to.equal( view._editableElement );
 			expect( view.element ).to.equal( view._editableElement );
 			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
+			expect( view.element.getAttribute( 'lang' ) ).to.equal( 'en' );
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
 			expect( view._hasExternalElement ).to.be.true;
 			expect( view._hasExternalElement ).to.be.true;
 			expect( view.isRendered ).to.be.true;
 			expect( view.isRendered ).to.be.true;
+
+			view.destroy();
+		} );
+
+		it( 'sets proper lang and dir attributes (implicit content language)', () => {
+			const locale = new Locale( { uiLanguage: 'ar' } );
+			const view = new EditableUIView( locale, editingView );
+			view.name = editingViewRoot.rootName;
+
+			view.render();
+
+			expect( view.element.getAttribute( 'lang' ) ).to.equal( 'ar' );
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
+
+			view.destroy();
+		} );
+
+		it( 'sets proper lang and dir attributes (explicit content language)', () => {
+			const locale = new Locale( {
+				uiLanguage: 'pl',
+				contentLanguage: 'ar'
+			} );
+			const view = new EditableUIView( locale, editingView );
+			view.name = editingViewRoot.rootName;
+
+			view.render();
+
+			expect( view.element.getAttribute( 'lang' ) ).to.equal( 'ar' );
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
+
+			view.destroy();
 		} );
 		} );
 	} );
 	} );
 
 
@@ -123,6 +166,7 @@ describe( 'EditableUIView', () => {
 				expect( secondEditingViewRoot.hasClass( 'ck-blurred' ), 12 ).to.be.false;
 				expect( secondEditingViewRoot.hasClass( 'ck-blurred' ), 12 ).to.be.false;
 
 
 				secondEditableElement.remove();
 				secondEditableElement.remove();
+				secondView.destroy();
 			} );
 			} );
 		} );
 		} );
 	} );
 	} );
@@ -130,12 +174,21 @@ describe( 'EditableUIView', () => {
 	describe( 'destroy()', () => {
 	describe( 'destroy()', () => {
 		it( 'calls super#destroy()', () => {
 		it( 'calls super#destroy()', () => {
 			const spy = testUtils.sinon.spy( View.prototype, 'destroy' );
 			const spy = testUtils.sinon.spy( View.prototype, 'destroy' );
+			const view = new EditableUIView( locale, editingView );
+			view.name = editingViewRoot.rootName;
 
 
+			view.render();
 			view.destroy();
 			view.destroy();
+
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledOnce( spy );
 		} );
 		} );
 
 
 		it( 'can be called multiple times', () => {
 		it( 'can be called multiple times', () => {
+			const view = new EditableUIView( locale, editingView );
+			view.name = editingViewRoot.rootName;
+
+			view.render();
+
 			expect( () => {
 			expect( () => {
 				view.destroy();
 				view.destroy();
 				view.destroy();
 				view.destroy();
@@ -144,11 +197,11 @@ describe( 'EditableUIView', () => {
 
 
 		describe( 'when #editableElement as an argument', () => {
 		describe( 'when #editableElement as an argument', () => {
 			it( 'reverts the template of editableElement', () => {
 			it( 'reverts the template of editableElement', () => {
-				editableElement = document.createElement( 'div' );
+				const editableElement = document.createElement( 'div' );
 				editableElement.classList.add( 'foo' );
 				editableElement.classList.add( 'foo' );
 				editableElement.contentEditable = false;
 				editableElement.contentEditable = false;
 
 
-				view = new EditableUIView( locale, editingView, editableElement );
+				const view = new EditableUIView( locale, editingView, editableElement );
 				view.name = editingViewRoot.rootName;
 				view.name = editingViewRoot.rootName;
 
 
 				view.render();
 				view.render();

+ 12 - 1
packages/ckeditor5-ui/tests/editorui/boxed/boxededitoruiview.js

@@ -11,7 +11,7 @@ describe( 'BoxedEditorUIView', () => {
 	let view, element;
 	let view, element;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		view = new BoxedEditorUIView( new Locale( 'en' ) );
+		view = new BoxedEditorUIView( new Locale() );
 		view.render();
 		view.render();
 		element = view.element;
 		element = view.element;
 	} );
 	} );
@@ -31,6 +31,7 @@ describe( 'BoxedEditorUIView', () => {
 			expect( view.element.classList.contains( 'ck-editor' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-editor' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-reset' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-reset' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
 			expect( element.attributes[ 'aria-labelledby' ].value )
 			expect( element.attributes[ 'aria-labelledby' ].value )
 				.to.equal( view.element.firstChild.id )
 				.to.equal( view.element.firstChild.id )
 				.to.match( /^ck-editor__aria-label_\w+$/ );
 				.to.match( /^ck-editor__aria-label_\w+$/ );
@@ -59,5 +60,15 @@ describe( 'BoxedEditorUIView', () => {
 			expect( element.childNodes[ 1 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
 			expect( element.childNodes[ 1 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
 			expect( element.childNodes[ 2 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
 			expect( element.childNodes[ 2 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
 		} );
 		} );
+
+		it( 'sets the proper "dir" attribute value when using RTL language', () => {
+			const view = new BoxedEditorUIView( new Locale( { uiLanguage: 'ar' } ) );
+
+			view.render();
+
+			expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
+
+			view.destroy();
+		} );
 	} );
 	} );
 } );
 } );

+ 20 - 1
packages/ckeditor5-ui/tests/editorui/editoruiview.js

@@ -16,7 +16,7 @@ describe( 'EditorUIView', () => {
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		locale = new Locale( 'en' );
+		locale = new Locale();
 		view = new EditorUIView( locale );
 		view = new EditorUIView( locale );
 
 
 		view.render();
 		view.render();
@@ -46,6 +46,25 @@ describe( 'EditorUIView', () => {
 			expect( el.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
 			expect( el.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
 			expect( el.classList.contains( 'ck-reset_all' ) ).to.be.true;
 			expect( el.classList.contains( 'ck-reset_all' ) ).to.be.true;
 		} );
 		} );
+
+		it( 'sets the right dir attribute to the body region (LTR)', () => {
+			const el = view._bodyCollectionContainer;
+
+			expect( el.getAttribute( 'dir' ) ).to.equal( 'ltr' );
+		} );
+
+		it( 'sets the right dir attribute to the body region (RTL)', () => {
+			const locale = new Locale( { uiLanguage: 'ar' } );
+			const view = new EditorUIView( locale );
+
+			view.render();
+
+			const el = view._bodyCollectionContainer;
+
+			expect( el.getAttribute( 'dir' ) ).to.equal( 'rtl' );
+
+			view.destroy();
+		} );
 	} );
 	} );
 
 
 	describe( 'destroy()', () => {
 	describe( 'destroy()', () => {

+ 4 - 0
packages/ckeditor5-ui/tests/manual/blocktoolbar/blocktoolbar.html

@@ -56,4 +56,8 @@
 	.ck-block-toolbar-button {
 	.ck-block-toolbar-button {
 		transform: translateX( -10px );
 		transform: translateX( -10px );
 	}
 	}
+
+	[dir="rtl"] .ck-block-toolbar-button {
+		transform: translateX( 10px );
+	}
 </style>
 </style>

+ 40 - 3
packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js

@@ -227,14 +227,14 @@ describe( 'BlockToolbar', () => {
 		} );
 		} );
 
 
 		it( 'should display the button when the first selected block is an object', () => {
 		it( 'should display the button when the first selected block is an object', () => {
-			setData( editor.model, '[<image src="foo.jpg"><caption>foo</caption></image>]' );
+			setData( editor.model, '[<image src="/assets/sample.png"><caption>foo</caption></image>]' );
 
 
 			expect( blockToolbar.buttonView.isVisible ).to.be.true;
 			expect( blockToolbar.buttonView.isVisible ).to.be.true;
 		} );
 		} );
 
 
 		// This test makes no sense now, but so do all other tests here (see https://github.com/ckeditor/ckeditor5/issues/1522).
 		// This test makes no sense now, but so do all other tests here (see https://github.com/ckeditor/ckeditor5/issues/1522).
 		it( 'should not display the button when the selection is inside a limit element', () => {
 		it( 'should not display the button when the selection is inside a limit element', () => {
-			setData( editor.model, '<image src="foo.jpg"><caption>f[]oo</caption></image>' );
+			setData( editor.model, '<image src="/assets/sample.png"><caption>f[]oo</caption></image>' );
 
 
 			expect( blockToolbar.buttonView.isVisible ).to.be.false;
 			expect( blockToolbar.buttonView.isVisible ).to.be.false;
 		} );
 		} );
@@ -258,7 +258,7 @@ describe( 'BlockToolbar', () => {
 			} ).then( editor => {
 			} ).then( editor => {
 				const blockToolbar = editor.plugins.get( BlockToolbar );
 				const blockToolbar = editor.plugins.get( BlockToolbar );
 
 
-				setData( editor.model, '[<image src="foo.jpg"></image>]' );
+				setData( editor.model, '[<image src="/assets/sample.png"></image>]' );
 
 
 				expect( blockToolbar.buttonView.isVisible ).to.be.false;
 				expect( blockToolbar.buttonView.isVisible ).to.be.false;
 
 
@@ -345,6 +345,43 @@ describe( 'BlockToolbar', () => {
 			expect( blockToolbar.buttonView.left ).to.equal( 100 );
 			expect( blockToolbar.buttonView.left ).to.equal( 100 );
 		} );
 		} );
 
 
+		it( 'should attach the left side of the button to the right side of the editable when language direction is RTL', () => {
+			editor.locale.uiLanguageDirection = 'rtl';
+
+			setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
+
+			const target = editor.ui.getEditableElement().querySelector( 'p' );
+			const styleMock = testUtils.sinon.stub( window, 'getComputedStyle' );
+
+			styleMock.withArgs( target ).returns( {
+				lineHeight: 'normal',
+				fontSize: '20px',
+				paddingTop: '10px'
+			} );
+
+			styleMock.callThrough();
+
+			testUtils.sinon.stub( editor.ui.getEditableElement(), 'getBoundingClientRect' ).returns( {
+				left: 200,
+				right: 600
+			} );
+
+			testUtils.sinon.stub( target, 'getBoundingClientRect' ).returns( {
+				top: 500,
+				left: 300
+			} );
+
+			testUtils.sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( {
+				width: 100,
+				height: 100
+			} );
+
+			editor.ui.fire( 'update' );
+
+			expect( blockToolbar.buttonView.top ).to.equal( 472 );
+			expect( blockToolbar.buttonView.left ).to.equal( 600 );
+		} );
+
 		it( 'should reposition the #panelView when open on ui#update', () => {
 		it( 'should reposition the #panelView when open on ui#update', () => {
 			blockToolbar.panelView.isVisible = false;
 			blockToolbar.panelView.isVisible = false;
 
 

+ 10 - 0
packages/ckeditor5-ui/theme/mixins/_dir.css

@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+@define-mixin ck-dir $direction {
+	@nest [dir="$(direction)"] & {
+		@mixin-content;
+	}
+}