| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module ui/focuscycler
- */
- import global from '@ckeditor/ckeditor5-utils/src/dom/global';
- /**
- * A utility class that helps cycling over focusable {@link module:ui/view~View views} in a
- * {@link module:ui/viewcollection~ViewCollection} when the focus is tracked by the
- * {@link module:utils/focustracker~FocusTracker} instance. It helps implementing keyboard
- * navigation in HTML forms, toolbars, lists and the like.
- *
- * To work properly it requires:
- * * a collection of focusable (HTML `tabindex` attribute) views that implement the `focus()` method,
- * * an associated focus tracker to determine which view is focused.
- *
- * A simple cycler setup can look like this:
- *
- * const focusables = new ViewCollection();
- * const focusTracker = new FocusTracker();
- *
- * // Add focusable views to the focus tracker.
- * focusTracker.add( ... );
- *
- * Then, the cycler can be used manually:
- *
- * const cycler = new FocusCycler( { focusables, focusTracker } );
- *
- * // Will focus the first focusable view in #focusables.
- * cycler.focusFirst();
- *
- * // Will log the next focusable item in #focusables.
- * console.log( cycler.next );
- *
- * Alternatively, it can work side by side with the {@link module:utils/keystrokehandler~KeystrokeHandler}:
- *
- * const keystrokeHandler = new KeystrokeHandler();
- *
- * // Activate the keystroke handler.
- * keystrokeHandler.listenTo( sourceOfEvents );
- *
- * const cycler = new FocusCycler( {
- * focusables, focusTracker, keystrokeHandler,
- * actions: {
- * // When arrowup of arrowleft is detected by the #keystrokeHandler,
- * // focusPrevious() will be called on the cycler.
- * focusPrevious: [ 'arrowup', 'arrowleft' ],
- * }
- * } );
- *
- * Check out the {@glink framework/guides/deep-dive/ui/focus-tracking "Deep dive into focus tracking" guide} to learn more.
- */
- export default class FocusCycler {
- /**
- * Creates an instance of the focus cycler utility.
- *
- * @param {Object} options Configuration options.
- * @param {module:utils/collection~Collection|Object} options.focusables
- * @param {module:utils/focustracker~FocusTracker} options.focusTracker
- * @param {module:utils/keystrokehandler~KeystrokeHandler} [options.keystrokeHandler]
- * @param {Object} [options.actions]
- */
- constructor( options ) {
- Object.assign( this, options );
- /**
- * A {@link module:ui/view~View view} collection that the cycler operates on.
- *
- * @readonly
- * @member {module:utils/collection~Collection} #focusables
- */
- /**
- * A focus tracker instance that the cycler uses to determine the current focus
- * state in {@link #focusables}.
- *
- * @readonly
- * @member {module:utils/focustracker~FocusTracker} #focusTracker
- */
- /**
- * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}
- * which can respond to certain keystrokes and cycle the focus.
- *
- * @readonly
- * @member {module:utils/keystrokehandler~KeystrokeHandler} #keystrokeHandler
- */
- /**
- * Actions that the cycler can take when a keystroke is pressed. Requires
- * `options.keystrokeHandler` to be passed and working. When an action is
- * performed, `preventDefault` and `stopPropagation` will be called on the event
- * the keystroke fired in the DOM.
- *
- * actions: {
- * // Will call #focusPrevious() when arrowleft or arrowup is pressed.
- * focusPrevious: [ 'arrowleft', 'arrowup' ],
- *
- * // Will call #focusNext() when arrowdown is pressed.
- * focusNext: 'arrowdown'
- * }
- *
- * @readonly
- * @member {Object} #actions
- */
- if ( options.actions && options.keystrokeHandler ) {
- for ( const methodName in options.actions ) {
- let actions = options.actions[ methodName ];
- if ( typeof actions == 'string' ) {
- actions = [ actions ];
- }
- for ( const keystroke of actions ) {
- options.keystrokeHandler.set( keystroke, ( data, cancel ) => {
- this[ methodName ]();
- cancel();
- } );
- }
- }
- }
- }
- /**
- * Returns the first focusable view in {@link #focusables}.
- * Returns `null` if there is none.
- *
- * @readonly
- * @member {module:ui/view~View|null} #first
- */
- get first() {
- return this.focusables.find( isFocusable ) || null;
- }
- /**
- * Returns the last focusable view in {@link #focusables}.
- * Returns `null` if there is none.
- *
- * @readonly
- * @member {module:ui/view~View|null} #last
- */
- get last() {
- return this.focusables.filter( isFocusable ).slice( -1 )[ 0 ] || null;
- }
- /**
- * Returns the next focusable view in {@link #focusables} based on {@link #current}.
- * Returns `null` if there is none.
- *
- * @readonly
- * @member {module:ui/view~View|null} #next
- */
- get next() {
- return this._getFocusableItem( 1 );
- }
- /**
- * Returns the previous focusable view in {@link #focusables} based on {@link #current}.
- * Returns `null` if there is none.
- *
- * @readonly
- * @member {module:ui/view~View|null} #previous
- */
- get previous() {
- return this._getFocusableItem( -1 );
- }
- /**
- * An index of the view in the {@link #focusables} which is focused according
- * to {@link #focusTracker}. Returns `null` when there is no such view.
- *
- * @readonly
- * @member {Number|null} #current
- */
- get current() {
- let index = null;
- // There's no focused view in the focusables.
- if ( this.focusTracker.focusedElement === null ) {
- return null;
- }
- this.focusables.find( ( view, viewIndex ) => {
- const focused = view.element === this.focusTracker.focusedElement;
- if ( focused ) {
- index = viewIndex;
- }
- return focused;
- } );
- return index;
- }
- /**
- * Focuses the {@link #first} item in {@link #focusables}.
- */
- focusFirst() {
- this._focus( this.first );
- }
- /**
- * Focuses the {@link #last} item in {@link #focusables}.
- */
- focusLast() {
- this._focus( this.last );
- }
- /**
- * Focuses the {@link #next} item in {@link #focusables}.
- */
- focusNext() {
- this._focus( this.next );
- }
- /**
- * Focuses the {@link #previous} item in {@link #focusables}.
- */
- focusPrevious() {
- this._focus( this.previous );
- }
- /**
- * Focuses the given view if it exists.
- *
- * @protected
- * @param {module:ui/view~View} view
- */
- _focus( view ) {
- if ( view ) {
- view.focus();
- }
- }
- /**
- * Returns the next or previous focusable view in {@link #focusables} with respect
- * to {@link #current}.
- *
- * @protected
- * @param {Number} step Either `1` for checking forward from {@link #current} or
- * `-1` for checking backwards.
- * @returns {module:ui/view~View|null}
- */
- _getFocusableItem( step ) {
- // Cache for speed.
- const current = this.current;
- const collectionLength = this.focusables.length;
- if ( !collectionLength ) {
- return null;
- }
- // Start from the beginning if no view is focused.
- // https://github.com/ckeditor/ckeditor5-ui/issues/206
- if ( current === null ) {
- return this[ step === 1 ? 'first' : 'last' ];
- }
- // Cycle in both directions.
- let index = ( current + collectionLength + step ) % collectionLength;
- do {
- const view = this.focusables.get( index );
- // TODO: Check if view is visible.
- if ( isFocusable( view ) ) {
- return view;
- }
- // Cycle in both directions.
- index = ( index + collectionLength + step ) % collectionLength;
- } while ( index !== current );
- return null;
- }
- }
- // Checks whether a view is focusable.
- //
- // @private
- // @param {module:ui/view~View} view A view to be checked.
- // @returns {Boolean}
- function isFocusable( view ) {
- return !!( view.focus && global.window.getComputedStyle( view.element ).display != 'none' );
- }
|