define([ "dojo/_base/array", // array.forEach "dojo/_base/declare", // declare "dojo/dom-attr", // domAttr.set "dojo/keys", // keys.END keys.HOME, keys.LEFT_ARROW etc. "dojo/_base/lang", // lang.hitch "dojo/on", "dijit/registry", "dijit/_FocusMixin" // to make _onBlur() work ], function(array, declare, domAttr, keys, lang, on, registry, _FocusMixin){ // module: // dijit/_KeyNavMixin return declare("dijit._KeyNavMixin", _FocusMixin, { // summary: // A mixin to allow arrow key and letter key navigation of child or descendant widgets. // It can be used by dijit/_Container based widgets with a flat list of children, // or more complex widgets like dijit/Tree. // // To use this mixin, the subclass must: // // - Implement _getNext(), _getFirst(), _getLast(), _onLeftArrow(), _onRightArrow() // _onDownArrow(), _onUpArrow() methods to handle home/end/left/right/up/down keystrokes. // Next and previous in this context refer to a linear ordering of the descendants used // by letter key search. // - Set all descendants' initial tabIndex to "-1"; both initial descendants and any // descendants added later, by for example addChild() // - Define childSelector to a function or string that identifies focusable descendant widgets // // Also, child widgets must implement a focus() method. /*===== // focusedChild: [protected readonly] Widget // The currently focused child widget, or null if there isn't one focusedChild: null, // _keyNavCodes: Object // Hash mapping key code (arrow keys and home/end key) to functions to handle those keys. // Usually not used directly, as subclasses can instead override _onLeftArrow() etc. _keyNavCodes: {}, =====*/ // tabIndex: String // Tab index of the container; same as HTML tabIndex attribute. // Note then when user tabs into the container, focus is immediately // moved to the first item in the container. tabIndex: "0", // childSelector: [protected abstract] Function||String // Selector (passed to on.selector()) used to identify what to treat as a child widget. Used to monitor // focus events and set this.focusedChild. Must be set by implementing class. If this is a string // (ex: "> *") then the implementing class must require dojo/query. childSelector: null, postCreate: function(){ this.inherited(arguments); // Set tabIndex on this.domNode. Will be automatic after #7381 is fixed. domAttr.set(this.domNode, "tabIndex", this.tabIndex); if(!this._keyNavCodes){ var keyCodes = this._keyNavCodes = {}; keyCodes[keys.HOME] = lang.hitch(this, "focusFirstChild"); keyCodes[keys.END] = lang.hitch(this, "focusLastChild"); keyCodes[this.isLeftToRight() ? keys.LEFT_ARROW : keys.RIGHT_ARROW] = lang.hitch(this, "_onLeftArrow"); keyCodes[this.isLeftToRight() ? keys.RIGHT_ARROW : keys.LEFT_ARROW] = lang.hitch(this, "_onRightArrow"); keyCodes[keys.UP_ARROW] = lang.hitch(this, "_onUpArrow"); keyCodes[keys.DOWN_ARROW] = lang.hitch(this, "_onDownArrow"); } var self = this, childSelector = typeof this.childSelector == "string" ? this.childSelector : lang.hitch(this, "childSelector"); this.own( on(this.domNode, "keypress", lang.hitch(this, "_onContainerKeypress")), on(this.domNode, "keydown", lang.hitch(this, "_onContainerKeydown")), on(this.domNode, "focus", lang.hitch(this, "_onContainerFocus")), on(this.containerNode, on.selector(childSelector, "focusin"), function(evt){ self._onChildFocus(registry.getEnclosingWidget(this), evt); }) ); }, _onLeftArrow: function(){ // summary: // Called on left arrow key, or right arrow key if widget is in RTL mode. // Should go back to the previous child in horizontal container widgets like Toolbar. // tags: // extension }, _onRightArrow: function(){ // summary: // Called on right arrow key, or left arrow key if widget is in RTL mode. // Should go to the next child in horizontal container widgets like Toolbar. // tags: // extension }, _onUpArrow: function(){ // summary: // Called on up arrow key. Should go to the previous child in vertical container widgets like Menu. // tags: // extension }, _onDownArrow: function(){ // summary: // Called on down arrow key. Should go to the next child in vertical container widgets like Menu. // tags: // extension }, focus: function(){ // summary: // Default focus() implementation: focus the first child. this.focusFirstChild(); }, _getFirstFocusableChild: function(){ // summary: // Returns first child that can be focused. // Leverage _getNextFocusableChild() to skip disabled children return this._getNextFocusableChild(null, 1); // dijit/_WidgetBase }, _getLastFocusableChild: function(){ // summary: // Returns last child that can be focused. // Leverage _getNextFocusableChild() to skip disabled children return this._getNextFocusableChild(null, -1); // dijit/_WidgetBase }, focusFirstChild: function(){ // summary: // Focus the first focusable child in the container. // tags: // protected this.focusChild(this._getFirstFocusableChild()); }, focusLastChild: function(){ // summary: // Focus the last focusable child in the container. // tags: // protected this.focusChild(this._getLastFocusableChild()); }, focusChild: function(/*dijit/_WidgetBase*/ widget, /*Boolean*/ last){ // summary: // Focus specified child widget. // widget: // Reference to container's child widget // last: // If true and if widget has multiple focusable nodes, focus the // last one instead of the first one // tags: // protected if(!widget){ return; } if(this.focusedChild && widget !== this.focusedChild){ this._onChildBlur(this.focusedChild); // used to be used by _MenuBase } widget.set("tabIndex", this.tabIndex); // for IE focus outline to appear, must set tabIndex before focus widget.focus(last ? "end" : "start"); // Don't set focusedChild here, because the focus event should trigger a call to _onChildFocus(), which will // set it. More importantly, _onChildFocus(), which may be executed asynchronously (after this function // returns) needs to know the old focusedChild to set its tabIndex to -1. }, _onContainerFocus: function(evt){ // summary: // Handler for when the container itself gets focus. // description: // Initially the container itself has a tabIndex, but when it gets // focus, switch focus to first child. // // TODO for 2.0 (or earlier): Instead of having the container tabbable, always maintain a single child // widget as tabbable, Requires code in startup(), addChild(), and removeChild(). // That would avoid various issues like #17347. // tags: // private // Note that we can't use _onFocus() because switching focus from the // _onFocus() handler confuses the focus.js code // (because it causes _onFocusNode() to be called recursively). // Also, _onFocus() would fire when focus went directly to a child widget due to mouse click. // Ignore spurious focus events: // 1. focus on a child widget bubbles on FF // 2. on IE, clicking the scrollbar of a select dropdown moves focus from the focused child item to me if(evt.target !== this.domNode || this.focusedChild){ return; } this.focus(); }, _onFocus: function(){ // When the container gets focus by being tabbed into, or a descendant gets focus by being clicked, // set the container's tabIndex to -1 (don't remove as that breaks Safari 4) so that tab or shift-tab // will go to the fields after/before the container, rather than the container itself domAttr.set(this.domNode, "tabIndex", "-1"); this.inherited(arguments); }, _onBlur: function(evt){ // When focus is moved away the container, and its descendant (popup) widgets, // then restore the container's tabIndex so that user can tab to it again. // Note that using _onBlur() so that this doesn't happen when focus is shifted // to one of my child widgets (typically a popup) // TODO: for 2.0 consider changing this to blur whenever the container blurs, to be truthful that there is // no focused child at that time. domAttr.set(this.domNode, "tabIndex", this.tabIndex); if(this.focusedChild){ this.focusedChild.set("tabIndex", "-1"); this.lastFocusedChild = this.focusedChild; this._set("focusedChild", null); } this.inherited(arguments); }, _onChildFocus: function(/*dijit/_WidgetBase*/ child){ // summary: // Called when a child widget gets focus, either by user clicking // it, or programatically by arrow key handling code. // description: // It marks that the current node is the selected one, and the previously // selected node no longer is. if(child && child != this.focusedChild){ if(this.focusedChild && !this.focusedChild._destroyed){ // mark that the previously focusable node is no longer focusable this.focusedChild.set("tabIndex", "-1"); } // mark that the new node is the currently selected one child.set("tabIndex", this.tabIndex); this.lastFocused = child; // back-compat for Tree, remove for 2.0 this._set("focusedChild", child); } }, _searchString: "", // multiCharSearchDuration: Number // If multiple characters are typed where each keystroke happens within // multiCharSearchDuration of the previous keystroke, // search for nodes matching all the keystrokes. // // For example, typing "ab" will search for entries starting with // "ab" unless the delay between "a" and "b" is greater than multiCharSearchDuration. multiCharSearchDuration: 1000, onKeyboardSearch: function(/*dijit/_WidgetBase*/ item, /*Event*/ evt, /*String*/ searchString, /*Number*/ numMatches){ // summary: // When a key is pressed that matches a child item, // this method is called so that a widget can take appropriate action is necessary. // tags: // protected if(item){ this.focusChild(item); } }, _keyboardSearchCompare: function(/*dijit/_WidgetBase*/ item, /*String*/ searchString){ // summary: // Compares the searchString to the widget's text label, returning: // // * -1: a high priority match and stop searching // * 0: not a match // * 1: a match but keep looking for a higher priority match // tags: // private var element = item.domNode, text = item.label || (element.focusNode ? element.focusNode.label : '') || element.innerText || element.textContent || "", currentString = text.replace(/^\s+/, '').substr(0, searchString.length).toLowerCase(); return (!!searchString.length && currentString == searchString) ? -1 : 0; // stop searching after first match by default }, _onContainerKeydown: function(evt){ // summary: // When a key is pressed, if it's an arrow key etc. then it's handled here. // tags: // private var func = this._keyNavCodes[evt.keyCode]; if(func){ func(evt, this.focusedChild); evt.stopPropagation(); evt.preventDefault(); this._searchString = ''; // so a DOWN_ARROW b doesn't search for ab }else if(evt.keyCode == keys.SPACE && this._searchTimer && !(evt.ctrlKey || evt.altKey || evt.metaKey)){ evt.stopImmediatePropagation(); // stop a11yclick and _HasDropdown from seeing SPACE if we're doing keyboard searching evt.preventDefault(); // stop IE from scrolling, and most browsers (except FF) from sending keypress this._keyboardSearch(evt, ' '); } }, _onContainerKeypress: function(evt){ // summary: // When a printable key is pressed, it's handled here, searching by letter. // tags: // private // Ignore: // - duplicate events on firefox (ex: arrow key that will be handled by keydown handler) // - control sequences like CMD-Q. // - the SPACE key (only occurs on FF) // // Note: if there's no search in progress, then SPACE should be ignored. If there is a search // in progress, then SPACE is handled in _onContainerKeyDown. if(evt.charCode <= keys.SPACE || evt.ctrlKey || evt.altKey || evt.metaKey){ return; } evt.preventDefault(); evt.stopPropagation(); this._keyboardSearch(evt, String.fromCharCode(evt.charCode).toLowerCase()); }, _keyboardSearch: function(/*Event*/ evt, /*String*/ keyChar){ // summary: // Perform a search of the widget's options based on the user's keyboard activity // description: // Called on keypress (and sometimes keydown), searches through this widget's children // looking for items that match the user's typed search string. Multiple characters // typed within 1 sec of each other are combined for multicharacter searching. // tags: // private var matchedItem = null, searchString, numMatches = 0, search = lang.hitch(this, function(){ if(this._searchTimer){ this._searchTimer.remove(); } this._searchString += keyChar; var allSameLetter = /^(.)\1*$/.test(this._searchString); var searchLen = allSameLetter ? 1 : this._searchString.length; searchString = this._searchString.substr(0, searchLen); // commented out code block to search again if the multichar search fails after a smaller timeout //this._searchTimer = this.defer(function(){ // this is the "failure" timeout // this._typingSlowly = true; // if the search fails, then treat as a full timeout // this._searchTimer = this.defer(function(){ // this is the "success" timeout // this._searchTimer = null; // this._searchString = ''; // }, this.multiCharSearchDuration >> 1); //}, this.multiCharSearchDuration >> 1); this._searchTimer = this.defer(function(){ // this is the "success" timeout this._searchTimer = null; this._searchString = ''; }, this.multiCharSearchDuration); var currentItem = this.focusedChild || null; if(searchLen == 1 || !currentItem){ currentItem = this._getNextFocusableChild(currentItem, 1); // skip current if(!currentItem){ return; } // no items } var stop = currentItem; do{ var rc = this._keyboardSearchCompare(currentItem, searchString); if(!!rc && numMatches++ == 0){ matchedItem = currentItem; } if(rc == -1){ // priority match numMatches = -1; break; } currentItem = this._getNextFocusableChild(currentItem, 1); }while(currentItem != stop); // commented out code block to search again if the multichar search fails after a smaller timeout //if(!numMatches && (this._typingSlowly || searchLen == 1)){ // this._searchString = ''; // if(searchLen > 1){ // // if no matches and they're typing slowly, then go back to first letter searching // search(); // } //} }); search(); // commented out code block to search again if the multichar search fails after a smaller timeout //this._typingSlowly = false; this.onKeyboardSearch(matchedItem, evt, searchString, numMatches); }, _onChildBlur: function(/*dijit/_WidgetBase*/ /*===== widget =====*/){ // summary: // Called when focus leaves a child widget to go // to a sibling widget. // Used to be used by MenuBase.js (remove for 2.0) // tags: // protected }, _getNextFocusableChild: function(child, dir){ // summary: // Returns the next or previous focusable descendant, compared to "child". // Implements and extends _KeyNavMixin._getNextFocusableChild() for a _Container. // child: Widget // The current widget // dir: Integer // - 1 = after // - -1 = before // tags: // abstract extension var wrappedValue = child; do{ if(!child){ child = this[dir > 0 ? "_getFirst" : "_getLast"](); if(!child){ break; } }else{ child = this._getNext(child, dir); } if(child != null && child != wrappedValue && child.isFocusable()){ return child; // dijit/_WidgetBase } }while(child != wrappedValue); // no focusable child found return null; // dijit/_WidgetBase }, _getFirst: function(){ // summary: // Returns the first child. // tags: // abstract extension return null; // dijit/_WidgetBase }, _getLast: function(){ // summary: // Returns the last descendant. // tags: // abstract extension return null; // dijit/_WidgetBase }, _getNext: function(child, dir){ // summary: // Returns the next descendant, compared to "child". // child: Widget // The current widget // dir: Integer // - 1 = after // - -1 = before // tags: // abstract extension if(child){ child = child.domNode; while(child){ child = child[dir < 0 ? "previousSibling" : "nextSibling"]; if(child && "getAttribute" in child){ var w = registry.byNode(child); if(w){ return w; // dijit/_WidgetBase } } } } return null; // dijit/_WidgetBase } }); });