You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

488 lines
17 KiB
JavaScript

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
}
});
});