瀏覽代碼

Merge pull request #451 from ckeditor/t/405

t/405: Selection & focus.
Piotrek Koszuliński 9 年之前
父節點
當前提交
6c8a11c97c
共有 23 個文件被更改,包括 317 次插入97 次删除
  1. 16 3
      packages/ckeditor5-engine/src/view/document.js
  2. 29 1
      packages/ckeditor5-engine/src/view/editableelement.js
  3. 4 14
      packages/ckeditor5-engine/src/view/observer/focusobserver.js
  4. 5 0
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  5. 24 28
      packages/ckeditor5-engine/src/view/renderer.js
  6. 1 30
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  7. 2 0
      packages/ckeditor5-engine/tests/editingcontroller.js
  8. 22 0
      packages/ckeditor5-engine/tests/view/_utils/createdocumentmock.js
  9. 16 0
      packages/ckeditor5-engine/tests/view/document/document.js
  10. 1 1
      packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.html
  11. 4 1
      packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js
  12. 2 0
      packages/ckeditor5-engine/tests/view/manual/immutable.js
  13. 4 1
      packages/ckeditor5-engine/tests/view/manual/inline-filler.js
  14. 25 0
      packages/ckeditor5-engine/tests/view/manual/noselection.js
  15. 4 0
      packages/ckeditor5-engine/tests/view/manual/noselection.md
  16. 3 1
      packages/ckeditor5-engine/tests/view/manual/selectionobserver.js
  17. 3 1
      packages/ckeditor5-engine/tests/view/manual/x-index.js
  18. 4 2
      packages/ckeditor5-engine/tests/view/node.js
  19. 26 9
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  20. 14 0
      packages/ckeditor5-engine/tests/view/observer/selectionobserver.js
  21. 51 0
      packages/ckeditor5-engine/tests/view/renderer.js
  22. 33 5
      packages/ckeditor5-engine/tests/view/rooteditableelement.js
  23. 24 0
      packages/ckeditor5-engine/tests/view/utils-tests/createdocumentmock.js

+ 16 - 3
packages/ckeditor5-engine/src/view/document.js

@@ -13,7 +13,7 @@ import RootEditableElement from './rooteditableelement.js';
 import { injectQuirksHandling } from './filler.js';
 
 import mix from '../../utils/mix.js';
-import EmitterMixin from '../../utils/emittermixin.js';
+import ObservableMixin from '../../utils/observablemixin.js';
 
 /**
  * Document class creates an abstract layer over the content editable area.
@@ -74,18 +74,31 @@ export default class Document {
 		this.roots = new Map();
 
 		/**
+		 * {@link engine.view.EditableElement} which is currently focused or null if all of them are blurred.
+		 *
+		 * 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
+		 */
+		this.set( 'focusedEditable', null );
+
+		/**
 		 * Instance of the {@link engine.view.Document#renderer renderer}.
 		 *
 		 * @readonly
 		 * @member {engine.view.Renderer} engine.view.Document#renderer
 		 */
 		this.renderer = new Renderer( this.domConverter, this.selection );
+		this.renderer.bind( 'focusedEditable' ).to( this, 'focusedEditable' );
 
 		/**
 		 * Map of registered {@link engine.view.Observer observers}.
 		 *
 		 * @private
-		 * @member {Map.<Function, engine.view.Observer>} engine.view.Document_#observers
+		 * @member {Map.<Function, engine.view.Observer>} engine.view.Document#_observers
 		 */
 		this._observers = new Map();
 
@@ -250,7 +263,7 @@ export default class Document {
 	}
 }
 
-mix( Document, EmitterMixin );
+mix( Document, ObservableMixin );
 
 /**
  * Enum representing type of the change.

+ 29 - 1
packages/ckeditor5-engine/src/view/editableelement.js

@@ -21,16 +21,44 @@ export default class EditableElement extends ContainerElement {
 	/**
 	 * Creates an editable element.
 	 */
