dojo.provide("dojox.grid.compat.VirtualGrid"); dojo.require("dojox.grid.compat._grid.lib"); dojo.require("dojox.grid.compat._grid.scroller"); dojo.require("dojox.grid.compat._grid.view"); dojo.require("dojox.grid.compat._grid.views"); dojo.require("dojox.grid.compat._grid.layout"); dojo.require("dojox.grid.compat._grid.rows"); dojo.require("dojox.grid.compat._grid.focus"); dojo.require("dojox.grid.compat._grid.selection"); dojo.require("dojox.grid.compat._grid.edit"); dojo.require("dojox.grid.compat._grid.rowbar"); dojo.require("dojox.grid.compat._grid.publicEvents"); dojo.declare('dojox.VirtualGrid', [ dijit._Widget, dijit._Templated ], { // summary: // A grid widget with virtual scrolling, cell editing, complex rows, // sorting, fixed columns, sizeable columns, etc. // // description: // VirtualGrid provides the full set of grid features without any // direct connection to a data store. // // The grid exposes a get function for the grid, or optionally // individual columns, to populate cell contents. // // The grid is rendered based on its structure, an object describing // column and cell layout. // // example: // A quick sample: // // define a get function // | function get(inRowIndex){ // called in cell context // | return [this.index, inRowIndex].join(', '); // | } // // define the grid structure: // | var structure = [ // array of view objects // | { cells: [// array of rows, a row is an array of cells // | [ // | { name: "Alpha", width: 6 }, // | { name: "Beta" }, // | { name: "Gamma", get: get }] // | ]} // | ]; // // |
templatePath: dojo.moduleUrl("dojox.grid.compat","resources/VirtualGrid.html"), // classTag: String // CSS class applied to the grid's domNode classTag: 'dojoxGrid', get: function(inRowIndex){ // summary: Default data getter. // description: // Provides data to display in a grid cell. Called in grid cell context. // So this.cell.index is the column index. // inRowIndex: Integer // Row for which to provide data // returns: // Data to display for a given grid cell. }, // settings // rowCount: Integer // Number of rows to display. rowCount: 5, // keepRows: Integer // Number of rows to keep in the rendering cache. keepRows: 75, // rowsPerPage: Integer // Number of rows to render at a time. rowsPerPage: 25, // autoWidth: Boolean // If autoWidth is true, grid width is automatically set to fit the data. autoWidth: false, // autoHeight: Boolean // If autoHeight is true, grid height is automatically set to fit the data. autoHeight: false, // autoRender: Boolean // If autoRender is true, grid will render itself after initialization. autoRender: true, // defaultHeight: String // default height of the grid, measured in any valid css unit. defaultHeight: '15em', // structure: Object|String // View layout defintion. Can be set to a layout object, or to the (string) name of a layout object. structure: '', // elasticView: Integer // Override defaults and make the indexed grid view elastic, thus filling available horizontal space. elasticView: -1, // singleClickEdit: boolean // Single-click starts editing. Default is double-click singleClickEdit: false, // Used to store the last two clicks, to ensure double-clicking occurs based on the intended row _click: null, // private sortInfo: 0, themeable: true, // initialization buildRendering: function(){ this.inherited(arguments); // reset get from blank function (needed for markup parsing) to null, if not changed if(this.get == dojox.VirtualGrid.prototype.get){ this.get = null; } if(!this.domNode.getAttribute('tabIndex')){ this.domNode.tabIndex = "0"; } this.createScroller(); this.createLayout(); this.createViews(); this.createManagers(); dojox.grid.initTextSizePoll(); this.connect(dojox.grid, "textSizeChanged", "textSizeChanged"); dojox.grid.funnelEvents(this.domNode, this, 'doKeyEvent', dojox.grid.keyEvents); this.connect(this, "onShow", "renderOnIdle"); }, postCreate: function(){ // replace stock styleChanged with one that triggers an update this.styleChanged = this._styleChanged; this.setStructure(this.structure); this._click = []; }, destroy: function(){ this.domNode.onReveal = null; this.domNode.onSizeChange = null; this.edit.destroy(); this.views.destroyViews(); this.inherited(arguments); }, styleChanged: function(){ this.setStyledClass(this.domNode, ''); }, _styleChanged: function(){ this.styleChanged(); this.update(); }, textSizeChanged: function(){ setTimeout(dojo.hitch(this, "_textSizeChanged"), 1); }, _textSizeChanged: function(){ if(this.domNode){ this.views.forEach(function(v){ v.content.update(); }); this.render(); } }, sizeChange: function(){ dojox.grid.jobs.job(this.id + 'SizeChange', 50, dojo.hitch(this, "update")); }, renderOnIdle: function() { setTimeout(dojo.hitch(this, "render"), 1); }, createManagers: function(){ // summary: // create grid managers for various tasks including rows, focus, selection, editing // row manager this.rows = new dojox.grid.rows(this); // focus manager this.focus = new dojox.grid.focus(this); // selection manager this.selection = new dojox.grid.selection(this); // edit manager this.edit = new dojox.grid.edit(this); }, createScroller: function(){ // summary: Creates a new virtual scroller this.scroller = new dojox.grid.scroller.columns(); this.scroller._pageIdPrefix = this.id + '-'; this.scroller.renderRow = dojo.hitch(this, "renderRow"); this.scroller.removeRow = dojo.hitch(this, "rowRemoved"); }, createLayout: function(){ // summary: Creates a new Grid layout this.layout = new dojox.grid.layout(this); }, // views createViews: function(){ this.views = new dojox.grid.views(this); this.views.createView = dojo.hitch(this, "createView"); }, createView: function(inClass){ if(dojo.isAIR){ var obj = window; var names = inClass.split('.'); for(var i=0;i 0){ }else if(this.domNode.clientHeight <= padBorder.h){ if(this.domNode.parentNode == document.body){ this.domNode.style.height = this.defaultHeight; }else{ this.fitTo = "parent"; } } // if we are given dimensions, size the grid's domNode to those dimensions if(this._sizeBox){ dojo.contentBox(this.domNode, this._sizeBox); }else if(this.fitTo == "parent"){ var h = dojo._getContentBox(this.domNode.parentNode).h; dojo.marginBox(this.domNode, { h: Math.max(0, h) }); } var h = dojo._getContentBox(this.domNode).h; if(h == 0 && !this.autoHeight){ // We need to hide the header, since the Grid is essentially hidden. this.viewsHeaderNode.style.display = "none"; }else{ // Otherwise, show the header and give it an appropriate height. this.viewsHeaderNode.style.display = "block"; } // NOTE: it is essential that width be applied before height // Header height can only be calculated properly after view widths have been set. // This is because flex column width is naturally 0 in Firefox. // Therefore prior to width sizing flex columns with spaces are maximally wrapped // and calculated to be too tall. this.adaptWidth(); this.adaptHeight(); // default row height (FIXME: use running average(?), remove magic #) this.scroller.defaultRowHeight = this.rows.getDefaultHeightPx() + 1; this.postresize(); }, adaptWidth: function() { // private: sets width and position for views and update grid width if necessary var w = this.autoWidth ? 0 : this.domNode.clientWidth || (this.domNode.offsetWidth - this._getPadBorder().w); var vw = this.views.arrange(1, w); this.views.onEach("adaptWidth"); if (this.autoWidth) this.domNode.style.width = vw + "px"; }, adaptHeight: function(){ // private: measures and normalizes header height, then sets view heights, and then updates scroller var vns = this.viewsHeaderNode.style, t = vns.display == "none" ? 0 : this.views.measureHeader(); vns.height = t + 'px'; // header heights are reset during measuring so must be normalized after measuring. this.views.normalizeHeaderNodeHeight(); // content extent var h = (this.autoHeight ? -1 : Math.max(this.domNode.clientHeight - t, 0) || 0); this.views.onEach('setSize', [0, h]); this.views.onEach('adaptHeight'); this.scroller.windowHeight = h; }, // render render: function(){ // summary: // Render the grid, headers, and views. Edit and scrolling states are reset. To retain edit and // scrolling states, see Update. if(!this.domNode){return;} if(!this.hasLayout()) { this.scroller.init(0, this.keepRows, this.rowsPerPage); return; } // this.update = this.defaultUpdate; this.scroller.init(this.rowCount, this.keepRows, this.rowsPerPage); this.prerender(); this.setScrollTop(0); this.postrender(); }, prerender: function(){ // if autoHeight, make sure scroller knows not to virtualize; everything must be rendered. this.keepRows = this.autoHeight ? 0 : this.constructor.prototype.keepRows; this.scroller.setKeepInfo(this.keepRows); this.views.render(); this._resize(); }, postrender: function(){ this.postresize(); this.focus.initFocusView(); // make rows unselectable dojo.setSelectable(this.domNode, false); }, postresize: function(){ // views are position absolute, so they do not inflate the parent if(this.autoHeight){ this.viewsNode.style.height = this.views.measureContent() + 'px'; } }, renderRow: function(inRowIndex, inNodes){ // summary: private, used internally to render rows this.views.renderRow(inRowIndex, inNodes); }, rowRemoved: function(inRowIndex){ // summary: private, used internally to remove rows this.views.rowRemoved(inRowIndex); }, invalidated: null, updating: false, beginUpdate: function(){ // summary: // Use to make multiple changes to rows while queueing row updating. if(this.invalidated == null){ this.invalidated = {rows: [], count: 1, all: false, rowCount: undefined}; }else{ this.invalidated.count++; } this.updating = true; }, endUpdate: function(){ // summary: // Use after calling beginUpdate to render any changes made to rows. var i = this.invalidated; if(--i.count === 0){ this.updating = false; if(i.rows.length > 0){ for(var r in i.rows){ this.updateRow(Number(r)); } this.invalidated.rows = []; } if(i.rowCount != undefined){ this.updateRowCount(i.rowCount); i.rowCount = undefined; } if(i.all){ this.update(); i.all = false; } } }, // update defaultUpdate: function(){ // note: initial update calls render and subsequently this function. if(!this.domNode){return;} if(this.updating){ this.invalidated.all = true; return; } //this.edit.saveState(inRowIndex); this.prerender(); this.scroller.invalidateNodes(); this.setScrollTop(this.scrollTop); this.postrender(); //this.edit.restoreState(inRowIndex); }, update: function(){ // summary: // Update the grid, retaining edit and scrolling states. this.render(); }, updateRow: function(inRowIndex){ // summary: // Render a single row. // inRowIndex: Integer // Index of the row to render inRowIndex = Number(inRowIndex); if(this.updating){ this.invalidated.rows[inRowIndex]=true; }else{ this.views.updateRow(inRowIndex, this.rows.getHeight(inRowIndex)); this.scroller.rowHeightChanged(inRowIndex); } }, updateRowCount: function(inRowCount){ //summary: // Change the number of rows. // inRowCount: int // Number of rows in the grid. if(this.updating){ this.invalidated.rowCount = inRowCount; }else{ this.rowCount = inRowCount; if(this.layout.cells.length){ this.scroller.updateRowCount(inRowCount); this.setScrollTop(this.scrollTop); } this._resize(); } }, updateRowStyles: function(inRowIndex){ // summary: // Update the styles for a row after it's state has changed. this.views.updateRowStyles(inRowIndex); }, rowHeightChanged: function(inRowIndex){ // summary: // Update grid when the height of a row has changed. Row height is handled automatically as rows // are rendered. Use this function only to update a row's height outside the normal rendering process. // inRowIndex: Integer // index of the row that has changed height this.views.renormalizeRow(inRowIndex); this.scroller.rowHeightChanged(inRowIndex); }, // fastScroll: Boolean // flag modifies vertical scrolling behavior. Defaults to true but set to false for slower // scroll performance but more immediate scrolling feedback fastScroll: true, delayScroll: false, // scrollRedrawThreshold: int // pixel distance a user must scroll vertically to trigger grid scrolling. scrollRedrawThreshold: (dojo.isIE ? 100 : 50), // scroll methods scrollTo: function(inTop){ // summary: // Vertically scroll the grid to a given pixel position // inTop: Integer // vertical position of the grid in pixels if(!this.fastScroll){ this.setScrollTop(inTop); return; } var delta = Math.abs(this.lastScrollTop - inTop); this.lastScrollTop = inTop; if(delta > this.scrollRedrawThreshold || this.delayScroll){ this.delayScroll = true; this.scrollTop = inTop; this.views.setScrollTop(inTop); dojox.grid.jobs.job('dojoxGrid-scroll', 200, dojo.hitch(this, "finishScrollJob")); }else{ this.setScrollTop(inTop); } }, finishScrollJob: function(){ this.delayScroll = false; this.setScrollTop(this.scrollTop); }, setScrollTop: function(inTop){ this.scrollTop = this.views.setScrollTop(inTop); this.scroller.scroll(this.scrollTop); }, scrollToRow: function(inRowIndex){ // summary: // Scroll the grid to a specific row. // inRowIndex: Integer // grid row index this.setScrollTop(this.scroller.findScrollTop(inRowIndex) + 1); }, // styling (private, used internally to style individual parts of a row) styleRowNode: function(inRowIndex, inRowNode){ if(inRowNode){ this.rows.styleRowNode(inRowIndex, inRowNode); } }, // cells getCell: function(inIndex){ // summary: // Retrieves the cell object for a given grid column. // inIndex: Integer // Grid column index of cell to retrieve // returns: // a grid cell return this.layout.cells[inIndex]; }, setCellWidth: function(inIndex, inUnitWidth) { this.getCell(inIndex).unitWidth = inUnitWidth; }, getCellName: function(inCell){ // summary: Returns the cell name of a passed cell return "Cell " + inCell.index; // String }, // sorting canSort: function(inSortInfo){ // summary: // Determines if the grid can be sorted // inSortInfo: Integer // Sort information, 1-based index of column on which to sort, positive for an ascending sort // and negative for a descending sort // returns: Boolean // True if grid can be sorted on the given column in the given direction }, sort: function(){ }, getSortAsc: function(inSortInfo){ // summary: // Returns true if grid is sorted in an ascending direction. inSortInfo = inSortInfo == undefined ? this.sortInfo : inSortInfo; return Boolean(inSortInfo > 0); // Boolean }, getSortIndex: function(inSortInfo){ // summary: // Returns the index of the column on which the grid is sorted inSortInfo = inSortInfo == undefined ? this.sortInfo : inSortInfo; return Math.abs(inSortInfo) - 1; // Integer }, setSortIndex: function(inIndex, inAsc){ // summary: // Sort the grid on a column in a specified direction // inIndex: Integer // Column index on which to sort. // inAsc: Boolean // If true, sort the grid in ascending order, otherwise in descending order var si = inIndex +1; if(inAsc != undefined){ si *= (inAsc ? 1 : -1); } else if(this.getSortIndex() == inIndex){ si = -this.sortInfo; } this.setSortInfo(si); }, setSortInfo: function(inSortInfo){ if(this.canSort(inSortInfo)){ this.sortInfo = inSortInfo; this.sort(); this.update(); } }, // DOM event handler doKeyEvent: function(e){ e.dispatch = 'do' + e.type; this.onKeyEvent(e); }, // event dispatch //: protected _dispatch: function(m, e){ if(m in this){ return this[m](e); } }, dispatchKeyEvent: function(e){ this._dispatch(e.dispatch, e); }, dispatchContentEvent: function(e){ this.edit.dispatchEvent(e) || e.sourceView.dispatchContentEvent(e) || this._dispatch(e.dispatch, e); }, dispatchHeaderEvent: function(e){ e.sourceView.dispatchHeaderEvent(e) || this._dispatch('doheader' + e.type, e); }, dokeydown: function(e){ this.onKeyDown(e); }, doclick: function(e){ if(e.cellNode){ this.onCellClick(e); }else{ this.onRowClick(e); } }, dodblclick: function(e){ if(e.cellNode){ this.onCellDblClick(e); }else{ this.onRowDblClick(e); } }, docontextmenu: function(e){ if(e.cellNode){ this.onCellContextMenu(e); }else{ this.onRowContextMenu(e); } }, doheaderclick: function(e){ if(e.cellNode){ this.onHeaderCellClick(e); }else{ this.onHeaderClick(e); } }, doheaderdblclick: function(e){ if(e.cellNode){ this.onHeaderCellDblClick(e); }else{ this.onHeaderDblClick(e); } }, doheadercontextmenu: function(e){ if(e.cellNode){ this.onHeaderCellContextMenu(e); }else{ this.onHeaderContextMenu(e); } }, // override to modify editing process doStartEdit: function(inCell, inRowIndex){ this.onStartEdit(inCell, inRowIndex); }, doApplyCellEdit: function(inValue, inRowIndex, inFieldIndex){ this.onApplyCellEdit(inValue, inRowIndex, inFieldIndex); }, doCancelEdit: function(inRowIndex){ this.onCancelEdit(inRowIndex); }, doApplyEdit: function(inRowIndex){ this.onApplyEdit(inRowIndex); }, // row editing addRow: function(){ // summary: // Add a row to the grid. this.updateRowCount(this.rowCount+1); }, removeSelectedRows: function(){ // summary: // Remove the selected rows from the grid. this.updateRowCount(Math.max(0, this.rowCount - this.selection.getSelected().length)); this.selection.clear(); } }); dojo.mixin(dojox.VirtualGrid.prototype, dojox.grid.publicEvents);