[483] | 1 | define([ |
---|
| 2 | "dojo/_base/kernel", |
---|
| 3 | "../main", |
---|
| 4 | "dojo/_base/declare", |
---|
| 5 | "dojo/_base/array", |
---|
| 6 | "dojo/_base/lang", |
---|
| 7 | "dojo/_base/event", |
---|
| 8 | "dojo/dom-attr", |
---|
| 9 | "dojo/dom-class", |
---|
| 10 | "dojo/query", |
---|
| 11 | "dojo/keys", |
---|
| 12 | "dijit/tree/ForestStoreModel", |
---|
| 13 | "./DataGrid", |
---|
| 14 | "./_Layout", |
---|
| 15 | "./_FocusManager", |
---|
| 16 | "./_RowManager", |
---|
| 17 | "./_EditManager", |
---|
| 18 | "./TreeSelection", |
---|
| 19 | "./cells/tree", |
---|
| 20 | "./_TreeView" |
---|
| 21 | ], function(dojo, dojox, declare, array, lang, event, domAttr, domClass, query, keys, ForestStoreModel, |
---|
| 22 | DataGrid, _Layout, _FocusManager, _RowManager, _EditManager, TreeSelection, TreeCell){ |
---|
| 23 | |
---|
| 24 | dojo.experimental("dojox.grid.TreeGrid"); |
---|
| 25 | |
---|
| 26 | var _TreeAggregator = declare("dojox.grid._TreeAggregator", null, { |
---|
| 27 | cells: [], |
---|
| 28 | grid: null, |
---|
| 29 | childFields: [], |
---|
| 30 | |
---|
| 31 | constructor: function(kwArgs){ |
---|
| 32 | this.cells = kwArgs.cells || []; |
---|
| 33 | this.childFields = kwArgs.childFields || []; |
---|
| 34 | this.grid = kwArgs.grid; |
---|
| 35 | this.store = this.grid.store; |
---|
| 36 | }, |
---|
| 37 | _cacheValue: function(cache, id, value){ |
---|
| 38 | cache[id] = value; |
---|
| 39 | return value; |
---|
| 40 | }, |
---|
| 41 | clearSubtotalCache: function(){ |
---|
| 42 | // summary: |
---|
| 43 | // Clears the subtotal cache so that we are forced to recalc it |
---|
| 44 | // (or reread it) again. This is needed, for example, when |
---|
| 45 | // column order is changed. |
---|
| 46 | if(this.store){ |
---|
| 47 | delete this.store._cachedAggregates; |
---|
| 48 | } |
---|
| 49 | }, |
---|
| 50 | |
---|
| 51 | cnt: function(cell, level, item){ |
---|
| 52 | // summary: |
---|
| 53 | // calculates the count of the children of item at the given level |
---|
| 54 | var total = 0; |
---|
| 55 | var store = this.store; |
---|
| 56 | var childFields = this.childFields; |
---|
| 57 | if(childFields[level]){ |
---|
| 58 | var children = store.getValues(item, childFields[level]); |
---|
| 59 | if (cell.index <= level + 1){ |
---|
| 60 | total = children.length; |
---|
| 61 | }else{ |
---|
| 62 | array.forEach(children, function(c){ |
---|
| 63 | total += this.getForCell(cell, level + 1, c, "cnt"); |
---|
| 64 | }, this); |
---|
| 65 | } |
---|
| 66 | }else{ |
---|
| 67 | total = 1; |
---|
| 68 | } |
---|
| 69 | return total; |
---|
| 70 | }, |
---|
| 71 | sum: function(cell, level, item){ |
---|
| 72 | // summary: |
---|
| 73 | // calculates the sum of the children of item at the given level |
---|
| 74 | var total = 0; |
---|
| 75 | var store = this.store; |
---|
| 76 | var childFields = this.childFields; |
---|
| 77 | if(childFields[level]){ |
---|
| 78 | array.forEach(store.getValues(item, childFields[level]), function(c){ |
---|
| 79 | total += this.getForCell(cell, level + 1, c, "sum"); |
---|
| 80 | }, this); |
---|
| 81 | }else{ |
---|
| 82 | total += store.getValue(item, cell.field); |
---|
| 83 | } |
---|
| 84 | return total; |
---|
| 85 | }, |
---|
| 86 | value: function(cell, level, item){ |
---|
| 87 | // summary: |
---|
| 88 | // Empty function so that we can set "aggregate='value'" to |
---|
| 89 | // force loading from the data - and bypass calculating |
---|
| 90 | }, |
---|
| 91 | getForCell: function(cell, level, item, type){ |
---|
| 92 | // summary: |
---|
| 93 | // Gets the value of the given cell at the given level and type. |
---|
| 94 | // type can be one of "sum", "cnt", or "value". If itemAggregates |
---|
| 95 | // is set and can be used, it is used instead. Values are also |
---|
| 96 | // cached to prevent calculating them too often. |
---|
| 97 | var store = this.store; |
---|
| 98 | if(!store || !item || !store.isItem(item)){ return ""; } |
---|
| 99 | var storeCache = store._cachedAggregates = store._cachedAggregates || {}; |
---|
| 100 | var id = store.getIdentity(item); |
---|
| 101 | var itemCache = storeCache[id] = storeCache[id] || []; |
---|
| 102 | if(!cell.getOpenState){ |
---|
| 103 | cell = this.grid.getCell(cell.layoutIndex + level + 1); |
---|
| 104 | } |
---|
| 105 | var idx = cell.index; |
---|
| 106 | var idxCache = itemCache[idx] = itemCache[idx] || {}; |
---|
| 107 | type = (type || (cell.parentCell ? cell.parentCell.aggregate : "sum"))||"sum"; |
---|
| 108 | var attr = cell.field; |
---|
| 109 | if(attr == store.getLabelAttributes()[0]){ |
---|
| 110 | // If our attribute is one of the label attributes, we should |
---|
| 111 | // use cnt instead (since it makes no sense to do a sum of labels) |
---|
| 112 | type = "cnt"; |
---|
| 113 | } |
---|
| 114 | var typeCache = idxCache[type] = idxCache[type] || []; |
---|
| 115 | |
---|
| 116 | // See if we have it in our cache immediately for easy returning |
---|
| 117 | if(typeCache[level] != undefined){ |
---|
| 118 | return typeCache[level]; |
---|
| 119 | } |
---|
| 120 | |
---|
| 121 | // See if they have specified a valid field |
---|
| 122 | var field = ((cell.parentCell && cell.parentCell.itemAggregates) ? |
---|
| 123 | cell.parentCell.itemAggregates[cell.idxInParent] : "")||""; |
---|
| 124 | if(field && store.hasAttribute(item, field)){ |
---|
| 125 | return this._cacheValue(typeCache, level, store.getValue(item, field)); |
---|
| 126 | }else if(field){ |
---|
| 127 | return this._cacheValue(typeCache, level, 0); |
---|
| 128 | } |
---|
| 129 | |
---|
| 130 | // Calculate it |
---|
| 131 | return this._cacheValue(typeCache, level, this[type](cell, level, item)); |
---|
| 132 | } |
---|
| 133 | }); |
---|
| 134 | |
---|
| 135 | var _TreeLayout = declare("dojox.grid._TreeLayout", _Layout, { |
---|
| 136 | // Whether or not we are collapsable - this is calculated when we |
---|
| 137 | // set our structure. |
---|
| 138 | _isCollapsable: false, |
---|
| 139 | |
---|
| 140 | _getInternalStructure: function(inStructure){ |
---|
| 141 | // Create a "Tree View" with 1 row containing references for |
---|
| 142 | // each column (recursively) |
---|
| 143 | var g = this.grid; |
---|
| 144 | |
---|
| 145 | var s = inStructure; |
---|
| 146 | var cells = s[0].cells[0]; |
---|
| 147 | var tree = { |
---|
| 148 | type: "dojox.grid._TreeView", |
---|
| 149 | cells: [[]] |
---|
| 150 | }; |
---|
| 151 | var cFields = []; |
---|
| 152 | var maxLevels = 0; |
---|
| 153 | var getTreeCells = function(parentCell, level){ |
---|
| 154 | var children = parentCell.children; |
---|
| 155 | var cloneTreeCell = function(originalCell, idx){ |
---|
| 156 | var k, n = {}; |
---|
| 157 | for(k in originalCell){ |
---|
| 158 | n[k] = originalCell[k]; |
---|
| 159 | } |
---|
| 160 | n = lang.mixin(n, { |
---|
| 161 | level: level, |
---|
| 162 | idxInParent: level > 0 ? idx : -1, |
---|
| 163 | parentCell: level > 0 ? parentCell : null |
---|
| 164 | }); |
---|
| 165 | return n; |
---|
| 166 | }; |
---|
| 167 | var ret = []; |
---|
| 168 | array.forEach(children, function(c, idx){ |
---|
| 169 | if("children" in c){ |
---|
| 170 | cFields.push(c.field); |
---|
| 171 | var last = ret[ret.length - 1]; |
---|
| 172 | last.isCollapsable = true; |
---|
| 173 | c.level = level; |
---|
| 174 | ret = ret.concat(getTreeCells(c, level + 1)); |
---|
| 175 | }else{ |
---|
| 176 | ret.push(cloneTreeCell(c, idx)); |
---|
| 177 | } |
---|
| 178 | }); |
---|
| 179 | maxLevels = Math.max(maxLevels, level); |
---|
| 180 | return ret; |
---|
| 181 | }; |
---|
| 182 | var tCell = {children: cells, itemAggregates: []}; |
---|
| 183 | tree.cells[0] = getTreeCells(tCell, 0); |
---|
| 184 | g.aggregator = new _TreeAggregator({cells: tree.cells[0], |
---|
| 185 | grid: g, |
---|
| 186 | childFields: cFields}); |
---|
| 187 | if(g.scroller && g.defaultOpen){ |
---|
| 188 | g.scroller.defaultRowHeight = g.scroller._origDefaultRowHeight * (2 * maxLevels + 1); |
---|
| 189 | } |
---|
| 190 | return [ tree ]; |
---|
| 191 | }, |
---|
| 192 | |
---|
| 193 | setStructure: function(inStructure){ |
---|
| 194 | // Mangle the structure a bit and make it work as desired |
---|
| 195 | var s = inStructure; |
---|
| 196 | var g = this.grid; |
---|
| 197 | // Only supporting single-view, single row or else we |
---|
| 198 | // are not collapsable |
---|
| 199 | if(g && g.treeModel && !array.every(s, function(i){ |
---|
| 200 | return ("cells" in i); |
---|
| 201 | })){ |
---|
| 202 | s = arguments[0] = [{cells:[s]}]; |
---|
| 203 | } |
---|
| 204 | if(s.length == 1 && s[0].cells.length == 1){ |
---|
| 205 | if(g && g.treeModel){ |
---|
| 206 | s[0].type = "dojox.grid._TreeView"; |
---|
| 207 | this._isCollapsable = true; |
---|
| 208 | s[0].cells[0][(this.grid.treeModel?this.grid.expandoCell:0)].isCollapsable = true; |
---|
| 209 | }else{ |
---|
| 210 | var childCells = array.filter(s[0].cells[0], function(c){ |
---|
| 211 | return ("children" in c); |
---|
| 212 | }); |
---|
| 213 | if(childCells.length === 1){ |
---|
| 214 | this._isCollapsable = true; |
---|
| 215 | } |
---|
| 216 | } |
---|
| 217 | } |
---|
| 218 | if(this._isCollapsable && (!g || !g.treeModel)){ |
---|
| 219 | arguments[0] = this._getInternalStructure(s); |
---|
| 220 | } |
---|
| 221 | this.inherited(arguments); |
---|
| 222 | }, |
---|
| 223 | |
---|
| 224 | addCellDef: function(inRowIndex, inCellIndex, inDef){ |
---|
| 225 | var obj = this.inherited(arguments); |
---|
| 226 | return lang.mixin(obj, TreeCell); |
---|
| 227 | } |
---|
| 228 | }); |
---|
| 229 | |
---|
| 230 | var TreePath = declare("dojox.grid.TreePath", null, { |
---|
| 231 | level: 0, |
---|
| 232 | _str: "", |
---|
| 233 | _arr: null, |
---|
| 234 | grid: null, |
---|
| 235 | store: null, |
---|
| 236 | cell: null, |
---|
| 237 | item: null, |
---|
| 238 | |
---|
| 239 | constructor: function(/*String|Integer[]|Integer|dojox.grid.TreePath*/ path, /*dojox.grid.TreeGrid*/ grid){ |
---|
| 240 | if(lang.isString(path)){ |
---|
| 241 | this._str = path; |
---|
| 242 | this._arr = array.map(path.split('/'), function(item){ return parseInt(item, 10); }); |
---|
| 243 | }else if(lang.isArray(path)){ |
---|
| 244 | this._str = path.join('/'); |
---|
| 245 | this._arr = path.slice(0); |
---|
| 246 | }else if(typeof path == "number"){ |
---|
| 247 | this._str = String(path); |
---|
| 248 | this._arr = [path]; |
---|
| 249 | }else{ |
---|
| 250 | this._str = path._str; |
---|
| 251 | this._arr = path._arr.slice(0); |
---|
| 252 | } |
---|
| 253 | this.level = this._arr.length-1; |
---|
| 254 | this.grid = grid; |
---|
| 255 | this.store = this.grid.store; |
---|
| 256 | if(grid.treeModel){ |
---|
| 257 | this.cell = grid.layout.cells[grid.expandoCell]; |
---|
| 258 | }else{ |
---|
| 259 | this.cell = grid.layout.cells[this.level]; |
---|
| 260 | } |
---|
| 261 | }, |
---|
| 262 | item: function(){ |
---|
| 263 | // summary: |
---|
| 264 | // gets the dojo.data item associated with this path |
---|
| 265 | if(!this._item){ |
---|
| 266 | this._item = this.grid.getItem(this._arr); |
---|
| 267 | } |
---|
| 268 | return this._item; |
---|
| 269 | }, |
---|
| 270 | compare: function(path /*dojox.grid.TreePath|String|Array*/){ |
---|
| 271 | // summary: |
---|
| 272 | // compares two paths |
---|
| 273 | if(lang.isString(path) || lang.isArray(path)){ |
---|
| 274 | if(this._str == path){ return 0; } |
---|
| 275 | if(path.join && this._str == path.join('/')){ return 0; } |
---|
| 276 | path = new TreePath(path, this.grid); |
---|
| 277 | }else if(path instanceof TreePath){ |
---|
| 278 | if(this._str == path._str){ return 0; } |
---|
| 279 | } |
---|
| 280 | for(var i=0, l=(this._arr.length < path._arr.length ? this._arr.length : path._arr.length); i<l; i++){ |
---|
| 281 | if(this._arr[i]<path._arr[i]){ return -1; } |
---|
| 282 | if(this._arr[i]>path._arr[i]){ return 1; } |
---|
| 283 | } |
---|
| 284 | if(this._arr.length<path._arr.length){ return -1; } |
---|
| 285 | if(this._arr.length>path._arr.length){ return 1; } |
---|
| 286 | return 0; |
---|
| 287 | }, |
---|
| 288 | isOpen: function(){ |
---|
| 289 | // summary: |
---|
| 290 | // Returns the open state of this cell. |
---|
| 291 | return this.cell.openStates && this.cell.getOpenState(this.item()); |
---|
| 292 | }, |
---|
| 293 | previous: function(){ |
---|
| 294 | // summary: |
---|
| 295 | // Returns the path that is before this path in the |
---|
| 296 | // grid. If no path is found, returns null. |
---|
| 297 | var new_path = this._arr.slice(0); |
---|
| 298 | |
---|
| 299 | if(this._str == "0"){ |
---|
| 300 | return null; |
---|
| 301 | } |
---|
| 302 | |
---|
| 303 | var last = new_path.length-1; |
---|
| 304 | |
---|
| 305 | if(new_path[last] === 0){ |
---|
| 306 | new_path.pop(); |
---|
| 307 | return new TreePath(new_path, this.grid); |
---|
| 308 | } |
---|
| 309 | |
---|
| 310 | new_path[last]--; |
---|
| 311 | var path = new TreePath(new_path, this.grid); |
---|
| 312 | return path.lastChild(true); |
---|
| 313 | }, |
---|
| 314 | next: function(){ |
---|
| 315 | // summary: |
---|
| 316 | // Returns the next path in the grid. If no path |
---|
| 317 | // is found, returns null. |
---|
| 318 | var new_path = this._arr.slice(0); |
---|
| 319 | |
---|
| 320 | if(this.isOpen()){ |
---|
| 321 | new_path.push(0); |
---|
| 322 | }else{ |
---|
| 323 | new_path[new_path.length-1]++; |
---|
| 324 | for(var i=this.level; i>=0; i--){ |
---|
| 325 | var item = this.grid.getItem(new_path.slice(0, i+1)); |
---|
| 326 | if(i>0){ |
---|
| 327 | if(!item){ |
---|
| 328 | new_path.pop(); |
---|
| 329 | new_path[i-1]++; |
---|
| 330 | } |
---|
| 331 | }else{ |
---|
| 332 | if(!item){ |
---|
| 333 | return null; |
---|
| 334 | } |
---|
| 335 | } |
---|
| 336 | } |
---|
| 337 | } |
---|
| 338 | |
---|
| 339 | return new TreePath(new_path, this.grid); |
---|
| 340 | }, |
---|
| 341 | children: function(alwaysReturn){ |
---|
| 342 | // summary: |
---|
| 343 | // Returns the child data items of this row. If this |
---|
| 344 | // row isn't open and alwaysReturn is falsey, returns null. |
---|
| 345 | if(!this.isOpen()&&!alwaysReturn){ |
---|
| 346 | return null; |
---|
| 347 | } |
---|
| 348 | var items = []; |
---|
| 349 | var model = this.grid.treeModel; |
---|
| 350 | if(model){ |
---|
| 351 | var item = this.item(); |
---|
| 352 | var store = model.store; |
---|
| 353 | if(!model.mayHaveChildren(item)){ |
---|
| 354 | return null; |
---|
| 355 | } |
---|
| 356 | array.forEach(model.childrenAttrs, function(attr){ |
---|
| 357 | items = items.concat(store.getValues(item, attr)); |
---|
| 358 | }); |
---|
| 359 | }else{ |
---|
| 360 | items = this.store.getValues(this.item(), this.grid.layout.cells[this.cell.level+1].parentCell.field); |
---|
| 361 | if(items.length>1&&this.grid.sortChildItems){ |
---|
| 362 | var sortProps = this.grid.getSortProps(); |
---|
| 363 | if(sortProps&&sortProps.length){ |
---|
| 364 | var attr = sortProps[0].attribute, |
---|
| 365 | grid = this.grid; |
---|
| 366 | if(attr&&items[0][attr]){ |
---|
| 367 | var desc = !!sortProps[0].descending; |
---|
| 368 | items = items.slice(0); // don't touch the array in the store, make a copy |
---|
| 369 | items.sort(function(a, b){ |
---|
| 370 | return grid._childItemSorter(a, b, attr, desc); |
---|
| 371 | }); |
---|
| 372 | } |
---|
| 373 | } |
---|
| 374 | } |
---|
| 375 | } |
---|
| 376 | return items; |
---|
| 377 | }, |
---|
| 378 | childPaths: function(){ |
---|
| 379 | var childItems = this.children(); |
---|
| 380 | if(!childItems){ |
---|
| 381 | return []; |
---|
| 382 | } |
---|
| 383 | return array.map(childItems, function(item, index){ |
---|
| 384 | return new TreePath(this._str + '/' + index, this.grid); |
---|
| 385 | }, this); |
---|
| 386 | }, |
---|
| 387 | parent: function(){ |
---|
| 388 | // summary: |
---|
| 389 | // Returns the parent path of this path. If this is a |
---|
| 390 | // top-level row, returns null. |
---|
| 391 | if(this.level === 0){ |
---|
| 392 | return null; |
---|
| 393 | } |
---|
| 394 | return new TreePath(this._arr.slice(0, this.level), this.grid); |
---|
| 395 | }, |
---|
| 396 | lastChild: function(/*Boolean?*/ traverse){ |
---|
| 397 | // summary: |
---|
| 398 | // Returns the last child row below this path. If traverse |
---|
| 399 | // is true, will traverse down to find the last child row |
---|
| 400 | // of this branch. If there are no children, returns itself. |
---|
| 401 | var children = this.children(); |
---|
| 402 | if(!children || !children.length){ |
---|
| 403 | return this; |
---|
| 404 | } |
---|
| 405 | var path = new TreePath(this._str + "/" + String(children.length-1), this.grid); |
---|
| 406 | if(!traverse){ |
---|
| 407 | return path; |
---|
| 408 | } |
---|
| 409 | return path.lastChild(true); |
---|
| 410 | }, |
---|
| 411 | toString: function(){ |
---|
| 412 | return this._str; |
---|
| 413 | } |
---|
| 414 | }); |
---|
| 415 | |
---|
| 416 | var _TreeFocusManager = declare("dojox.grid._TreeFocusManager", _FocusManager, { |
---|
| 417 | setFocusCell: function(inCell, inRowIndex){ |
---|
| 418 | if(inCell && inCell.getNode(inRowIndex)){ |
---|
| 419 | this.inherited(arguments); |
---|
| 420 | } |
---|
| 421 | }, |
---|
| 422 | isLastFocusCell: function(){ |
---|
| 423 | if(this.cell && this.cell.index == this.grid.layout.cellCount-1){ |
---|
| 424 | var path = new TreePath(this.grid.rowCount-1, this.grid); |
---|
| 425 | path = path.lastChild(true); |
---|
| 426 | return this.rowIndex == path._str; |
---|
| 427 | } |
---|
| 428 | return false; |
---|
| 429 | }, |
---|
| 430 | next: function(){ |
---|
| 431 | // summary: |
---|
| 432 | // focus next grid cell |
---|
| 433 | if(this.cell){ |
---|
| 434 | var row=this.rowIndex, col=this.cell.index+1, cc=this.grid.layout.cellCount-1; |
---|
| 435 | var path = new TreePath(this.rowIndex, this.grid); |
---|
| 436 | if(col > cc){ |
---|
| 437 | var new_path = path.next(); |
---|
| 438 | if(!new_path){ |
---|
| 439 | col--; |
---|
| 440 | }else{ |
---|
| 441 | col = 0; |
---|
| 442 | path = new_path; |
---|
| 443 | } |
---|
| 444 | } |
---|
| 445 | if(this.grid.edit.isEditing()){ //when editing, only navigate to editable cells |
---|
| 446 | var nextCell = this.grid.getCell(col); |
---|
| 447 | if (!this.isLastFocusCell() && !nextCell.editable){ |
---|
| 448 | this._focusifyCellNode(false); |
---|
| 449 | this.cell=nextCell; |
---|
| 450 | this.rowIndex=path._str; |
---|
| 451 | this.next(); |
---|
| 452 | return; |
---|
| 453 | } |
---|
| 454 | } |
---|
| 455 | this.setFocusIndex(path._str, col); |
---|
| 456 | } |
---|
| 457 | }, |
---|
| 458 | previous: function(){ |
---|
| 459 | // summary: |
---|
| 460 | // focus previous grid cell |
---|
| 461 | if(this.cell){ |
---|
| 462 | var row=(this.rowIndex || 0), col=(this.cell.index || 0) - 1; |
---|
| 463 | var path = new TreePath(row, this.grid); |
---|
| 464 | if(col < 0){ |
---|
| 465 | var new_path = path.previous(); |
---|
| 466 | if(!new_path){ |
---|
| 467 | col = 0; |
---|
| 468 | }else{ |
---|
| 469 | col = this.grid.layout.cellCount-1; |
---|
| 470 | path = new_path; |
---|
| 471 | } |
---|
| 472 | } |
---|
| 473 | if(this.grid.edit.isEditing()){ //when editing, only navigate to editable cells |
---|
| 474 | var prevCell = this.grid.getCell(col); |
---|
| 475 | if (!this.isFirstFocusCell() && !prevCell.editable){ |
---|
| 476 | this._focusifyCellNode(false); |
---|
| 477 | this.cell=prevCell; |
---|
| 478 | this.rowIndex=path._str; |
---|
| 479 | this.previous(); |
---|
| 480 | return; |
---|
| 481 | } |
---|
| 482 | } |
---|
| 483 | this.setFocusIndex(path._str, col); |
---|
| 484 | } |
---|
| 485 | }, |
---|
| 486 | move: function(inRowDelta, inColDelta){ |
---|
| 487 | if(this.isNavHeader()){ |
---|
| 488 | this.inherited(arguments); |
---|
| 489 | return; |
---|
| 490 | } |
---|
| 491 | if(!this.cell){ return; } |
---|
| 492 | // Handle grid proper. |
---|
| 493 | var sc = this.grid.scroller, |
---|
| 494 | r = this.rowIndex, |
---|
| 495 | rc = this.grid.rowCount-1, |
---|
| 496 | path = new TreePath(this.rowIndex, this.grid); |
---|
| 497 | if(inRowDelta){ |
---|
| 498 | var row; |
---|
| 499 | if(inRowDelta>0){ |
---|
| 500 | path = path.next(); |
---|
| 501 | row = path._arr[0]; |
---|
| 502 | if(row > sc.getLastPageRow(sc.page)){ |
---|
| 503 | //need to load additional data, let scroller do that |
---|
| 504 | this.grid.setScrollTop(this.grid.scrollTop+sc.findScrollTop(row)-sc.findScrollTop(r)); |
---|
| 505 | } |
---|
| 506 | }else if(inRowDelta<0){ |
---|
| 507 | path = path.previous(); |
---|
| 508 | row = path._arr[0]; |
---|
| 509 | if(row <= sc.getPageRow(sc.page)){ |
---|
| 510 | //need to load additional data, let scroller do that |
---|
| 511 | this.grid.setScrollTop(this.grid.scrollTop-sc.findScrollTop(r)-sc.findScrollTop(row)); |
---|
| 512 | } |
---|
| 513 | } |
---|
| 514 | } |
---|
| 515 | var cc = this.grid.layout.cellCount-1, |
---|
| 516 | i = this.cell.index, |
---|
| 517 | col = Math.min(cc, Math.max(0, i+inColDelta)); |
---|
| 518 | var cell = this.grid.getCell(col); |
---|
| 519 | var colDir = inColDelta < 0 ? -1 : 1; |
---|
| 520 | while(col>=0 && col < cc && cell && cell.hidden === true){ |
---|
| 521 | // skip hidden cells |
---|
| 522 | col += colDir; |
---|
| 523 | cell = this.grid.getCell(col); |
---|
| 524 | } |
---|
| 525 | if (!cell || cell.hidden === true){ |
---|
| 526 | // don't change col if would move to hidden |
---|
| 527 | col = i; |
---|
| 528 | } |
---|
| 529 | if(inRowDelta){ |
---|
| 530 | this.grid.updateRow(r); |
---|
| 531 | } |
---|
| 532 | this.setFocusIndex(path._str, col); |
---|
| 533 | } |
---|
| 534 | }); |
---|
| 535 | |
---|
| 536 | var TreeGrid = declare("dojox.grid.TreeGrid", DataGrid, { |
---|
| 537 | // summary: |
---|
| 538 | // A grid that supports nesting rows - it provides an expando function |
---|
| 539 | // similar to dijit.Tree. It also provides mechanisms for aggregating |
---|
| 540 | // the values of subrows |
---|
| 541 | // description: |
---|
| 542 | // TreeGrid currently only works on "simple" structures. That is, |
---|
| 543 | // single-view structures with a single row in them. |
---|
| 544 | // |
---|
| 545 | // The TreeGrid works using the concept of "levels" - level 0 are the |
---|
| 546 | // top-level items. |
---|
| 547 | |
---|
| 548 | // defaultOpen: Boolean |
---|
| 549 | // Whether or not we default to open (all levels). This defaults to |
---|
| 550 | // false for grids with a treeModel. |
---|
| 551 | defaultOpen: true, |
---|
| 552 | |
---|
| 553 | // sortChildItems: Boolean |
---|
| 554 | // If true, child items will be returned sorted according to the sorting |
---|
| 555 | // properties of the grid. |
---|
| 556 | sortChildItems: false, |
---|
| 557 | |
---|
| 558 | // openAtLevels: Array |
---|
| 559 | // Which levels we are open at (overrides defaultOpen for the values |
---|
| 560 | // that exist here). Its values can be a boolean (true/false) or an |
---|
| 561 | // integer (for the # of children to be closed if there are more than |
---|
| 562 | // that) |
---|
| 563 | openAtLevels: [], |
---|
| 564 | |
---|
| 565 | // treeModel: dijit.tree.ForestStoreModel |
---|
| 566 | // A dijit.Tree model that will be used instead of using aggregates. |
---|
| 567 | // Setting this value will make the TreeGrid behave like a columnar |
---|
| 568 | // tree. When setting this value, defaultOpen will default to false, |
---|
| 569 | // and openAtLevels will be ignored. |
---|
| 570 | treeModel: null, |
---|
| 571 | |
---|
| 572 | // expandoCell: Integer |
---|
| 573 | // When used in conjunction with a treeModel (see above), this is a 0-based |
---|
| 574 | // index of the cell in which to place the actual expando |
---|
| 575 | expandoCell: 0, |
---|
| 576 | |
---|
| 577 | // aggregator: Object |
---|
| 578 | // The aggregator class - it will be populated automatically if we |
---|
| 579 | // are a collapsible grid |
---|
| 580 | aggregator: null, |
---|
| 581 | |
---|
| 582 | |
---|
| 583 | // Override this to get our "magic" layout |
---|
| 584 | _layoutClass: _TreeLayout, |
---|
| 585 | |
---|
| 586 | createSelection: function(){ |
---|
| 587 | this.selection = new TreeSelection(this); |
---|
| 588 | }, |
---|
| 589 | |
---|
| 590 | _childItemSorter: function(a, b, attribute, descending){ |
---|
| 591 | var av = this.store.getValue(a, attribute); |
---|
| 592 | var bv = this.store.getValue(b, attribute); |
---|
| 593 | if(av != bv){ |
---|
| 594 | return av < bv == descending ? 1 : -1; |
---|
| 595 | } |
---|
| 596 | return 0; |
---|
| 597 | }, |
---|
| 598 | |
---|
| 599 | _onNew: function(item, parentInfo){ |
---|
| 600 | if(!parentInfo || !parentInfo.item){ |
---|
| 601 | this.inherited(arguments); |
---|
| 602 | }else{ |
---|
| 603 | var idx = this.getItemIndex(parentInfo.item); |
---|
| 604 | if(typeof idx == "string"){ |
---|
| 605 | this.updateRow(idx.split('/')[0]); |
---|
| 606 | }else if(idx > -1){ |
---|
| 607 | this.updateRow(idx); |
---|
| 608 | } |
---|
| 609 | } |
---|
| 610 | }, |
---|
| 611 | |
---|
| 612 | _onSet: function(item, attribute, oldValue, newValue){ |
---|
| 613 | this._checkUpdateStatus(); |
---|
| 614 | if(this.aggregator){ |
---|
| 615 | this.aggregator.clearSubtotalCache(); |
---|
| 616 | } |
---|
| 617 | var idx = this.getItemIndex(item); |
---|
| 618 | if(typeof idx == "string"){ |
---|
| 619 | this.updateRow(idx.split('/')[0]); |
---|
| 620 | }else if(idx > -1){ |
---|
| 621 | this.updateRow(idx); |
---|
| 622 | } |
---|
| 623 | }, |
---|
| 624 | |
---|
| 625 | _onDelete: function(item){ |
---|
| 626 | this._cleanupExpandoCache(this._getItemIndex(item, true), this.store.getIdentity(item), item); |
---|
| 627 | this.inherited(arguments); |
---|
| 628 | }, |
---|
| 629 | |
---|
| 630 | _clearData: function() { |
---|
| 631 | this.inherited(arguments); |
---|
| 632 | this._by_idty_paths = {}; |
---|
| 633 | }, |
---|
| 634 | |
---|
| 635 | _cleanupExpandoCache: function(index, identity, item){}, |
---|
| 636 | |
---|
| 637 | _addItem: function(item, index, noUpdate, dontUpdateRoot){ |
---|
| 638 | // add our root items to the root of the model's children |
---|
| 639 | // list since we don't query the model |
---|
| 640 | if(!dontUpdateRoot && this.model && array.indexOf(this.model.root.children, item) == -1){ |
---|
| 641 | this.model.root.children[index] = item; |
---|
| 642 | } |
---|
| 643 | this.inherited(arguments); |
---|
| 644 | }, |
---|
| 645 | |
---|
| 646 | getItem: function(/*integer|Array|String*/ idx){ |
---|
| 647 | // summary: |
---|
| 648 | // overridden so that you can pass in a '/' delimited string of indexes to get the |
---|
| 649 | // item based off its path...that is, passing in "1/3/2" will get the |
---|
| 650 | // 3rd (0-based) child from the 4th child of the 2nd top-level item. |
---|
| 651 | var isArray = lang.isArray(idx); |
---|
| 652 | if(lang.isString(idx) && idx.indexOf('/')){ |
---|
| 653 | idx = idx.split('/'); |
---|
| 654 | isArray = true; |
---|
| 655 | } |
---|
| 656 | if(isArray && idx.length == 1){ |
---|
| 657 | idx = idx[0]; |
---|
| 658 | isArray = false; |
---|
| 659 | } |
---|
| 660 | if(!isArray){ |
---|
| 661 | return DataGrid.prototype.getItem.call(this, idx); |
---|
| 662 | } |
---|
| 663 | var s = this.store; |
---|
| 664 | var itm = DataGrid.prototype.getItem.call(this, idx[0]); |
---|
| 665 | var cf, i, j; |
---|
| 666 | if(this.aggregator){ |
---|
| 667 | cf = this.aggregator.childFields||[]; |
---|
| 668 | if(cf){ |
---|
| 669 | for(i = 0; i < idx.length - 1 && itm; i++){ |
---|
| 670 | if(cf[i]){ |
---|
| 671 | itm = (s.getValues(itm, cf[i])||[])[idx[i + 1]]; |
---|
| 672 | }else{ |
---|
| 673 | itm = null; |
---|
| 674 | } |
---|
| 675 | } |
---|
| 676 | } |
---|
| 677 | }else if(this.treeModel){ |
---|
| 678 | cf = this.treeModel.childrenAttrs||[]; |
---|
| 679 | if(cf&&itm){ |
---|
| 680 | for(i=1, il=idx.length; (i<il) && itm; i++) { |
---|
| 681 | for(j=0, jl=cf.length; j<jl; j++) { |
---|
| 682 | if(cf[j]){ |
---|
| 683 | itm = (s.getValues(itm, cf[j])||[])[idx[i]]; |
---|
| 684 | }else{ |
---|
| 685 | itm = null; |
---|
| 686 | } |
---|
| 687 | if(itm){ break; } |
---|
| 688 | } |
---|
| 689 | } |
---|
| 690 | } |
---|
| 691 | } |
---|
| 692 | return itm || null; |
---|
| 693 | }, |
---|
| 694 | |
---|
| 695 | _getItemIndex: function(item, isDeleted){ |
---|
| 696 | if(!isDeleted && !this.store.isItem(item)){ |
---|
| 697 | return -1; |
---|
| 698 | } |
---|
| 699 | var idx = this.inherited(arguments); |
---|
| 700 | if(idx == -1){ |
---|
| 701 | var idty = this.store.getIdentity(item); |
---|
| 702 | return this._by_idty_paths[idty] || -1; |
---|
| 703 | } |
---|
| 704 | return idx; |
---|
| 705 | }, |
---|
| 706 | |
---|
| 707 | postMixInProperties: function(){ |
---|
| 708 | if(this.treeModel && !("defaultOpen" in this.params)){ |
---|
| 709 | // Default open to false for tree models, true for other tree |
---|
| 710 | // grids. |
---|
| 711 | this.defaultOpen = false; |
---|
| 712 | } |
---|
| 713 | var def = this.defaultOpen; |
---|
| 714 | this.openAtLevels = array.map(this.openAtLevels, function(l){ |
---|
| 715 | if(typeof l == "string"){ |
---|
| 716 | switch(l.toLowerCase()){ |
---|
| 717 | case "true": |
---|
| 718 | return true; |
---|
| 719 | break; |
---|
| 720 | case "false": |
---|
| 721 | return false; |
---|
| 722 | break; |
---|
| 723 | default: |
---|
| 724 | var r = parseInt(l, 10); |
---|
| 725 | if(isNaN(r)){ |
---|
| 726 | return def; |
---|
| 727 | } |
---|
| 728 | return r; |
---|
| 729 | break; |
---|
| 730 | } |
---|
| 731 | } |
---|
| 732 | return l; |
---|
| 733 | }); |
---|
| 734 | this._by_idty_paths = {}; |
---|
| 735 | this.inherited(arguments); |
---|
| 736 | }, |
---|
| 737 | |
---|
| 738 | postCreate: function(){ |
---|
| 739 | this.inherited(arguments); |
---|
| 740 | if(this.treeModel){ |
---|
| 741 | this._setModel(this.treeModel); |
---|
| 742 | } |
---|
| 743 | }, |
---|
| 744 | |
---|
| 745 | setModel: function(treeModel){ |
---|
| 746 | this._setModel(treeModel); |
---|
| 747 | this._refresh(true); |
---|
| 748 | }, |
---|
| 749 | |
---|
| 750 | _setModel: function(treeModel){ |
---|
| 751 | if(treeModel && (!ForestStoreModel || !(treeModel instanceof ForestStoreModel))){ |
---|
| 752 | throw new Error("dojox.grid.TreeGrid: treeModel must be an instance of dijit.tree.ForestStoreModel"); |
---|
| 753 | } |
---|
| 754 | this.treeModel = treeModel; |
---|
| 755 | domClass.toggle(this.domNode, "dojoxGridTreeModel", this.treeModel ? true : false); |
---|
| 756 | this._setQuery(treeModel ? treeModel.query : null); |
---|
| 757 | this._setStore(treeModel ? treeModel.store : null); |
---|
| 758 | }, |
---|
| 759 | |
---|
| 760 | createScroller: function(){ |
---|
| 761 | this.inherited(arguments); |
---|
| 762 | this.scroller._origDefaultRowHeight = this.scroller.defaultRowHeight; |
---|
| 763 | }, |
---|
| 764 | |
---|
| 765 | createManagers: function(){ |
---|
| 766 | // summary: |
---|
| 767 | // create grid managers for various tasks including rows, focus, selection, editing |
---|
| 768 | |
---|
| 769 | // row manager |
---|
| 770 | this.rows = new _RowManager(this); |
---|
| 771 | // focus manager |
---|
| 772 | this.focus = new _TreeFocusManager(this); |
---|
| 773 | // edit manager |
---|
| 774 | this.edit = new _EditManager(this); |
---|
| 775 | }, |
---|
| 776 | |
---|
| 777 | _setStore: function(store){ |
---|
| 778 | this.inherited(arguments); |
---|
| 779 | if(this.treeModel&&!this.treeModel.root.children){ |
---|
| 780 | this.treeModel.root.children = []; |
---|
| 781 | } |
---|
| 782 | if(this.aggregator){ |
---|
| 783 | this.aggregator.store = store; |
---|
| 784 | } |
---|
| 785 | }, |
---|
| 786 | |
---|
| 787 | getDefaultOpenState: function(cellDef, item){ |
---|
| 788 | // summary: |
---|
| 789 | // Returns the default open state for the given definition and item |
---|
| 790 | // It reads from the openAtLevels and defaultOpen values of the |
---|
| 791 | // grid to calculate if the given item should default to open or |
---|
| 792 | // not. |
---|
| 793 | var cf; |
---|
| 794 | var store = this.store; |
---|
| 795 | if(this.treeModel){ return this.defaultOpen; } |
---|
| 796 | if(!cellDef || !store || !store.isItem(item) || |
---|
| 797 | !(cf = this.aggregator.childFields[cellDef.level])){ |
---|
| 798 | return this.defaultOpen; |
---|
| 799 | } |
---|
| 800 | if(this.openAtLevels.length > cellDef.level){ |
---|
| 801 | var dVal = this.openAtLevels[cellDef.level]; |
---|
| 802 | if(typeof dVal == "boolean"){ |
---|
| 803 | return dVal; |
---|
| 804 | }else if(typeof dVal == "number"){ |
---|
| 805 | return (store.getValues(item, cf).length <= dVal); |
---|
| 806 | } |
---|
| 807 | } |
---|
| 808 | return this.defaultOpen; |
---|
| 809 | }, |
---|
| 810 | onStyleRow: function(row){ |
---|
| 811 | if(!this.layout._isCollapsable){ |
---|
| 812 | this.inherited(arguments); |
---|
| 813 | return; |
---|
| 814 | } |
---|
| 815 | var base = domAttr.get(row.node, 'dojoxTreeGridBaseClasses'); |
---|
| 816 | if(base){ |
---|
| 817 | row.customClasses = base; |
---|
| 818 | } |
---|
| 819 | var i = row; |
---|
| 820 | var tagName = i.node.tagName.toLowerCase(); |
---|
| 821 | i.customClasses += (i.odd?" dojoxGridRowOdd":"") + |
---|
| 822 | (i.selected&&tagName=='tr'?" dojoxGridRowSelected":"") + |
---|
| 823 | (i.over&&tagName=='tr'?" dojoxGridRowOver":""); |
---|
| 824 | this.focus.styleRow(i); |
---|
| 825 | this.edit.styleRow(i); |
---|
| 826 | }, |
---|
| 827 | styleRowNode: function(inRowIndex, inRowNode){ |
---|
| 828 | if(inRowNode){ |
---|
| 829 | if(inRowNode.tagName.toLowerCase() == 'div' && this.aggregator){ |
---|
| 830 | query("tr[dojoxTreeGridPath]", inRowNode).forEach(function(rowNode){ |
---|
| 831 | this.rows.styleRowNode(domAttr.get(rowNode, 'dojoxTreeGridPath'), rowNode); |
---|
| 832 | },this); |
---|
| 833 | } |
---|
| 834 | this.rows.styleRowNode(inRowIndex, inRowNode); |
---|
| 835 | } |
---|
| 836 | }, |
---|
| 837 | onCanSelect: function(inRowIndex){ |
---|
| 838 | var nodes = query("tr[dojoxTreeGridPath='" + inRowIndex + "']", this.domNode); |
---|
| 839 | if(nodes.length){ |
---|
| 840 | if(domClass.contains(nodes[0], 'dojoxGridSummaryRow')){ |
---|
| 841 | return false; |
---|
| 842 | } |
---|
| 843 | } |
---|
| 844 | return this.inherited(arguments); |
---|
| 845 | }, |
---|
| 846 | onKeyDown: function(e){ |
---|
| 847 | if(e.altKey || e.metaKey){ |
---|
| 848 | return; |
---|
| 849 | } |
---|
| 850 | switch(e.keyCode){ |
---|
| 851 | case keys.UP_ARROW: |
---|
| 852 | if(!this.edit.isEditing() && this.focus.rowIndex != "0"){ |
---|
| 853 | event.stop(e); |
---|
| 854 | this.focus.move(-1, 0); |
---|
| 855 | } |
---|
| 856 | break; |
---|
| 857 | case keys.DOWN_ARROW: |
---|
| 858 | var currPath = new TreePath(this.focus.rowIndex, this); |
---|
| 859 | var lastPath = new TreePath(this.rowCount-1, this); |
---|
| 860 | lastPath = lastPath.lastChild(true); |
---|
| 861 | if(!this.edit.isEditing() && currPath.toString() != lastPath.toString()){ |
---|
| 862 | event.stop(e); |
---|
| 863 | this.focus.move(1, 0); |
---|
| 864 | } |
---|
| 865 | break; |
---|
| 866 | default: |
---|
| 867 | this.inherited(arguments); |
---|
| 868 | break; |
---|
| 869 | } |
---|
| 870 | }, |
---|
| 871 | canEdit: function(inCell, inRowIndex){ |
---|
| 872 | var node = inCell.getNode(inRowIndex); |
---|
| 873 | return node && this._canEdit; |
---|
| 874 | }, |
---|
| 875 | doApplyCellEdit: function(inValue, inRowIndex, inAttrName){ |
---|
| 876 | var item = this.getItem(inRowIndex); |
---|
| 877 | var oldValue = this.store.getValue(item, inAttrName); |
---|
| 878 | if(typeof oldValue == 'number'){ |
---|
| 879 | inValue = isNaN(inValue) ? inValue : parseFloat(inValue); |
---|
| 880 | }else if(typeof oldValue == 'boolean'){ |
---|
| 881 | inValue = inValue == 'true' ? true : inValue == 'false' ? false : inValue; |
---|
| 882 | }else if(oldValue instanceof Date){ |
---|
| 883 | var asDate = new Date(inValue); |
---|
| 884 | inValue = isNaN(asDate.getTime()) ? inValue : asDate; |
---|
| 885 | } |
---|
| 886 | this.store.setValue(item, inAttrName, inValue); |
---|
| 887 | this.onApplyCellEdit(inValue, inRowIndex, inAttrName); |
---|
| 888 | } |
---|
| 889 | }); |
---|
| 890 | TreeGrid.markupFactory = function(props, node, ctor, cellFunc){ |
---|
| 891 | var widthFromAttr = function(n){ |
---|
| 892 | var w = domAttr.get(n, "width")||"auto"; |
---|
| 893 | if((w != "auto")&&(w.slice(-2) != "em")&&(w.slice(-1) != "%")){ |
---|
| 894 | w = parseInt(w, 10)+"px"; |
---|
| 895 | } |
---|
| 896 | return w; |
---|
| 897 | }; |
---|
| 898 | |
---|
| 899 | var cellsFromMarkup = function(table){ |
---|
| 900 | var rows; |
---|
| 901 | // Don't support colgroup on our grid - single view, single row only |
---|
| 902 | if(table.nodeName.toLowerCase() == "table" && |
---|
| 903 | query("> colgroup", table).length === 0 && |
---|
| 904 | (rows = query("> thead > tr", table)).length == 1){ |
---|
| 905 | var tr = rows[0]; |
---|
| 906 | return query("> th", rows[0]).map(function(th){ |
---|
| 907 | // Grab type and field (the only ones that are shared |
---|
| 908 | var cell = { |
---|
| 909 | type: lang.trim(domAttr.get(th, "cellType")||""), |
---|
| 910 | field: lang.trim(domAttr.get(th, "field")||"") |
---|
| 911 | }; |
---|
| 912 | if(cell.type){ |
---|
| 913 | cell.type = lang.getObject(cell.type); |
---|
| 914 | } |
---|
| 915 | |
---|
| 916 | var subTable = query("> table", th)[0]; |
---|
| 917 | if(subTable){ |
---|
| 918 | // If we have a subtable, we are an aggregate and a summary cell |
---|
| 919 | cell.name = ""; |
---|
| 920 | cell.children = cellsFromMarkup(subTable); |
---|
| 921 | if(domAttr.has(th, "itemAggregates")){ |
---|
| 922 | cell.itemAggregates = array.map(domAttr.get(th, "itemAggregates").split(","), function(v){ |
---|
| 923 | return lang.trim(v); |
---|
| 924 | }); |
---|
| 925 | }else{ |
---|
| 926 | cell.itemAggregates = []; |
---|
| 927 | } |
---|
| 928 | if(domAttr.has(th, "aggregate")){ |
---|
| 929 | cell.aggregate = domAttr.get(th, "aggregate"); |
---|
| 930 | } |
---|
| 931 | cell.type = cell.type || dojox.grid.cells.SubtableCell; |
---|
| 932 | }else{ |
---|
| 933 | // Grab our other stuff we need (mostly what's in the normal |
---|
| 934 | // Grid) |
---|
| 935 | cell.name = lang.trim(domAttr.get(th, "name")||th.innerHTML); |
---|
| 936 | if(domAttr.has(th, "width")){ |
---|
| 937 | cell.width = widthFromAttr(th); |
---|
| 938 | } |
---|
| 939 | if(domAttr.has(th, "relWidth")){ |
---|
| 940 | cell.relWidth = window.parseInt(domAttr.get(th, "relWidth"), 10); |
---|
| 941 | } |
---|
| 942 | if(domAttr.has(th, "hidden")){ |
---|
| 943 | cell.hidden = domAttr.get(th, "hidden") == "true"; |
---|
| 944 | } |
---|
| 945 | cell.field = cell.field||cell.name; |
---|
| 946 | DataGrid.cell_markupFactory(cellFunc, th, cell); |
---|
| 947 | cell.type = cell.type || dojox.grid.cells.Cell; |
---|
| 948 | } |
---|
| 949 | if(cell.type && cell.type.markupFactory){ |
---|
| 950 | cell.type.markupFactory(th, cell); |
---|
| 951 | } |
---|
| 952 | return cell; |
---|
| 953 | }); |
---|
| 954 | } |
---|
| 955 | return []; |
---|
| 956 | }; |
---|
| 957 | |
---|
| 958 | var rows; |
---|
| 959 | if( !props.structure ){ |
---|
| 960 | var row = cellsFromMarkup(node); |
---|
| 961 | if(row.length){ |
---|
| 962 | // Set our structure here - so that we don't try and set it in the |
---|
| 963 | // markup factory |
---|
| 964 | props.structure = [{__span: Infinity, cells:[row]}]; |
---|
| 965 | } |
---|
| 966 | } |
---|
| 967 | return DataGrid.markupFactory(props, node, ctor, cellFunc); |
---|
| 968 | }; |
---|
| 969 | |
---|
| 970 | return TreeGrid; |
---|
| 971 | |
---|
| 972 | }); |
---|