source: Dev/trunk/src/client/dijit/_KeyNavMixin.js @ 513

Last change on this file since 513 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 16.7 KB
Line 
1define([
2        "dojo/_base/array", // array.forEach
3        "dojo/_base/declare", // declare
4        "dojo/dom-attr", // domAttr.set
5        "dojo/keys", // keys.END keys.HOME, keys.LEFT_ARROW etc.
6        "dojo/_base/lang", // lang.hitch
7        "dojo/on",
8        "dijit/registry",
9        "dijit/_FocusMixin"        // to make _onBlur() work
10], function(array, declare, domAttr, keys, lang, on, registry, _FocusMixin){
11
12        // module:
13        //              dijit/_KeyNavMixin
14
15        return declare("dijit._KeyNavMixin", _FocusMixin, {
16                // summary:
17                //              A mixin to allow arrow key and letter key navigation of child or descendant widgets.
18                //              It can be used by dijit/_Container based widgets with a flat list of children,
19                //              or more complex widgets like dijit/Tree.
20                //
21                //              To use this mixin, the subclass must:
22                //
23                //                      - Implement  _getNext(), _getFirst(), _getLast(), _onLeftArrow(), _onRightArrow()
24                //                        _onDownArrow(), _onUpArrow() methods to handle home/end/left/right/up/down keystrokes.
25                //                        Next and previous in this context refer to a linear ordering of the descendants used
26                //                        by letter key search.
27                //                      - Set all descendants' initial tabIndex to "-1"; both initial descendants and any
28                //                        descendants added later, by for example addChild()
29                //                      - Define childSelector to a function or string that identifies focusable descendant widgets
30                //
31                //              Also, child widgets must implement a focus() method.
32
33                /*=====
34                 // focusedChild: [protected readonly] Widget
35                 //             The currently focused child widget, or null if there isn't one
36                 focusedChild: null,
37
38                 // _keyNavCodes: Object
39                 //             Hash mapping key code (arrow keys and home/end key) to functions to handle those keys.
40                 //             Usually not used directly, as subclasses can instead override _onLeftArrow() etc.
41                 _keyNavCodes: {},
42                 =====*/
43
44                // tabIndex: String
45                //              Tab index of the container; same as HTML tabIndex attribute.
46                //              Note then when user tabs into the container, focus is immediately
47                //              moved to the first item in the container.
48                tabIndex: "0",
49
50                // childSelector: [protected abstract] Function||String
51                //              Selector (passed to on.selector()) used to identify what to treat as a child widget.   Used to monitor
52                //              focus events and set this.focusedChild.   Must be set by implementing class.   If this is a string
53                //              (ex: "> *") then the implementing class must require dojo/query.
54                childSelector: null,
55
56                postCreate: function(){
57                        this.inherited(arguments);
58
59                        // Set tabIndex on this.domNode.  Will be automatic after #7381 is fixed.
60                        domAttr.set(this.domNode, "tabIndex", this.tabIndex);
61
62                        if(!this._keyNavCodes){
63                                var keyCodes = this._keyNavCodes = {};
64                                keyCodes[keys.HOME] = lang.hitch(this, "focusFirstChild");
65                                keyCodes[keys.END] = lang.hitch(this, "focusLastChild");
66                                keyCodes[this.isLeftToRight() ? keys.LEFT_ARROW : keys.RIGHT_ARROW] = lang.hitch(this, "_onLeftArrow");
67                                keyCodes[this.isLeftToRight() ? keys.RIGHT_ARROW : keys.LEFT_ARROW] = lang.hitch(this, "_onRightArrow");
68                                keyCodes[keys.UP_ARROW] = lang.hitch(this, "_onUpArrow");
69                                keyCodes[keys.DOWN_ARROW] = lang.hitch(this, "_onDownArrow");
70                        }
71
72                        var self = this,
73                                childSelector = typeof this.childSelector == "string"
74                                        ? this.childSelector
75                                        : lang.hitch(this, "childSelector");
76                        this.own(
77                                on(this.domNode, "keypress", lang.hitch(this, "_onContainerKeypress")),
78                                on(this.domNode, "keydown", lang.hitch(this, "_onContainerKeydown")),
79                                on(this.domNode, "focus", lang.hitch(this, "_onContainerFocus")),
80                                on(this.containerNode, on.selector(childSelector, "focusin"), function(evt){
81                                        self._onChildFocus(registry.getEnclosingWidget(this), evt);
82                                })
83                        );
84                },
85
86                _onLeftArrow: function(){
87                        // summary:
88                        //              Called on left arrow key, or right arrow key if widget is in RTL mode.
89                        //              Should go back to the previous child in horizontal container widgets like Toolbar.
90                        // tags:
91                        //              extension
92                },
93
94                _onRightArrow: function(){
95                        // summary:
96                        //              Called on right arrow key, or left arrow key if widget is in RTL mode.
97                        //              Should go to the next child in horizontal container widgets like Toolbar.
98                        // tags:
99                        //              extension
100                },
101
102                _onUpArrow: function(){
103                        // summary:
104                        //              Called on up arrow key. Should go to the previous child in vertical container widgets like Menu.
105                        // tags:
106                        //              extension
107                },
108
109                _onDownArrow: function(){
110                        // summary:
111                        //              Called on down arrow key. Should go to the next child in vertical container widgets like Menu.
112                        // tags:
113                        //              extension
114                },
115
116                focus: function(){
117                        // summary:
118                        //              Default focus() implementation: focus the first child.
119                        this.focusFirstChild();
120                },
121
122                _getFirstFocusableChild: function(){
123                        // summary:
124                        //              Returns first child that can be focused.
125
126                        // Leverage _getNextFocusableChild() to skip disabled children
127                        return this._getNextFocusableChild(null, 1);    // dijit/_WidgetBase
128                },
129
130                _getLastFocusableChild: function(){
131                        // summary:
132                        //              Returns last child that can be focused.
133
134                        // Leverage _getNextFocusableChild() to skip disabled children
135                        return this._getNextFocusableChild(null, -1);   // dijit/_WidgetBase
136                },
137
138                focusFirstChild: function(){
139                        // summary:
140                        //              Focus the first focusable child in the container.
141                        // tags:
142                        //              protected
143
144                        this.focusChild(this._getFirstFocusableChild());
145                },
146
147                focusLastChild: function(){
148                        // summary:
149                        //              Focus the last focusable child in the container.
150                        // tags:
151                        //              protected
152
153                        this.focusChild(this._getLastFocusableChild());
154                },
155
156                focusChild: function(/*dijit/_WidgetBase*/ widget, /*Boolean*/ last){
157                        // summary:
158                        //              Focus specified child widget.
159                        // widget:
160                        //              Reference to container's child widget
161                        // last:
162                        //              If true and if widget has multiple focusable nodes, focus the
163                        //              last one instead of the first one
164                        // tags:
165                        //              protected
166
167                        if(!widget){
168                                return;
169                        }
170
171                        if(this.focusedChild && widget !== this.focusedChild){
172                                this._onChildBlur(this.focusedChild);   // used to be used by _MenuBase
173                        }
174                        widget.set("tabIndex", this.tabIndex);  // for IE focus outline to appear, must set tabIndex before focus
175                        widget.focus(last ? "end" : "start");
176
177                        // Don't set focusedChild here, because the focus event should trigger a call to _onChildFocus(), which will
178                        // set it.   More importantly, _onChildFocus(), which may be executed asynchronously (after this function
179                        // returns) needs to know the old focusedChild to set its tabIndex to -1.
180                },
181
182                _onContainerFocus: function(evt){
183                        // summary:
184                        //              Handler for when the container itself gets focus.
185                        // description:
186                        //              Initially the container itself has a tabIndex, but when it gets
187                        //              focus, switch focus to first child.
188                        //
189                        //              TODO for 2.0 (or earlier): Instead of having the container tabbable, always maintain a single child
190                        //              widget as tabbable, Requires code in startup(), addChild(), and removeChild().
191                        //              That would avoid various issues like #17347.
192                        // tags:
193                        //              private
194
195                        // Note that we can't use _onFocus() because switching focus from the
196                        // _onFocus() handler confuses the focus.js code
197                        // (because it causes _onFocusNode() to be called recursively).
198                        // Also, _onFocus() would fire when focus went directly to a child widget due to mouse click.
199
200                        // Ignore spurious focus events:
201                        //      1. focus on a child widget bubbles on FF
202                        //      2. on IE, clicking the scrollbar of a select dropdown moves focus from the focused child item to me
203                        if(evt.target !== this.domNode || this.focusedChild){
204                                return;
205                        }
206
207                        this.focus();
208                },
209
210                _onFocus: function(){
211                        // When the container gets focus by being tabbed into, or a descendant gets focus by being clicked,
212                        // set the container's tabIndex to -1 (don't remove as that breaks Safari 4) so that tab or shift-tab
213                        // will go to the fields after/before the container, rather than the container itself
214                        domAttr.set(this.domNode, "tabIndex", "-1");
215
216                        this.inherited(arguments);
217                },
218
219                _onBlur: function(evt){
220                        // When focus is moved away the container, and its descendant (popup) widgets,
221                        // then restore the container's tabIndex so that user can tab to it again.
222                        // Note that using _onBlur() so that this doesn't happen when focus is shifted
223                        // to one of my child widgets (typically a popup)
224
225                        // TODO: for 2.0 consider changing this to blur whenever the container blurs, to be truthful that there is
226                        // no focused child at that time.
227
228                        domAttr.set(this.domNode, "tabIndex", this.tabIndex);
229                        if(this.focusedChild){
230                                this.focusedChild.set("tabIndex", "-1");
231                                this.lastFocusedChild = this.focusedChild;
232                                this._set("focusedChild", null);
233                        }
234                        this.inherited(arguments);
235                },
236
237                _onChildFocus: function(/*dijit/_WidgetBase*/ child){
238                        // summary:
239                        //              Called when a child widget gets focus, either by user clicking
240                        //              it, or programatically by arrow key handling code.
241                        // description:
242                        //              It marks that the current node is the selected one, and the previously
243                        //              selected node no longer is.
244
245                        if(child && child != this.focusedChild){
246                                if(this.focusedChild && !this.focusedChild._destroyed){
247                                        // mark that the previously focusable node is no longer focusable
248                                        this.focusedChild.set("tabIndex", "-1");
249                                }
250
251                                // mark that the new node is the currently selected one
252                                child.set("tabIndex", this.tabIndex);
253                                this.lastFocused = child;               // back-compat for Tree, remove for 2.0
254                                this._set("focusedChild", child);
255                        }
256                },
257
258                _searchString: "",
259                // multiCharSearchDuration: Number
260                //              If multiple characters are typed where each keystroke happens within
261                //              multiCharSearchDuration of the previous keystroke,
262                //              search for nodes matching all the keystrokes.
263                //
264                //              For example, typing "ab" will search for entries starting with
265                //              "ab" unless the delay between "a" and "b" is greater than multiCharSearchDuration.
266                multiCharSearchDuration: 1000,
267
268                onKeyboardSearch: function(/*dijit/_WidgetBase*/ item, /*Event*/ evt, /*String*/ searchString, /*Number*/ numMatches){
269                        // summary:
270                        //              When a key is pressed that matches a child item,
271                        //              this method is called so that a widget can take appropriate action is necessary.
272                        // tags:
273                        //              protected
274                        if(item){
275                                this.focusChild(item);
276                        }
277                },
278
279                _keyboardSearchCompare: function(/*dijit/_WidgetBase*/ item, /*String*/ searchString){
280                        // summary:
281                        //              Compares the searchString to the widget's text label, returning:
282                        //
283                        //                      * -1: a high priority match  and stop searching
284                        //                      * 0: not a match
285                        //                      * 1: a match but keep looking for a higher priority match
286                        // tags:
287                        //              private
288
289                        var element = item.domNode,
290                                text = item.label || (element.focusNode ? element.focusNode.label : '') || element.innerText || element.textContent || "",
291                                currentString = text.replace(/^\s+/, '').substr(0, searchString.length).toLowerCase();
292
293                        return (!!searchString.length && currentString == searchString) ? -1 : 0; // stop searching after first match by default
294                },
295
296                _onContainerKeydown: function(evt){
297                        // summary:
298                        //              When a key is pressed, if it's an arrow key etc. then it's handled here.
299                        // tags:
300                        //              private
301
302                        var func = this._keyNavCodes[evt.keyCode];
303                        if(func){
304                                func(evt, this.focusedChild);
305                                evt.stopPropagation();
306                                evt.preventDefault();
307                                this._searchString = ''; // so a DOWN_ARROW b doesn't search for ab
308                        }else if(evt.keyCode == keys.SPACE && this._searchTimer && !(evt.ctrlKey || evt.altKey || evt.metaKey)){
309                                evt.stopImmediatePropagation(); // stop a11yclick and _HasDropdown from seeing SPACE if we're doing keyboard searching
310                                evt.preventDefault(); // stop IE from scrolling, and most browsers (except FF) from sending keypress
311                                this._keyboardSearch(evt, ' ');
312                        }
313                },
314
315                _onContainerKeypress: function(evt){
316                        // summary:
317                        //              When a printable key is pressed, it's handled here, searching by letter.
318                        // tags:
319                        //              private
320
321                        if(evt.charCode < keys.SPACE || evt.ctrlKey || evt.altKey || evt.metaKey ||
322                                        (evt.charCode == keys.SPACE && this._searchTimer)){
323                                // Avoid duplicate events on firefox (ex: arrow key that will be handled by keydown handler),
324                                // and also control sequences like CMD-Q
325                                return;
326                        }
327                        evt.preventDefault();
328                        evt.stopPropagation();
329
330                        this._keyboardSearch(evt, String.fromCharCode(evt.charCode).toLowerCase());
331                },
332
333                _keyboardSearch: function(/*Event*/ evt, /*String*/ keyChar){
334                        // summary:
335                        //              Perform a search of the widget's options based on the user's keyboard activity
336                        // description:
337                        //              Called on keypress (and sometimes keydown), searches through this widget's children
338                        //              looking for items that match the user's typed search string.  Multiple characters
339                        //              typed within 1 sec of each other are combined for multicharacter searching.
340                        // tags:
341                        //              private
342                        var
343                                matchedItem = null,
344                                searchString,
345                                numMatches = 0,
346                                search = lang.hitch(this, function(){
347                                        if(this._searchTimer){
348                                                this._searchTimer.remove();
349                                        }
350                                        this._searchString += keyChar;
351                                        var allSameLetter = /^(.)\1*$/.test(this._searchString);
352                                        var searchLen = allSameLetter ? 1 : this._searchString.length;
353                                        searchString = this._searchString.substr(0, searchLen);
354                                        // commented out code block to search again if the multichar search fails after a smaller timeout
355                                        //this._searchTimer = this.defer(function(){ // this is the "failure" timeout
356                                        //      this._typingSlowly = true; // if the search fails, then treat as a full timeout
357                                        //      this._searchTimer = this.defer(function(){ // this is the "success" timeout
358                                        //              this._searchTimer = null;
359                                        //              this._searchString = '';
360                                        //      }, this.multiCharSearchDuration >> 1);
361                                        //}, this.multiCharSearchDuration >> 1);
362                                        this._searchTimer = this.defer(function(){ // this is the "success" timeout
363                                                this._searchTimer = null;
364                                                this._searchString = '';
365                                        }, this.multiCharSearchDuration);
366                                        var currentItem = this.focusedChild || null;
367                                        if(searchLen == 1 || !currentItem){
368                                                currentItem = this._getNextFocusableChild(currentItem, 1); // skip current
369                                                if(!currentItem){
370                                                        return;
371                                                } // no items
372                                        }
373                                        var stop = currentItem;
374                                        do{
375                                                var rc = this._keyboardSearchCompare(currentItem, searchString);
376                                                if(!!rc && numMatches++ == 0){
377                                                        matchedItem = currentItem;
378                                                }
379                                                if(rc == -1){ // priority match
380                                                        numMatches = -1;
381                                                        break;
382                                                }
383                                                currentItem = this._getNextFocusableChild(currentItem, 1);
384                                        }while(currentItem != stop);
385                                        // commented out code block to search again if the multichar search fails after a smaller timeout
386                                        //if(!numMatches && (this._typingSlowly || searchLen == 1)){
387                                        //      this._searchString = '';
388                                        //      if(searchLen > 1){
389                                        //              // if no matches and they're typing slowly, then go back to first letter searching
390                                        //              search();
391                                        //      }
392                                        //}
393                                });
394
395                        search();
396                        // commented out code block to search again if the multichar search fails after a smaller timeout
397                        //this._typingSlowly = false;
398                        this.onKeyboardSearch(matchedItem, evt, searchString, numMatches);
399                },
400
401                _onChildBlur: function(/*dijit/_WidgetBase*/ /*===== widget =====*/){
402                        // summary:
403                        //              Called when focus leaves a child widget to go
404                        //              to a sibling widget.
405                        //              Used to be used by MenuBase.js (remove for 2.0)
406                        // tags:
407                        //              protected
408                },
409
410                _getNextFocusableChild: function(child, dir){
411                        // summary:
412                        //              Returns the next or previous focusable descendant, compared to "child".
413                        //              Implements and extends _KeyNavMixin._getNextFocusableChild() for a _Container.
414                        // child: Widget
415                        //              The current widget
416                        // dir: Integer
417                        //              - 1 = after
418                        //              - -1 = before
419                        // tags:
420                        //              abstract extension
421
422                        var wrappedValue = child;
423                        do{
424                                if(!child){
425                                        child = this[dir > 0 ? "_getFirst" : "_getLast"]();
426                                        if(!child){ break; }
427                                }else{
428                                        child = this._getNext(child, dir);
429                                }
430                                if(child != null && child != wrappedValue && child.isFocusable()){
431                                        return child;   // dijit/_WidgetBase
432                                }
433                        }while(child != wrappedValue);
434                        // no focusable child found
435                        return null;    // dijit/_WidgetBase
436                },
437
438                _getFirst: function(){
439                        // summary:
440                        //              Returns the first child.
441                        // tags:
442                        //              abstract extension
443
444                        return null;    // dijit/_WidgetBase
445                },
446
447                _getLast: function(){
448                        // summary:
449                        //              Returns the last descendant.
450                        // tags:
451                        //              abstract extension
452
453                        return null;    // dijit/_WidgetBase
454                },
455
456                _getNext: function(child, dir){
457                        // summary:
458                        //              Returns the next descendant, compared to "child".
459                        // child: Widget
460                        //              The current widget
461                        // dir: Integer
462                        //              - 1 = after
463                        //              - -1 = before
464                        // tags:
465                        //              abstract extension
466
467                        if(child){
468                                child = child.domNode;
469                                while(child){
470                                        child = child[dir < 0 ? "previousSibling" : "nextSibling"];
471                                        if(child  && "getAttribute" in child){
472                                                var w = registry.byNode(child);
473                                                if(w){
474                                                        return w; // dijit/_WidgetBase
475                                                }
476                                        }
477                                }
478                        }
479                        return null;    // dijit/_WidgetBase
480                }
481        });
482});
Note: See TracBrowser for help on using the repository browser.