dijit
Version:
Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible u
488 lines (426 loc) • 17.2 kB
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 && 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
}
});
});