Переглянути джерело

Merge pull request #504 from ckeditor/t/488

Added view.Document#focus()
Piotr Jasiun 9 роки тому
батько
коміт
ec6899566e

+ 25 - 0
packages/ckeditor5-engine/src/view/document.js

@@ -11,6 +11,7 @@ import Writer from './writer.js';
 import DomConverter from './domconverter.js';
 import RootEditableElement from './rooteditableelement.js';
 import { injectQuirksHandling } from './filler.js';
+import log from '../../utils/log.js';
 
 import mix from '../../utils/mix.js';
 import ObservableMixin from '../../utils/observablemixin.js';
@@ -255,6 +256,30 @@ export default class Document {
 		this.enableObservers();
 	}
 
+	/**
+	 * Focuses document. It will focus {@link engine.view.EditableElement EditableElement} that is currently having
+	 * selection inside.
+	 */
+	focus() {
+		if ( !this.isFocused ) {
+			const editable = this.selectedEditable;
+
+			if ( editable ) {
+				this.domConverter.focus( editable );
+				this.render();
+			} else {
+				/**
+				 * Before focusing view document, selection should be placed inside one of the view's editables.
+				 * Normally its selection will be converted from model document (which have default selection), but
+				 * when using view document on its own, we need to manually place selection before focusing it.
+				 *
+				 * @error view-focus-no-selection
+				 */
+				log.warn( 'view-focus-no-selection: There is no selection in any editable to focus.' );
+			}
+		}
+	}
+
 	/**
 	 * Disables all added observers.
 	 */

+ 13 - 0
packages/ckeditor5-engine/src/view/domconverter.js

@@ -606,4 +606,17 @@ export default class DomConverter {
 
 		return null;
 	}
+
+	/**
+	 * Focuses DOM editable that is corresponding to provided {@link engine.view.EditableElement EditableElement}.
+	 *
+	 * @param {engine.view.EditableElement} viewEditable
+	 */
+	focus( viewEditable ) {
+		const domEditable = this.getCorrespondingDomElement( viewEditable );
+
+		if ( domEditable && domEditable.ownerDocument.activeElement !== domEditable ) {
+			domEditable.focus();
+		}
+	}
 }

+ 16 - 0
packages/ckeditor5-engine/src/view/renderer.js

@@ -188,6 +188,7 @@ export default class Renderer {
 		}
 
 		this._updateSelection();
+		this._updateFocus();
 
 		this.markedTexts.clear();
 		this.markedAttributes.clear();
@@ -443,6 +444,21 @@ export default class Renderer {
 			}
 		}
 	}
+
+	/**
+	 * Checks if focus needs to be updated and possibly updates it.
+	 *
+	 * @private
+	 */
+	_updateFocus() {
+		if ( this.isFocused ) {
+			const editable = this.selection.getEditableElement();
+
+			if ( editable ) {
+				this.domConverter.focus( editable );
+			}
+		}
+	}
 }
 
 mix( Renderer, ObservableMixin );

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

@@ -13,8 +13,11 @@ 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';
-
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import count from '/ckeditor5/utils/count.js';
+import log from '/ckeditor5/utils/log.js';
+
+testUtils.createSinonSandbox();
 
 describe( 'Document', () => {
 	let ObserverMock, ObserverMockGlobalCount, instantiated, enabled;
@@ -364,4 +367,58 @@ describe( 'Document', () => {
 			expect( viewDocument.selectedEditable ).to.equal( viewRoot );
 		} );
 	} );
+
+	describe( 'focus', () => {
+		let viewDocument, domEditable, viewEditable;
+
+		beforeEach( () => {
+			viewDocument = new Document();
+			domEditable = document.createElement( 'div' );
+			domEditable.setAttribute( 'contenteditable', 'true' );
+			document.body.appendChild( domEditable );
+			viewEditable = viewDocument.createRoot( domEditable );
+			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
+		} );
+
+		afterEach( () => {
+			document.body.removeChild( domEditable );
+		} );
+
+		it( 'should focus editable with selection', () => {
+			const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' );
+			const renderSpy = testUtils.sinon.spy( viewDocument, 'render' );
+
+			viewDocument.focus();
+
+			expect( converterFocusSpy.calledOnce ).to.be.true;
+			expect( renderSpy.calledOnce ).to.be.true;
+			expect( document.activeElement ).to.equal( domEditable );
+			const domSelection = document.getSelection();
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			const domRange = domSelection.getRangeAt( 0 );
+			expect( domRange.startContainer ).to.equal( domEditable );
+			expect( domRange.startOffset ).to.equal( 0 );
+			expect( domRange.collapsed ).to.be.true;
+		} );
+
+		it( 'should not focus if document is already focused', () => {
+			const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' );
+			const renderSpy = testUtils.sinon.spy( viewDocument, 'render' );
+			viewDocument.isFocused = true;
+
+			viewDocument.focus();
+
+			expect( converterFocusSpy.called ).to.be.false;
+			expect( renderSpy.called ).to.be.false;
+		} );
+
+		it( 'should log warning when no selection', () => {
+			const logSpy = testUtils.sinon.stub( log, 'warn' );
+			viewDocument.selection.removeAllRanges();
+
+			viewDocument.focus();
+			expect( logSpy.calledOnce ).to.be.true;
+			expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^view-focus-no-selection/ );
+		} );
+	} );
 } );

