source: Dev/trunk/src/client/dojox/grid/_Grid.js @ 529

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

Added Dojo 1.9.3 release.

File size: 40.9 KB
Line 
1define([
2        "dojo/_base/kernel",
3        "../main",
4        "dojo/_base/declare",
5        "./_Events",
6        "./_Scroller",
7        "./_Layout",
8        "./_View",
9        "./_ViewManager",
10        "./_RowManager",
11        "./_FocusManager",
12        "./_EditManager",
13        "./Selection",
14        "./_RowSelector",
15        "./util",
16        "dijit/_Widget",
17        "dijit/_TemplatedMixin",
18        "dijit/CheckedMenuItem",
19        "dojo/text!./resources/_Grid.html",
20        "dojo/string",
21        "dojo/_base/array",
22        "dojo/_base/lang",
23        "dojo/_base/sniff",
24        "dojox/html/metrics",
25        "dojo/_base/html",
26        "dojo/query",
27        "dojo/dnd/common",
28        "dojo/i18n!dijit/nls/loading"
29], function(dojo, dojox, declare, _Events, _Scroller, _Layout, _View, _ViewManager,
30        _RowManager, _FocusManager, _EditManager, Selection, _RowSelector, util, _Widget,
31         _TemplatedMixin, CheckedMenuItem, template, string, array, lang, has, metrics, html, query){
32
33        // NOTE: this is for backwards compatibility with Dojo 1.3
34        if(!dojo.isCopyKey){
35                dojo.isCopyKey = dojo.dnd.getCopyKeyState;
36        }
37        /*=====
38        dojox.grid.__CellDef = {
39                // name: String?
40                //              The text to use in the header of the grid for this cell.
41                // get: Function?
42                //              function(rowIndex){} rowIndex is of type Integer.  This
43                //              function will be called when a cell     requests data.  Returns the
44                //              unformatted data for the cell.
45                // value: String?
46                //              If "get" is not specified, this is used as the data for the cell.
47                // defaultValue: String?
48                //              If "get" and "value" aren't specified or if "get" returns an undefined
49                //              value, this is used as the data for the cell.  "formatter" is not run
50                //              on this if "get" returns an undefined value.
51                // formatter: Function?
52                //              function(data, rowIndex){} data is of type anything, rowIndex
53                //              is of type Integer.  This function will be called after the cell
54                //              has its data but before it passes it back to the grid to render.
55                //              Returns the formatted version of the cell's data.
56                // type: dojox.grid.cells._Base|Function?
57                //              TODO
58                // editable: Boolean?
59                //              Whether this cell should be editable or not.
60                // hidden: Boolean?
61                //              If true, the cell will not be displayed.
62                // noresize: Boolean?
63                //              If true, the cell will not be able to be resized.
64                // width: Integer|String?
65                //              A CSS size.  If it's an Integer, the width will be in em's.
66                // colSpan: Integer?
67                //              How many columns to span this cell.  Will not work in the first
68                //              sub-row of cells.
69                // rowSpan: Integer?
70                //              How many sub-rows to span this cell.
71                // styles: String?
72                //              A string of styles to apply to both the header cell and main
73                //              grid cells.  Must end in a ';'.
74                // headerStyles: String?
75                //              A string of styles to apply to just the header cell.  Must end
76                //              in a ';'
77                // cellStyles: String?
78                //              A string of styles to apply to just the main grid cells.  Must
79                //              end in a ';'
80                // classes: String?
81                //              A space separated list of classes to apply to both the header
82                //              cell and the main grid cells.
83                // headerClasses: String?
84                //              A space separated list of classes to apply to just the header
85                //              cell.
86                // cellClasses: String?
87                //              A space separated list of classes to apply to just the main
88                //              grid cells.
89                // attrs: String?
90                //              A space separated string of attribute='value' pairs to add to
91                //              the header cell element and main grid cell elements.
92        };
93        =====*/
94
95        /*=====
96        dojox.grid.__ViewDef = {
97                // noscroll: Boolean?
98                //              If true, no scrollbars will be rendered without scrollbars.
99                // width: Integer|String?
100                //              A CSS size.  If it's an Integer, the width will be in em's. If
101                //              "noscroll" is true, this value is ignored.
102                // cells: dojox.grid.__CellDef[]|Array[dojox.grid.__CellDef[]]?
103                //              The structure of the cells within this grid.
104                // type: String?
105                //              A string containing the constructor of a subclass of
106                //              dojox.grid._View.  If this is not specified, dojox.grid._View
107                //              is used.
108                // defaultCell: dojox.grid.__CellDef?
109                //              A cell definition with default values for all cells in this view.  If
110                //              a property is defined in a cell definition in the "cells" array and
111                //              this property, the cell definition's property will override this
112                //              property's property.
113                // onBeforeRow: Function?
114                //              function(rowIndex, cells){} rowIndex is of type Integer, cells
115                //              is of type Array[dojox.grid.__CellDef[]].  This function is called
116                //              before each row of data is rendered.  Before the header is
117                //              rendered, rowIndex will be -1.  "cells" is a reference to the
118                //              internal structure of this view's cells so any changes you make to
119                //              it will persist between calls.
120                // onAfterRow: Function?
121                //              function(rowIndex, cells, rowNode){} rowIndex is of type Integer, cells
122                //              is of type Array[dojox.grid.__CellDef[]], rowNode is of type DOMNode.
123                //              This function is called after each row of data is rendered.  After the
124                //              header is rendered, rowIndex will be -1.  "cells" is a reference to the
125                //              internal structure of this view's cells so any changes you make to
126                //              it will persist between calls.
127        };
128        =====*/
129
130        var _Grid = declare('dojox.grid._Grid',
131                [ _Widget, _TemplatedMixin, _Events ],
132                {
133                // summary:
134                //              A grid widget with virtual scrolling, cell editing, complex rows,
135                //              sorting, fixed columns, sizeable columns, etc.
136                //
137                // description:
138                //              _Grid provides the full set of grid features without any
139                //              direct connection to a data store.
140                //
141                //              The grid exposes a get function for the grid, or optionally
142                //              individual columns, to populate cell contents.
143                //
144                //              The grid is rendered based on its structure, an object describing
145                //              column and cell layout.
146                //
147                // example:
148                //              A quick sample:
149                //
150                //              define a get function
151                //      |       function get(inRowIndex){ // called in cell context
152                //      |               return [this.index, inRowIndex].join(', ');
153                //      |       }
154                //
155                //              define the grid structure:
156                //      |       var structure = [ // array of view objects
157                //      |               { cells: [// array of rows, a row is an array of cells
158                //      |                       [
159                //      |                               { name: "Alpha", width: 6 },
160                //      |                               { name: "Beta" },
161                //      |                               { name: "Gamma", get: get }]
162                //      |               ]}
163                //      |       ];
164                //
165                //      |       <div id="grid"
166                //      |               rowCount="100" get="get"
167                //      |               structure="structure"
168                //      |               dojoType="dojox.grid._Grid"></div>
169
170                templateString: template,
171
172                // classTag: String
173                //              CSS class applied to the grid's domNode
174                classTag: 'dojoxGrid',
175
176                // settings
177                // rowCount: Integer
178                //              Number of rows to display.
179                rowCount: 5,
180
181                // keepRows: Integer
182                //              Number of rows to keep in the rendering cache.
183                keepRows: 75,
184
185                // rowsPerPage: Integer
186                //              Number of rows to render at a time.
187                rowsPerPage: 25,
188
189                // autoWidth: Boolean
190                //              If autoWidth is true, grid width is automatically set to fit the data.
191                autoWidth: false,
192               
193                // initialWidth: String
194                //              A css string to use to set our initial width (only used if autoWidth
195                //              is true).  The first rendering of the grid will be this width, any
196                //              resizing of columns, etc will result in the grid switching to
197                //              autoWidth mode.  Note, this width will override any styling in a
198                //              stylesheet or directly on the node.
199                initialWidth: "",
200
201                // autoHeight: Boolean|Integer
202                //              If autoHeight is true, grid height is automatically set to fit the data.
203                //              If it is an integer, the height will be automatically set to fit the data
204                //              if there are fewer than that many rows - and the height will be set to show
205                //              that many rows if there are more
206                autoHeight: '',
207
208                // rowHeight: Integer
209                //              If rowHeight is set to a positive number, it will define the height of the rows
210                //              in pixels. This can provide a significant performance advantage, since it
211                //              eliminates the need to measure row sizes during rendering, which is one
212                //              the primary bottlenecks in the DataGrid's performance.
213                rowHeight: 0,
214               
215                // autoRender: Boolean
216                //              If autoRender is true, grid will render itself after initialization.
217                autoRender: true,
218
219                // defaultHeight: String
220                //              default height of the grid, measured in any valid css unit.
221                defaultHeight: '15em',
222               
223                // height: String
224                //              explicit height of the grid, measured in any valid css unit.  This will be populated (and overridden)
225                //              if the height: css attribute exists on the source node.
226                height: '',
227
228                // structure: dojox.grid.__ViewDef|dojox.grid.__ViewDef[]|dojox.grid.__CellDef[]|Array[dojox.grid.__CellDef[]]
229                //              View layout defintion.
230                structure: null,
231
232                // elasticView: Integer
233                //      Override defaults and make the indexed grid view elastic, thus filling available horizontal space.
234                elasticView: -1,
235
236                // singleClickEdit: boolean
237                //              Single-click starts editing. Default is double-click
238                singleClickEdit: false,
239
240                // selectionMode: String
241                //              Set the selection mode of grid's Selection.  Value must be 'single', 'multiple',
242                //              or 'extended'.  Default is 'extended'.
243                selectionMode: 'extended',
244
245                // rowSelector: Boolean|String
246                //              If set to true, will add a row selector view to this grid.  If set to a CSS width, will add
247                //              a row selector of that width to this grid.
248                rowSelector: '',
249
250                // columnReordering: Boolean
251                //              If set to true, will add drag and drop reordering to views with one row of columns.
252                columnReordering: false,
253
254                // headerMenu: dijit.Menu
255                //              If set to a dijit.Menu, will use this as a context menu for the grid headers.
256                headerMenu: null,
257
258                // placeholderLabel: String
259                //              Label of placeholders to search for in the header menu to replace with column toggling
260                //              menu items.
261                placeholderLabel: "GridColumns",
262               
263                // selectable: Boolean
264                //              Set to true if you want to be able to select the text within the grid.
265                selectable: false,
266               
267                // Used to store the last two clicks, to ensure double-clicking occurs based on the intended row
268                _click: null,
269               
270                // loadingMessage: String
271                //              Message that shows while the grid is loading
272                loadingMessage: "<span class='dojoxGridLoading'>${loadingState}</span>",
273
274                // errorMessage: String
275                //              Message that shows when the grid encounters an error loading
276                errorMessage: "<span class='dojoxGridError'>${errorState}</span>",
277
278                // noDataMessage: String
279                //              Message that shows if the grid has no data - wrap it in a
280                //              span with class 'dojoxGridNoData' if you want it to be
281                //              styled similar to the loading and error messages
282                noDataMessage: "",
283               
284                // escapeHTMLInData: Boolean
285                //              This will escape HTML brackets from the data to prevent HTML from
286                //              user-inputted data being rendered with may contain JavaScript and result in
287                //              XSS attacks. This is true by default, and it is recommended that it remain
288                //              true. Setting this to false will allow data to be displayed in the grid without
289                //              filtering, and should be only used if it is known that the data won't contain
290                //              malicious scripts. If HTML is needed in grid cells, it is recommended that
291                //              you use the formatter function to generate the HTML (the output of
292                //              formatter functions is not filtered, even with escapeHTMLInData set to true).
293                escapeHTMLInData: true,
294               
295                // formatterScope: Object
296                //              An object to execute format functions within.  If not set, the
297                //              format functions will execute within the scope of the cell that
298                //              has a format function.
299                formatterScope: null,
300               
301                // editable: boolean
302                //              indicates if the grid contains editable cells, default is false
303                //              set to true if editable cell encountered during rendering
304                editable: false,
305
306                // summary: String
307                //              Customizable summary descriptions which will be added to grid.domNode
308                summary: '',
309                _setSummaryAttr: 'domNode',
310               
311                // sortInfo: [private] Number
312                sortInfo: 0,
313
314                // _placeholders: [private] Array
315                _placeholders: null,
316
317                // _layoutClass: Object
318                //      The class to use for our layout - can be overridden by grid subclasses
319                _layoutClass: _Layout,
320
321                // initialization
322                buildRendering: function(){
323                        this.inherited(arguments);
324                        if(!this.domNode.getAttribute('tabIndex')){
325                                this.domNode.tabIndex = "0";
326                        }
327                        this.createScroller();
328                        this.createLayout();
329                        this.createViews();
330                        this.createManagers();
331
332                        this.createSelection();
333
334                        this.connect(this.selection, "onSelected", "onSelected");
335                        this.connect(this.selection, "onDeselected", "onDeselected");
336                        this.connect(this.selection, "onChanged", "onSelectionChanged");
337
338                        metrics.initOnFontResize();
339                        this.connect(metrics, "onFontResize", "textSizeChanged");
340                        util.funnelEvents(this.domNode, this, 'doKeyEvent', util.keyEvents);
341                        if (this.selectionMode != "none") {
342                                this.domNode.setAttribute("aria-multiselectable", this.selectionMode == "single" ? "false" : "true");
343                        }
344
345                        html.addClass(this.domNode, this.classTag);
346                        if(!this.isLeftToRight()){
347                                html.addClass(this.domNode, this.classTag+"Rtl");
348                        }
349                        if(this.rowHeight > 0){
350                                html.addClass(this.viewsNode, this.classTag + "FixedRowHeight");
351                        }
352                },
353               
354                postMixInProperties: function(){
355                        this.inherited(arguments);
356                        var messages = dojo.i18n.getLocalization("dijit", "loading", this.lang);
357                        this.loadingMessage = string.substitute(this.loadingMessage, messages);
358                        this.errorMessage = string.substitute(this.errorMessage, messages);
359                        if(this.srcNodeRef && this.srcNodeRef.style.height){
360                                this.height = this.srcNodeRef.style.height;
361                        }
362                        // Call this to update our autoheight to start out
363                        this._setAutoHeightAttr(this.autoHeight, true);
364                        this.lastScrollTop = this.scrollTop = 0;
365                },
366               
367                postCreate: function(){
368                        this._placeholders = [];
369                        this._setHeaderMenuAttr(this.headerMenu);
370                        this._setStructureAttr(this.structure);
371                        this._click = [];
372                        this.inherited(arguments);
373                        if(this.domNode && this.autoWidth && this.initialWidth){
374                                this.domNode.style.width = this.initialWidth;
375                        }
376                        if (this.domNode && !this.editable){
377                                // default value for aria-readonly is false, set to true if grid is not editable
378                                html.attr(this.domNode,"aria-readonly", "true");
379                        }
380                },
381
382                destroy: function(){
383                        this.domNode.onReveal = null;
384                        this.domNode.onSizeChange = null;
385
386                        // Fixes IE domNode leak
387                        delete this._click;
388
389                        if(this.scroller){
390                                this.scroller.destroy();
391                                delete this.scroller;
392                        }
393                        this.edit.destroy();
394                        delete this.edit;
395                        this.views.destroyViews();
396                        if(this.focus){
397                                this.focus.destroy();
398                                delete this.focus;
399                        }
400                        if(this.headerMenu&&this._placeholders.length){
401                                array.forEach(this._placeholders, function(p){ p.unReplace(true); });
402                                this.headerMenu.unBindDomNode(this.viewsHeaderNode);
403                        }
404                        this.inherited(arguments);
405                },
406
407                _setAutoHeightAttr: function(ah, skipRender){
408                        // Calculate our autoheight - turn it into a boolean or an integer
409                        if(typeof ah == "string"){
410                                if(!ah || ah == "false"){
411                                        ah = false;
412                                }else if (ah == "true"){
413                                        ah = true;
414                                }else{
415                                        ah = window.parseInt(ah, 10);
416                                }
417                        }
418                        if(typeof ah == "number"){
419                                if(isNaN(ah)){
420                                        ah = false;
421                                }
422                                // Autoheight must be at least 1, if it's a number.  If it's
423                                // less than 0, we'll take that to mean "all" rows (same as
424                                // autoHeight=true - if it is equal to zero, we'll take that
425                                // to mean autoHeight=false
426                                if(ah < 0){
427                                        ah = true;
428                                }else if (ah === 0){
429                                        ah = false;
430                                }
431                        }
432                        this.autoHeight = ah;
433                        if(typeof ah == "boolean"){
434                                this._autoHeight = ah;
435                        }else if(typeof ah == "number"){
436                                this._autoHeight = (ah >= this.get('rowCount'));
437                        }else{
438                                this._autoHeight = false;
439                        }
440                        if(this._started && !skipRender){
441                                this.render();
442                        }
443                },
444
445                _getRowCountAttr: function(){
446                        return this.updating && this.invalidated && this.invalidated.rowCount != undefined ?
447                                this.invalidated.rowCount : this.rowCount;
448                },
449               
450                textSizeChanged: function(){
451                        this.render();
452                },
453
454                sizeChange: function(){
455                        this.update();
456                },
457
458                createManagers: function(){
459                        // summary:
460                        //              create grid managers for various tasks including rows, focus, selection, editing
461
462                        // row manager
463                        this.rows = new _RowManager(this);
464                        // focus manager
465                        this.focus = new _FocusManager(this);
466                        // edit manager
467                        this.edit = new _EditManager(this);
468                },
469
470                createSelection: function(){
471                        // summary:     Creates a new Grid selection manager.
472
473                        // selection manager
474                        this.selection = new Selection(this);
475                },
476
477                createScroller: function(){
478                        // summary:
479                        //              Creates a new virtual scroller
480                        this.scroller = new _Scroller();
481                        this.scroller.grid = this;
482                        this.scroller.renderRow = lang.hitch(this, "renderRow");
483                        this.scroller.removeRow = lang.hitch(this, "rowRemoved");
484                },
485
486                createLayout: function(){
487                        // summary:
488                        //              Creates a new Grid layout
489                        this.layout = new this._layoutClass(this);
490                        this.connect(this.layout, "moveColumn", "onMoveColumn");
491                },
492
493                onMoveColumn: function(){
494                        this.update();
495                },
496               
497                onResizeColumn: function(/*int*/ cellIdx){
498                        // Called when a column is resized.
499                },
500
501                // views
502                createViews: function(){
503                        this.views = new _ViewManager(this);
504                        this.views.createView = lang.hitch(this, "createView");
505                },
506
507                createView: function(inClass, idx){
508                        var c = lang.getObject(inClass);
509                        var view = new c({ grid: this, index: idx });
510                        this.viewsNode.appendChild(view.domNode);
511                        this.viewsHeaderNode.appendChild(view.headerNode);
512                        this.views.addView(view);
513                        html.attr(this.domNode, "align", this.isLeftToRight() ? 'left' : 'right');
514                        return view;
515                },
516
517                buildViews: function(){
518                        for(var i=0, vs; (vs=this.layout.structure[i]); i++){
519                                this.createView(vs.type || dojox._scopeName + ".grid._View", i).setStructure(vs);
520                        }
521                        this.scroller.setContentNodes(this.views.getContentNodes());
522                },
523
524                _setStructureAttr: function(structure){
525                        var s = structure;
526                        if(s && lang.isString(s)){
527                                dojo.deprecated("dojox.grid._Grid.set('structure', 'objVar')", "use dojox.grid._Grid.set('structure', objVar) instead", "2.0");
528                                s=lang.getObject(s);
529                        }
530                        this.structure = s;
531                        if(!s){
532                                if(this.layout.structure){
533                                        s = this.layout.structure;
534                                }else{
535                                        return;
536                                }
537                        }
538                        this.views.destroyViews();
539                        this.focus.focusView = null;
540                        if(s !== this.layout.structure){
541                                this.layout.setStructure(s);
542                        }
543                        this._structureChanged();
544                },
545
546                setStructure: function(/* dojox.grid.__ViewDef|dojox.grid.__ViewDef[]|dojox.grid.__CellDef[]|Array[dojox.grid.__CellDef[]] */ inStructure){
547                        // summary:
548                        //              Install a new structure and rebuild the grid.
549                        dojo.deprecated("dojox.grid._Grid.setStructure(obj)", "use dojox.grid._Grid.set('structure', obj) instead.", "2.0");
550                        this._setStructureAttr(inStructure);
551                },
552               
553                getColumnTogglingItems: function(){
554                        // summary:
555                        //              returns an array of dijit.CheckedMenuItem widgets that can be
556                        //              added to a menu for toggling columns on and off.
557                        var items, checkedItems = [];
558                        items = array.map(this.layout.cells, function(cell){
559                                if(!cell.menuItems){ cell.menuItems = []; }
560
561                                var self = this;
562                                var item = new CheckedMenuItem({
563                                        label: cell.name,
564                                        checked: !cell.hidden,
565                                        _gridCell: cell,
566                                        onChange: function(checked){
567                                                if(self.layout.setColumnVisibility(this._gridCell.index, checked)){
568                                                        var items = this._gridCell.menuItems;
569                                                        if(items.length > 1){
570                                                                array.forEach(items, function(item){
571                                                                        if(item !== this){
572                                                                                item.setAttribute("checked", checked);
573                                                                        }
574                                                                }, this);
575                                                        }
576                                                        checked = array.filter(self.layout.cells, function(c){
577                                                                if(c.menuItems.length > 1){
578                                                                        array.forEach(c.menuItems, "item.set('disabled', false);");
579                                                                }else{
580                                                                        c.menuItems[0].set('disabled', false);
581                                                                }
582                                                                return !c.hidden;
583                                                        });
584                                                        if(checked.length == 1){
585                                                                array.forEach(checked[0].menuItems, "item.set('disabled', true);");
586                                                        }
587                                                }
588                                        },
589                                        destroy: function(){
590                                                var index = array.indexOf(this._gridCell.menuItems, this);
591                                                this._gridCell.menuItems.splice(index, 1);
592                                                delete this._gridCell;
593                                                CheckedMenuItem.prototype.destroy.apply(this, arguments);
594                                        }
595                                });
596                                cell.menuItems.push(item);
597                                if(!cell.hidden) {
598                                        checkedItems.push(item);
599                                }
600                                return item;
601                        }, this); // dijit.CheckedMenuItem[]
602                        if(checkedItems.length == 1) {
603                                checkedItems[0].set('disabled', true);
604                        }
605                        return items;
606                },
607
608                _setHeaderMenuAttr: function(menu){
609                        if(this._placeholders && this._placeholders.length){
610                                array.forEach(this._placeholders, function(p){
611                                        p.unReplace(true);
612                                });
613                                this._placeholders = [];
614                        }
615                        if(this.headerMenu){
616                                this.headerMenu.unBindDomNode(this.viewsHeaderNode);
617                        }
618                        this.headerMenu = menu;
619                        if(!menu){ return; }
620
621                        this.headerMenu.bindDomNode(this.viewsHeaderNode);
622                        if(this.headerMenu.getPlaceholders){
623                                this._placeholders = this.headerMenu.getPlaceholders(this.placeholderLabel);
624                        }
625                },
626
627                setHeaderMenu: function(/* dijit.Menu */ menu){
628                        dojo.deprecated("dojox.grid._Grid.setHeaderMenu(obj)", "use dojox.grid._Grid.set('headerMenu', obj) instead.", "2.0");
629                        this._setHeaderMenuAttr(menu);
630                },
631               
632                setupHeaderMenu: function(){
633                        if(this._placeholders && this._placeholders.length){
634                                array.forEach(this._placeholders, function(p){
635                                        if(p._replaced){
636                                                p.unReplace(true);
637                                        }
638                                        p.replace(this.getColumnTogglingItems());
639                                }, this);
640                        }
641                },
642
643                _fetch: function(start){
644                        this.setScrollTop(0);
645                },
646
647                getItem: function(inRowIndex){
648                        return null;
649                },
650               
651                showMessage: function(message){
652                        if(message){
653                                this.messagesNode.innerHTML = message;
654                                this.messagesNode.style.display = "";
655                        }else{
656                                this.messagesNode.innerHTML = "";
657                                this.messagesNode.style.display = "none";
658                        }
659                },
660
661                _structureChanged: function() {
662                        this.buildViews();
663                        if(this.autoRender && this._started){
664                                this.render();
665                        }
666                },
667
668                hasLayout: function() {
669                        return this.layout.cells.length;
670                },
671
672                // sizing
673                resize: function(changeSize, resultSize){
674                        // summary:
675                        //              Update the grid's rendering dimensions and resize it
676                       
677                        // Calling sizeChange calls update() which calls _resize...so let's
678                        // save our input values, if any, and use them there when it gets
679                        // called.  This saves us an extra call to _resize(), which can
680                        // get kind of heavy.
681                       
682                        this._pendingChangeSize = changeSize;
683                        this._pendingResultSize = resultSize;
684                        this.sizeChange();
685                },
686
687                _getPadBorder: function() {
688                        this._padBorder = this._padBorder || html._getPadBorderExtents(this.domNode);
689                        return this._padBorder;
690                },
691
692                _getHeaderHeight: function(){
693                        var vns = this.viewsHeaderNode.style, t = vns.display == "none" ? 0 : this.views.measureHeader();
694                        vns.height = t + 'px';
695                        // header heights are reset during measuring so must be normalized after measuring.
696                        this.views.normalizeHeaderNodeHeight();
697                        return t;
698                },
699               
700                _resize: function(changeSize, resultSize){
701                        // Restore our pending values, if any
702                        changeSize = changeSize || this._pendingChangeSize;
703                        resultSize = resultSize || this._pendingResultSize;
704                        delete this._pendingChangeSize;
705                        delete this._pendingResultSize;
706                        // if we have set up everything except the DOM, we cannot resize
707                        if(!this.domNode){ return; }
708                        var pn = this.domNode.parentNode;
709                        if(!pn || pn.nodeType != 1 || !this.hasLayout() || pn.style.visibility == "hidden" || pn.style.display == "none"){
710                                return;
711                        }
712                        // useful measurement
713                        var padBorder = this._getPadBorder();
714                        var hh = undefined;
715                        var h;
716                        // grid height
717                        if(this._autoHeight){
718                                this.domNode.style.height = 'auto';
719                        }else if(typeof this.autoHeight == "number"){
720                                h = hh = this._getHeaderHeight();
721                                h += (this.scroller.averageRowHeight * this.autoHeight);
722                                this.domNode.style.height = h + "px";
723                        }else if(this.domNode.clientHeight <= padBorder.h){
724                                if(pn == document.body){
725                                        this.domNode.style.height = this.defaultHeight;
726                                }else if(this.height){
727                                        this.domNode.style.height = this.height;
728                                }else{
729                                        this.fitTo = "parent";
730                                }
731                        }
732                        // if we are given dimensions, size the grid's domNode to those dimensions
733                        if(resultSize){
734                                changeSize = resultSize;
735                        }
736                        if(!this._autoHeight && changeSize){
737                                html.marginBox(this.domNode, changeSize);
738                                this.height = this.domNode.style.height;
739                                delete this.fitTo;
740                        }else if(this.fitTo == "parent"){
741                                h = this._parentContentBoxHeight = (this._parentContentBoxHeight > 0 ? this._parentContentBoxHeight : html._getContentBox(pn).h);
742                                this.domNode.style.height = Math.max(0, h) + "px";
743                        }
744                       
745                        var hasFlex = array.some(this.views.views, function(v){ return v.flexCells; });
746
747                        if(!this._autoHeight && (h || html._getContentBox(this.domNode).h) === 0){
748                                // We need to hide the header, since the Grid is essentially hidden.
749                                this.viewsHeaderNode.style.display = "none";
750                        }else{
751                                // Otherwise, show the header and give it an appropriate height.
752                                this.viewsHeaderNode.style.display = "block";
753                                if(!hasFlex && hh === undefined){
754                                        hh = this._getHeaderHeight();
755                                }
756                        }
757                        if(hasFlex){
758                                hh = undefined;
759                        }
760
761                        // NOTE: it is essential that width be applied before height
762                        // Header height can only be calculated properly after view widths have been set.
763                        // This is because flex column width is naturally 0 in Firefox.
764                        // Therefore prior to width sizing flex columns with spaces are maximally wrapped
765                        // and calculated to be too tall.
766                        this.adaptWidth();
767                        this.adaptHeight(hh);
768
769                        this.postresize();
770                },
771
772                adaptWidth: function() {
773                        // summary:
774                        //              sets width and position for views and update grid width if necessary
775                        // tags:
776                        //              private
777                        var doAutoWidth = (!this.initialWidth && this.autoWidth);
778                        var w = doAutoWidth ? 0 : this.domNode.clientWidth || (this.domNode.offsetWidth - this._getPadBorder().w),
779                                vw = this.views.arrange(1, w);
780                        this.views.onEach("adaptWidth");
781                        if(doAutoWidth){
782                                this.domNode.style.width = vw + "px";
783                        }
784                },
785
786                adaptHeight: function(inHeaderHeight){
787                        // summary:
788                        //              measures and normalizes header height, then sets view heights, and then updates scroller
789                        //              content extent
790                        // tags:
791                        //              private
792                        var t = inHeaderHeight === undefined ? this._getHeaderHeight() : inHeaderHeight;
793                        var h = (this._autoHeight ? -1 : Math.max(this.domNode.clientHeight - t, 0) || 0);
794                        this.views.onEach('setSize', [0, h]);
795                        this.views.onEach('adaptHeight');
796                        if(!this._autoHeight){
797                                var numScroll = 0, numNoScroll = 0;
798                                var noScrolls = array.filter(this.views.views, function(v){
799                                        var has = v.hasHScrollbar();
800                                        if(has){ numScroll++; }else{ numNoScroll++; }
801                                        return (!has);
802                                });
803                                if(numScroll > 0 && numNoScroll > 0){
804                                        array.forEach(noScrolls, function(v){
805                                                v.adaptHeight(true);
806                                        });
807                                }
808                        }
809                        if(this.autoHeight === true || h != -1 || (typeof this.autoHeight == "number" && this.autoHeight >= this.get('rowCount'))){
810                                this.scroller.windowHeight = h;
811                        }else{
812                                this.scroller.windowHeight = Math.max(this.domNode.clientHeight - t, 0);
813                        }
814                },
815
816                // startup
817                startup: function(){
818                        if(this._started){return;}
819                        this.inherited(arguments);
820                        if(this.autoRender){
821                                this.render();
822                        }
823                },
824
825                // render
826                render: function(){
827                        // summary:
828                        //      Render the grid, headers, and views. Edit and scrolling states are reset. To retain edit and
829                        //      scrolling states, see Update.
830
831                        if(!this.domNode){return;}
832                        if(!this._started){return;}
833
834                        if(!this.hasLayout()) {
835                                this.scroller.init(0, this.keepRows, this.rowsPerPage);
836                                return;
837                        }
838                        //
839                        this.update = this.defaultUpdate;
840                        this._render();
841                },
842
843                _render: function(){
844                        this.scroller.init(this.get('rowCount'), this.keepRows, this.rowsPerPage);
845                        this.prerender();
846                        this.setScrollTop(0);
847                        this.postrender();
848                },
849
850                prerender: function(){
851                        // if autoHeight, make sure scroller knows not to virtualize; everything must be rendered.
852                        this.keepRows = this._autoHeight ? 0 : this.keepRows;
853                        this.scroller.setKeepInfo(this.keepRows);
854                        this.views.render();
855                        this._resize();
856                },
857
858                postrender: function(){
859                        this.postresize();
860                        this.focus.initFocusView();
861                        // make rows unselectable
862                        html.setSelectable(this.domNode, this.selectable);
863                },
864
865                postresize: function(){
866                        // views are position absolute, so they do not inflate the parent
867                        if(this._autoHeight){
868                                var size = Math.max(this.views.measureContent()) + 'px';
869                               
870                                this.viewsNode.style.height = size;
871                        }
872                },
873
874                renderRow: function(inRowIndex, inNodes){
875                        // summary:
876                        //              used internally to render rows
877                        // tags:
878                        //              private
879                        this.views.renderRow(inRowIndex, inNodes, this._skipRowRenormalize);
880                },
881
882                rowRemoved: function(inRowIndex){
883                        // summary:
884                        //              used internally to remove rows
885                        // tags:
886                        //              private
887                        this.views.rowRemoved(inRowIndex);
888                },
889
890                invalidated: null,
891
892                updating: false,
893
894                beginUpdate: function(){
895                        // summary:
896                        //              Use to make multiple changes to rows while queueing row updating.
897                        // NOTE: not currently supporting nested begin/endUpdate calls
898                        this.invalidated = [];
899                        this.updating = true;
900                },
901
902                endUpdate: function(){
903                        // summary:
904                        //              Use after calling beginUpdate to render any changes made to rows.
905                        this.updating = false;
906                        var i = this.invalidated, r;
907                        if(i.all){
908                                this.update();
909                        }else if(i.rowCount != undefined){
910                                this.updateRowCount(i.rowCount);
911                        }else{
912                                for(r in i){
913                                        this.updateRow(Number(r));
914                                }
915                        }
916                        this.invalidated = [];
917                },
918
919                // update
920                defaultUpdate: function(){
921                        // note: initial update calls render and subsequently this function.
922                        if(!this.domNode){return;}
923                        if(this.updating){
924                                this.invalidated.all = true;
925                                return;
926                        }
927                        //this.edit.saveState(inRowIndex);
928                        this.lastScrollTop = this.scrollTop;
929                        this.prerender();
930                        this.scroller.invalidateNodes();
931                        this.setScrollTop(this.lastScrollTop);
932                        this.postrender();
933                        //this.edit.restoreState(inRowIndex);
934                },
935
936                update: function(){
937                        // summary:
938                        //              Update the grid, retaining edit and scrolling states.
939                        this.render();
940                },
941
942                updateRow: function(inRowIndex){
943                        // summary:
944                        //              Render a single row.
945                        // inRowIndex: Integer
946                        //              Index of the row to render
947                        inRowIndex = Number(inRowIndex);
948                        if(this.updating){
949                                this.invalidated[inRowIndex]=true;
950                        }else{
951                                this.views.updateRow(inRowIndex);
952                                this.scroller.rowHeightChanged(inRowIndex);
953                        }
954                },
955
956                updateRows: function(startIndex, howMany){
957                        // summary:
958                        //              Render consecutive rows at once.
959                        // startIndex: Integer
960                        //              Index of the starting row to render
961                        // howMany: Integer
962                        //              How many rows to update.
963                        startIndex = Number(startIndex);
964                        howMany = Number(howMany);
965                        var i;
966                        if(this.updating){
967                                for(i=0; i<howMany; i++){
968                                        this.invalidated[i+startIndex]=true;
969                                }
970                        }else{
971                                for(i=0; i<howMany; i++){
972                                        this.views.updateRow(i+startIndex, this._skipRowRenormalize);
973                                }
974                                this.scroller.rowHeightChanged(startIndex);
975                        }
976                },
977
978                updateRowCount: function(inRowCount){
979                        // summary:
980                        //              Change the number of rows.
981                        // inRowCount: int
982                        //              Number of rows in the grid.
983                        if(this.updating){
984                                this.invalidated.rowCount = inRowCount;
985                        }else{
986                                this.rowCount = inRowCount;
987                                this._setAutoHeightAttr(this.autoHeight, true);
988                                if(this.layout.cells.length){
989                                        this.scroller.updateRowCount(inRowCount);
990                                }
991                                this._resize();
992                                if(this.layout.cells.length){
993                                        this.setScrollTop(this.scrollTop);
994                                }
995                        }
996                },
997
998                updateRowStyles: function(inRowIndex){
999                        // summary:
1000                        //              Update the styles for a row after it's state has changed.
1001                        this.views.updateRowStyles(inRowIndex);
1002                },
1003                getRowNode: function(inRowIndex){
1004                        // summary:
1005                        //              find the rowNode that is not a rowSelector
1006                        if (this.focus.focusView && !(this.focus.focusView instanceof _RowSelector)){
1007                                        return this.focus.focusView.rowNodes[inRowIndex];
1008                        }else{ // search through views
1009                                for (var i = 0, cView; (cView = this.views.views[i]); i++) {
1010                                        if (!(cView instanceof _RowSelector)) {
1011                                                return cView.rowNodes[inRowIndex];
1012                                        }
1013                                }
1014                        }
1015                        return null;
1016                },
1017                rowHeightChanged: function(inRowIndex){
1018                        // summary:
1019                        //              Update grid when the height of a row has changed. Row height is handled automatically as rows
1020                        //              are rendered. Use this function only to update a row's height outside the normal rendering process.
1021                        // inRowIndex: Integer
1022                        //              index of the row that has changed height
1023
1024                        this.views.renormalizeRow(inRowIndex);
1025                        this.scroller.rowHeightChanged(inRowIndex);
1026                },
1027
1028                // fastScroll: Boolean
1029                //              flag modifies vertical scrolling behavior. Defaults to true but set to false for slower
1030                //              scroll performance but more immediate scrolling feedback
1031                fastScroll: true,
1032
1033                delayScroll: false,
1034
1035                // scrollRedrawThreshold: int
1036                //      pixel distance a user must scroll vertically to trigger grid scrolling.
1037                scrollRedrawThreshold: (has('ie') ? 100 : 50),
1038
1039                // scroll methods
1040                scrollTo: function(inTop){
1041                        // summary:
1042                        //              Vertically scroll the grid to a given pixel position
1043                        // inTop: Integer
1044                        //              vertical position of the grid in pixels
1045                        if(!this.fastScroll){
1046                                this.setScrollTop(inTop);
1047                                return;
1048                        }
1049                        var delta = Math.abs(this.lastScrollTop - inTop);
1050                        this.lastScrollTop = inTop;
1051                        if(delta > this.scrollRedrawThreshold || this.delayScroll){
1052                                this.delayScroll = true;
1053                                this.scrollTop = inTop;
1054                                this.views.setScrollTop(inTop);
1055                                if(this._pendingScroll){
1056                                        window.clearTimeout(this._pendingScroll);
1057                                }
1058                                var _this = this;
1059                                this._pendingScroll = window.setTimeout(function(){
1060                                        delete _this._pendingScroll;
1061                                        _this.finishScrollJob();
1062                                }, 200);
1063                        }else{
1064                                this.setScrollTop(inTop);
1065                        }
1066                },
1067
1068                finishScrollJob: function(){
1069                        this.delayScroll = false;
1070                        this.setScrollTop(this.scrollTop);
1071                },
1072
1073                setScrollTop: function(inTop){
1074                        this.scroller.scroll(this.views.setScrollTop(inTop));
1075                },
1076
1077                scrollToRow: function(inRowIndex){
1078                        // summary:
1079                        //              Scroll the grid to a specific row.
1080                        // inRowIndex: Integer
1081                        //              grid row index
1082                        this.setScrollTop(this.scroller.findScrollTop(inRowIndex) + 1);
1083                },
1084
1085                styleRowNode: function(inRowIndex, inRowNode){
1086                        // summary:
1087                        //              styling (used internally to style individual parts of a row)
1088                        // tags:
1089                        //              private
1090                        if(inRowNode){
1091                                this.rows.styleRowNode(inRowIndex, inRowNode);
1092                        }
1093                },
1094               
1095                // called when the mouse leaves the grid so we can deselect all hover rows
1096                _mouseOut: function(e){
1097                        this.rows.setOverRow(-2);
1098                },
1099       
1100                // cells
1101                getCell: function(inIndex){
1102                        // summary:
1103                        //              Retrieves the cell object for a given grid column.
1104                        // inIndex: Integer
1105                        //              Grid column index of cell to retrieve
1106                        // returns:
1107                        //              a grid cell
1108                        return this.layout.cells[inIndex];
1109                },
1110
1111                setCellWidth: function(inIndex, inUnitWidth){
1112                        this.getCell(inIndex).unitWidth = inUnitWidth;
1113                },
1114
1115                getCellName: function(inCell){
1116                        // summary:
1117                        //              Returns the cell name of a passed cell
1118                        return "Cell " + inCell.index; // String
1119                },
1120
1121                // sorting
1122                canSort: function(inSortInfo){
1123                        // summary:
1124                        //              Determines if the grid can be sorted
1125                        // inSortInfo: Integer
1126                        //              Sort information, 1-based index of column on which to sort, positive for an ascending sort
1127                        //              and negative for a descending sort
1128                        // returns: Boolean
1129                        //              True if grid can be sorted on the given column in the given direction
1130                },
1131
1132                sort: function(){
1133                },
1134
1135                getSortAsc: function(inSortInfo){
1136                        // summary:
1137                        //              Returns true if grid is sorted in an ascending direction.
1138                        inSortInfo = inSortInfo == undefined ? this.sortInfo : inSortInfo;
1139                        return Boolean(inSortInfo > 0); // Boolean
1140                },
1141
1142                getSortIndex: function(inSortInfo){
1143                        // summary:
1144                        //              Returns the index of the column on which the grid is sorted
1145                        inSortInfo = inSortInfo == undefined ? this.sortInfo : inSortInfo;
1146                        return Math.abs(inSortInfo) - 1; // Integer
1147                },
1148
1149                setSortIndex: function(inIndex, inAsc){
1150                        // summary:
1151                        //              Sort the grid on a column in a specified direction
1152                        // inIndex: Integer
1153                        //              Column index on which to sort.
1154                        // inAsc: Boolean
1155                        //              If true, sort the grid in ascending order, otherwise in descending order
1156                        var si = inIndex +1;
1157                        if(inAsc != undefined){
1158                                si *= (inAsc ? 1 : -1);
1159                        } else if(this.getSortIndex() == inIndex){
1160                                si = -this.sortInfo;
1161                        }
1162                        this.setSortInfo(si);
1163                },
1164
1165                setSortInfo: function(inSortInfo){
1166                        if(this.canSort(inSortInfo)){
1167                                this.sortInfo = inSortInfo;
1168                                this.sort();
1169                                this.update();
1170                        }
1171                },
1172
1173                // DOM event handler
1174                doKeyEvent: function(e){
1175                        e.dispatch = 'do' + e.type;
1176                        this.onKeyEvent(e);
1177                },
1178
1179                // event dispatch
1180                //: protected
1181                _dispatch: function(m, e){
1182                        if(m in this){
1183                                return this[m](e);
1184                        }
1185                        return false;
1186                },
1187
1188                dispatchKeyEvent: function(e){
1189                        this._dispatch(e.dispatch, e);
1190                },
1191
1192                dispatchContentEvent: function(e){
1193                        this.edit.dispatchEvent(e) || e.sourceView.dispatchContentEvent(e) || this._dispatch(e.dispatch, e);
1194                },
1195
1196                dispatchHeaderEvent: function(e){
1197                        e.sourceView.dispatchHeaderEvent(e) || this._dispatch('doheader' + e.type, e);
1198                },
1199
1200                dokeydown: function(e){
1201                        this.onKeyDown(e);
1202                },
1203
1204                doclick: function(e){
1205                        if(e.cellNode){
1206                                this.onCellClick(e);
1207                        }else{
1208                                this.onRowClick(e);
1209                        }
1210                },
1211
1212                dodblclick: function(e){
1213                        if(e.cellNode){
1214                                this.onCellDblClick(e);
1215                        }else{
1216                                this.onRowDblClick(e);
1217                        }
1218                },
1219
1220                docontextmenu: function(e){
1221                        if(e.cellNode){
1222                                this.onCellContextMenu(e);
1223                        }else{
1224                                this.onRowContextMenu(e);
1225                        }
1226                },
1227
1228                doheaderclick: function(e){
1229                        if(e.cellNode){
1230                                this.onHeaderCellClick(e);
1231                        }else{
1232                                this.onHeaderClick(e);
1233                        }
1234                },
1235
1236                doheaderdblclick: function(e){
1237                        if(e.cellNode){
1238                                this.onHeaderCellDblClick(e);
1239                        }else{
1240                                this.onHeaderDblClick(e);
1241                        }
1242                },
1243
1244                doheadercontextmenu: function(e){
1245                        if(e.cellNode){
1246                                this.onHeaderCellContextMenu(e);
1247                        }else{
1248                                this.onHeaderContextMenu(e);
1249                        }
1250                },
1251
1252                // override to modify editing process
1253                doStartEdit: function(inCell, inRowIndex){
1254                        this.onStartEdit(inCell, inRowIndex);
1255                },
1256
1257                doApplyCellEdit: function(inValue, inRowIndex, inFieldIndex){
1258                        this.onApplyCellEdit(inValue, inRowIndex, inFieldIndex);
1259                },
1260
1261                doCancelEdit: function(inRowIndex){
1262                        this.onCancelEdit(inRowIndex);
1263                },
1264
1265                doApplyEdit: function(inRowIndex){
1266                        this.onApplyEdit(inRowIndex);
1267                },
1268
1269                // row editing
1270                addRow: function(){
1271                        // summary:
1272                        //              Add a row to the grid.
1273                        this.updateRowCount(this.get('rowCount')+1);
1274                },
1275
1276                removeSelectedRows: function(){
1277                        // summary:
1278                        //              Remove the selected rows from the grid.
1279                        if(this.allItemsSelected){
1280                                this.updateRowCount(0);
1281                        }else{
1282                                this.updateRowCount(Math.max(0, this.get('rowCount') - this.selection.getSelected().length));
1283                        }
1284                        this.selection.clear();
1285                }
1286
1287        });
1288
1289        _Grid.markupFactory = function(props, node, ctor, cellFunc){
1290                var widthFromAttr = function(n){
1291                        var w = html.attr(n, "width")||"auto";
1292                        if((w != "auto")&&(w.slice(-2) != "em")&&(w.slice(-1) != "%")){
1293                                w = parseInt(w, 10)+"px";
1294                        }
1295                        return w;
1296                };
1297                // if(!props.store){ console.debug("no store!"); }
1298                // if a structure isn't referenced, do we have enough
1299                // data to try to build one automatically?
1300                if(     !props.structure &&
1301                        node.nodeName.toLowerCase() == "table"){
1302
1303                        // try to discover a structure
1304                        props.structure = query("> colgroup", node).map(function(cg){
1305                                var sv = html.attr(cg, "span");
1306                                var v = {
1307                                        noscroll: (html.attr(cg, "noscroll") == "true") ? true : false,
1308                                        __span: (!!sv ? parseInt(sv, 10) : 1),
1309                                        cells: []
1310                                };
1311                                if(html.hasAttr(cg, "width")){
1312                                        v.width = widthFromAttr(cg);
1313                                }
1314                                return v; // for vendetta
1315                        });
1316                        if(!props.structure.length){
1317                                props.structure.push({
1318                                        __span: Infinity,
1319                                        cells: [] // catch-all view
1320                                });
1321                        }
1322                        // check to see if we're gonna have more than one view
1323
1324                        // for each tr in our th, create a row of cells
1325                        query("thead > tr", node).forEach(function(tr, tr_idx){
1326                                var cellCount = 0;
1327                                var viewIdx = 0;
1328                                var lastViewIdx;
1329                                var cView = null;
1330                                query("> th", tr).map(function(th){
1331                                        // what view will this cell go into?
1332
1333                                        // NOTE:
1334                                        //              to prevent extraneous iteration, we start counters over
1335                                        //              for each row, incrementing over the surface area of the
1336                                        //              structure that colgroup processing generates and
1337                                        //              creating cell objects for each <th> to place into those
1338                                        //              cell groups.  There's a lot of state-keepking logic
1339                                        //              here, but it is what it has to be.
1340                                        if(!cView){ // current view book keeping
1341                                                lastViewIdx = 0;
1342                                                cView = props.structure[0];
1343                                        }else if(cellCount >= (lastViewIdx+cView.__span)){
1344                                                viewIdx++;
1345                                                // move to allocating things into the next view
1346                                                lastViewIdx += cView.__span;
1347                                                var lastView = cView;
1348                                                cView = props.structure[viewIdx];
1349                                        }
1350
1351                                        // actually define the cell from what markup hands us
1352                                        var cell = {
1353                                                name: lang.trim(html.attr(th, "name")||th.innerHTML),
1354                                                colSpan: parseInt(html.attr(th, "colspan")||1, 10),
1355                                                type: lang.trim(html.attr(th, "cellType")||""),
1356                                                id: lang.trim(html.attr(th,"id")||"")
1357                                        };
1358                                        cellCount += cell.colSpan;
1359                                        var rowSpan = html.attr(th, "rowspan");
1360                                        if(rowSpan){
1361                                                cell.rowSpan = rowSpan;
1362                                        }
1363                                        if(html.hasAttr(th, "width")){
1364                                                cell.width = widthFromAttr(th);
1365                                        }
1366                                        if(html.hasAttr(th, "relWidth")){
1367                                                cell.relWidth = window.parseInt(html.attr(th, "relWidth"), 10);
1368                                        }
1369                                        if(html.hasAttr(th, "hidden")){
1370                                                cell.hidden = (html.attr(th, "hidden") == "true" || html.attr(th, "hidden") === true/*always boolean true in Chrome*/);
1371                                        }
1372
1373                                        if(cellFunc){
1374                                                cellFunc(th, cell);
1375                                        }
1376
1377                                        cell.type = cell.type ? lang.getObject(cell.type) : dojox.grid.cells.Cell;
1378
1379                                        if(cell.type && cell.type.markupFactory){
1380                                                cell.type.markupFactory(th, cell);
1381                                        }
1382
1383                                        if(!cView.cells[tr_idx]){
1384                                                cView.cells[tr_idx] = [];
1385                                        }
1386                                        cView.cells[tr_idx].push(cell);
1387                                });
1388                        });
1389                }
1390
1391                return new ctor(props, node);
1392        };
1393
1394        return _Grid;
1395
1396});
Note: See TracBrowser for help on using the repository browser.