Browse Source

Merge pull request #496 from ckeditor/t/491

Changes to selection and focus in view.Document.
Piotr Jasiun 9 năm trước cách đây
mục cha
commit
816f32a0f3
26 tập tin đã thay đổi với 363 bổ sung150 xóa
  1. 15 4
      packages/ckeditor5-engine/src/view/document.js
  2. 9 2
      packages/ckeditor5-engine/src/view/editableelement.js
  3. 4 5
      packages/ckeditor5-engine/src/view/observer/focusobserver.js
  4. 1 5
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  5. 20 0
      packages/ckeditor5-engine/src/view/position.js
  6. 9 9
      packages/ckeditor5-engine/src/view/renderer.js
  7. 14 0
      packages/ckeditor5-engine/src/view/selection.js
  8. 6 0
      packages/ckeditor5-engine/tests/_utils-tests/view.js
  9. 1 1
      packages/ckeditor5-engine/tests/_utils/view.js
  10. 2 1
      packages/ckeditor5-engine/tests/editingcontroller.js
  11. 2 1
      packages/ckeditor5-engine/tests/view/_utils/createdocumentmock.js
  12. 23 9
      packages/ckeditor5-engine/tests/view/document/document.js
  13. 2 1
      packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js
  14. 103 0
      packages/ckeditor5-engine/tests/view/editableelement.js
  15. 0 2
      packages/ckeditor5-engine/tests/view/manual/focusobserver.html
  16. 16 2
      packages/ckeditor5-engine/tests/view/manual/focusobserver.js
  17. 2 3
      packages/ckeditor5-engine/tests/view/manual/inline-filler.js
  18. 3 3
      packages/ckeditor5-engine/tests/view/manual/selectionobserver.js
  19. 2 1
      packages/ckeditor5-engine/tests/view/manual/x-index.js
  20. 19 15
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  21. 4 3
      packages/ckeditor5-engine/tests/view/observer/selectionobserver.js
  22. 19 0
      packages/ckeditor5-engine/tests/view/position.js
  23. 42 10
      packages/ckeditor5-engine/tests/view/renderer.js
  24. 0 71
      packages/ckeditor5-engine/tests/view/rooteditableelement.js
  25. 38 0
      packages/ckeditor5-engine/tests/view/selection.js
  26. 7 2
      packages/ckeditor5-engine/tests/view/utils-tests/createdocumentmock.js

+ 15 - 4
packages/ckeditor5-engine/src/view/document.js

