浏览代码

Introduced Focus Manager.

Oskar Wrobel 9 年之前
父节点
当前提交
040b7c12aa
共有 2 个文件被更改,包括 285 次插入0 次删除
  1. 123 0
      packages/ckeditor5-utils/src/focusmanager.js
  2. 162 0
      packages/ckeditor5-utils/tests/focusmanager.js

+ 123 - 0
packages/ckeditor5-utils/src/focusmanager.js

@@ -0,0 +1,123 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global setTimeout, clearTimeout */
+
+import DOMEmitterMixin from '../ui/domemittermixin.js';
+import ObservableMixin from './observablemixin.js';
+import CKEditorError from './ckeditorerror.js';
+import mix from './mix.js';
+
+/**
+ * Observes if registered HTMLElements are focused. It is mainly used for keeping information if at least one of the editor component
+ * is currently focused which means that editor is focused.
+ *
+ * **Note** `focus` and `blur` listeners use event capturing, so it is only needed to register wrapper HTMLElement
+ * which contain other `focusable` elements. But note that this wrapper element has to be `focusable` too.
+ *
+ * @memberOf utils
+ * @mixes DOMEmitterMixin
+ * @mixes ObservableMixin
+ */
+export default class FocusManager {
+	constructor() {
+		/**
+		 * True when one of the registered elements is focused.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} utils.FocusManager#isFocused
+		 */
+		this.set( 'isFocused', false );
+
+		/**
+		 * List of registered elements.
+		 *
+		 * @private
+		 * @member {Array<HTMLElement>} utils.FocusManager#_elements
+		 */
+		this._elements = [];
+
+		/**
+		 * Event loop timeout.
+		 *
+		 * @private
+		 * @member {Number} utils.FocusManager#_nextEventLoopTimeout
+		 */
+		this._nextEventLoopTimeout = null;
+
+		/**
+		 * Currently focused element.
+		 *
+		 * @private
+		 * @member {HTMLElement} utils.FocusManager#_focusedElement
+		 */
+		this._focusedElement = null;
+	}
+
+	/**
+	 * Adds specified element and and start listening `focus` and `blur` events on this element.
+	 *
+	 * @param {HTMLElement} element
+	 */
+	add( element ) {
+		if ( this._elements.indexOf( element ) >= 0 ) {
+			throw new CKEditorError( 'focusManager-add-element-already-exist' );
+		}
+
+		this.listenTo( element, 'focus', () => this._focus( element ), { useCapture: true } );
+		this.listenTo( element, 'blur', () => this._blur(), { useCapture: true } );
+		this._elements.push( element );
+	}
+
+	/**
+	 * Removes specified element and stop listening events on this element.
+	 * When element is focused then blur it firs.
+	 *
+	 * @param {HTMLElement} element
+	 */
+	remove( element ) {
+		if ( element === this._focusedElement ) {
+			this._blur( element );
+		}
+
+		const elementIndex = this._elements.indexOf( element );
+
+		if ( elementIndex > -1 ) {
+			this.stopListening( element );
+			this._elements.slice( elementIndex, 1 );
+		}
+	}
+
+	/**
+	 * Stores currently focused element and set {utils.FocusManager#isFocused} as `true`.
+	 *
+	 * @private
+	 * @param {HTMLElement} element Element which has been focused.
+	 */
+	_focus( element ) {
+		clearTimeout( this._nextEventLoopTimeout );
+
+		this._focusedElement = element;
+		this.isFocused = true;
+	}
+
+	/**
+	 * Clears currently focused element and set {utils.FocusManager#isFocused} as `false`.
+	 * This method uses `setTimeout` to change order of fires `blur` and `focus` events.
+	 *
+	 * @private
+	 * @fires utils.FocusManager#blur
+	 */
+	_blur() {
+		this._nextEventLoopTimeout = setTimeout( () => {
+			this._focusedElement = null;
+			this.isFocused = false;
+		}, 0 );
+	}
+}
+
+mix( FocusManager, DOMEmitterMixin );
+mix( FocusManager, ObservableMixin );

+ 162 - 0
packages/ckeditor5-utils/tests/focusmanager.js

