瀏覽代碼

Selection Observer and renderer check focus.

Piotr Jasiun 9 年之前
父節點
當前提交
97950fa73a

+ 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.
@@ -85,10 +85,22 @@ export default class Document {
 		 * 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();
 
+		/**
+		 * {@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 );
+
 		injectQuirksHandling( this );
 	}
 
@@ -165,6 +177,7 @@ export default class Document {
 		viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
 		viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
 		viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
+		this.on( 'change:focusedEditable', () => this.renderer.focusedEditable = this.focusedEditable );
 
 		if ( domRoot instanceof HTMLElement ) {
 			this.attachDomRoot( domRoot, name );
@@ -250,7 +263,7 @@ export default class Document {
 	}
 }
 
-mix( Document, EmitterMixin );
+mix( Document, ObservableMixin );
 
 /**
  * Enum representing type of the change.

+ 7 - 1
packages/ckeditor5-engine/src/view/observer/focusobserver.js

@@ -23,7 +23,7 @@ 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;
 
@@ -32,6 +32,8 @@ export default class FocusObserver extends DomEventObserver {
 					root.isFocused = true;
 				}
 			}
+
+			document.focusedEditable = target;
 		} );
 
 		document.on( 'blur', ( evt, data ) => {
@@ -42,6 +44,10 @@ export default class FocusObserver extends DomEventObserver {
 					root.isFocused = false;
 				}
 			}
+
+			if ( document.focusedEditable == target ) {
+				document.focusedEditable = null;
+			}
 		} );
 	}
 

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

@@ -93,6 +93,10 @@ export default class SelectionObserver extends Observer {
 	 * @param {Document} domDocument DOM document.
 	 */
 	_handleSelectionChange( domDocument ) {
+		if ( !this.document.focusedEditable ) {
+			return;
+		}
+
 		if ( !this.isEnabled ) {
 			return;
 		}
@@ -103,6 +107,7 @@ export default class SelectionObserver extends Observer {
 		// 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 ) ) {

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

@@ -87,17 +87,7 @@ export default class Renderer {
 		 */
 		this._inlineFillerPosition = null;
 
-		/**
-		 * Last DOM selection object.
-		 *
-		 * 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
-		 */
-		this._domSelection = null;
+		this.focusedEditable = null;
 	}
 
 	/**
@@ -407,13 +397,14 @@ 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;
 		}
@@ -422,20 +413,16 @@ export default class Renderer {
 			domSelection.removeAllRanges();
 		}
 
-		domSelection = null;
-
 		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 );
+			// Updated 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;
 	}
 }

+ 1 - 0
packages/ckeditor5-engine/tests/__template__.html

@@ -1 +1,2 @@
+<input type="text" id="focusPlaceholder" />
 <div contenteditable="true" id="editor"></div>

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

@@ -131,6 +131,8 @@ describe( 'EditingController', () => {
 		} );
 
 		beforeEach( () => {
+			document.getElementById( 'focusPlaceholder' ).focus();
+
 			model.selection.removeAllRanges();
 			modelRoot.removeChildren( 0, modelRoot.getChildCount() );
 

+ 1 - 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;
 		} );
 	} );
 

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

@@ -1 +1,2 @@
-<div contenteditable="true" id="editor"></div>
+<input type="text" id="focusPlaceholder" />
+<div contenteditable="true" id="editor"></div>

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

@@ -9,6 +9,7 @@
 
 import ViewRange from '/ckeditor5/engine/view/range.js';
 import ViewDocument from '/ckeditor5/engine/view/document.js';
+import FocusObserver from '/ckeditor5/engine/view/observer/focusobserver.js';
 import KeyObserver from '/ckeditor5/engine/view/observer/keyobserver.js';
 import { INLINE_FILLER_LENGTH, isInlineFiller, startsWithFiller } from '/ckeditor5/engine/view/filler.js';
 
@@ -20,12 +21,18 @@ describe( 'Document', () => {
 	let viewDocument;
 
 	beforeEach( () => {
+		document.getElementById( 'focusPlaceholder' ).focus();
+
 		viewDocument = new ViewDocument();
 
 		viewDocument.addObserver( KeyObserver );
+		viewDocument.addObserver( FocusObserver );
+
 		viewDocument.createRoot( document.getElementById( 'editor' ) );
 
 		document.getSelection().removeAllRanges();
+
+		viewDocument.getDomRoot().focus();
 	} );
 
 	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>' +

+ 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.

+ 1 - 0
packages/ckeditor5-engine/tests/view/observer/__template__.html

@@ -1,2 +1,3 @@
+<input type="text" id="focusPlaceholder" />
 <div contenteditable="true" id="main"></div>
 <div contenteditable="true" id="additional"></div>

+ 49 - 0
packages/ckeditor5-engine/tests/view/observer/focusobserver.js

@@ -81,4 +81,53 @@ describe( 'FocusObserver', () => {
 			expect( viewHeader.isFocused ).to.be.true;
 		} );
 	} );
+
+	describe( 'handle focusedEditable property of the document', () => {
+		let domMain, domHeader, viewMain, viewHeader;
+
+		beforeEach( () => {
+			domMain = document.createElement( 'div' );
+			domHeader = document.createElement( 'h1' );
+
+			viewMain = viewDocument.createRoot( domMain );
+			viewHeader = viewDocument.createRoot( domHeader, 'header' );
+		} );
+
+		it( 'should set focusedEditable on focus', () => {
+			observer.onDomEvent( { type: 'focus', target: domMain } );
+
+			expect( viewDocument.focusedEditable ).to.equal( viewMain );
+		} );
+
+		it( 'should change focusedEditable on focus', () => {
+			observer.onDomEvent( { type: 'focus', target: domMain } );
+
+			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( viewDocument.focusedEditable ).to.equal( viewHeader );
+		} );
+	} );
 } );

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

@@ -12,6 +12,7 @@ import ViewSelection from '/ckeditor5/engine/view/selection.js';
 import ViewDocument from '/ckeditor5/engine/view/document.js';
 import SelectionObserver from '/ckeditor5/engine/view/observer/selectionobserver.js';
 import MutationObserver from '/ckeditor5/engine/view/observer/mutationobserver.js';
+import FocusObserver from '/ckeditor5/engine/view/observer/focusobserver.js';
 
 import EmitterMixin from '/ckeditor5/utils/emittermixin.js';
 
@@ -29,6 +30,7 @@ describe( 'SelectionObserver', () => {
 
 		mutationObserver = viewDocument.addObserver( MutationObserver );
 		selectionObserver = viewDocument.addObserver( SelectionObserver );
+		viewDocument.addObserver( FocusObserver );
 
 		viewRoot = viewDocument.getRoot();
 
@@ -38,6 +40,8 @@ describe( 'SelectionObserver', () => {
 	} );
 
 	beforeEach( ( done ) => {
+		document.getElementById( 'focusPlaceholder' ).focus();
+
 		viewDocument.selection.removeAllRanges();
 		document.getSelection().removeAllRanges();
 

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

@@ -112,6 +112,8 @@ describe( 'Renderer', () => {
 			renderer.markedAttributes.clear();
 			renderer.markedChildren.clear();
 
+			renderer.focusedEditable = viewRoot;
+
 			selection.removeAllRanges();
 		} );