@@ -74,16 +74,27 @@ export default class Document {
 		this.roots = new Map();
 
 		/**
-		 * {@link engine.view.EditableElement} which is currently focused or null if all of them are blurred.
+		 * True if document is focused.
 		 *
 		 * This property is updated by the {@link engine.view.obsever.FocusObserver}.
 		 * If the {@link engine.view.obsever.FocusObserver} is disabled this property will not change.
 		 *
 		 * @readonly
 		 * @observable
-		 * @member {engine.view.EditableElement|null} engine.view.Document#focusedEditable
+		 * @member {Boolean} engine.view.Document#isFocused
 		 */
-		this.set( 'focusedEditable', null );
+		this.set( 'isFocused', false );
+
+		/**
+		 * {@link engine.view.EditableElement EditableElement} which is containing selection. It can be `null` if
+		 * there is no selection found.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {engine.view.EditableElement|null} engine.view.Document#selectedEditable
+		 */
+		this.set( 'selectedEditable', this.selection.getEditableElement() );
+		this.selection.on( 'change', () => this.selectedEditable = this.selection.getEditableElement() );
 
 		/**
 		 * Instance of the {@link engine.view.Document#renderer renderer}.
@@ -92,7 +103,7 @@ export default class Document {
 		 * @member {engine.view.Renderer} engine.view.Document#renderer
 		 */
 		this.renderer = new Renderer( this.domConverter, this.selection );
-		this.renderer.bind( 'focusedEditable' ).to( this, 'focusedEditable' );
+		this.renderer.bind( 'isFocused' ).to( this, 'isFocused' );
 
 		/**
 		 * Map of registered {@link engine.view.Observer observers}.

+ 9 - 2
packages/ckeditor5-engine/src/view/editableelement.js

@@ -43,13 +43,20 @@ export default class EditableElement extends ContainerElement {
 		/**
 		 * Whether the editable is focused.
 		 *
-		 * This property updates when {@link engine.view.Document#focusedEditable} changes.
+		 * This property updates when {@link engine.view.Document#isFocused} or {@link engine.view.Document#selectedEditable}
+		 * change.
 		 *
 		 * @readonly
 		 * @observable
 		 * @member {Boolean} engine.view.RootEditableElement#isFocused
 		 */
-		this.bind( 'isFocused' ).to( document, 'focusedEditable', ( focusedEditable ) => focusedEditable == this );
+		this.bind( 'isFocused' ).to(
+			document,
+			'isFocused',
+			document,
+			'selectedEditable',
+			( isFocused, selectedEditable ) => isFocused && selectedEditable == this
+		);
 	}
 
 	/**

+ 4 - 5
packages/ckeditor5-engine/src/view/observer/focusobserver.js

@@ -23,14 +23,13 @@ export default class FocusObserver extends DomEventObserver {
 
 		this.domEventType = [ 'focus', 'blur' ];
 
-		// Update `isFocus` property of root elements and `document#focusedEditable`.
-		document.on( 'focus', ( evt, data ) => {
-			document.focusedEditable = data.target;
+		document.on( 'focus', () => {
+			document.isFocused = true;
 		} );
 
 		document.on( 'blur', ( evt, data ) => {
-			if ( document.focusedEditable == data.target ) {
-				document.focusedEditable = null;
+			if ( document.selectedEditable === null || document.selectedEditable === data.target ) {
+				document.isFocused = false;
 			}
 		} );
 	}

+ 1 - 5
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -116,11 +116,7 @@ export default class SelectionObserver extends Observer {
 	 * @param {Document} domDocument DOM document.
 	 */
 	_handleSelectionChange( domDocument ) {
-		if ( !this.isEnabled ) {
-			return;
-		}
-
-		if ( !this.document.focusedEditable ) {
+		if ( !this.isEnabled || !this.document.isFocused ) {
 			return;
 		}
 

+ 20 - 0
packages/ckeditor5-engine/src/view/position.js

@@ -9,6 +9,7 @@ import Text from './text.js';
 
 import compareArrays from '../../utils/comparearrays.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
+import EditableElement from './editableelement.js';
 
 /**
  * Position in the tree. Position is always located before or after a node.
@@ -186,6 +187,25 @@ export default class Position {
 		return index < 0 ? 'BEFORE' : 'AFTER';
 	}
 
+	/**
+	 * Returns {@link engine.view.EditableElement EditableElement} instance that contains this position.
+	 *
+	 * @returns {engine.view.EditableElement|null} Returns closest EditableElement or null if none is found.
+	 */
+	getEditableElement() {
+		let editable = this.parent;
+
+		while ( !( editable instanceof EditableElement ) ) {
+			if ( editable.parent ) {
+				editable = editable.parent;
+			} else {
+				return null;
+			}
+		}
+
+		return editable;
+	}
+
 	/**
 	 * Creates a new position after the given node.
 	 *

+ 9 - 9
packages/ckeditor5-engine/src/view/renderer.js

@@ -90,13 +90,12 @@ export default class Renderer {
 		this._inlineFillerPosition = null;
 
 		/**
-		 * {@link engine.view.EditableElement} in which selection is allowed to be rendered.
-		 * If it is null, then selection will not be rendered.
+		 * Indicates if view document is focused and selection can be rendered. Selection will not be rendered if
+		 * this is set to `false`.
 		 *
-		 * @readonly
-		 * @member {engine.view.EditableElement|null} engine.view.Renderer#focusedEditable
+		 * @type {boolean}
 		 */
-		this.focusedEditable = null;
+		this.isFocused = false;
 	}
 
 	/**
@@ -412,11 +411,12 @@ export default class Renderer {
 	 * @private
 	 */
 	_updateSelection() {
-		if ( !this.focusedEditable ) {
+		if ( !this.isFocused ) {
 			return;
 		}
 
-		const domRoot = this.domConverter.getCorrespondingDomElement( this.focusedEditable );
+		const selectedEditable = this.selection.getEditableElement();
+		const domRoot = this.domConverter.getCorrespondingDomElement( selectedEditable );
 
 		if ( !domRoot ) {
 			return;
@@ -432,8 +432,8 @@ export default class Renderer {
 		domSelection.removeAllRanges();
 
 		for ( let range of this.selection.getRanges() ) {
-			// Update ranges only in currently focused editable.
-			if ( range.start.parent.getRoot() == this.focusedEditable ) {
+			// Update ranges only in currently selected editable.
+			if ( range.start.parent.getRoot() == selectedEditable ) {
 				const domRangeStart = this.domConverter.viewPositionToDom( range.start );
 				const domRangeEnd = this.domConverter.viewPositionToDom( range.end );
 				const domRange = new Range();

+ 14 - 0
packages/ckeditor5-engine/src/view/selection.js

@@ -279,6 +279,7 @@ export default class Selection {
 		}
 
 		this._lastRangeBackward = otherSelection._lastRangeBackward;
+		this.fire( 'change' );
 	}
 
 	/**
@@ -313,6 +314,19 @@ export default class Selection {
 		}
 	}
 
+	/**
+	 * Returns {@link engine.view.EditableElement EditableElement} instance that contains this selection.
+	 *
+	 * @returns {engine.view.EditableElement|null} Returns closest EditableElement or null if none is found.
+	 */
+	getEditableElement() {
+		if ( this.rangeCount ) {
+			return this.getFirstPosition().getEditableElement();
+		}
+
+		return null;
+	}
+
 	/**
 	 * Adds range to selection - creates copy of given range so it can be safely used and modified.
 	 *

+ 6 - 0
packages/ckeditor5-engine/tests/_utils-tests/view.js

@@ -317,6 +317,12 @@ describe( 'view test utils', () => {
 			expect( text.data ).to.equal( 'foobar' );
 		} );
 
+		it( 'should parse text with spaces', () => {
+			const text = parse( 'foo bar' );
+			expect( text ).to.be.instanceOf( Text );
+			expect( text.data ).to.equal( 'foo bar' );
+		} );
+
 		it( 'should parse elements and texts', () => {
 			const view = parse( '<b>foobar</b>' );
 			const element = new Element( 'b' );

+ 1 - 1
packages/ckeditor5-engine/tests/_utils/view.js

@@ -331,7 +331,7 @@ class RangeParser {
 
 		if ( node instanceof ViewText ) {
 			const regexp = new RegExp(
-				`[ ${ TEXT_RANGE_START_TOKEN }${ TEXT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_START_TOKEN } ]`,
+				`[${ TEXT_RANGE_START_TOKEN }${ TEXT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_START_TOKEN }]`,
 				'g'
 			);
 			let text = node.data;

+ 2 - 1
packages/ckeditor5-engine/tests/editingcontroller.js

@@ -192,7 +192,8 @@ describe( 'EditingController', () => {
 				} );
 			} );
 
-			editing.view.focusedEditable = viewRoot;
+			editing.view.selectedEditable = viewRoot;
+			editing.view.isFocused = true;
 
 			const domSelection = document.getSelection();
 			domSelection.removeAllRanges();

+ 2 - 1
packages/ckeditor5-engine/tests/view/_utils/createdocumentmock.js

@@ -16,7 +16,8 @@ import ObservableMixin from '/ckeditor5/utils/observablemixin.js';
  */
 export default function createDocumentMock() {
 	const doc = Object.create( ObservableMixin );
-	doc.set( 'focusedEditable', null );
+	doc.set( 'isFocused', false );
+	doc.set( 'selectedEditable', null );
 
 	return doc;
 }

+ 23 - 9
packages/ckeditor5-engine/tests/view/document/document.js

@@ -10,6 +10,7 @@
 import Document from '/ckeditor5/engine/view/document.js';
 import Observer from '/ckeditor5/engine/view/observer/observer.js';
 import Renderer from '/ckeditor5/engine/view/renderer.js';
+import ViewRange from '/ckeditor5/engine/view/range.js';
 import Writer from '/ckeditor5/engine/view/writer.js';
 import DomConverter from '/ckeditor5/engine/view/domconverter.js';
 
@@ -56,7 +57,8 @@ describe( 'Document', () => {
 			expect( viewDocument ).to.have.property( 'renderer' ).that.is.instanceOf( Renderer );
 			expect( viewDocument ).to.have.property( 'writer' ).that.is.instanceOf( Writer );
 			expect( viewDocument ).to.have.property( 'domConverter' ).that.is.instanceOf( DomConverter );
-			expect( viewDocument ).to.have.property( 'focusedEditable' ).that.is.null;
+			expect( viewDocument ).to.have.property( 'selectedEditable' ).that.is.null;
+			expect( viewDocument ).to.have.property( 'isFocused' ).that.is.false;
 		} );
 	} );
 
@@ -336,18 +338,30 @@ describe( 'Document', () => {
 		} );
 	} );
 
-	describe( 'focusedEditable', () => {
-		it( 'should change renderer.focusedEditable too', () => {
+	describe( 'isFocused', () => {
+		it( 'should change renderer.isFocused too', () => {
 			const viewDocument = new Document();
-			const viewRoot = viewDocument.createRoot( 'div' );
 
-			expect( viewDocument.focusedEditable ).to.equal( null );
-			expect( viewDocument.renderer.focusedEditable ).to.equal( null );
+			expect( viewDocument.isFocused ).to.equal( false );
+			expect( viewDocument.renderer.isFocused ).to.equal( false );
+
+			viewDocument.isFocused = true;
+
+			expect( viewDocument.isFocused ).to.equal( true );
+			expect( viewDocument.renderer.isFocused ).to.equal( true );
+		} );
+	} );
 
-			viewDocument.focusedEditable = viewRoot;
+	describe( 'selectedEditable', () => {
+		it( 'should change when selection is moved to different editable', () => {
+			const viewDocument = new Document();
+
+			expect( viewDocument.selectedEditable ).to.equal( null );
+
+			const viewRoot = viewDocument.createRoot( 'div' );
+			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewRoot, 0, viewRoot, 0 ) );
 
-			expect( viewDocument.focusedEditable ).to.equal( viewRoot );
-			expect( viewDocument.renderer.focusedEditable ).to.equal( viewRoot );
+			expect( viewDocument.selectedEditable ).to.equal( viewRoot );
 		} );
 	} );
 } );

+ 2 - 1
packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js

@@ -28,7 +28,8 @@ describe( 'Document', () => {
 
 		document.getSelection().removeAllRanges();
 
-		viewDocument.focusedEditable = viewRoot;
+		viewDocument.selectedEditable = viewRoot;
+		viewDocument.isFocused = true;
 	} );
 
 	describe( 'jump over inline filler hack', () => {

+ 103 - 0
packages/ckeditor5-engine/tests/view/editableelement.js

@@ -0,0 +1,103 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: view */
+
+'use strict';
+
+import createDocumentMock from '/tests/engine/view/_utils/createdocumentmock.js';
+import RootEditableElement from '/ckeditor5/engine/view/rooteditableelement.js';
+
+describe( 'EditableElement', () => {
+	describe( 'isFocused', () => {
+		let docMock, viewMain, viewHeader;
+
+		beforeEach( () => {
+			docMock = createDocumentMock();
+
+			viewMain = new RootEditableElement( docMock, 'div' );
+			viewHeader = new RootEditableElement( docMock, 'h1', 'header' );
+		} );
+
+		it( 'should be observable', () => {
+			const root = new RootEditableElement( createDocumentMock(), 'div' );
+
+			expect( root.isFocused ).to.be.false;
+
+			const isFocusedSpy = sinon.spy();
+
+			root.on( 'change:isFocused', isFocusedSpy );
+
+			root.isFocused = true;
+
+			expect( root.isFocused ).to.be.true;
+
+			expect( isFocusedSpy.calledOnce ).to.be.true;
+		} );
+
+		it( 'should change isFocused when selectedEditable changes', () => {
+			docMock.selectedEditable = viewMain;
+			docMock.isFocused = true;
+
+			expect( viewMain.isFocused ).to.be.true;
+			expect( viewHeader.isFocused ).to.be.false;
+
+			docMock.selectedEditable = viewHeader;
+
+			expect( viewMain.isFocused ).to.be.false;
+			expect( viewHeader.isFocused ).to.be.true;
+
+			docMock.selectedEditable = null;
+
+			expect( viewMain.isFocused ).to.be.false;
+			expect( viewHeader.isFocused ).to.be.false;
+		} );
+
+		it( 'should change isFocused when document.isFocus changes', () => {
+			docMock.selectedEditable = viewMain;
+			docMock.isFocused = true;
+
+			expect( viewMain.isFocused ).to.be.true;
+			expect( viewHeader.isFocused ).to.be.false;
+
+			docMock.isFocused = false;
+
+			expect( viewMain.isFocused ).to.be.false;
+			expect( viewHeader.isFocused ).to.be.false;
+
+			docMock.selectedEditable = viewHeader;
+
+			expect( viewMain.isFocused ).to.be.false;
+			expect( viewHeader.isFocused ).to.be.false;
+		} );
+	} );
+
+	describe( 'isReadOnly', () => {
+		it( 'should be observable', () => {
+			const root = new RootEditableElement( createDocumentMock(), 'div' );
+
+			expect( root.isReadOnly ).to.be.false;
+
+			const isReadOnlySpy = sinon.spy();
+
+			root.on( 'change:isReadOnly', isReadOnlySpy );
+
+			root.isReadOnly = true;
+
+			expect( root.isReadOnly ).to.be.true;
+
+			expect( isReadOnlySpy.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'getDocument', ()=> {
+		it( 'should return document', () => {
+			const docMock = createDocumentMock();
+			const root = new RootEditableElement( docMock, 'div' );
+
+			expect( root.getDocument() ).to.equal( docMock );
+		} );
+	} );
+} );

+ 0 - 2
packages/ckeditor5-engine/tests/view/manual/focusobserver.html

@@ -1,6 +1,4 @@
 <div contenteditable="true" id="editable1" style="border: solid 1px black; background-color: red">
-	<p>First editable.</p>
 </div>
 <div contenteditable="true" id="editable2" style="border: solid 1px black; background-color: red">
-	<p>Second editable.</p>
 </div>

+ 16 - 2
packages/ckeditor5-engine/tests/view/manual/focusobserver.js

@@ -8,20 +8,34 @@
 'use strict';
 
 import Document from '/ckeditor5/engine/view/document.js';
+import MutationObserver from '/ckeditor5/engine/view/observer/mutationobserver.js';
+import SelectionObserver from '/ckeditor5/engine/view/observer/selectionobserver.js';
 import FocusObserver from '/ckeditor5/engine/view/observer/focusobserver.js';
+import { setData } from '/tests/engine/_utils/view.js';
 
 const viewDocument = new Document();
 
 viewDocument.on( 'focus', ( evt, data ) => console.log( 'event:focus', data.domTarget ) );
 viewDocument.on( 'blur', ( evt, data ) => console.log( 'event:blur', data.domTarget ) );
 
-viewDocument.addObserver( FocusObserver );
-
 const domEditable1 = document.getElementById( 'editable1' );
 const domEditable2 = document.getElementById( 'editable2' );
 
 const editable1 = viewDocument.createRoot( domEditable1, 'editable1' );
 const editable2 = viewDocument.createRoot( domEditable2, 'editable2' );
 
+viewDocument.on( 'selectionChange', ( evt, data ) => {
+	viewDocument.selection.setTo( data.newSelection );
+} );
+
+viewDocument.addObserver( MutationObserver );
+viewDocument.addObserver( SelectionObserver );
+viewDocument.addObserver( FocusObserver );
+
+setData( viewDocument, '<p>First editable.</p>', { rootName: 'editable1' } );
+setData( viewDocument, '<p>Second editable.</p>', { rootName: 'editable2' } );
+
 editable1.on( 'change:isFocused', () => domEditable1.style.backgroundColor = editable1.isFocused ? 'green' : 'red' );
 editable2.on( 'change:isFocused', () => domEditable2.style.backgroundColor = editable2.isFocused ? 'green' : 'red' );
+
+viewDocument.render();

+ 2 - 3
packages/ckeditor5-engine/tests/view/manual/inline-filler.js

@@ -12,13 +12,13 @@ import KeyObserver from '/ckeditor5/engine/view/observer/keyobserver.js';
 import { setData } from '/tests/engine/_utils/view.js';
 
 const viewDocument = new Document();
-const viewRoot = viewDocument.createRoot( document.getElementById( 'editor' ) );
+viewDocument.createRoot( document.getElementById( 'editor' ) );
 
 viewDocument.addObserver( MutationObserver );
 viewDocument.addObserver( SelectionObserver );
 viewDocument.addObserver( KeyObserver );
 
-viewDocument.focusedEditable = viewRoot;
+viewDocument.isFocused = true;
 
 setData( viewDocument,
 	'<container:p><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>' );
@@ -28,4 +28,3 @@ viewDocument.on( 'selectionChange', ( evt, data ) => {
 } );
 
 viewDocument.render();
-

+ 3 - 3
packages/ckeditor5-engine/tests/view/manual/selectionobserver.js

@@ -10,15 +10,15 @@
 import Document from '/ckeditor5/engine/view/document.js';
 import MutationObserver from '/ckeditor5/engine/view/observer/mutationobserver.js';
 import SelectionObserver from '/ckeditor5/engine/view/observer/selectionobserver.js';
+import FocusObserver from '/ckeditor5/engine/view/observer/focusobserver.js';
 import { setData } from '/tests/engine/_utils/view.js';
 
 const viewDocument = new Document();
-const viewRoot = viewDocument.createRoot( document.getElementById( 'editor' ) );
+viewDocument.createRoot( document.getElementById( 'editor' ) );
 
 viewDocument.addObserver( MutationObserver );
 viewDocument.addObserver( SelectionObserver );
-
-viewDocument.focusedEditable = viewRoot;
+viewDocument.addObserver( FocusObserver );
 
 setData( viewDocument,
 	'<container:p><attribute:b>foo</attribute:b>bar</container:p>' +

+ 2 - 1
packages/ckeditor5-engine/tests/view/manual/x-index.js

@@ -18,7 +18,8 @@ const viewRoot = viewDocument.createRoot( document.getElementById( 'editor' ) );
 viewDocument.addObserver( MutationObserver );
 viewDocument.addObserver( SelectionObserver );
 
-viewDocument.focusedEditable = viewRoot;
+viewDocument.selectedEditable = viewRoot;
+viewDocument.isFocused = true;
 
 setData( viewDocument,
 	'<container:p>fo{}o</container:p>' +

+ 19 - 15
packages/ckeditor5-engine/tests/view/observer/focusobserver.js

@@ -9,6 +9,7 @@
 
 import FocusObserver from '/ckeditor5/engine/view/observer/focusobserver.js';
 import ViewDocument from '/ckeditor5/engine/view/document.js';
+import ViewRange from '/ckeditor5/engine/view/range.js';
 
 describe( 'FocusObserver', () => {
 	let viewDocument, observer;
@@ -50,7 +51,7 @@ describe( 'FocusObserver', () => {
 		} );
 	} );
 
-	describe( 'handle focusedEditable property of the document', () => {
+	describe( 'handle isFocused property of the document', () => {
 		let domMain, domHeader, viewMain, viewHeader;
 
 		beforeEach( () => {
@@ -61,41 +62,44 @@ describe( 'FocusObserver', () => {
 			viewHeader = viewDocument.createRoot( domHeader, 'header' );
 		} );
 
-		it( 'should set focusedEditable on focus', () => {
+		it( 'should set isFocused to true on focus', () => {
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
-			expect( viewDocument.focusedEditable ).to.equal( viewMain );
+			expect( viewDocument.isFocused ).to.equal( true );
 		} );
 
-		it( 'should change focusedEditable on focus', () => {
+		it( 'should set isFocused to false on blur', () => {
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
-			expect( viewDocument.focusedEditable ).to.equal( viewMain );
+			expect( viewDocument.isFocused ).to.equal( true );
 
-			observer.onDomEvent( { type: 'focus', target: domHeader } );
+			observer.onDomEvent( { type: 'blur', target: domMain } );
 
-			expect( viewDocument.focusedEditable ).to.equal( viewHeader );
+			expect( viewDocument.isFocused ).to.be.false;
 		} );
 
-		it( 'should set focusedEditable to null on blur', () => {
+		it( 'should set isFocused to false on blur when selection in same editable', () => {
+			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
+
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
-			expect( viewDocument.focusedEditable ).to.equal( viewMain );
+			expect( viewDocument.isFocused ).to.equal( true );
 
 			observer.onDomEvent( { type: 'blur', target: domMain } );
 
-			expect( viewDocument.focusedEditable ).to.be.null;
+			expect( viewDocument.isFocused ).to.be.false;
 		} );
 
-		it( 'should not touch focusedEditable on blur if it is already changed', () => {
+		it( 'should not set isFocused to false on blur when it is fired on other editable', () => {
+			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
+
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
-			expect( viewDocument.focusedEditable ).to.equal( viewMain );
+			expect( viewDocument.isFocused ).to.equal( true );
 
-			observer.onDomEvent( { type: 'focus', target: domHeader } );
-			observer.onDomEvent( { type: 'blur', target: domMain } );
+			observer.onDomEvent( { type: 'blur', target: domHeader } );
 
-			expect( viewDocument.focusedEditable ).to.equal( viewHeader );
+			expect( viewDocument.isFocused ).to.be.true;
 		} );
 	} );
 } );

+ 4 - 3
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -47,7 +47,8 @@ describe( 'SelectionObserver', () => {
 		viewDocument.selection.removeAllRanges();
 		document.getSelection().removeAllRanges();
 
-		viewDocument.focusedEditable = viewRoot;
+		viewDocument.selectedEditable = viewRoot;
+		viewDocument.isFocused = true;
 
 		viewDocument.getObserver( SelectionObserver ).enable();
 
@@ -118,8 +119,8 @@ describe( 'SelectionObserver', () => {
 		changeDomSelection();
 	} );
 
-	it( 'should not fired if there is no focusedEditable', ( done ) => {
-		viewDocument.focusedEditable = null;
+	it( 'should not fired if there is no focus', ( done ) => {
+		viewDocument.isFocused = false;
 
 		listenter.listenTo( viewDocument, 'selectionChange', () => {
 			throw 'selectionChange on render';

+ 19 - 0
packages/ckeditor5-engine/tests/view/position.js

@@ -10,6 +10,8 @@
 import Position from '/ckeditor5/engine/view/position.js';
 import Node from '/ckeditor5/engine/view/node.js';
 import Element from '/ckeditor5/engine/view/element.js';
+import EditableElement from '/ckeditor5/engine/view/editableelement.js';
+import Document from '/ckeditor5/engine/view/document.js';
 import Text from '/ckeditor5/engine/view/text.js';
 
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
@@ -319,4 +321,21 @@ describe( 'Position', () => {
 			} ).to.throw( CKEditorError, /position-after-root/ );
 		} );
 	} );
+
+	describe( 'getEditableElement', () => {
+		it( 'should return null if position is not inside EditableElement', () => {
+			const position = new Position( new Element( 'p' ), 0 );
+
+			expect( position.getEditableElement() ).to.be.null;
+		} );
+
+		it( 'should return EditableElement when position is placed inside', () => {
+			const document = new Document();
+			const p = new Element( 'p' );
+			const editable = new EditableElement( document, 'div', null, p );
+			const position = new Position( p, 0 );
+
+			expect( position.getEditableElement() ).to.equal( editable );
+		} );
+	} );
 } );

+ 42 - 10
packages/ckeditor5-engine/tests/view/renderer.js

@@ -16,9 +16,11 @@ import Renderer from '/ckeditor5/engine/view/renderer.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import { parse } from '/tests/engine/_utils/view.js';
 import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, BR_FILLER } from '/ckeditor5/engine/view/filler.js';
-
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import createElement from '/ckeditor5/utils/dom/createelement.js';
 
+testUtils.createSinonSandbox();
+
 describe( 'Renderer', () => {
 	let selection, domConverter, renderer;
 
@@ -101,7 +103,7 @@ describe( 'Renderer', () => {
 	} );
 
 	describe( 'render', () => {
-		let viewRoot, domRoot;
+		let viewRoot, domRoot, selectionEditable;
 
 		beforeEach( () => {
 			viewRoot = new ViewElement( 'div' );
@@ -114,9 +116,14 @@ describe( 'Renderer', () => {
 			renderer.markedAttributes.clear();
 			renderer.markedChildren.clear();
 
-			renderer.focusedEditable = viewRoot;
-
 			selection.removeAllRanges();
+
+			selectionEditable = viewRoot;
+
+			renderer.isFocused = true;
+
+			// Fake selection editable - it is needed to render selection properly.
+			testUtils.sinon.stub( selection, 'getEditableElement', () => selectionEditable );
 		} );
 
 		it( 'should update attributes', () => {
@@ -287,8 +294,6 @@ describe( 'Renderer', () => {
 			viewRoot.appendChildren( viewP );
 			selection.setTo( newSelection );
 
-			renderer.focusedEditable = viewRoot;
-
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
 
@@ -779,7 +784,34 @@ describe( 'Renderer', () => {
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
 
-		it( 'should not change selection if there is no focusedEditable', () => {
+		it( 'should not change selection if there is no editable with selection', () => {
+			const domDiv = createElement( document, 'div', null, 'not editable' );
+			document.body.appendChild( domDiv );
+
+			const domSelection = document.getSelection();
+
+			domSelection.removeAllRanges();
+			const domRange = new Range();
+			domRange.setStart( domDiv, 0 );
+			domRange.collapse( true );
+			domSelection.addRange( domRange );
+
+			selectionEditable = null;
+
+			const { view: viewP, selection: newSelection } = parse( '<container:p>fo{o}</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.render();
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domDiv );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 0 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.equal( true );
+		} );
+
+		it( 'should not change selection if there is no focus', () => {
 			const domDiv = createElement( document, 'div', null, 'not editable' );
 			document.body.appendChild( domDiv );
 
@@ -791,7 +823,7 @@ describe( 'Renderer', () => {
 			domRange.collapse( true );
 			domSelection.addRange( domRange );
 
-			renderer.focusedEditable = null;
+			renderer.isFocused = false;
 
 			const { view: viewP, selection: newSelection } = parse( '<container:p>fo{o}</container:p>' );
 
@@ -806,14 +838,14 @@ describe( 'Renderer', () => {
 			expect( domSelection.getRangeAt( 0 ).collapsed ).to.equal( true );
 		} );
 
-		it( 'should not add ranges if different editable is focused', () => {
+		it( 'should not add ranges if different editable is selected', () => {
 			const domHeader = document.createElement( 'h1' );
 			const viewHeader = new ViewElement( 'h1' );
 			document.body.appendChild( domHeader );
 
 			domConverter.bindElements( domHeader, viewHeader );
 
-			renderer.focusedEditable = viewHeader;
+			selectionEditable = viewHeader;
 
 			const { view: viewP, selection: newSelection } = parse( '<container:p>fo{o}</container:p>' );
 

+ 0 - 71
packages/ckeditor5-engine/tests/view/rooteditableelement.js

@@ -38,75 +38,4 @@ describe( 'RootEditableElement', () => {
 			expect( root.isReadOnly ).to.be.false;
 		} );
 	} );
-
-	describe( 'isFocused', () => {
-		let docMock, viewMain, viewHeader;
-
-		beforeEach( () => {
-			docMock = createDocumentMock();
-
-			viewMain = new RootEditableElement( docMock, 'div' );
-			viewHeader = new RootEditableElement( docMock, 'h1', 'header' );
-		} );
-
-		it( 'should be observable', () => {
-			const root = new RootEditableElement( createDocumentMock(), 'div' );
-
-			expect( root.isFocused ).to.be.false;
-
-			const isFocusedSpy = sinon.spy();
-
-			root.on( 'change:isFocused', isFocusedSpy );
-
-			root.isFocused = true;
-
-			expect( root.isFocused ).to.be.true;
-
-			expect( isFocusedSpy.calledOnce ).to.be.true;
-		} );
-
-		it( 'should change isFocused when focusedEditable changes', () => {
-			docMock.focusedEditable = viewMain;
-
-			expect( viewMain.isFocused ).to.be.true;
-			expect( viewHeader.isFocused ).to.be.false;
-
-			docMock.focusedEditable = viewHeader;
-
-			expect( viewMain.isFocused ).to.be.false;
-			expect( viewHeader.isFocused ).to.be.true;
-
-			docMock.focusedEditable = null;
-
-			expect( viewMain.isFocused ).to.be.false;
-			expect( viewHeader.isFocused ).to.be.false;
-		} );
-	} );
-
-	describe( 'isReadOnly', () => {
-		it( 'should be observable', () => {
-			const root = new RootEditableElement( createDocumentMock(), 'div' );
-
-			expect( root.isReadOnly ).to.be.false;
-
-			const isReadOnlySpy = sinon.spy();
-
-			root.on( 'change:isReadOnly', isReadOnlySpy );
-
-			root.isReadOnly = true;
-
-			expect( root.isReadOnly ).to.be.true;
-
-			expect( isReadOnlySpy.calledOnce ).to.be.true;
-		} );
-	} );
-
-	describe( 'getDocument', ()=> {
-		it( 'should return document', () => {
-			const docMock = createDocumentMock();
-			const root = new RootEditableElement( docMock, 'div' );
-
-			expect( root.getDocument() ).to.equal( docMock );
-		} );
-	} );
 } );

+ 38 - 0
packages/ckeditor5-engine/tests/view/selection.js

@@ -9,6 +9,7 @@
 
 import Selection from '/ckeditor5/engine/view/selection.js';
 import Range from '/ckeditor5/engine/view/range.js';
+import Document from '/ckeditor5/engine/view/document.js';
 import Element from '/ckeditor5/engine/view/element.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
@@ -361,6 +362,19 @@ describe( 'Selection', () => {
 
 			expect( selection.anchor.isEqual( range3.end ) ).to.be.true;
 		} );
+
+		it( 'should fire change event', ( done ) => {
+			selection.on( 'change', () => {
+				expect( selection.rangeCount ).to.equal( 1 );
+				expect( selection.getFirstRange().isEqual( range1 ) ).to.be.true;
+				done();
+			} );
+
+			const otherSelection = new Selection();
+			otherSelection.addRange( range1 );
+
+			selection.setTo( otherSelection );
+		} );
 	} );
 
 	describe( 'collapseToStart', () => {
@@ -408,4 +422,28 @@ describe( 'Selection', () => {
 			expect( fireSpy.notCalled ).to.be.true;
 		} );
 	} );
+
+	describe( 'getEditableElement', () => {
+		it( 'should return null if no ranges in selection', () => {
+			expect( selection.getEditableElement() ).to.be.null;
+		} );
+
+		it( 'should return null if selection is placed in container that is not EditableElement', () => {
+			selection.addRange( range1 );
+
+			expect( selection.getEditableElement() ).to.be.null;
+		} );
+
+		it( 'should return EditableElement when selection is placed inside', () => {
+			const viewDocument = new Document();
+			const selection = viewDocument.selection;
+			const root = viewDocument.createRoot( 'div' );
+			const element = new Element( 'p' );
+			root.appendChildren( element );
+
+			selection.addRange( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
+
+			expect( selection.getEditableElement() ).to.equal( root );
+		} );
+	} );
 } );

+ 7 - 2
packages/ckeditor5-engine/tests/view/utils-tests/createdocumentmock.js

@@ -14,11 +14,16 @@ describe( 'createDocumentMock', () => {
 		const docMock = createDocumentMock();
 		const rootMock = {};
 
-		docMock.on( 'change:focusedEditable', ( evt, key, value ) => {
+		docMock.on( 'change:selectedEditable', ( evt, key, value ) => {
 			expect( value ).to.equal( rootMock );
+		} );
+
+		docMock.on( 'change:isFocused', ( evt, key, value ) => {
+			expect( value ).to.be.true;
 			done();
 		} );
 
-		docMock.focusedEditable = rootMock;
+		docMock.selectedEditable = rootMock;
+		docMock.isFocused = true;
 	} );
 } );