@@ -0,0 +1,162 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import FocusManager from '/ckeditor5/utils/focusmanager.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import testUtils from '/tests/core/_utils/utils.js';
+
+testUtils.createSinonSandbox();
+
+describe( 'FocusManager', () => {
+	let focusManager, container, containerFirstInput, containerSecondInput, inputOuOfContainer;
+
+	beforeEach( () => {
+		container = document.createElement( 'div' );
+		containerFirstInput = document.createElement( 'input' );
+		containerSecondInput = document.createElement( 'input' );
+		inputOuOfContainer = document.createElement( 'input' );
+
+		container.appendChild( containerFirstInput );
+		container.appendChild( containerSecondInput );
+		document.body.appendChild( container );
+		document.body.appendChild( inputOuOfContainer );
+
+		testUtils.sinon.useFakeTimers();
+
+		focusManager = new FocusManager();
+	} );
+
+	afterEach( () => {
+		document.body.removeChild( container );
+		document.body.removeChild( inputOuOfContainer );
+	} );
+
+	describe( 'constructor', () => {
+		describe( 'isFocused', () => {
+			it( 'should be false at default', () => {
+				expect( focusManager.isFocused ).to.false;
+			} );
+
+			it( 'should be observable', () => {
+				const observableSpy = testUtils.sinon.spy();
+
+				focusManager.listenTo( focusManager, 'change:isFocused', observableSpy );
+
+				focusManager.isFocused = true;
+
+				expect( observableSpy.calledOnce ).to.true;
+			} );
+		} );
+	} );
+
+	describe( 'add', () => {
+		it( 'should throw an error when element has been already added', () => {
+			focusManager.add( containerFirstInput );
+
+			expect( () => {
+				focusManager.add( containerFirstInput );
+			} ).to.throw( CKEditorError, /focusManager-add-element-already-exist/ );
+		} );
+
+		describe( 'single element', () => {
+			it( 'should start listening on element focus and update `isFocused` property', () => {
+				focusManager.add( containerFirstInput );
+
+				expect( focusManager.isFocused ).to.false;
+
+				containerFirstInput.focus();
+
+				expect( focusManager.isFocused ).to.true;
+			} );
+
+			it( 'should start listening on element blur and update `isFocused` property', () => {
+				focusManager.add( containerFirstInput );
+
+				containerFirstInput.focus();
+
+				expect( focusManager.isFocused ).to.true;
+
+				containerSecondInput.focus();
+				testUtils.sinon.clock.tick( 0 );
+
+				expect( focusManager.isFocused ).to.false;
+			} );
+		} );
+
+		describe( 'container element', () => {
+			it( 'should start listening on element focus using event capturing and update `isFocused` property', () => {
+				focusManager.add( container );
+
+				expect( focusManager.isFocused ).to.false;
+
+				containerFirstInput.focus();
+
+				expect( focusManager.isFocused ).to.true;
+			} );
+
+			it( 'should start listening on element blur using event capturing and update `isFocused` property', () => {
+				focusManager.add( container );
+
+				containerFirstInput.focus();
+
+				expect( focusManager.isFocused ).to.true;
+
+				inputOuOfContainer.focus();
+				testUtils.sinon.clock.tick( 0 );
+
+				expect( focusManager.isFocused ).to.false;
+			} );
+
+			it( 'should not change `isFocused` property when focus is going between child elements', () => {
+				const changeSpy = testUtils.sinon.spy();
+
+				focusManager.add( container );
+
+				containerFirstInput.focus();
+
+				focusManager.listenTo( focusManager, 'change:isFocused', changeSpy );
+
+				expect( focusManager.isFocused ).to.true;
+
+				containerSecondInput.focus();
+				testUtils.sinon.clock.tick( 0 );
+
+				expect( focusManager.isFocused ).to.true;
+				expect( changeSpy.notCalled ).to.true;
+			} );
+		} );
+	} );
+
+	describe( 'remove', () => {
+		it( 'should do nothing when element was not added', () => {
+			expect( () => {
+				focusManager.remove( container );
+			} ).to.not.throw();
+		} );
+
+		it( 'should stop listening on element focus and update `isFocused` property', () => {
+			focusManager.add( containerFirstInput );
+			focusManager.remove( containerFirstInput );
+
+			containerFirstInput.focus();
+
+			expect( focusManager.isFocused ).to.false;
+		} );
+
+		it( 'should blur element before removing when is focused', () => {
+			focusManager.add( containerFirstInput );
+			containerFirstInput.focus();
+
+			expect( focusManager.isFocused ).to.true;
+
+			focusManager.remove( containerFirstInput );
+			testUtils.sinon.clock.tick( 0 );
+
+			expect( focusManager.isFocused ).to.false;
+		} );
+	} );
+} );