-	constructor( name, attrs, children ) {
+	constructor( document, name, attrs, children ) {
 		super( name, attrs, children );
 
 		/**
+		 * {@link engine.view.Document} that is an owner of this root.
+		 *
+		 * @private
+		 * @member {engine.view.Document} engine.view.RootEditableElement#_document
+		 */
+		this._document = document;
+
+		/**
 		 * Whether the editable is in read-write or read-only mode.
 		 *
 		 * @observable
 		 * @member {Boolean} engine.view.EditableElement#isReadOnly
 		 */
 		this.set( 'isReadOnly', false );
+
+		/**
+		 * Whether the editable is focused.
+		 *
+		 * This property updates when {@link engine.view.Document#focusedEditable} changes.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} engine.view.RootEditableElement#isFocused
+		 */
+		this.bind( 'isFocused' ).to( document, 'focusedEditable', ( focusedEditable ) => focusedEditable == this );
+	}
+
+	/**
+	 * Gets the {@link engine.view.Document} reference.
+	 *
+	 * @returns {engine.view.Document|null} View Document of the node or `null`.
+	 */
+	getDocument() {
+		return this._document;
 	}
 }
 

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

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

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

@@ -97,12 +97,17 @@ export default class SelectionObserver extends Observer {
 			return;
 		}
 
+		if ( !this.document.focusedEditable ) {
+			return;
+		}
+
 		// Ensure the mutation event will be before selection event on all browsers.
 		this.mutationObserver.flush();
 
 		// If there were mutations then the view will be re-rendered by the mutation observer and selection
 		// will be updated, so selections will equal and event will not be fired, as expected.
 		const domSelection = domDocument.defaultView.getSelection();
+
 		const newViewSelection = this.domConverter.domSelectionToView( domSelection );
 
 		if ( this.selection.isEqual( newViewSelection ) ) {

+ 24 - 28
packages/ckeditor5-engine/src/view/renderer.js

@@ -10,9 +10,11 @@ import ViewElement from './element.js';
 import ViewPosition from './position.js';
 import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller, isBlockFiller } from './filler.js';
 
+import mix from '../../utils/mix.js';
 import diff from '../../utils/diff.js';
 import insertAt from '../../utils/dom/insertat.js';
 import remove from '../../utils/dom/remove.js';
+import ObservableMixin from '../../utils/observablemixin.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
@@ -88,16 +90,13 @@ export default class Renderer {
 		this._inlineFillerPosition = null;
 
 		/**
-		 * Last DOM selection object.
+		 * {@link engine.view.EditableElement} in which selection is allowed to be rendered.
+		 * If it is null, then selection will not be rendered.
 		 *
-		 * Because renderer handles multiple roots, and because these roots might be in different documents (in case of
-		 * using `iframes`) renderer needs to keep last DOM selection object to remove ranges from it before new selection
-		 * is rendered.
-		 *
-		 * @private
-		 * @member {Selection} engine.view.Renderer#_domSelection
+		 * @readonly
+		 * @member {engine.view.EditableElement|null} engine.view.Renderer#focusedEditable
 		 */
-		this._domSelection = null;
+		this.focusedEditable = null;
 	}
 
 	/**
@@ -407,35 +406,32 @@ export default class Renderer {
 	 * @private
 	 */
 	_updateSelection() {
-		let domSelection = this._domSelection;
-		const oldViewSelection = domSelection && this.domConverter.domSelectionToView( domSelection );
-
-		if ( !oldViewSelection && !this.selection.rangeCount ) {
+		if ( !this.focusedEditable ) {
 			return;
 		}
 
+		const domRoot = this.domConverter.getCorrespondingDomElement( this.focusedEditable );
+		const domSelection = domRoot.ownerDocument.defaultView.getSelection();
+		const oldViewSelection = domSelection && this.domConverter.domSelectionToView( domSelection );
+
 		if ( oldViewSelection && this.selection.isEqual( oldViewSelection ) ) {
 			return;
 		}
 
-		if ( domSelection ) {
-			domSelection.removeAllRanges();
-		}
-
-		domSelection = null;
+		domSelection.removeAllRanges();
 
 		for ( let range of this.selection.getRanges() ) {
-			const domRangeStart = this.domConverter.viewPositionToDom( range.start );
-			const domRangeEnd = this.domConverter.viewPositionToDom( range.end );
-
-			domSelection = domSelection || domRangeStart.parent.ownerDocument.defaultView.getSelection();
-
-			const domRange = new Range();
-			domRange.setStart( domRangeStart.parent, domRangeStart.offset );
-			domRange.setEnd( domRangeEnd.parent, domRangeEnd.offset );
-			domSelection.addRange( domRange );
+			// Update ranges only in currently focused editable.
+			if ( range.start.parent.getRoot() == this.focusedEditable ) {
+				const domRangeStart = this.domConverter.viewPositionToDom( range.start );
+				const domRangeEnd = this.domConverter.viewPositionToDom( range.end );
+				const domRange = new Range();
+				domRange.setStart( domRangeStart.parent, domRangeStart.offset );
+				domRange.setEnd( domRangeEnd.parent, domRangeEnd.offset );
+				domSelection.addRange( domRange );
+			}
 		}
-
-		this._domSelection = domSelection;
 	}
 }