+ 39 - 2
packages/ckeditor5-engine/tests/view/domconverter/domconverter.js

@@ -8,18 +8,22 @@
 'use strict';
 
 import DomConverter from '/ckeditor5/engine/view/domconverter.js';
+import ViewEditable from '/ckeditor5/engine/view/editableelement.js';
+import ViewDocument from '/ckeditor5/engine/view/document.js';
 import { BR_FILLER, NBSP_FILLER } from '/ckeditor5/engine/view/filler.js';
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
+
+testUtils.createSinonSandbox();
 
 describe( 'DomConverter', () => {
 	let converter;
 
-	before( () => {
+	beforeEach( () => {
 		converter = new DomConverter();
 	} );
 
 	describe( 'constructor', () => {
 		it( 'should create converter with BR block filler by default', () => {
-			converter = new DomConverter();
 			expect( converter.blockFiller ).to.equal( BR_FILLER );
 		} );
 
@@ -28,4 +32,37 @@ describe( 'DomConverter', () => {
 			expect( converter.blockFiller ).to.equal( NBSP_FILLER );
 		} );
 	} );
+
+	describe( 'focus', () => {
+		let viewEditable, domEditable;
+
+		beforeEach( () => {
+			viewEditable = new ViewEditable( new ViewDocument(), 'div' );
+			domEditable = document.createElement( 'div' );
+			converter.bindElements( domEditable, viewEditable );
+			domEditable.setAttribute( 'contenteditable', 'true' );
+			document.body.appendChild( domEditable );
+		} );
+
+		afterEach( () => {
+			document.body.removeChild( domEditable );
+		} );
+
+		it( 'should call focus on corresponding DOM editable', () => {
+			const focusSpy = testUtils.sinon.spy( domEditable, 'focus' );
+
+			converter.focus( viewEditable );
+
+			expect( focusSpy.calledOnce ).to.be.true;
+		} );
+
+		it( 'should not focus already focused editable', () => {
+			const focusSpy = testUtils.sinon.spy( domEditable, 'focus' );
+
+			converter.focus( viewEditable );
+			converter.focus( viewEditable );
+
+			expect( focusSpy.calledOnce ).to.be.true;
+		} );
+	} );
 } );

+ 6 - 0
packages/ckeditor5-engine/tests/view/manual/focus.html

@@ -0,0 +1,6 @@
+<div contenteditable="true" id="editable1">
+</div>
+<div contenteditable="true" id="editable2">
+</div>
+<button id="button1">Focus first editable</button>
+<button id="button2">Focus second editable</button>

+ 43 - 0
packages/ckeditor5-engine/tests/view/manual/focus.js

@@ -0,0 +1,43 @@
+/**
+ * @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 { parse } from '/tests/engine/_utils/view.js';
+
+const viewDocument = new Document();
+
+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 );
+
+const { selection: selection1 } = parse( '<p>Foo {bar} baz.</p>', { rootElement: editable1 } );
+const { selection: selection2 } = parse( '<p>{Foo} bar baz.</p>', { rootElement: editable2 } );
+
+document.getElementById( 'button1' ).addEventListener( 'click', () => {
+	viewDocument.selection.setTo( selection1 );
+	viewDocument.focus();
+} );
+
+document.getElementById( 'button2' ).addEventListener( 'click', () => {
+	viewDocument.selection.setTo( selection2 );
+	viewDocument.focus();
+} );
+
+viewDocument.render();

+ 7 - 0
packages/ckeditor5-engine/tests/view/manual/focus.md

@@ -0,0 +1,7 @@
+@bender-ui: collapsed
+@bender-tags: view
+
+* Click `Focus first editable` button.
+* First editable should be focused and selection around `bar` should be visible.
+* Click `Focus second editable` button.
+* Second editable should be focused and selection around `Foo` should be visible.

+ 30 - 1
packages/ckeditor5-engine/tests/view/renderer.js

@@ -24,7 +24,7 @@ testUtils.createSinonSandbox();
 describe( 'Renderer', () => {
 	let selection, domConverter, renderer;
 
-	before( () => {
+	beforeEach( () => {
 		selection = new Selection();
 		domConverter = new DomConverter();
 		renderer = new Renderer( domConverter, selection );
@@ -905,6 +905,35 @@ describe( 'Renderer', () => {
 				renderer.render();
 			} ).to.throw();
 		} );
+
+		it( 'should handle focusing element', () => {
+			const domFocusSpy = testUtils.sinon.spy( domRoot, 'focus' );
+			const editable = selection.getEditableElement();
+
+			renderer.render();
+
+			expect( editable ).to.equal( viewRoot );
+			expect( domFocusSpy.calledOnce ).to.be.true;
+		} );
+
+		it( 'should not focus editable if isFocues is set to false', () => {
+			const domFocusSpy = testUtils.sinon.spy( domRoot, 'focus' );
+
+			renderer.isFocused = false;
+			renderer.render();
+
+			expect( domFocusSpy.calledOnce ).to.be.false;
+		} );
+
+		it( 'should not focus already focused DOM element', () => {
+			domRoot.setAttribute( 'contentEditable', true );
+			domRoot.focus();
+			const domFocusSpy = testUtils.sinon.spy( domRoot, 'focus' );
+
+			renderer.render();
+
+			expect( domFocusSpy.called ).to.be.false;
+		} );
 	} );
 } );