You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
487 lines
17 KiB
487 lines
17 KiB
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 |
|
} |
|
}); |
|
});
|
|
|