+
+mix( Renderer, ObservableMixin );

+ 1 - 30
packages/ckeditor5-engine/src/view/rooteditableelement.js

@@ -24,15 +24,7 @@ export default class RootEditableElement extends EditableElement {
 	 * @param {String} [rootName='main'] Root name inside parent {@link engine.view.Document}.
 	 */
 	constructor( document, name, rootName = 'main' ) {
-		super( name );
-
-		/**
-		 * {@link engine.view.Document} that is an owner of this root.
-		 *
-		 * @private
-		 * @member {engine.view.Document} engine.view.RootEditableElement#_document
-		 */
-		this._document = document;
+		super( document, name );
 
 		/**
 		 * Name of this root inside {@link engine.view.Document} that is an owner of this root.
@@ -41,26 +33,5 @@ export default class RootEditableElement extends EditableElement {
 		 * @member {String} engine.view.RootEditableElement#rootName
 		 */
 		this.rootName = rootName;
-
-		/**
-		 * Whether the editable 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 {Boolean} engine.view.RootEditableElement#isFocused
-		 */
-		this.set( 'isFocused', false );
-	}
-
-	/**
-	 * Gets the {@link engine.view.Document} reference.
-	 *
-	 * @returns {engine.view.Document|null} View Document of the node or `null`.
-	 */
-	getDocument() {
-		return this._document;
 	}
 }

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

@@ -190,6 +190,8 @@ describe( 'EditingController', () => {
 				} );
 			} );
 
+			editing.view.focusedEditable = viewRoot;
+
 			const domSelection = document.getSelection();
 			domSelection.removeAllRanges();
 			const domBar = domRoot.childNodes[ 2 ].childNodes[ 0 ];

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

@@ -0,0 +1,22 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: view */
+
+'use strict';
+
+import ObservableMixin from '/ckeditor5/utils/observablemixin.js';
+
+/**
+ * Creates {@link engine.view.Document view Document} mock.
+ *
+ * @returns {utils.ObservableMixin} Document mock
+ */
+export default function createDocumentMock() {
+	const doc = Object.create( ObservableMixin );
+	doc.set( 'focusedEditable', null );
+
+	return doc;
+}

+ 16 - 0
packages/ckeditor5-engine/tests/view/document/document.js

@@ -56,6 +56,7 @@ 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;
 		} );
 	} );
 
@@ -334,4 +335,19 @@ describe( 'Document', () => {
 			expect( addedObserverMock.disable.calledOnce ).to.be.true;
 		} );
 	} );
