define("dojox/grid/_FocusManager", [ "dojo/_base/array", "dojo/_base/lang", "dojo/_base/declare", "dojo/_base/connect", "dojo/_base/event", "dojo/_base/sniff", "dojo/query", "./util", "dojo/_base/html" ], function(array, lang, declare, connect, event, has, query, util, html){ // focus management return declare("dojox.grid._FocusManager", null, { // summary: // Controls grid cell focus. Owned by grid and used internally for focusing. // Note: grid cell actually receives keyboard input only when cell is being edited. constructor: function(inGrid){ this.grid = inGrid; this.cell = null; this.rowIndex = -1; this._connects = []; this._headerConnects = []; this.headerMenu = this.grid.headerMenu; this._connects.push(connect.connect(this.grid.domNode, "onfocus", this, "doFocus")); this._connects.push(connect.connect(this.grid.domNode, "onblur", this, "doBlur")); this._connects.push(connect.connect(this.grid.domNode, "mousedown", this, "_mouseDown")); this._connects.push(connect.connect(this.grid.domNode, "mouseup", this, "_mouseUp")); this._connects.push(connect.connect(this.grid.domNode, "oncontextmenu", this, "doContextMenu")); this._connects.push(connect.connect(this.grid.lastFocusNode, "onfocus", this, "doLastNodeFocus")); this._connects.push(connect.connect(this.grid.lastFocusNode, "onblur", this, "doLastNodeBlur")); this._connects.push(connect.connect(this.grid,"_onFetchComplete", this, "_delayedCellFocus")); this._connects.push(connect.connect(this.grid,"postrender", this, "_delayedHeaderFocus")); }, destroy: function(){ array.forEach(this._connects, connect.disconnect); array.forEach(this._headerConnects, connect.disconnect); delete this.grid; delete this.cell; }, _colHeadNode: null, _colHeadFocusIdx: null, _contextMenuBindNode: null, tabbingOut: false, focusClass: "dojoxGridCellFocus", focusView: null, initFocusView: function(){ this.focusView = this.grid.views.getFirstScrollingView() || this.focusView || this.grid.views.views[0]; this._initColumnHeaders(); }, isFocusCell: function(inCell, inRowIndex){ // summary: // states if the given cell is focused // inCell: object // grid cell object // inRowIndex: int // grid row index // returns: // true of the given grid cell is focused return (this.cell == inCell) && (this.rowIndex == inRowIndex); }, isLastFocusCell: function(){ if(this.cell){ return (this.rowIndex == this.grid.rowCount-1) && (this.cell.index == this.grid.layout.cellCount-1); } return false; }, isFirstFocusCell: function(){ if(this.cell){ return (this.rowIndex === 0) && (this.cell.index === 0); } return false; }, isNoFocusCell: function(){ return (this.rowIndex < 0) || !this.cell; }, isNavHeader: function(){ // summary: // states whether currently navigating among column headers. // returns: // true if focus is on a column header; false otherwise. return (!!this._colHeadNode); }, getHeaderIndex: function(){ // summary: // if one of the column headers currently has focus, return its index. // returns: // index of the focused column header, or -1 if none have focus. if(this._colHeadNode){ return array.indexOf(this._findHeaderCells(), this._colHeadNode); }else{ return -1; } }, _focusifyCellNode: function(inBork){ var n = this.cell && this.cell.getNode(this.rowIndex); if(n){ html.toggleClass(n, this.focusClass, inBork); if(inBork){ var sl = this.scrollIntoView(); try{ if(has("webkit") || !this.grid.edit.isEditing()){ util.fire(n, "focus"); if(sl){ this.cell.view.scrollboxNode.scrollLeft = sl; } } }catch(e){} } } }, _delayedCellFocus: function(){ if(this.isNavHeader()||!this.grid.focused){ return; } var n = this.cell && this.cell.getNode(this.rowIndex); if(n){ try{ if(!this.grid.edit.isEditing()){ html.toggleClass(n, this.focusClass, true); if(this._colHeadNode){ this.blurHeader(); } util.fire(n, "focus"); } } catch(e){} } }, _delayedHeaderFocus: function(){ if(this.isNavHeader()){ this.focusHeader(); //this.grid.domNode.focus(); } }, _initColumnHeaders: function(){ array.forEach(this._headerConnects, connect.disconnect); this._headerConnects = []; var headers = this._findHeaderCells(); for(var i = 0; i < headers.length; i++){ this._headerConnects.push(connect.connect(headers[i], "onfocus", this, "doColHeaderFocus")); this._headerConnects.push(connect.connect(headers[i], "onblur", this, "doColHeaderBlur")); } }, _findHeaderCells: function(){ // This should be a one liner: // query("th[tabindex=-1]", this.grid.viewsHeaderNode); // But there is a bug in query() for IE -- see trac #7037. var allHeads = query("th", this.grid.viewsHeaderNode); var headers = []; for (var i = 0; i < allHeads.length; i++){ var aHead = allHeads[i]; var hasTabIdx = html.hasAttr(aHead, "tabIndex"); var tabindex = html.attr(aHead, "tabIndex"); if (hasTabIdx && tabindex < 0) { headers.push(aHead); } } return headers; }, _setActiveColHeader: function(/*Node*/colHeaderNode, /*Integer*/colFocusIdx, /*Integer*/ prevColFocusIdx){ //console.log("setActiveColHeader() - colHeaderNode:colFocusIdx:prevColFocusIdx = " + colHeaderNode + ":" + colFocusIdx + ":" + prevColFocusIdx); this.grid.domNode.setAttribute("aria-activedescendant",colHeaderNode.id); if (prevColFocusIdx != null && prevColFocusIdx >= 0 && prevColFocusIdx != colFocusIdx){ html.toggleClass(this._findHeaderCells()[prevColFocusIdx],this.focusClass,false); } html.toggleClass(colHeaderNode,this.focusClass, true); this._colHeadNode = colHeaderNode; this._colHeadFocusIdx = colFocusIdx; this._scrollHeader(this._colHeadFocusIdx); }, scrollIntoView: function(){ var info = (this.cell ? this._scrollInfo(this.cell) : null); if(!info || !info.s){ return null; } var rt = this.grid.scroller.findScrollTop(this.rowIndex); // place cell within horizontal view if(info.n && info.sr){ if(info.n.offsetLeft + info.n.offsetWidth > info.sr.l + info.sr.w){ info.s.scrollLeft = info.n.offsetLeft + info.n.offsetWidth - info.sr.w; }else if(info.n.offsetLeft < info.sr.l){ info.s.scrollLeft = info.n.offsetLeft; } } // place cell within vertical view if(info.r && info.sr){ if(rt + info.r.offsetHeight > info.sr.t + info.sr.h){ this.grid.setScrollTop(rt + info.r.offsetHeight - info.sr.h); }else if(rt < info.sr.t){ this.grid.setScrollTop(rt); } } return info.s.scrollLeft; }, _scrollInfo: function(cell, domNode){ if(cell){ var cl = cell, sbn = cl.view.scrollboxNode, sbnr = { w: sbn.clientWidth, l: sbn.scrollLeft, t: sbn.scrollTop, h: sbn.clientHeight }, rn = cl.view.getRowNode(this.rowIndex); return { c: cl, s: sbn, sr: sbnr, n: (domNode ? domNode : cell.getNode(this.rowIndex)), r: rn }; } return null; }, _scrollHeader: function(currentIdx){ var info = null; if(this._colHeadNode){ var cell = this.grid.getCell(currentIdx); if(!cell){ return; } info = this._scrollInfo(cell, cell.getNode(0)); } if(info && info.s && info.sr && info.n){ // scroll horizontally as needed. var scroll = info.sr.l + info.sr.w; if(info.n.offsetLeft + info.n.offsetWidth > scroll){ info.s.scrollLeft = info.n.offsetLeft + info.n.offsetWidth - info.sr.w; }else if(info.n.offsetLeft < info.sr.l){ info.s.scrollLeft = info.n.offsetLeft; }else if(has('ie') <= 7 && cell && cell.view.headerNode){ // Trac 7158: scroll dojoxGridHeader for IE7 and lower cell.view.headerNode.scrollLeft = info.s.scrollLeft; } } }, _isHeaderHidden: function(){ // summary: // determine if the grid headers are hidden // relies on documented technique of setting .dojoxGridHeader { display:none; } // returns: Boolean // true if headers are hidden // false if headers are not hidden var curView = this.focusView; if (!curView){ // find one so we can determine if headers are hidden // there is no focusView after adding items to empty grid (test_data_grid_empty.html) for (var i = 0, cView; (cView = this.grid.views.views[i]); i++) { if(cView.headerNode ){ curView=cView; break; } } } return (curView && html.getComputedStyle(curView.headerNode).display == "none"); }, colSizeAdjust: function (e, colIdx, delta){ // adjust the column specified by colIdx by the specified delta px var headers = this._findHeaderCells(); var view = this.focusView; if(!view || !view.header.tableMap.map){ for(var i = 0, cView; (cView = this.grid.views.views[i]); i++){ // find first view with a tableMap in order to work with empty grid if(cView.header.tableMap.map){ view=cView; break; } } } var curHeader = headers[colIdx]; if (!view || (colIdx == headers.length-1 && colIdx === 0)){ return; // can't adjust single col. grid } view.content.baseDecorateEvent(e); // need to adjust event with header cell info since focus is no longer on header cell e.cellNode = curHeader; //this.findCellTarget(e.target, e.rowNode); e.cellIndex = view.content.getCellNodeIndex(e.cellNode); e.cell = (e.cellIndex >= 0 ? this.grid.getCell(e.cellIndex) : null); if (view.header.canResize(e)){ var deltaObj = { l: delta }; var drag = view.header.colResizeSetup(e,false); view.header.doResizeColumn(drag, null, deltaObj); view.update(); } }, styleRow: function(inRow){ return; }, setFocusIndex: function(inRowIndex, inCellIndex){ // summary: // focuses the given grid cell // inRowIndex: int // grid row index // inCellIndex: int // grid cell index this.setFocusCell(this.grid.getCell(inCellIndex), inRowIndex); }, setFocusCell: function(inCell, inRowIndex){ // summary: // focuses the given grid cell // inCell: object // grid cell object // inRowIndex: int // grid row index if(inCell && !this.isFocusCell(inCell, inRowIndex)){ this.tabbingOut = false; if (this._colHeadNode){ this.blurHeader(); } this._colHeadNode = this._colHeadFocusIdx = null; this.focusGridView(); this._focusifyCellNode(false); this.cell = inCell; this.rowIndex = inRowIndex; this._focusifyCellNode(true); } // even if this cell isFocusCell, the document focus may need to be rejiggered // call opera on delay to prevent keypress from altering focus if(has('opera')){ setTimeout(lang.hitch(this.grid, 'onCellFocus', this.cell, this.rowIndex), 1); }else{ this.grid.onCellFocus(this.cell, this.rowIndex); } }, next: function(){ // summary: // focus next grid cell if(this.cell){ var row=this.rowIndex, col=this.cell.index+1, cc=this.grid.layout.cellCount-1, rc=this.grid.rowCount-1; if(col > cc){ col = 0; row++; } if(row > rc){ col = cc; row = rc; } if(this.grid.edit.isEditing()){ //when editing, only navigate to editable cells var nextCell = this.grid.getCell(col); if (!this.isLastFocusCell() && (!nextCell.editable || this.grid.canEdit && !this.grid.canEdit(nextCell, row))){ this.cell=nextCell; this.rowIndex=row; this.next(); return; } } this.setFocusIndex(row, col); } }, previous: function(){ // summary: // focus previous grid cell if(this.cell){ var row=(this.rowIndex || 0), col=(this.cell.index || 0) - 1; if(col < 0){ col = this.grid.layout.cellCount-1; row--; } if(row < 0){ row = 0; col = 0; } if(this.grid.edit.isEditing()){ //when editing, only navigate to editable cells var prevCell = this.grid.getCell(col); if (!this.isFirstFocusCell() && !prevCell.editable){ this.cell=prevCell; this.rowIndex=row; this.previous(); return; } } this.setFocusIndex(row, col); } }, move: function(inRowDelta, inColDelta) { // summary: // focus grid cell or simulate focus to column header based on position relative to current focus // inRowDelta: int // vertical distance from current focus // inColDelta: int // horizontal distance from current focus var colDir = inColDelta < 0 ? -1 : 1; // Handle column headers. if(this.isNavHeader()){ var headers = this._findHeaderCells(); var savedIdx = currentIdx = array.indexOf(headers, this._colHeadNode); currentIdx += inColDelta; while(currentIdx >=0 && currentIdx < headers.length && headers[currentIdx].style.display == "none"){ // skip over hidden column headers currentIdx += colDir; } if((currentIdx >= 0) && (currentIdx < headers.length)){ this._setActiveColHeader(headers[currentIdx],currentIdx, savedIdx); } }else{ if(this.cell){ // Handle grid proper. var sc = this.grid.scroller, r = this.rowIndex, rc = this.grid.rowCount-1, row = Math.min(rc, Math.max(0, r+inRowDelta)); if(inRowDelta){ if(inRowDelta>0){ if(row > sc.getLastPageRow(sc.page)){ //need to load additional data, let scroller do that this.grid.setScrollTop(this.grid.scrollTop+sc.findScrollTop(row)-sc.findScrollTop(r)); } }else if(inRowDelta<0){ if(row <= sc.getPageRow(sc.page)){ //need to load additional data, let scroller do that this.grid.setScrollTop(this.grid.scrollTop-sc.findScrollTop(r)-sc.findScrollTop(row)); } } } var cc = this.grid.layout.cellCount-1, i = this.cell.index, col = Math.min(cc, Math.max(0, i+inColDelta)); var cell = this.grid.getCell(col); while(col>=0 && col < cc && cell && cell.hidden === true){ // skip hidden cells col += colDir; cell = this.grid.getCell(col); } if (!cell || cell.hidden === true){ // don't change col if would move to hidden col = i; } //skip hidden row|cell var n = cell.getNode(row); if(!n && inRowDelta){ if((row + inRowDelta) >= 0 && (row + inRowDelta) <= rc){ this.move(inRowDelta > 0 ? ++inRowDelta : --inRowDelta, inColDelta); } return; }else if((!n || html.style(n, "display") === "none") && inColDelta){ if((col + inColDelta) >= 0 && (col + inColDelta) <= cc){ this.move(inRowDelta, inColDelta > 0 ? ++inColDelta : --inColDelta); } return; } this.setFocusIndex(row, col); if(inRowDelta){ this.grid.updateRow(r); } } } }, previousKey: function(e){ if(this.grid.edit.isEditing()){ event.stop(e); this.previous(); }else if(!this.isNavHeader() && !this._isHeaderHidden()) { this.grid.domNode.focus(); // will call doFocus and set focus into header. event.stop(e); }else{ this.tabOut(this.grid.domNode); if (this._colHeadFocusIdx != null) { // clear grid header focus html.toggleClass(this._findHeaderCells()[this._colHeadFocusIdx], this.focusClass, false); this._colHeadFocusIdx = null; } this._focusifyCellNode(false); } }, nextKey: function(e) { var isEmpty = (this.grid.rowCount === 0); if(e.target === this.grid.domNode && this._colHeadFocusIdx == null){ this.focusHeader(); event.stop(e); }else if(this.isNavHeader()){ // if tabbing from col header, then go to grid proper. this.blurHeader(); if(!this.findAndFocusGridCell()){ this.tabOut(this.grid.lastFocusNode); } this._colHeadNode = this._colHeadFocusIdx= null; }else if(this.grid.edit.isEditing()){ event.stop(e); this.next(); }else{ this.tabOut(this.grid.lastFocusNode); } }, tabOut: function(inFocusNode){ this.tabbingOut = true; inFocusNode.focus(); }, focusGridView: function(){ util.fire(this.focusView, "focus"); }, focusGrid: function(inSkipFocusCell){ this.focusGridView(); this._focusifyCellNode(true); }, findAndFocusGridCell: function(){ // summary: // find the first focusable grid cell // returns: Boolean // true if focus was set to a cell // false if no cell found to set focus onto var didFocus = true; var isEmpty = (this.grid.rowCount === 0); // If grid is empty this.grid.rowCount == 0 if (this.isNoFocusCell() && !isEmpty){ var cellIdx = 0; var cell = this.grid.getCell(cellIdx); if (cell.hidden) { // if first cell isn't visible, use _colHeadFocusIdx // could also use a while loop to find first visible cell - not sure that is worth it cellIdx = this.isNavHeader() ? this._colHeadFocusIdx : 0; } this.setFocusIndex(0, cellIdx); } else if (this.cell && !isEmpty){ if (this.focusView && !this.focusView.rowNodes[this.rowIndex]){ // if rowNode for current index is undefined (likely as a result of a sort and because of #7304) // scroll to that row this.grid.scrollToRow(this.rowIndex); } this.focusGrid(); }else { didFocus = false; } this._colHeadNode = this._colHeadFocusIdx= null; return didFocus; }, focusHeader: function(){ var headerNodes = this._findHeaderCells(); var saveColHeadFocusIdx = this._colHeadFocusIdx; if (this._isHeaderHidden()){ // grid header is hidden, focus a cell this.findAndFocusGridCell(); } else if (!this._colHeadFocusIdx) { if (this.isNoFocusCell()) { this._colHeadFocusIdx = 0; } else { this._colHeadFocusIdx = this.cell.index; } } this._colHeadNode = headerNodes[this._colHeadFocusIdx]; while(this._colHeadNode && this._colHeadFocusIdx >=0 && this._colHeadFocusIdx < headerNodes.length && this._colHeadNode.style.display == "none"){ // skip over hidden column headers this._colHeadFocusIdx++; this._colHeadNode = headerNodes[this._colHeadFocusIdx]; } if(this._colHeadNode && this._colHeadNode.style.display != "none"){ // Column header cells know longer receive actual focus. So, for keyboard invocation of // contextMenu to work, the contextMenu must be bound to the grid.domNode rather than the viewsHeaderNode. // unbind the contextmenu from the viewsHeaderNode and to the grid when header cells are active. Reset // the binding back to the viewsHeaderNode when header cells are no longer acive (in blurHeader) #10483 if (this.headerMenu && this._contextMenuBindNode != this.grid.domNode){ this.headerMenu.unBindDomNode(this.grid.viewsHeaderNode); this.headerMenu.bindDomNode(this.grid.domNode); this._contextMenuBindNode = this.grid.domNode; } this._setActiveColHeader(this._colHeadNode, this._colHeadFocusIdx, saveColHeadFocusIdx); this._scrollHeader(this._colHeadFocusIdx); this._focusifyCellNode(false); }else { // all col head nodes are hidden - focus the grid this.findAndFocusGridCell(); } }, blurHeader: function(){ html.removeClass(this._colHeadNode, this.focusClass); html.removeAttr(this.grid.domNode,"aria-activedescendant"); // reset contextMenu onto viewsHeaderNode so right mouse on header will invoke (see focusHeader) if (this.headerMenu && this._contextMenuBindNode == this.grid.domNode) { var viewsHeader = this.grid.viewsHeaderNode; this.headerMenu.unBindDomNode(this.grid.domNode); this.headerMenu.bindDomNode(viewsHeader); this._contextMenuBindNode = viewsHeader; } }, doFocus: function(e){ // trap focus only for grid dom node if(e && e.target != e.currentTarget){ event.stop(e); return; } // don't change focus if clicking on scroller bar if(this._clickFocus){ return; } // do not focus for scrolling if grid is about to blur if(!this.tabbingOut){ this.focusHeader(); } this.tabbingOut = false; event.stop(e); }, doBlur: function(e){ event.stop(e); // FF2 }, doContextMenu: function(e){ //stop contextMenu event if no header Menu to prevent default/browser contextMenu if (!this.headerMenu){ event.stop(e); } }, doLastNodeFocus: function(e){ if (this.tabbingOut){ this._focusifyCellNode(false); }else if(this.grid.rowCount >0){ if (this.isNoFocusCell()){ this.setFocusIndex(0,0); } this._focusifyCellNode(true); }else { this.focusHeader(); } this.tabbingOut = false; event.stop(e); // FF2 }, doLastNodeBlur: function(e){ event.stop(e); // FF2 }, doColHeaderFocus: function(e){ this._setActiveColHeader(e.target,html.attr(e.target, "idx"),this._colHeadFocusIdx); this._scrollHeader(this.getHeaderIndex()); event.stop(e); }, doColHeaderBlur: function(e){ html.toggleClass(e.target, this.focusClass, false); }, _mouseDown: function(e){ // a flag indicating grid is being focused by clicking this._clickFocus = dojo.some(this.grid.views.views, function(v){ return v.scrollboxNode === e.target; }); }, _mouseUp: function(e){ this._clickFocus = false; } }); });