dojo.provide("dojox.grid._Grid"); dojo.require("dijit.dijit"); dojo.require("dijit.Menu"); dojo.require("dojox.html.metrics"); dojo.require("dojox.grid.util"); dojo.require("dojox.grid._Scroller"); dojo.require("dojox.grid._Layout"); dojo.require("dojox.grid._View"); dojo.require("dojox.grid._ViewManager"); dojo.require("dojox.grid._RowManager"); dojo.require("dojox.grid._FocusManager"); dojo.require("dojox.grid._EditManager"); dojo.require("dojox.grid.Selection"); dojo.require("dojox.grid._RowSelector"); dojo.require("dojox.grid._Events"); dojo.requireLocalization("dijit", "loading"); (function(){ var jobs = { cancel: function(inHandle){ if(inHandle){ clearTimeout(inHandle); } }, jobs: [], job: function(inName, inDelay, inJob){ jobs.cancelJob(inName); var job = function(){ delete jobs.jobs[inName]; inJob(); } jobs.jobs[inName] = setTimeout(job, inDelay); }, cancelJob: function(inName){ jobs.cancel(jobs.jobs[inName]); } }; /*===== dojox.grid.__CellDef = function(){ // name: String? // The text to use in the header of the grid for this cell. // get: Function? // function(rowIndex){} rowIndex is of type Integer. This // function will be called when a cell requests data. Returns the // unformatted data for the cell. // value: String? // If "get" is not specified, this is used as the data for the cell. // defaultValue: String? // If "get" and "value" aren't specified or if "get" returns an undefined // value, this is used as the data for the cell. "formatter" is not run // on this if "get" returns an undefined value. // formatter: Function? // function(data, rowIndex){} data is of type anything, rowIndex // is of type Integer. This function will be called after the cell // has its data but before it passes it back to the grid to render. // Returns the formatted version of the cell's data. // type: dojox.grid.cells._Base|Function? // TODO // editable: Boolean? // Whether this cell should be editable or not. // hidden: Boolean? // If true, the cell will not be displayed. // noresize: Boolean? // If true, the cell will not be able to be resized. // width: Integer|String? // A CSS size. If it's an Integer, the width will be in em's. // colSpan: Integer? // How many columns to span this cell. Will not work in the first // sub-row of cells. // rowSpan: Integer? // How many sub-rows to span this cell. // styles: String? // A string of styles to apply to both the header cell and main // grid cells. Must end in a ';'. // headerStyles: String? // A string of styles to apply to just the header cell. Must end // in a ';' // cellStyles: String? // A string of styles to apply to just the main grid cells. Must // end in a ';' // classes: String? // A space separated list of classes to apply to both the header // cell and the main grid cells. // headerClasses: String? // A space separated list of classes to apply to just the header // cell. // cellClasses: String? // A space separated list of classes to apply to just the main // grid cells. // attrs: String? // A space separated string of attribute='value' pairs to add to // the header cell element and main grid cell elements. this.name = name; this.value = value; this.get = get; this.formatter = formatter; this.type = type; this.editable = editable; this.hidden = hidden; this.width = width; this.colSpan = colSpan; this.rowSpan = rowSpan; this.styles = styles; this.headerStyles = headerStyles; this.cellStyles = cellStyles; this.classes = classes; this.headerClasses = headerClasses; this.cellClasses = cellClasses; this.attrs = attrs; } =====*/ /*===== dojox.grid.__ViewDef = function(){ // noscroll: Boolean? // If true, no scrollbars will be rendered without scrollbars. // width: Integer|String? // A CSS size. If it's an Integer, the width will be in em's. If // "noscroll" is true, this value is ignored. // cells: dojox.grid.__CellDef[]|Array[dojox.grid.__CellDef[]]? // The structure of the cells within this grid. // type: String? // A string containing the constructor of a subclass of // dojox.grid._View. If this is not specified, dojox.grid._View // is used. // defaultCell: dojox.grid.__CellDef? // A cell definition with default values for all cells in this view. If // a property is defined in a cell definition in the "cells" array and // this property, the cell definition's property will override this // property's property. // onBeforeRow: Function? // function(rowIndex, cells){} rowIndex is of type Integer, cells // is of type Array[dojox.grid.__CellDef[]]. This function is called // before each row of data is rendered. Before the header is // rendered, rowIndex will be -1. "cells" is a reference to the // internal structure of this view's cells so any changes you make to // it will persist between calls. // onAfterRow: Function? // function(rowIndex, cells, rowNode){} rowIndex is of type Integer, cells // is of type Array[dojox.grid.__CellDef[]], rowNode is of type DOMNode. // This function is called after each row of data is rendered. After the // header is rendered, rowIndex will be -1. "cells" is a reference to the // internal structure of this view's cells so any changes you make to // it will persist between calls. this.noscroll = noscroll; this.width = width; this.cells = cells; this.type = type; this.defaultCell = defaultCell; this.onBeforeRow = onBeforeRow; this.onAfterRow = onAfterRow; } =====*/ dojo.declare('dojox.grid._Grid', [ dijit._Widget, dijit._Templated, dojox.grid._Events ], { // summary: // A grid widget with virtual scrolling, cell editing, complex rows, // sorting, fixed columns, sizeable columns, etc. // // description: // _Grid provides the full set of grid features without any // direct connection to a data store. // // The grid exposes a get function for the grid, or optionally // individual columns, to populate cell contents. // // The grid is rendered based on its structure, an object describing // column and cell layout. // // example: // A quick sample: // // define a get function // | function get(inRowIndex){ // called in cell context // | return [this.index, inRowIndex].join(', '); // | } // // define the grid structure: // | var structure = [ // array of view objects // | { cells: [// array of rows, a row is an array of cells // | [ // | { name: "Alpha", width: 6 }, // | { name: "Beta" }, // | { name: "Gamma", get: get }] // | ]} // | ]; // // |
templatePath: dojo.moduleUrl("dojox.grid","resources/_Grid.html"), // classTag: String // CSS class applied to the grid's domNode classTag: 'dojoxGrid', get: function(inRowIndex){ // summary: Default data getter. // description: // Provides data to display in a grid cell. Called in grid cell context. // So this.cell.index is the column index. // inRowIndex: Integer // Row for which to provide data // returns: // Data to display for a given grid cell. }, // settings // rowCount: Integer // Number of rows to display. rowCount: 5, // keepRows: Integer // Number of rows to keep in the rendering cache. keepRows: 75, // rowsPerPage: Integer // Number of rows to render at a time. rowsPerPage: 25, // autoWidth: Boolean // If autoWidth is true, grid width is automatically set to fit the data. autoWidth: false, // autoHeight: Boolean|Integer // If autoHeight is true, grid height is automatically set to fit the data. // If it is an integer, the height will be automatically set to fit the data // if there are fewer than that many rows - and the height will be set to show // that many rows if there are more autoHeight: '', // autoRender: Boolean // If autoRender is true, grid will render itself after initialization. autoRender: true, // defaultHeight: String // default height of the grid, measured in any valid css unit. defaultHeight: '15em', // height: String // explicit height of the grid, measured in any valid css unit. This will be populated (and overridden) // if the height: css attribute exists on the source node. height: '', // structure: dojox.grid.__ViewDef|dojox.grid.__ViewDef[]|dojox.grid.__CellDef[]|Array[dojox.grid.__CellDef[]] // View layout defintion. structure: null, // elasticView: Integer // Override defaults and make the indexed grid view elastic, thus filling available horizontal space. elasticView: -1, // singleClickEdit: boolean // Single-click starts editing. Default is double-click singleClickEdit: false, // selectionMode: String // Set the selection mode of grid's Selection. Value must be 'single', 'multiple', // or 'extended'. Default is 'extended'. selectionMode: 'extended', // rowSelector: Boolean|String // If set to true, will add a row selector view to this grid. If set to a CSS width, will add // a row selector of that width to this grid. rowSelector: '', // columnReordering: Boolean // If set to true, will add drag and drop reordering to views with one row of columns. columnReordering: false, // headerMenu: dijit.Menu // If set to a dijit.Menu, will use this as a context menu for the grid headers. headerMenu: null, // placeholderLabel: String // Label of placeholders to search for in the header menu to replace with column toggling // menu items. placeholderLabel: "GridColumns", // selectable: Boolean // Set to true if you want to be able to select the text within the grid. selectable: false, // Used to store the last two clicks, to ensure double-clicking occurs based on the intended row _click: null, // loadingMessage: String // Message that shows while the grid is loading loadingMessage: "${loadingState}", // errorMessage: String // Message that shows when the grid encounters an error loading errorMessage: "${errorState}", // noDataMessage: String // Message that shows if the grid has no data - wrap it in a // span with class 'dojoxGridNoData' if you want it to be // styled similar to the loading and error messages noDataMessage: "", // escapeHTMLInData: Boolean // This will escape HTML brackets from the data to prevent HTML from // user-inputted data being rendered with may contain JavaScript and result in // XSS attacks. This is true by default, and it is recommended that it remain // true. Setting this to false will allow data to be displayed in the grid without // filtering, and should be only used if it is known that the data won't contain // malicious scripts. If HTML is needed in grid cells, it is recommended that // you use the formatter function to generate the HTML (the output of // formatter functions is not filtered, even with escapeHTMLInData set to true). escapeHTMLInData: true, // private sortInfo: 0, themeable: true, _placeholders: null, // initialization buildRendering: function(){ this.inherited(arguments); // reset get from blank function (needed for markup parsing) to null, if not changed if(this.get == dojox.grid._Grid.prototype.get){ this.get = null; } if(!this.domNode.getAttribute('tabIndex')){ this.domNode.tabIndex = "0"; } this.createScroller(); this.createLayout(); this.createViews(); this.createManagers(); this.createSelection(); this.connect(this.selection, "onSelected", "onSelected"); this.connect(this.selection, "onDeselected", "onDeselected"); this.connect(this.selection, "onChanged", "onSelectionChanged"); dojox.html.metrics.initOnFontResize(); this.connect(dojox.html.metrics, "onFontResize", "textSizeChanged"); dojox.grid.util.funnelEvents(this.domNode, this, 'doKeyEvent', dojox.grid.util.keyEvents); this.connect(this, "onShow", "renderOnIdle"); }, postMixInProperties: function(){ this.inherited(arguments); var messages = dojo.i18n.getLocalization("dijit", "loading", this.lang); this.loadingMessage = dojo.string.substitute(this.loadingMessage, messages); this.errorMessage = dojo.string.substitute(this.errorMessage, messages); if(this.srcNodeRef && this.srcNodeRef.style.height){ this.height = this.srcNodeRef.style.height; } // Call this to update our autoheight to start out this._setAutoHeightAttr(this.autoHeight, true); }, postCreate: function(){ // replace stock styleChanged with one that triggers an update this.styleChanged = this._styleChanged; this._placeholders = []; this._setHeaderMenuAttr(this.headerMenu); this._setStructureAttr(this.structure); this._click = []; }, destroy: function(){ this.domNode.onReveal = null; this.domNode.onSizeChange = null; // Fixes IE domNode leak delete this._click; this.edit.destroy(); delete this.edit; this.views.destroyViews(); if(this.scroller){ this.scroller.destroy(); delete this.scroller; } if(this.focus){ this.focus.destroy(); delete this.focus; } if(this.headerMenu&&this._placeholders.length){ dojo.forEach(this._placeholders, function(p){ p.unReplace(true); }); this.headerMenu.unBindDomNode(this.viewsHeaderNode); } this.inherited(arguments); }, _setAutoHeightAttr: function(ah, skipRender){ // Calculate our autoheight - turn it into a boolean or an integer if(typeof ah == "string"){ if(!ah || ah == "false"){ ah = false; }else if (ah == "true"){ ah = true; }else{ ah = window.parseInt(ah, 10); } } if(typeof ah == "number"){ if(isNaN(ah)){ ah = false; } // Autoheight must be at least 1, if it's a number. If it's // less than 0, we'll take that to mean "all" rows (same as // autoHeight=true - if it is equal to zero, we'll take that // to mean autoHeight=false if(ah < 0){ ah = true; }else if (ah === 0){ ah = false; } } this.autoHeight = ah; if(typeof ah == "boolean"){ this._autoHeight = ah; }else if(typeof ah == "number"){ this._autoHeight = (ah >= this.attr('rowCount')); }else{ this._autoHeight = false; } if(this._started && !skipRender){ this.render(); } }, _getRowCountAttr: function(){ return this.updating && this.invalidated && this.invalidated.rowCount != undefined ? this.invalidated.rowCount : this.rowCount; }, styleChanged: function(){ this.setStyledClass(this.domNode, ''); }, _styleChanged: function(){ this.styleChanged(); this.update(); }, textSizeChanged: function(){ setTimeout(dojo.hitch(this, "_textSizeChanged"), 1); }, _textSizeChanged: function(){ if(this.domNode){ this.views.forEach(function(v){ v.content.update(); }); this.render(); } }, sizeChange: function(){ jobs.job(this.id + 'SizeChange', 50, dojo.hitch(this, "update")); }, renderOnIdle: function() { setTimeout(dojo.hitch(this, "render"), 1); }, createManagers: function(){ // summary: // create grid managers for various tasks including rows, focus, selection, editing // row manager this.rows = new dojox.grid._RowManager(this); // focus manager this.focus = new dojox.grid._FocusManager(this); // edit manager this.edit = new dojox.grid._EditManager(this); }, createSelection: function(){ // summary: Creates a new Grid selection manager. // selection manager this.selection = new dojox.grid.Selection(this); }, createScroller: function(){ // summary: Creates a new virtual scroller this.scroller = new dojox.grid._Scroller(); this.scroller.grid = this; this.scroller._pageIdPrefix = this.id + '-'; this.scroller.renderRow = dojo.hitch(this, "renderRow"); this.scroller.removeRow = dojo.hitch(this, "rowRemoved"); }, createLayout: function(){ // summary: Creates a new Grid layout this.layout = new dojox.grid._Layout(this); this.connect(this.layout, "moveColumn", "onMoveColumn"); }, onMoveColumn: function(){ this.render(); this._resize(); }, // views createViews: function(){ this.views = new dojox.grid._ViewManager(this); this.views.createView = dojo.hitch(this, "createView"); }, createView: function(inClass, idx){ var c = dojo.getObject(inClass); var view = new c({ grid: this, index: idx }); this.viewsNode.appendChild(view.domNode); this.viewsHeaderNode.appendChild(view.headerNode); this.views.addView(view); return view; }, buildViews: function(){ for(var i=0, vs; (vs=this.layout.structure[i]); i++){ this.createView(vs.type || dojox._scopeName + ".grid._View", i).setStructure(vs); } this.scroller.setContentNodes(this.views.getContentNodes()); }, _setStructureAttr: function(structure){ var s = structure; if(s && dojo.isString(s)){ dojo.deprecated("dojox.grid._Grid.attr('structure', 'objVar')", "use dojox.grid._Grid.attr('structure', objVar) instead", "2.0"); s=dojo.getObject(s); } this.structure = s; if(!s){ if(this.layout.structure){ s = this.layout.structure; }else{ return; } } this.views.destroyViews(); if(s !== this.layout.structure){ this.layout.setStructure(s); } this._structureChanged(); }, setStructure: function(/* dojox.grid.__ViewDef|dojox.grid.__ViewDef[]|dojox.grid.__CellDef[]|Array[dojox.grid.__CellDef[]] */ inStructure){ // summary: // Install a new structure and rebuild the grid. dojo.deprecated("dojox.grid._Grid.setStructure(obj)", "use dojox.grid._Grid.attr('structure', obj) instead.", "2.0"); this._setStructureAttr(inStructure); }, getColumnTogglingItems: function(){ // Summary: returns an array of dijit.CheckedMenuItem widgets that can be // added to a menu for toggling columns on and off. return dojo.map(this.layout.cells, function(cell){ if(!cell.menuItems){ cell.menuItems = []; } var self = this; var item = new dijit.CheckedMenuItem({ label: cell.name, checked: !cell.hidden, _gridCell: cell, onChange: function(checked){ if(self.layout.setColumnVisibility(this._gridCell.index, checked)){ var items = this._gridCell.menuItems; if(items.length > 1){ dojo.forEach(items, function(item){ if(item !== this){ item.setAttribute("checked", checked); } }, this); } var checked = dojo.filter(self.layout.cells, function(c){ if(c.menuItems.length > 1){ dojo.forEach(c.menuItems, "item.attr('disabled', false);"); }else{ c.menuItems[0].attr('disabled', false); } return !c.hidden; }); if(checked.length == 1){ dojo.forEach(checked[0].menuItems, "item.attr('disabled', true);"); } } }, destroy: function(){ var index = dojo.indexOf(this._gridCell.menuItems, this); this._gridCell.menuItems.splice(index, 1); delete this._gridCell; dijit.CheckedMenuItem.prototype.destroy.apply(this, arguments); } }); cell.menuItems.push(item); return item; }, this); // dijit.CheckedMenuItem[] }, _setHeaderMenuAttr: function(menu){ if(this._placeholders && this._placeholders.length){ dojo.forEach(this._placeholders, function(p){ p.unReplace(true); }); this._placeholders = []; } if(this.headerMenu){ this.headerMenu.unBindDomNode(this.viewsHeaderNode); } this.headerMenu = menu; if(!menu){ return; } this.headerMenu.bindDomNode(this.viewsHeaderNode); if(this.headerMenu.getPlaceholders){ this._placeholders = this.headerMenu.getPlaceholders(this.placeholderLabel); } }, setHeaderMenu: function(/* dijit.Menu */ menu){ dojo.deprecated("dojox.grid._Grid.setHeaderMenu(obj)", "use dojox.grid._Grid.attr('headerMenu', obj) instead.", "2.0"); this._setHeaderMenuAttr(menu); }, setupHeaderMenu: function(){ if(this._placeholders && this._placeholders.length){ dojo.forEach(this._placeholders, function(p){ if(p._replaced){ p.unReplace(true); } p.replace(this.getColumnTogglingItems()); }, this); } }, _fetch: function(start){ this.setScrollTop(0); }, getItem: function(inRowIndex){ return null; }, showMessage: function(message){ if(message){ this.messagesNode.innerHTML = message; this.messagesNode.style.display = ""; }else{ this.messagesNode.innerHTML = ""; this.messagesNode.style.display = "none"; } }, _structureChanged: function() { this.buildViews(); if(this.autoRender && this._started){ this.render(); } }, hasLayout: function() { return this.layout.cells.length; }, // sizing resize: function(changeSize, resultSize){ // summary: // Update the grid's rendering dimensions and resize it this._resize(changeSize, resultSize); this.sizeChange(); }, _getPadBorder: function() { this._padBorder = this._padBorder || dojo._getPadBorderExtents(this.domNode); return this._padBorder; }, _getHeaderHeight: function(){ var vns = this.viewsHeaderNode.style, t = vns.display == "none" ? 0 : this.views.measureHeader(); vns.height = t + 'px'; // header heights are reset during measuring so must be normalized after measuring. this.views.normalizeHeaderNodeHeight(); return t; }, _resize: function(changeSize, resultSize){ // if we have set up everything except the DOM, we cannot resize var pn = this.domNode.parentNode; if(!pn || pn.nodeType != 1 || !this.hasLayout() || pn.style.visibility == "hidden" || pn.style.display == "none"){ return; } // useful measurement var padBorder = this._getPadBorder(); var hh = 0; // grid height if(this._autoHeight){ this.domNode.style.height = 'auto'; this.viewsNode.style.height = ''; }else if(typeof this.autoHeight == "number"){ var h = hh = this._getHeaderHeight(); h += (this.scroller.averageRowHeight * this.autoHeight); this.domNode.style.height = h + "px"; }else if(this.flex > 0){ }else if(this.domNode.clientHeight <= padBorder.h){ if(pn == document.body){ this.domNode.style.height = this.defaultHeight; }else if(this.height){ this.domNode.style.height = this.height; }else{ this.fitTo = "parent"; } } // if we are given dimensions, size the grid's domNode to those dimensions if(resultSize){ changeSize = resultSize; } if(changeSize){ dojo.marginBox(this.domNode, changeSize); this.height = this.domNode.style.height; delete this.fitTo; }else if(this.fitTo == "parent"){ var h = dojo._getContentBox(pn).h; dojo.marginBox(this.domNode, { h: Math.max(0, h) }); } var h = dojo._getContentBox(this.domNode).h; if(h == 0 && !this._autoHeight){ // We need to hide the header, since the Grid is essentially hidden. this.viewsHeaderNode.style.display = "none"; }else{ // Otherwise, show the header and give it an appropriate height. this.viewsHeaderNode.style.display = "block"; hh = this._getHeaderHeight(); } // NOTE: it is essential that width be applied before height // Header height can only be calculated properly after view widths have been set. // This is because flex column width is naturally 0 in Firefox. // Therefore prior to width sizing flex columns with spaces are maximally wrapped // and calculated to be too tall. this.adaptWidth(); this.adaptHeight(hh); this.postresize(); }, adaptWidth: function() { // private: sets width and position for views and update grid width if necessary var w = this.autoWidth ? 0 : this.domNode.clientWidth || (this.domNode.offsetWidth - this._getPadBorder().w), vw = this.views.arrange(1, w); this.views.onEach("adaptWidth"); if (this.autoWidth) this.domNode.style.width = vw + "px"; }, adaptHeight: function(inHeaderHeight){ // private: measures and normalizes header height, then sets view heights, and then updates scroller // content extent var t = inHeaderHeight || this._getHeaderHeight(); var h = (this._autoHeight ? -1 : Math.max(this.domNode.clientHeight - t, 0) || 0); this.views.onEach('setSize', [0, h]); this.views.onEach('adaptHeight'); if(!this._autoHeight){ var numScroll = 0, numNoScroll = 0; var noScrolls = dojo.filter(this.views.views, function(v){ var has = v.hasHScrollbar(); if(has){ numScroll++; }else{ numNoScroll++; } return (!has); }); if(numScroll > 0 && numNoScroll > 0){ dojo.forEach(noScrolls, function(v){ v.adaptHeight(true); }); } } if(this.autoHeight === true || h != -1 || (typeof this.autoHeight == "number" && this.autoHeight >= this.attr('rowCount'))){ this.scroller.windowHeight = h; }else{ this.scroller.windowHeight = Math.max(this.domNode.clientHeight - t, 0); } }, // startup startup: function(){ if(this._started){return;} this.inherited(arguments); if(this.autoRender){ this.render(); } }, // render render: function(){ // summary: // Render the grid, headers, and views. Edit and scrolling states are reset. To retain edit and // scrolling states, see Update. if(!this.domNode){return;} if(!this._started){return;} if(!this.hasLayout()) { this.scroller.init(0, this.keepRows, this.rowsPerPage); return; } // this.update = this.defaultUpdate; this._render(); }, _render: function(){ this.scroller.init(this.attr('rowCount'), this.keepRows, this.rowsPerPage); this.prerender(); this.setScrollTop(0); this.postrender(); }, prerender: function(){ // if autoHeight, make sure scroller knows not to virtualize; everything must be rendered. this.keepRows = this._autoHeight ? 0 : this.keepRows; this.scroller.setKeepInfo(this.keepRows); this.views.render(); this._resize(); }, postrender: function(){ this.postresize(); this.focus.initFocusView(); // make rows unselectable dojo.setSelectable(this.domNode, this.selectable); }, postresize: function(){ // views are position absolute, so they do not inflate the parent if(this._autoHeight){ var size = Math.max(this.views.measureContent()) + 'px'; this.viewsNode.style.height = size; } }, renderRow: function(inRowIndex, inNodes){ // summary: private, used internally to render rows this.views.renderRow(inRowIndex, inNodes); }, rowRemoved: function(inRowIndex){ // summary: private, used internally to remove rows this.views.rowRemoved(inRowIndex); }, invalidated: null, updating: false, beginUpdate: function(){ // summary: // Use to make multiple changes to rows while queueing row updating. // NOTE: not currently supporting nested begin/endUpdate calls this.invalidated = []; this.updating = true; }, endUpdate: function(){ // summary: // Use after calling beginUpdate to render any changes made to rows. this.updating = false; var i = this.invalidated, r; if(i.all){ this.update(); }else if(i.rowCount != undefined){ this.updateRowCount(i.rowCount); }else{ for(r in i){ this.updateRow(Number(r)); } } this.invalidated = null; }, // update defaultUpdate: function(){ // note: initial update calls render and subsequently this function. if(!this.domNode){return;} if(this.updating){ this.invalidated.all = true; return; } //this.edit.saveState(inRowIndex); var lastScrollTop = this.scrollTop; this.prerender(); this.scroller.invalidateNodes(); this.setScrollTop(lastScrollTop); this.postrender(); //this.edit.restoreState(inRowIndex); }, update: function(){ // summary: // Update the grid, retaining edit and scrolling states. this.render(); }, updateRow: function(inRowIndex){ // summary: // Render a single row. // inRowIndex: Integer // Index of the row to render inRowIndex = Number(inRowIndex); if(this.updating){ this.invalidated[inRowIndex]=true; }else{ this.views.updateRow(inRowIndex); this.scroller.rowHeightChanged(inRowIndex); } }, updateRows: function(startIndex, howMany){ // summary: // Render consecutive rows at once. // startIndex: Integer // Index of the starting row to render // howMany: Integer // How many rows to update. startIndex = Number(startIndex); howMany = Number(howMany); if(this.updating){ for(var i=0; i