+
+	describe( 'focusedEditable', () => {
+		it( 'should change renderer.focusedEditable too', () => {
+			const viewDocument = new Document();
+			const viewRoot = viewDocument.createRoot( 'div' );
+
+			expect( viewDocument.focusedEditable ).to.equal( null );
+			expect( viewDocument.renderer.focusedEditable ).to.equal( null );
+
+			viewDocument.focusedEditable = viewRoot;
+
+			expect( viewDocument.focusedEditable ).to.equal( viewRoot );
+			expect( viewDocument.renderer.focusedEditable ).to.equal( viewRoot );
+		} );
+	} );
 } );

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

@@ -1 +1 @@
-<div contenteditable="true" id="editor"></div>
+<div contenteditable="true" id="editor"></div>

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

@@ -23,9 +23,12 @@ describe( 'Document', () => {
 		viewDocument = new ViewDocument();
 
 		viewDocument.addObserver( KeyObserver );
-		viewDocument.createRoot( document.getElementById( 'editor' ) );
+
+		const viewRoot = viewDocument.createRoot( document.getElementById( 'editor' ) );
 
 		document.getSelection().removeAllRanges();
+
+		viewDocument.focusedEditable = viewRoot;
 	} );
 
 	describe( 'jump over inline filler hack', () => {

+ 2 - 0
packages/ckeditor5-engine/tests/view/manual/immutable.js

@@ -8,6 +8,7 @@
 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 KeyObserver from '/ckeditor5/engine/view/observer/keyobserver.js';
 import { setData } from '/tests/engine/_utils/view.js';
 
@@ -17,6 +18,7 @@ viewDocument.createRoot( document.getElementById( 'editor' ) );
 viewDocument.addObserver( MutationObserver );
 viewDocument.addObserver( SelectionObserver );
 viewDocument.addObserver( KeyObserver );
+viewDocument.addObserver( FocusObserver );
 
 setData( viewDocument,
 	'<container:p><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>' +

+ 4 - 1
packages/ckeditor5-engine/tests/view/manual/inline-filler.js

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

+ 25 - 0
packages/ckeditor5-engine/tests/view/manual/noselection.js

@@ -0,0 +1,25 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'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.createRoot( document.getElementById( 'editor' ) );
+
+viewDocument.addObserver( MutationObserver );
+viewDocument.addObserver( SelectionObserver );
+viewDocument.addObserver( FocusObserver );
+
+setData( viewDocument,
+    '<container:p>foo</container:p>' +
+    '<container:p>bar</container:p>' );
+
+viewDocument.render();

+ 4 - 0
packages/ckeditor5-engine/tests/view/manual/noselection.md

@@ -0,0 +1,4 @@
+@bender-ui: collapsed
+@bender-tags: view
+
+It should not be possible to put selection in the editor.

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

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

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

@@ -13,11 +13,13 @@ import MutationObserver from '/ckeditor5/engine/view/observer/mutationobserver.j
 import { setData } from '/tests/engine/_utils/view.js';
 
 const viewDocument = new Document();
-viewDocument.createRoot( document.getElementById( 'editor' ) );
+const viewRoot = viewDocument.createRoot( document.getElementById( 'editor' ) );
 
 viewDocument.addObserver( MutationObserver );
 viewDocument.addObserver( SelectionObserver );
 
+viewDocument.focusedEditable = viewRoot;
+
 setData( viewDocument,
 	'<container:p>fo{}o</container:p>' +
 	'<container:p></container:p>' +

+ 4 - 2
packages/ckeditor5-engine/tests/view/node.js

@@ -12,6 +12,8 @@ import Text from '/ckeditor5/engine/view/text.js';
 import RootEditableElement from '/ckeditor5/engine/view/rooteditableelement.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
+import createDocumentMock from '/tests/engine/view/_utils/createdocumentmock.js';
+
 describe( 'Node', () => {
 	let root;
 	let one, two, three;
@@ -135,7 +137,7 @@ describe( 'Node', () => {
 		} );
 
 		it( 'should return Document attached to the parent element', () => {
-			const docMock = {};
+			const docMock = createDocumentMock();
 			const parent = new RootEditableElement( docMock, 'div' );
 			const child = new Element( 'p' );
 
@@ -154,7 +156,7 @@ describe( 'Node', () => {
 		} );
 
 		it( 'should return root element', () => {
-			const parent = new RootEditableElement( {}, 'div' );
+			const parent = new RootEditableElement( createDocumentMock(), 'div' );
 			const child = new Element( 'p' );
 
 			child.parent = parent;

+ 26 - 9
packages/ckeditor5-engine/tests/view/observer/focusobserver.js

@@ -50,7 +50,7 @@ describe( 'FocusObserver', () => {
 		} );
 	} );
 
-	describe( 'handle isFocused property of root elements', () => {
+	describe( 'handle focusedEditable property of the document', () => {
 		let domMain, domHeader, viewMain, viewHeader;
 
 		beforeEach( () => {
@@ -61,24 +61,41 @@ describe( 'FocusObserver', () => {
 			viewHeader = viewDocument.createRoot( domHeader, 'header' );
 		} );
 
-		it( 'should change isFocused on focus event', () => {
+		it( 'should set focusedEditable on focus', () => {
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
-			expect( viewMain.isFocused ).to.be.true;
-			expect( viewHeader.isFocused ).to.be.false;
+			expect( viewDocument.focusedEditable ).to.equal( viewMain );
 		} );
 
-		it( 'should change isFocused on blur event', () => {
+		it( 'should change focusedEditable on focus', () => {
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
-			expect( viewMain.isFocused ).to.be.true;
-			expect( viewHeader.isFocused ).to.be.false;
+			expect( viewDocument.focusedEditable ).to.equal( viewMain );
+
+			observer.onDomEvent( { type: 'focus', target: domHeader } );
+
+			expect( viewDocument.focusedEditable ).to.equal( viewHeader );
+		} );
+
+		it( 'should set focusedEditable to null on blur', () => {
+			observer.onDomEvent( { type: 'focus', target: domMain } );
+
+			expect( viewDocument.focusedEditable ).to.equal( viewMain );
 
 			observer.onDomEvent( { type: 'blur', target: domMain } );
+
+			expect( viewDocument.focusedEditable ).to.be.null;
+		} );
+
+		it( 'should not touch focusedEditable on blur if it is already changed', () => {
+			observer.onDomEvent( { type: 'focus', target: domMain } );
+
+			expect( viewDocument.focusedEditable ).to.equal( viewMain );
+
 			observer.onDomEvent( { type: 'focus', target: domHeader } );
+			observer.onDomEvent( { type: 'blur', target: domMain } );
 
-			expect( viewMain.isFocused ).to.be.false;
-			expect( viewHeader.isFocused ).to.be.true;
+			expect( viewDocument.focusedEditable ).to.equal( viewHeader );
 		} );
 	} );
 } );

+ 14 - 0
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -41,6 +41,8 @@ describe( 'SelectionObserver', () => {
 		viewDocument.selection.removeAllRanges();
 		document.getSelection().removeAllRanges();
 
+		viewDocument.focusedEditable = viewRoot;
+
 		viewDocument.getObserver( SelectionObserver ).enable();
 
 		// Ensure selectionchange will not be fired.
@@ -110,6 +112,18 @@ describe( 'SelectionObserver', () => {
 		changeDomSelection();
 	} );
 
+	it( 'should not fired if there is no focusedEditable', ( done ) => {
+		viewDocument.focusedEditable = null;
+
+		listenter.listenTo( viewDocument, 'selectionChange', () => {
+			throw 'selectionChange on render';
+		} );
+
+		setTimeout( () => done(), 70 );
+
+		changeDomSelection();
+	} );
+
 	it( 'should call render after selection change which reset selection if it was not changed', ( done ) => {
 		const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 );
 		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 0, viewBar, 1 ) );

+ 51 - 0
packages/ckeditor5-engine/tests/view/renderer.js

@@ -17,6 +17,8 @@ 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 createElement from '/ckeditor5/utils/dom/createelement.js';
+
 describe( 'Renderer', () => {
 	let selection, domConverter, renderer;
 
@@ -112,6 +114,8 @@ describe( 'Renderer', () => {
 			renderer.markedAttributes.clear();
 			renderer.markedChildren.clear();
 
+			renderer.focusedEditable = viewRoot;
+
 			selection.removeAllRanges();
 		} );
 
@@ -758,6 +762,53 @@ describe( 'Renderer', () => {
 			renderAndExpectNoChanges( renderer, domRoot );
 		} );
 
+		it( 'should not change selection if there is no focusedEditable', () => {
+			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 );
+
+			renderer.focusedEditable = 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 add ranges if different editable is focused', () => {
+			const domHeader = document.createElement( 'h1' );
+			const viewHeader = new ViewElement( 'h1' );
+			document.body.appendChild( domHeader );
+
+			domConverter.bindElements( domHeader, viewHeader );
+
+			renderer.focusedEditable = viewHeader;
+
+			const { view: viewP, selection: newSelection } = parse( '<container:p>fo{o}</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.render();
+
+			const domSelection = document.getSelection();
+			expect( domSelection.rangeCount ).to.equal( 0 );
+		} );
+
 		it( 'should not add inline filler after text node', () => {
 			const domSelection = document.getSelection();
 

+ 33 - 5
packages/ckeditor5-engine/tests/view/rooteditableelement.js

@@ -11,10 +11,12 @@ import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
 import EditableElement from '/ckeditor5/engine/view/editableelement.js';
 import RootEditableElement from '/ckeditor5/engine/view/rooteditableelement.js';
 
+import createDocumentMock from '/tests/engine/view/_utils/createdocumentmock.js';
+
 describe( 'RootEditableElement', () => {
 	describe( 'constructor', () => {
 		it( 'should create an element with default root name', () => {
-			const root = new RootEditableElement( {}, 'div' );
+			const root = new RootEditableElement( createDocumentMock(), 'div' );
 
 			expect( root ).to.be.instanceof( EditableElement );
 			expect( root ).to.be.instanceof( ContainerElement );
@@ -27,7 +29,7 @@ describe( 'RootEditableElement', () => {
 		} );
 
 		it( 'should create an element with custom root name', () => {
-			const root = new RootEditableElement( {}, 'h1', 'header' );
+			const root = new RootEditableElement( createDocumentMock(), 'h1', 'header' );
 
 			expect( root.rootName ).to.equal( 'header' );
 			expect( root.name ).to.equal( 'h1' );
@@ -38,8 +40,17 @@ describe( 'RootEditableElement', () => {
 	} );
 
 	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( {}, 'div' );
+			const root = new RootEditableElement( createDocumentMock(), 'div' );
 
 			expect( root.isFocused ).to.be.false;
 
@@ -53,11 +64,28 @@ describe( 'RootEditableElement', () => {
 
 			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( {}, 'div' );
+			const root = new RootEditableElement( createDocumentMock(), 'div' );
 
 			expect( root.isReadOnly ).to.be.false;
 
@@ -75,7 +103,7 @@ describe( 'RootEditableElement', () => {
 
 	describe( 'getDocument', ()=> {
 		it( 'should return document', () => {
-			const docMock = {};
+			const docMock = createDocumentMock();
 			const root = new RootEditableElement( docMock, 'div' );
 
 			expect( root.getDocument() ).to.equal( docMock );

+ 24 - 0
packages/ckeditor5-engine/tests/view/utils-tests/createdocumentmock.js

@@ -0,0 +1,24 @@
+/**
+ * @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';
+
+describe( 'createDocumentMock', () => {
+	it( 'should create document mock', ( done ) => {
+		const docMock = createDocumentMock();
+		const rootMock = {};
+
+		docMock.on( 'change:focusedEditable', ( evt, key, value ) => {
+			expect( value ).to.equal( rootMock );
+			done();
+		} );
+
+		docMock.focusedEditable = rootMock;
+	} );
+} );