1 if(!dojo._hasResource["dojox.grid.VirtualGrid"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2 dojo._hasResource["dojox.grid.VirtualGrid"] = true;
3 dojo.provide("dojox.grid.VirtualGrid");
5 dojo.require("dojox.grid._grid.lib");
6 dojo.require("dojox.grid._grid.scroller");
7 dojo.require("dojox.grid._grid.view");
8 dojo.require("dojox.grid._grid.views");
9 dojo.require("dojox.grid._grid.layout");
10 dojo.require("dojox.grid._grid.rows");
11 dojo.require("dojox.grid._grid.focus");
12 dojo.require("dojox.grid._grid.selection");
13 dojo.require("dojox.grid._grid.edit");
14 dojo.require("dojox.grid._grid.rowbar");
15 dojo.require("dojox.grid._grid.publicEvents");
17 dojo.declare('dojox.VirtualGrid',
18 [ dijit._Widget, dijit._Templated ],
21 // A grid widget with virtual scrolling, cell editing, complex rows,
22 // sorting, fixed columns, sizeable columns, etc.
25 // VirtualGrid provides the full set of grid features without any
26 // direct connection to a data store.
28 // The grid exposes a get function for the grid, or optionally
29 // individual columns, to populate cell contents.
31 // The grid is rendered based on its structure, an object describing
32 // column and cell layout.
37 // define a get function
38 // | function get(inRowIndex){ // called in cell context
39 // | return [this.index, inRowIndex].join(', ');
42 // define the grid structure:
43 // | var structure = [ // array of view objects
44 // | { cells: [// array of rows, a row is an array of cells
46 // | { name: "Alpha", width: 6 },
47 // | { name: "Beta" },
48 // | { name: "Gamma", get: get }]
53 // | rowCount="100" get="get"
54 // | structure="structure"
55 // | dojoType="dojox.VirtualGrid"></div>
57 templateString:"<div class=\"dojoxGrid\" hidefocus=\"hidefocus\" role=\"wairole:grid\">\n\t<div class=\"dojoxGrid-master-header\" dojoAttachPoint=\"viewsHeaderNode\"></div>\n\t<div class=\"dojoxGrid-master-view\" dojoAttachPoint=\"viewsNode\"></div>\n\t<span dojoAttachPoint=\"lastFocusNode\" tabindex=\"0\"></span>\n</div>\n",
60 // CSS class applied to the grid's domNode
61 classTag: 'dojoxGrid',
63 get: function(inRowIndex){
64 // summary: Default data getter.
66 // Provides data to display in a grid cell. Called in grid cell context.
67 // So this.cell.index is the column index.
68 // inRowIndex: Integer
69 // Row for which to provide data
71 // Data to display for a given grid cell.
76 // Number of rows to display.
80 // Number of rows to keep in the rendering cache.
83 // rowsPerPage: Integer
84 // Number of rows to render at a time.
88 // If autoWidth is true, grid width is automatically set to fit the data.
91 // autoHeight: Boolean
92 // If autoHeight is true, grid height is automatically set to fit the data.
95 // autoRender: Boolean
96 // If autoRender is true, grid will render itself after initialization.
99 // defaultHeight: String
100 // default height of the grid, measured in any valid css unit.
101 defaultHeight: '15em',
103 // structure: Object|String
104 // View layout defintion. Can be set to a layout object, or to the (string) name of a layout object.
107 // elasticView: Integer
108 // Override defaults and make the indexed grid view elastic, thus filling available horizontal space.
111 // singleClickEdit: boolean
112 // Single-click starts editing. Default is double-click
113 singleClickEdit: false,
115 // Used to store the last two clicks, to ensure double-clicking occurs based on the intended row
123 buildRendering: function(){
124 this.inherited(arguments);
125 // reset get from blank function (needed for markup parsing) to null, if not changed
126 if(this.get == dojox.VirtualGrid.prototype.get){
129 if(!this.domNode.getAttribute('tabIndex')){
130 this.domNode.tabIndex = "0";
132 this.createScroller();
135 this.createManagers();
136 dojox.grid.initTextSizePoll();
137 this.connect(dojox.grid, "textSizeChanged", "textSizeChanged");
138 dojox.grid.funnelEvents(this.domNode, this, 'doKeyEvent', dojox.grid.keyEvents);
139 this.connect(this, "onShow", "renderOnIdle");
141 postCreate: function(){
142 // replace stock styleChanged with one that triggers an update
143 this.styleChanged = this._styleChanged;
144 this.setStructure(this.structure);
149 this.domNode.onReveal = null;
150 this.domNode.onSizeChange = null;
152 this.views.destroyViews();
153 this.inherited(arguments);
156 styleChanged: function(){
157 this.setStyledClass(this.domNode, '');
160 _styleChanged: function(){
165 textSizeChanged: function(){
166 setTimeout(dojo.hitch(this, "_textSizeChanged"), 1);
169 _textSizeChanged: function(){
171 this.views.forEach(function(v){
178 sizeChange: function(){
179 dojox.grid.jobs.job(this.id + 'SizeChange', 50, dojo.hitch(this, "update"));
182 renderOnIdle: function() {
183 setTimeout(dojo.hitch(this, "render"), 1);
186 createManagers: function(){
188 // create grid managers for various tasks including rows, focus, selection, editing
191 this.rows = new dojox.grid.rows(this);
193 this.focus = new dojox.grid.focus(this);
195 this.selection = new dojox.grid.selection(this);
197 this.edit = new dojox.grid.edit(this);
200 createScroller: function(){
201 // summary: Creates a new virtual scroller
202 this.scroller = new dojox.grid.scroller.columns();
203 this.scroller._pageIdPrefix = this.id + '-';
204 this.scroller.renderRow = dojo.hitch(this, "renderRow");
205 this.scroller.removeRow = dojo.hitch(this, "rowRemoved");
208 createLayout: function(){
209 // summary: Creates a new Grid layout
210 this.layout = new dojox.grid.layout(this);
214 createViews: function(){
215 this.views = new dojox.grid.views(this);
216 this.views.createView = dojo.hitch(this, "createView");
219 createView: function(inClass){
222 var names = inClass.split('.');
223 for(var i=0;i<names.length;i++){
224 if(typeof obj[names[i]]=='undefined'){
225 var undefstring = names[0];
226 for(var j=1;j<=i;j++){
227 undefstring+="."+names[j];
229 throw new Error(undefstring+" is undefined");
235 var c = eval(inClass);
237 var view = new c({ grid: this });
238 this.viewsNode.appendChild(view.domNode);
239 this.viewsHeaderNode.appendChild(view.headerNode);
240 this.views.addView(view);
244 buildViews: function(){
245 for(var i=0, vs; (vs=this.layout.structure[i]); i++){
246 this.createView(vs.type || dojox._scopeName + ".GridView").setStructure(vs);
248 this.scroller.setContentNodes(this.views.getContentNodes());
251 setStructure: function(inStructure){
253 // Install a new structure and rebuild the grid.
254 // inStructure: Object
255 // Structure object defines the grid layout and provides various
256 // options for grid views and columns
258 // A grid structure is an array of view objects. A view object can
259 // specify a view type (view class), width, noscroll (boolean flag
260 // for view scrolling), and cells. Cells is an array of objects
261 // corresponding to each grid column. The view cells object is an
262 // array of subrows comprising a single row. Each subrow is an
263 // array of column objects. A column object can have a name,
264 // width, value (default), get function to provide data, styles,
265 // and span attributes (rowSpan, colSpan).
267 this.views.destroyViews();
268 this.structure = inStructure;
269 if((this.structure)&&(dojo.isString(this.structure))){
270 this.structure=dojox.grid.getProp(this.structure);
273 this.structure=window["layout"];
278 this.layout.setStructure(this.structure);
279 this._structureChanged();
282 _structureChanged: function() {
289 hasLayout: function() {
290 return this.layout.cells.length;
294 resize: function(sizeBox){
296 // Update the grid's rendering dimensions and resize it
298 // {w: int, h: int, l: int, t: int}
300 // FIXME: If grid is not sized explicitly, sometimes bogus scrollbars
301 // can appear in our container, which may require an extra call to 'resize'
303 this._sizeBox = sizeBox;
308 _getPadBorder: function() {
309 this._padBorder = this._padBorder || dojo._getPadBorderExtents(this.domNode);
310 return this._padBorder;
314 // if we have set up everything except the DOM, we cannot resize
315 if(!this.domNode.parentNode || this.domNode.parentNode.nodeType != 1 || !this.hasLayout()){
318 // useful measurement
319 var padBorder = this._getPadBorder();
322 this.domNode.style.height = 'auto';
323 this.viewsNode.style.height = '';
324 }else if(this.flex > 0){
325 }else if(this.domNode.clientHeight <= padBorder.h){
326 if(this.domNode.parentNode == document.body){
327 this.domNode.style.height = this.defaultHeight;
329 this.fitTo = "parent";
332 // if we are given dimensions, size the grid's domNode to those dimensions
334 dojo.contentBox(this.domNode, this._sizeBox);
335 }else if(this.fitTo == "parent"){
336 var h = dojo._getContentBox(this.domNode.parentNode).h;
337 dojo.marginBox(this.domNode, { h: Math.max(0, h) });
340 var h = dojo._getContentBox(this.domNode).h;
341 if(h == 0 && !this.autoHeight){
342 // We need to hide the header, since the Grid is essentially hidden.
343 this.viewsHeaderNode.style.display = "none";
345 // Otherwise, show the header and give it an appropriate height.
346 this.viewsHeaderNode.style.display = "block";
349 // NOTE: it is essential that width be applied before height
350 // Header height can only be calculated properly after view widths have been set.
351 // This is because flex column width is naturally 0 in Firefox.
352 // Therefore prior to width sizing flex columns with spaces are maximally wrapped
353 // and calculated to be too tall.
357 // default row height (FIXME: use running average(?), remove magic #)
358 this.scroller.defaultRowHeight = this.rows.getDefaultHeightPx() + 1;
362 adaptWidth: function() {
363 // private: sets width and position for views and update grid width if necessary
365 w = this.autoWidth ? 0 : this.domNode.clientWidth || (this.domNode.offsetWidth - this._getPadBorder().w);
366 vw = this.views.arrange(1, w);
367 this.views.onEach("adaptWidth");
369 this.domNode.style.width = vw + "px";
372 adaptHeight: function(){
373 // private: measures and normalizes header height, then sets view heights, and then updates scroller
374 var vns = this.viewsHeaderNode.style, t = vns.display == "none" ? 0 : this.views.measureHeader();
375 vns.height = t + 'px';
376 // header heights are reset during measuring so must be normalized after measuring.
377 this.views.normalizeHeaderNodeHeight();
379 var h = (this.autoHeight ? -1 : Math.max(this.domNode.clientHeight - t, 0) || 0);
380 this.views.onEach('setSize', [0, h]);
381 this.views.onEach('adaptHeight');
382 this.scroller.windowHeight = h;
388 // Render the grid, headers, and views. Edit and scrolling states are reset. To retain edit and
389 // scrolling states, see Update.
391 if(!this.domNode){return;}
393 if(!this.hasLayout()) {
394 this.scroller.init(0, this.keepRows, this.rowsPerPage);
398 this.update = this.defaultUpdate;
399 this.scroller.init(this.rowCount, this.keepRows, this.rowsPerPage);
401 this.setScrollTop(0);
405 prerender: function(){
406 // if autoHeight, make sure scroller knows not to virtualize; everything must be rendered.
407 this.keepRows = this.autoHeight ? 0 : this.constructor.prototype.keepRows;
408 this.scroller.setKeepInfo(this.keepRows);
413 postrender: function(){
415 this.focus.initFocusView();
416 // make rows unselectable
417 dojo.setSelectable(this.domNode, false);
420 postresize: function(){
421 // views are position absolute, so they do not inflate the parent
423 this.viewsNode.style.height = this.views.measureContent() + 'px';
427 renderRow: function(inRowIndex, inNodes){
428 // summary: private, used internally to render rows
429 this.views.renderRow(inRowIndex, inNodes);
432 rowRemoved: function(inRowIndex){
433 // summary: private, used internally to remove rows
434 this.views.rowRemoved(inRowIndex);
441 beginUpdate: function(){
443 // Use to make multiple changes to rows while queueing row updating.
444 // NOTE: not currently supporting nested begin/endUpdate calls
445 this.invalidated = [];
446 this.updating = true;
449 endUpdate: function(){
451 // Use after calling beginUpdate to render any changes made to rows.
452 this.updating = false;
453 var i = this.invalidated;
456 }else if(i.rowCount != undefined){
457 this.updateRowCount(i.rowCount);
460 this.updateRow(Number(r));
463 this.invalidated = null;
467 defaultUpdate: function(){
468 // note: initial update calls render and subsequently this function.
469 if(!this.domNode){return;}
471 this.invalidated.all = true;
474 //this.edit.saveState(inRowIndex);
476 this.scroller.invalidateNodes();
477 this.setScrollTop(this.scrollTop);
479 //this.edit.restoreState(inRowIndex);
484 // Update the grid, retaining edit and scrolling states.
488 updateRow: function(inRowIndex){
490 // Render a single row.
491 // inRowIndex: Integer
492 // Index of the row to render
493 inRowIndex = Number(inRowIndex);
495 this.invalidated[inRowIndex]=true;
497 this.views.updateRow(inRowIndex, this.rows.getHeight(inRowIndex));
498 this.scroller.rowHeightChanged(inRowIndex);
502 updateRowCount: function(inRowCount){
504 // Change the number of rows.
506 // Number of rows in the grid.
508 this.invalidated.rowCount = inRowCount;
510 this.rowCount = inRowCount;
511 if(this.layout.cells.length){
512 this.scroller.updateRowCount(inRowCount);
513 this.setScrollTop(this.scrollTop);
519 updateRowStyles: function(inRowIndex){
521 // Update the styles for a row after it's state has changed.
522 this.views.updateRowStyles(inRowIndex);
525 rowHeightChanged: function(inRowIndex){
527 // Update grid when the height of a row has changed. Row height is handled automatically as rows
528 // are rendered. Use this function only to update a row's height outside the normal rendering process.
529 // inRowIndex: Integer
530 // index of the row that has changed height
532 this.views.renormalizeRow(inRowIndex);
533 this.scroller.rowHeightChanged(inRowIndex);
536 // fastScroll: Boolean
537 // flag modifies vertical scrolling behavior. Defaults to true but set to false for slower
538 // scroll performance but more immediate scrolling feedback
543 // scrollRedrawThreshold: int
544 // pixel distance a user must scroll vertically to trigger grid scrolling.
545 scrollRedrawThreshold: (dojo.isIE ? 100 : 50),
548 scrollTo: function(inTop){
550 // Vertically scroll the grid to a given pixel position
552 // vertical position of the grid in pixels
553 if(!this.fastScroll){
554 this.setScrollTop(inTop);
557 var delta = Math.abs(this.lastScrollTop - inTop);
558 this.lastScrollTop = inTop;
559 if(delta > this.scrollRedrawThreshold || this.delayScroll){
560 this.delayScroll = true;
561 this.scrollTop = inTop;
562 this.views.setScrollTop(inTop);
563 dojox.grid.jobs.job('dojoxGrid-scroll', 200, dojo.hitch(this, "finishScrollJob"));
565 this.setScrollTop(inTop);
569 finishScrollJob: function(){
570 this.delayScroll = false;
571 this.setScrollTop(this.scrollTop);
574 setScrollTop: function(inTop){
575 this.scrollTop = this.views.setScrollTop(inTop);
576 this.scroller.scroll(this.scrollTop);
579 scrollToRow: function(inRowIndex){
581 // Scroll the grid to a specific row.
582 // inRowIndex: Integer
584 this.setScrollTop(this.scroller.findScrollTop(inRowIndex) + 1);
587 // styling (private, used internally to style individual parts of a row)
588 styleRowNode: function(inRowIndex, inRowNode){
590 this.rows.styleRowNode(inRowIndex, inRowNode);
595 getCell: function(inIndex){
597 // Retrieves the cell object for a given grid column.
599 // Grid column index of cell to retrieve
602 return this.layout.cells[inIndex];
605 setCellWidth: function(inIndex, inUnitWidth) {
606 this.getCell(inIndex).unitWidth = inUnitWidth;
609 getCellName: function(inCell){
610 // summary: Returns the cell name of a passed cell
611 return "Cell " + inCell.index; // String
615 canSort: function(inSortInfo){
617 // Determines if the grid can be sorted
618 // inSortInfo: Integer
619 // Sort information, 1-based index of column on which to sort, positive for an ascending sort
620 // and negative for a descending sort
622 // True if grid can be sorted on the given column in the given direction
628 getSortAsc: function(inSortInfo){
630 // Returns true if grid is sorted in an ascending direction.
631 inSortInfo = inSortInfo == undefined ? this.sortInfo : inSortInfo;
632 return Boolean(inSortInfo > 0); // Boolean
635 getSortIndex: function(inSortInfo){
637 // Returns the index of the column on which the grid is sorted
638 inSortInfo = inSortInfo == undefined ? this.sortInfo : inSortInfo;
639 return Math.abs(inSortInfo) - 1; // Integer
642 setSortIndex: function(inIndex, inAsc){
644 // Sort the grid on a column in a specified direction
646 // Column index on which to sort.
648 // If true, sort the grid in ascending order, otherwise in descending order
650 if(inAsc != undefined){
651 si *= (inAsc ? 1 : -1);
652 } else if(this.getSortIndex() == inIndex){
655 this.setSortInfo(si);
658 setSortInfo: function(inSortInfo){
659 if(this.canSort(inSortInfo)){
660 this.sortInfo = inSortInfo;
667 doKeyEvent: function(e){
668 e.dispatch = 'do' + e.type;
674 _dispatch: function(m, e){
680 dispatchKeyEvent: function(e){
681 this._dispatch(e.dispatch, e);
684 dispatchContentEvent: function(e){
685 this.edit.dispatchEvent(e) || e.sourceView.dispatchContentEvent(e) || this._dispatch(e.dispatch, e);
688 dispatchHeaderEvent: function(e){
689 e.sourceView.dispatchHeaderEvent(e) || this._dispatch('doheader' + e.type, e);
692 dokeydown: function(e){
696 doclick: function(e){
704 dodblclick: function(e){
706 this.onCellDblClick(e);
708 this.onRowDblClick(e);
712 docontextmenu: function(e){
714 this.onCellContextMenu(e);
716 this.onRowContextMenu(e);
720 doheaderclick: function(e){
722 this.onHeaderCellClick(e);
724 this.onHeaderClick(e);
728 doheaderdblclick: function(e){
730 this.onHeaderCellDblClick(e);
732 this.onHeaderDblClick(e);
736 doheadercontextmenu: function(e){
738 this.onHeaderCellContextMenu(e);
740 this.onHeaderContextMenu(e);
744 // override to modify editing process
745 doStartEdit: function(inCell, inRowIndex){
746 this.onStartEdit(inCell, inRowIndex);
749 doApplyCellEdit: function(inValue, inRowIndex, inFieldIndex){
750 this.onApplyCellEdit(inValue, inRowIndex, inFieldIndex);
753 doCancelEdit: function(inRowIndex){
754 this.onCancelEdit(inRowIndex);
757 doApplyEdit: function(inRowIndex){
758 this.onApplyEdit(inRowIndex);
764 // Add a row to the grid.
765 this.updateRowCount(this.rowCount+1);
768 removeSelectedRows: function(){
770 // Remove the selected rows from the grid.
771 this.updateRowCount(Math.max(0, this.rowCount - this.selection.getSelected().length));
772 this.selection.clear();
777 dojo.mixin(dojox.VirtualGrid.prototype, dojox.grid.publicEvents);