123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- define([
- "dojo/_base/declare",
- "dojo/aspect",
- "dojo/on",
- "dojo/_base/lang",
- "dojo/has",
- "put-selector/put",
- "dojo/_base/Deferred",
- "dojo/_base/sniff"
- ], function(declare, aspect, on, lang, has, put, Deferred){
- var delegatingInputTypes = {
- checkbox: 1,
- radio: 1,
- button: 1
- },
- hasGridCellClass = /\bdgrid-cell\b/,
- hasGridRowClass = /\bdgrid-row\b/;
- has.add("dom-contains", function(global, doc, element){
- return !!element.contains; // not supported by FF < 9
- });
- function contains(parent, node){
- // summary:
- // Checks to see if an element is contained by another element.
-
- if(has("dom-contains")){
- return parent.contains(node);
- }else{
- return parent.compareDocumentPosition(node) & 8 /* DOCUMENT_POSITION_CONTAINS */;
- }
- }
- var Keyboard = declare(null, {
- // summary:
- // Adds keyboard navigation capability to a list or grid.
-
- // pageSkip: Number
- // Number of rows to jump by when page up or page down is pressed.
- pageSkip: 10,
-
- tabIndex: 0,
-
- // keyMap: Object
- // Hash which maps key codes to functions to be executed (in the context
- // of the instance) for key events within the grid's body.
- keyMap: null,
-
- // headerKeyMap: Object
- // Hash which maps key codes to functions to be executed (in the context
- // of the instance) for key events within the grid's header row.
- headerKeyMap: null,
-
- postMixInProperties: function(){
- this.inherited(arguments);
-
- if(!this.keyMap){
- this.keyMap = lang.mixin({}, Keyboard.defaultKeyMap);
- }
- if(!this.headerKeyMap){
- this.headerKeyMap = lang.mixin({}, Keyboard.defaultHeaderKeyMap);
- }
- },
-
- postCreate: function(){
- this.inherited(arguments);
- var grid = this;
-
- function handledEvent(event){
- // text boxes and other inputs that can use direction keys should be ignored and not affect cell/row navigation
- var target = event.target;
- return target.type && (!delegatingInputTypes[target.type] || event.keyCode == 32);
- }
-
- function enableNavigation(areaNode){
- var cellNavigation = grid.cellNavigation,
- isFocusableClass = cellNavigation ? hasGridCellClass : hasGridRowClass,
- isHeader = areaNode === grid.headerNode,
- initialNode = areaNode;
-
- function initHeader(){
- if(grid._focusedHeaderNode){
- // Remove the tab index for the node that previously had it.
- grid._focusedHeaderNode.tabIndex = -1;
- }
- if(grid.showHeader){
- // Set the tab index only if the header is visible.
- grid._focusedHeaderNode = initialNode =
- cellNavigation ? grid.headerNode.getElementsByTagName("th")[0] : grid.headerNode;
- if(initialNode){ initialNode.tabIndex = grid.tabIndex; }
- }
- }
-
- if(isHeader){
- // Initialize header now (since it's already been rendered),
- // and aspect after future renderHeader calls to reset focus.
- initHeader();
- aspect.after(grid, "renderHeader", initHeader, true);
- }else{
- aspect.after(grid, "renderArray", function(ret){
- // summary:
- // Ensures the first element of a grid is always keyboard selectable after data has been
- // retrieved if there is not already a valid focused element.
-
- return Deferred.when(ret, function(ret){
- var focusedNode = grid._focusedNode || initialNode;
-
- // do not update the focused element if we already have a valid one
- if(isFocusableClass.test(focusedNode.className) && contains(areaNode, focusedNode)){
- return ret;
- }
-
- // ensure that the focused element is actually a grid cell, not a
- // dgrid-preload or dgrid-content element, which should not be focusable,
- // even when data is loaded asynchronously
- for(var i = 0, elements = areaNode.getElementsByTagName("*"), element; (element = elements[i]); ++i){
- if(isFocusableClass.test(element.className)){
- focusedNode = grid._focusedNode = element;
- break;
- }
- }
-
- focusedNode.tabIndex = grid.tabIndex;
- return ret;
- });
- });
- }
-
- grid._listeners.push(on(areaNode, "mousedown", function(event){
- if(!handledEvent(event)){
- grid._focusOnNode(event.target, isHeader, event);
- }
- }));
-
- grid._listeners.push(on(areaNode, "keydown", function(event){
- // For now, don't squash browser-specific functionalities by letting
- // ALT and META function as they would natively
- if(event.metaKey || event.altKey) {
- return;
- }
-
- var handler = grid[isHeader ? "headerKeyMap" : "keyMap"][event.keyCode];
-
- // Text boxes and other inputs that can use direction keys should be ignored and not affect cell/row navigation
- if(handler && !handledEvent(event)){
- handler.call(grid, event);
- }
- }));
- }
-
- if(this.tabableHeader){
- enableNavigation(this.headerNode);
- on(this.headerNode, "dgrid-cellfocusin", function(){
- grid.scrollTo({ x: this.scrollLeft });
- });
- }
- enableNavigation(this.contentNode);
- },
-
- removeRow: function(rowElement){
- if(!this._focusedNode){
- // Nothing special to do if we have no record of anything focused
- return this.inherited(arguments);
- }
-
- var self = this,
- isActive = document.activeElement === this._focusedNode,
- focusedTarget = this[this.cellNavigation ? "cell" : "row"](this._focusedNode),
- focusedRow = focusedTarget.row || focusedTarget,
- sibling;
- rowElement = rowElement.element || rowElement;
-
- // If removed row previously had focus, temporarily store information
- // to be handled in an immediately-following insertRow call, or next turn
- if(rowElement === focusedRow.element){
- sibling = this.down(focusedRow, true);
-
- // Check whether down call returned the same row, or failed to return
- // any (e.g. during a partial unrendering)
- if (!sibling || sibling.element === rowElement) {
- sibling = this.up(focusedRow, true);
- }
-
- this._removedFocus = {
- active: isActive,
- rowId: focusedRow.id,
- columnId: focusedTarget.column && focusedTarget.column.id,
- siblingId: !sibling || sibling.element === rowElement ? undefined : sibling.id
- };
-
- // Call _restoreFocus on next turn, to restore focus to sibling
- // if no replacement row was immediately inserted.
- // Pass original row's id in case it was re-inserted in a renderArray
- // call (and thus was found, but couldn't be focused immediately)
- setTimeout(function() {
- if(self._removedFocus){
- self._restoreFocus(focusedRow.id);
- }
- }, 0);
-
- // Clear _focusedNode until _restoreFocus is called, to avoid
- // needlessly re-running this logic
- this._focusedNode = null;
- }
-
- this.inherited(arguments);
- },
-
- insertRow: function(object){
- var rowElement = this.inherited(arguments);
- if(this._removedFocus && !this._removedFocus.wait){
- this._restoreFocus(rowElement);
- }
- return rowElement;
- },
-
- _restoreFocus: function(row) {
- // summary:
- // Restores focus to the newly inserted row if it matches the
- // previously removed row, or to the nearest sibling otherwise.
-
- var focusInfo = this._removedFocus,
- newTarget;
-
- row = row && this.row(row);
- newTarget = row && row.element && row.id === focusInfo.rowId ? row :
- typeof focusInfo.siblingId !== "undefined" && this.row(focusInfo.siblingId);
-
- if(newTarget && newTarget.element){
- if(!newTarget.element.parentNode.parentNode){
- // This was called from renderArray, so the row hasn't
- // actually been placed in the DOM yet; handle it on the next
- // turn (called from removeRow).
- focusInfo.wait = true;
- return;
- }
- newTarget = typeof focusInfo.columnId !== "undefined" ?
- this.cell(newTarget, focusInfo.columnId) : newTarget;
- if(focusInfo.active){
- // Row/cell was previously focused, so focus the new one immediately
- this.focus(newTarget);
- }else{
- // Row/cell was not focused, but we still need to update tabIndex
- // and the element's class to be consistent with the old one
- put(newTarget.element, ".dgrid-focus");
- newTarget.element.tabIndex = this.tabIndex;
- }
- }
-
- delete this._removedFocus;
- },
-
- addKeyHandler: function(key, callback, isHeader){
- // summary:
- // Adds a handler to the keyMap on the instance.
- // Supports binding additional handlers to already-mapped keys.
- // key: Number
- // Key code representing the key to be handled.
- // callback: Function
- // Callback to be executed (in instance context) when the key is pressed.
- // isHeader: Boolean
- // Whether the handler is to be added for the grid body (false, default)
- // or the header (true).
-
- // Aspects may be about 10% slower than using an array-based appraoch,
- // but there is significantly less code involved (here and above).
- return aspect.after( // Handle
- this[isHeader ? "headerKeyMap" : "keyMap"], key, callback, true);
- },
-
- _focusOnNode: function(element, isHeader, event){
- var focusedNodeProperty = "_focused" + (isHeader ? "Header" : "") + "Node",
- focusedNode = this[focusedNodeProperty],
- cellOrRowType = this.cellNavigation ? "cell" : "row",
- cell = this[cellOrRowType](element),
- inputs,
- input,
- numInputs,
- inputFocused,
- i;
-
- element = cell && cell.element;
- if(!element){ return; }
-
- if(this.cellNavigation){
- inputs = element.getElementsByTagName("input");
- for(i = 0, numInputs = inputs.length; i < numInputs; i++){
- input = inputs[i];
- if((input.tabIndex != -1 || "lastValue" in input) && !input.disabled){
- // Employ workaround for focus rectangle in IE < 8
- if(has("ie") < 8){ input.style.position = "relative"; }
- input.focus();
- if(has("ie") < 8){ input.style.position = ""; }
- inputFocused = true;
- break;
- }
- }
- }
-
- event = lang.mixin({ grid: this }, event);
- if(event.type){
- event.parentType = event.type;
- }
- if(!event.bubbles){
- // IE doesn't always have a bubbles property already true.
- // Opera throws if you try to set it to true if it is already true.
- event.bubbles = true;
- }
- if(focusedNode){
- // Clean up previously-focused element
- // Remove the class name and the tabIndex attribute
- put(focusedNode, "!dgrid-focus[!tabIndex]");
- if(has("ie") < 8){
- // Clean up after workaround below (for non-input cases)
- focusedNode.style.position = "";
- }
-
- // Expose object representing focused cell or row losing focus, via
- // event.cell or event.row; which is set depends on cellNavigation.
- event[cellOrRowType] = this[cellOrRowType](focusedNode);
- on.emit(element, "dgrid-cellfocusout", event);
- }
- focusedNode = this[focusedNodeProperty] = element;
-
- // Expose object representing focused cell or row gaining focus, via
- // event.cell or event.row; which is set depends on cellNavigation.
- // Note that yes, the same event object is being reused; on.emit
- // performs a shallow copy of properties into a new event object.
- event[cellOrRowType] = cell;
-
- if(!inputFocused){
- if(has("ie") < 8){
- // setting the position to relative magically makes the outline
- // work properly for focusing later on with old IE.
- // (can't be done a priori with CSS or screws up the entire table)
- element.style.position = "relative";
- }
- element.tabIndex = this.tabIndex;
- element.focus();
- }
- put(element, ".dgrid-focus");
- on.emit(focusedNode, "dgrid-cellfocusin", event);
- },
-
- focusHeader: function(element){
- this._focusOnNode(element || this._focusedHeaderNode, true);
- },
-
- focus: function(element){
- this._focusOnNode(element || this._focusedNode, false);
- }
- });
- // Common functions used in default keyMap (called in instance context)
- var moveFocusVertical = Keyboard.moveFocusVertical = function(event, steps){
- var cellNavigation = this.cellNavigation,
- target = this[cellNavigation ? "cell" : "row"](event),
- columnId = cellNavigation && target.column.id,
- next = this.down(this._focusedNode, steps, true);
-
- // Navigate within same column if cell navigation is enabled
- if(cellNavigation){ next = this.cell(next, columnId); }
- this._focusOnNode(next, false, event);
-
- event.preventDefault();
- };
- var moveFocusUp = Keyboard.moveFocusUp = function(event){
- moveFocusVertical.call(this, event, -1);
- };
- var moveFocusDown = Keyboard.moveFocusDown = function(event){
- moveFocusVertical.call(this, event, 1);
- };
- var moveFocusPageUp = Keyboard.moveFocusPageUp = function(event){
- moveFocusVertical.call(this, event, -this.pageSkip);
- };
- var moveFocusPageDown = Keyboard.moveFocusPageDown = function(event){
- moveFocusVertical.call(this, event, this.pageSkip);
- };
- var moveFocusHorizontal = Keyboard.moveFocusHorizontal = function(event, steps){
- if(!this.cellNavigation){ return; }
- var isHeader = !this.row(event), // header reports row as undefined
- currentNode = this["_focused" + (isHeader ? "Header" : "") + "Node"];
-
- this._focusOnNode(this.right(currentNode, steps), isHeader, event);
- event.preventDefault();
- };
- var moveFocusLeft = Keyboard.moveFocusLeft = function(event){
- moveFocusHorizontal.call(this, event, -1);
- };
- var moveFocusRight = Keyboard.moveFocusRight = function(event){
- moveFocusHorizontal.call(this, event, 1);
- };
- var moveHeaderFocusEnd = Keyboard.moveHeaderFocusEnd = function(event, scrollToBeginning){
- // Header case is always simple, since all rows/cells are present
- var nodes;
- if(this.cellNavigation){
- nodes = this.headerNode.getElementsByTagName("th");
- this._focusOnNode(nodes[scrollToBeginning ? 0 : nodes.length - 1], true, event);
- }
- // In row-navigation mode, there's nothing to do - only one row in header
-
- // Prevent browser from scrolling entire page
- event.preventDefault();
- };
- var moveHeaderFocusHome = Keyboard.moveHeaderFocusHome = function(event){
- moveHeaderFocusEnd.call(this, event, true);
- };
- var moveFocusEnd = Keyboard.moveFocusEnd = function(event, scrollToTop){
- // summary:
- // Handles requests to scroll to the beginning or end of the grid.
-
- // Assume scrolling to top unless event is specifically for End key
- var self = this,
- cellNavigation = this.cellNavigation,
- contentNode = this.contentNode,
- contentPos = scrollToTop ? 0 : contentNode.scrollHeight,
- scrollPos = contentNode.scrollTop + contentPos,
- endChild = contentNode[scrollToTop ? "firstChild" : "lastChild"],
- hasPreload = endChild.className.indexOf("dgrid-preload") > -1,
- endTarget = hasPreload ? endChild[(scrollToTop ? "next" : "previous") + "Sibling"] : endChild,
- endPos = endTarget.offsetTop + (scrollToTop ? 0 : endTarget.offsetHeight),
- handle;
-
- if(hasPreload){
- // Find the nearest dgrid-row to the relevant end of the grid
- while(endTarget && endTarget.className.indexOf("dgrid-row") < 0){
- endTarget = endTarget[(scrollToTop ? "next" : "previous") + "Sibling"];
- }
- // If none is found, there are no rows, and nothing to navigate
- if(!endTarget){ return; }
- }
-
- // Grid content may be lazy-loaded, so check if content needs to be
- // loaded first
- if(!hasPreload || endChild.offsetHeight < 1){
- // End row is loaded; focus the first/last row/cell now
- if(cellNavigation){
- // Preserve column that was currently focused
- endTarget = this.cell(endTarget, this.cell(event).column.id);
- }
- this._focusOnNode(endTarget, false, event);
- }else{
- // In IE < 9, the event member references will become invalid by the time
- // _focusOnNode is called, so make a (shallow) copy up-front
- if(!has("dom-addeventlistener")){
- event = lang.mixin({}, event);
- }
-
- // If the topmost/bottommost row rendered doesn't reach the top/bottom of
- // the contentNode, we are using OnDemandList and need to wait for more
- // data to render, then focus the first/last row in the new content.
- handle = aspect.after(this, "renderArray", function(rows){
- handle.remove();
- return Deferred.when(rows, function(rows){
- var target = rows[scrollToTop ? 0 : rows.length - 1];
- if(cellNavigation){
- // Preserve column that was currently focused
- target = self.cell(target, self.cell(event).column.id);
- }
- self._focusOnNode(target, false, event);
- });
- });
- }
-
- if(scrollPos === endPos){
- // Grid body is already scrolled to end; prevent browser from scrolling
- // entire page instead
- event.preventDefault();
- }
- };
- var moveFocusHome = Keyboard.moveFocusHome = function(event){
- moveFocusEnd.call(this, event, true);
- };
- function preventDefault(event){
- event.preventDefault();
- }
- Keyboard.defaultKeyMap = {
- 32: preventDefault, // space
- 33: moveFocusPageUp, // page up
- 34: moveFocusPageDown, // page down
- 35: moveFocusEnd, // end
- 36: moveFocusHome, // home
- 37: moveFocusLeft, // left
- 38: moveFocusUp, // up
- 39: moveFocusRight, // right
- 40: moveFocusDown // down
- };
- // Header needs fewer default bindings (no vertical), so bind it separately
- Keyboard.defaultHeaderKeyMap = {
- 32: preventDefault, // space
- 35: moveHeaderFocusEnd, // end
- 36: moveHeaderFocusHome, // home
- 37: moveFocusLeft, // left
- 39: moveFocusRight // right
- };
- return Keyboard;
- });
|