8
0
Quellcode durchsuchen

Merge pull request #82 from ckeditor/t/81

Introduced Focus Tracker.
Aleksander Nowodzinski vor 9 Jahren
Ursprung
Commit
4cb475ad55

+ 123 - 0
packages/ckeditor5-utils/src/focustracker.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';
+
+/**
+ * Allows observing a group of `HTMLElement`s whether at least one of them is focused.
+ *
+ * Used by the {@link core.Editor} in order to track whether the focus is still within the application,
+ * or were used outside of its UI.
+ *
+ * **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
+ * (have e.g. `tabindex="-1"`).
+ *
+ * @memberOf utils
+ * @mixes utils.DOMEmitterMixin
+ * @mixes utils.ObservableMixin
+ */
+export default class FocusTracker {
+	constructor() {
+		/**
+		 * True when one of the registered elements is focused.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} utils.FocusTracker#isFocused
+		 */
+		this.set( 'isFocused', false );
+
+		/**
+		 * List of registered elements.
+		 *
+		 * @private
+		 * @member {Set<HTMLElement>} utils.FocusTracker#_elements
+		 */
+		this._elements = new Set();
+
+		/**
+		 * Event loop timeout.
+		 *
+		 * @private
+		 * @member {Number} utils.FocusTracker#_nextEventLoopTimeout
+		 */
+		this._nextEventLoopTimeout = null;
+
+		/**
+		 * Currently focused element.
+		 *
+		 * @private
+		 * @member {HTMLElement} utils.FocusTracker#_focusedElement
+		 */
+		this._focusedElement = null;
+	}
+
+	/**
+	 * Starts tracking the specified element.
+	 *
+	 * @param {HTMLElement} element
+	 */
+	add( element ) {
+		if ( this._elements.has( element ) ) {
+			throw new CKEditorError( 'focusTracker-add-element-already-exist' );
+		}
+
+		this.listenTo( element, 'focus', () => this._focus( element ), { useCapture: true } );
+		this.listenTo( element, 'blur', () => this._blur(), { useCapture: true } );
+		this._elements.add( element );
+	}
+
+	/**
+	 * Stops tracking the specified element and stops listening on this element.
+	 *
+	 * @param {HTMLElement} element
+	 */
+	remove( element ) {
+		if ( element === this._focusedElement ) {
+			this._blur( element );
+		}
+
+		if ( this._elements.has( element ) ) {
+			this.stopListening( element );
+			this._elements.delete( element );
+		}
+	}
+
+	/**
+	 * Stores currently focused element and set {utils.FocusTracker#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.FocusTracker#isFocused} as `false`.
+	 * This method uses `setTimeout` to change order of fires `blur` and `focus` events.
+	 *
+	 * @private
+	 * @fires utils.FocusTracker#blur
+	 */
+	_blur() {
+		this._nextEventLoopTimeout = setTimeout( () => {
+			this._focusedElement = null;
+			this.isFocused = false;
+		}, 0 );
+	}
+}
+
+mix( FocusTracker, DOMEmitterMixin );
+mix( FocusTracker, ObservableMixin );

+ 160 - 0
packages/ckeditor5-utils/tests/focustracker.js

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

+ 42 - 0
packages/ckeditor5-utils/tests/manual/focustracker.html

@@ -0,0 +1,42 @@
+<head>
+	<meta charset="utf-8">
+
+	<style>
+		div[contenteditable],
+		div[tabindex] {
+			padding: 10px;
+			border: solid 1px #888;
+		}
+
+		span {
+			padding-left: 10px;
+		}
+
+		.status {
+			font-weight: normal;
+		}
+	</style>
+</head>
+
+
+<h2 class="status">Status <span>focus: <b>0</b></span> <span>blur: <b>0</b></span></h2>
+<hr>
+
+<h2>Tracked by Focus Tracker:</h2>
+
+<h3>Input:</h3>
+<input type="text" class="track">
+
+<h3>Content editable:</h3>
+<div contenteditable="true" class="track">Some editable content</div>
+
+<h3>HTML Wrapper (uses event capturing):</h3>
+<div tabindex="-1" class="track">
+	<input type="text">
+	<input type="text">
+</div>
+
+<h2>Not tracked by Focus Tracker:</h2>
+
+<h3>Input:</h3>
+<input type="text">

+ 18 - 0
packages/ckeditor5-utils/tests/manual/focustracker.js

@@ -0,0 +1,18 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import FocusTracker from '/ckeditor5/utils/focustracker.js';
+
+const focusTracker = new FocusTracker();
+const counters = document.querySelectorAll( '.status b' );
+
+[].forEach.call( document.querySelectorAll( '.track' ), el => focusTracker.add( el ) );
+
+focusTracker.on( 'change:isFocused', ( evt, name, value ) => {
+	const el = counters[ value ? 0 : 1 ];
+	el.textContent = parseInt( el.textContent ) + 1;
+} );

+ 14 - 0
packages/ckeditor5-utils/tests/manual/focustracker.md

@@ -0,0 +1,14 @@
+
+@bender-ui: collapsed
+@bender-tags: focustracker
+
+## Focus Tracker
+
+1. Set focus to first tracked input,
+2. Check if status is equal to: focus: **1** blur: **0**,
+3. Move focus to tracked content editable element,
+4. Move focus to first input of tracked HTML Wrapper,
+5. Move focus to second input of tracked HTML Wrapper,
+6. Check if status is still equal to: focus: **1** blur: **0**,
+7. Move focus to not tracked input,
+8. Check if status is equal to: focus: **1** blur: **1**.