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