Grid.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. define(["dojo/_base/kernel", "dojo/_base/declare", "dojo/on", "dojo/has", "put-selector/put", "./List", "./util/misc", "dojo/_base/sniff"],
  2. function(kernel, declare, listen, has, put, List, miscUtil){
  3. var contentBoxSizing = has("ie") < 8 && !has("quirks");
  4. var invalidClassChars = /[^\._a-zA-Z0-9-]/g;
  5. function appendIfNode(parent, subNode){
  6. if(subNode && subNode.nodeType){
  7. parent.appendChild(subNode);
  8. }
  9. }
  10. var Grid = declare(List, {
  11. columns: null,
  12. // cellNavigation: Boolean
  13. // This indicates that focus is at the cell level. This may be set to false to cause
  14. // focus to be at the row level, which is useful if you want only want row-level
  15. // navigation.
  16. cellNavigation: true,
  17. tabableHeader: true,
  18. showHeader: true,
  19. column: function(target){
  20. // summary:
  21. // Get the column object by node, or event, or a columnId
  22. if(typeof target != "object"){
  23. return this.columns[target];
  24. }else{
  25. return this.cell(target).column;
  26. }
  27. },
  28. listType: "grid",
  29. cell: function(target, columnId){
  30. // summary:
  31. // Get the cell object by node, or event, id, plus a columnId
  32. if(target.column && target.element){ return target; }
  33. if(target.target && target.target.nodeType){
  34. // event
  35. target = target.target;
  36. }
  37. var element;
  38. if(target.nodeType){
  39. var object;
  40. do{
  41. if(this._rowIdToObject[target.id]){
  42. break;
  43. }
  44. var colId = target.columnId;
  45. if(colId){
  46. columnId = colId;
  47. element = target;
  48. break;
  49. }
  50. target = target.parentNode;
  51. }while(target && target != this.domNode);
  52. }
  53. if(!element && typeof columnId != "undefined"){
  54. var row = this.row(target),
  55. rowElement = row && row.element;
  56. if(rowElement){
  57. var elements = rowElement.getElementsByTagName("td");
  58. for(var i = 0; i < elements.length; i++){
  59. if(elements[i].columnId == columnId){
  60. element = elements[i];
  61. break;
  62. }
  63. }
  64. }
  65. }
  66. if(target != null){
  67. return {
  68. row: row || this.row(target),
  69. column: columnId && this.column(columnId),
  70. element: element
  71. };
  72. }
  73. },
  74. createRowCells: function(tag, each, subRows, object){
  75. // summary:
  76. // Generates the grid for each row (used by renderHeader and and renderRow)
  77. var row = put("table.dgrid-row-table[role=presentation]"),
  78. cellNavigation = this.cellNavigation,
  79. // IE < 9 needs an explicit tbody; other browsers do not
  80. tbody = (has("ie") < 9 || has("quirks")) ? put(row, "tbody") : row,
  81. tr,
  82. si, sl, i, l, // iterators
  83. subRow, column, id, extraClasses, className,
  84. cell, innerCell, colSpan, rowSpan; // used inside loops
  85. // Allow specification of custom/specific subRows, falling back to
  86. // those defined on the instance.
  87. subRows = subRows || this.subRows;
  88. for(si = 0, sl = subRows.length; si < sl; si++){
  89. subRow = subRows[si];
  90. // for single-subrow cases in modern browsers, TR can be skipped
  91. // http://jsperf.com/table-without-trs
  92. tr = put(tbody, "tr");
  93. if(subRow.className){
  94. put(tr, "." + subRow.className);
  95. }
  96. for(i = 0, l = subRow.length; i < l; i++){
  97. // iterate through the columns
  98. column = subRow[i];
  99. id = column.id;
  100. extraClasses = column.field ? ".field-" + column.field : "";
  101. className = typeof column.className === "function" ?
  102. column.className(object) : column.className;
  103. if(className){
  104. extraClasses += "." + className;
  105. }
  106. cell = put(tag + (
  107. ".dgrid-cell.dgrid-cell-padding" +
  108. (id ? ".dgrid-column-" + id : "") +
  109. extraClasses.replace(/ +/g, ".")
  110. ).replace(invalidClassChars,"-") +
  111. "[role=" + (tag === "th" ? "columnheader" : "gridcell") + "]");
  112. cell.columnId = id;
  113. if(contentBoxSizing){
  114. // The browser (IE7-) does not support box-sizing: border-box, so we emulate it with a padding div
  115. innerCell = put(cell, "!dgrid-cell-padding div.dgrid-cell-padding");// remove the dgrid-cell-padding, and create a child with that class
  116. cell.contents = innerCell;
  117. }else{
  118. innerCell = cell;
  119. }
  120. colSpan = column.colSpan;
  121. if(colSpan){
  122. cell.colSpan = colSpan;
  123. }
  124. rowSpan = column.rowSpan;
  125. if(rowSpan){
  126. cell.rowSpan = rowSpan;
  127. }
  128. each(innerCell, column);
  129. // add the td to the tr at the end for better performance
  130. tr.appendChild(cell);
  131. }
  132. }
  133. return row;
  134. },
  135. left: function(cell, steps){
  136. if(!cell.element){ cell = this.cell(cell); }
  137. return this.cell(this._move(cell, -(steps || 1), "dgrid-cell"));
  138. },
  139. right: function(cell, steps){
  140. if(!cell.element){ cell = this.cell(cell); }
  141. return this.cell(this._move(cell, steps || 1, "dgrid-cell"));
  142. },
  143. renderRow: function(object, options){
  144. var self = this;
  145. var row = this.createRowCells("td", function(td, column){
  146. var data = object;
  147. // Support get function or field property (similar to DataGrid)
  148. if(column.get){
  149. data = column.get(object);
  150. }else if("field" in column && column.field != "_item"){
  151. data = data[column.field];
  152. }
  153. if(column.renderCell){
  154. // A column can provide a renderCell method to do its own DOM manipulation,
  155. // event handling, etc.
  156. appendIfNode(td, column.renderCell(object, data, td, options));
  157. }else{
  158. defaultRenderCell.call(column, object, data, td, options);
  159. }
  160. }, options && options.subRows, object);
  161. // row gets a wrapper div for a couple reasons:
  162. // 1. So that one can set a fixed height on rows (heights can't be set on <table>'s AFAICT)
  163. // 2. So that outline style can be set on a row when it is focused, and Safari's outline style is broken on <table>
  164. return put("div[role=row]>", row);
  165. },
  166. renderHeader: function(){
  167. // summary:
  168. // Setup the headers for the grid
  169. var
  170. grid = this,
  171. columns = this.columns,
  172. headerNode = this.headerNode,
  173. i = headerNode.childNodes.length;
  174. headerNode.setAttribute("role", "row");
  175. // clear out existing header in case we're resetting
  176. while(i--){
  177. put(headerNode.childNodes[i], "!");
  178. }
  179. var row = this.createRowCells("th", function(th, column){
  180. var contentNode = column.headerNode = th;
  181. if(contentBoxSizing){
  182. // we're interested in the th, but we're passed the inner div
  183. th = th.parentNode;
  184. }
  185. var field = column.field;
  186. if(field){
  187. th.field = field;
  188. }
  189. // allow for custom header content manipulation
  190. if(column.renderHeaderCell){
  191. appendIfNode(contentNode, column.renderHeaderCell(contentNode));
  192. }else if("label" in column || column.field){
  193. contentNode.appendChild(document.createTextNode(
  194. "label" in column ? column.label : column.field));
  195. }
  196. if(column.sortable !== false && field && field != "_item"){
  197. th.sortable = true;
  198. th.className += " dgrid-sortable";
  199. }
  200. }, this.subRows && this.subRows.headerRows);
  201. this._rowIdToObject[row.id = this.id + "-header"] = this.columns;
  202. headerNode.appendChild(row);
  203. // If the columns are sortable, re-sort on clicks.
  204. // Use a separate listener property to be managed by renderHeader in case
  205. // of subsequent calls.
  206. if(this._sortListener){
  207. this._sortListener.remove();
  208. }
  209. this._sortListener = listen(row, "click,keydown", function(event){
  210. // respond to click, space keypress, or enter keypress
  211. if(event.type == "click" || event.keyCode == 32 /* space bar */ || (!has("opera") && event.keyCode == 13) /* enter */){
  212. var target = event.target,
  213. field, sort, newSort, eventObj;
  214. do{
  215. if(target.sortable){
  216. // If the click is on the same column as the active sort,
  217. // reverse sort direction
  218. newSort = [{
  219. attribute: (field = target.field || target.columnId),
  220. descending: (sort = grid._sort[0]) && sort.attribute == field &&
  221. !sort.descending
  222. }];
  223. // Emit an event with the new sort
  224. eventObj = {
  225. bubbles: true,
  226. cancelable: true,
  227. grid: grid,
  228. parentType: event.type,
  229. sort: newSort
  230. };
  231. if (listen.emit(event.target, "dgrid-sort", eventObj)){
  232. // Stash node subject to DOM manipulations,
  233. // to be referenced then removed by sort()
  234. grid._sortNode = target;
  235. grid.set("sort", newSort);
  236. }
  237. break;
  238. }
  239. }while((target = target.parentNode) && target != headerNode);
  240. }
  241. });
  242. },
  243. resize: function(){
  244. // extension of List.resize to allow accounting for
  245. // column sizes larger than actual grid area
  246. var
  247. headerTableNode = this.headerNode.firstChild,
  248. contentNode = this.contentNode,
  249. width;
  250. this.inherited(arguments);
  251. if(!has("ie") || (has("ie") > 7 && !has("quirks"))){
  252. // Force contentNode width to match up with header width.
  253. // (Old IEs don't have a problem due to how they layout.)
  254. contentNode.style.width = ""; // reset first
  255. if(contentNode && headerTableNode){
  256. if((width = headerTableNode.offsetWidth) != contentNode.offsetWidth){
  257. // update size of content node if necessary (to match size of rows)
  258. // (if headerTableNode can't be found, there isn't much we can do)
  259. contentNode.style.width = width + "px";
  260. }
  261. }
  262. }
  263. },
  264. destroy: function(){
  265. // Run _destroyColumns first to perform any column plugin tear-down logic.
  266. this._destroyColumns();
  267. if(this._sortListener){
  268. this._sortListener.remove();
  269. }
  270. this.inherited(arguments);
  271. },
  272. _setSort: function(property, descending){
  273. // summary:
  274. // Extension of List.js sort to update sort arrow in UI
  275. // Normalize _sort first via inherited logic, then update the sort arrow
  276. this.inherited(arguments);
  277. this.updateSortArrow(this._sort);
  278. },
  279. updateSortArrow: function(sort, updateSort){
  280. // summary:
  281. // Method responsible for updating the placement of the arrow in the
  282. // appropriate header cell. Typically this should not be called (call
  283. // set("sort", ...) when actually updating sort programmatically), but
  284. // this method may be used by code which is customizing sort (e.g.
  285. // by reacting to the dgrid-sort event, canceling it, then
  286. // performing logic and calling this manually).
  287. // sort: Array
  288. // Standard sort parameter - array of object(s) containing attribute
  289. // and optionally descending property
  290. // updateSort: Boolean?
  291. // If true, will update this._sort based on the passed sort array
  292. // (i.e. to keep it in sync when custom logic is otherwise preventing
  293. // it from being updated); defaults to false
  294. // Clean up UI from any previous sort
  295. if(this._lastSortedArrow){
  296. // Remove the sort classes from the parent node
  297. put(this._lastSortedArrow, "<!dgrid-sort-up!dgrid-sort-down");
  298. // Destroy the lastSortedArrow node
  299. put(this._lastSortedArrow, "!");
  300. delete this._lastSortedArrow;
  301. }
  302. if(updateSort){ this._sort = sort; }
  303. if(!sort[0]){ return; } // nothing to do if no sort is specified
  304. var prop = sort[0].attribute,
  305. desc = sort[0].descending,
  306. target = this._sortNode, // stashed if invoked from header click
  307. columns, column, i;
  308. delete this._sortNode;
  309. if(!target){
  310. columns = this.columns;
  311. for(i in columns){
  312. column = columns[i];
  313. if(column.field == prop){
  314. target = column.headerNode;
  315. break;
  316. }
  317. }
  318. }
  319. // skip this logic if field being sorted isn't actually displayed
  320. if(target){
  321. target = target.contents || target;
  322. // place sort arrow under clicked node, and add up/down sort class
  323. this._lastSortedArrow = put(target.firstChild, "-div.dgrid-sort-arrow.ui-icon[role=presentation]");
  324. this._lastSortedArrow.innerHTML = "&nbsp;";
  325. put(target, desc ? ".dgrid-sort-down" : ".dgrid-sort-up");
  326. // call resize in case relocation of sort arrow caused any height changes
  327. this.resize();
  328. }
  329. },
  330. styleColumn: function(colId, css){
  331. // summary:
  332. // Dynamically creates a stylesheet rule to alter a column's style.
  333. return this.addCssRule("#" + miscUtil.escapeCssIdentifier(this.domNode.id) +
  334. " .dgrid-column-" + colId, css);
  335. },
  336. /*=====
  337. _configColumn: function(column, columnId, rowColumns, prefix){
  338. // summary:
  339. // Method called when normalizing base configuration of a single
  340. // column. Can be used as an extension point for behavior requiring
  341. // access to columns when a new configuration is applied.
  342. },=====*/
  343. _configColumns: function(prefix, rowColumns){
  344. // configure the current column
  345. var subRow = [],
  346. isArray = rowColumns instanceof Array;
  347. function configColumn(column, columnId){
  348. if(typeof column == "string"){
  349. rowColumns[columnId] = column = {label:column};
  350. }
  351. if(!isArray && !column.field){
  352. column.field = columnId;
  353. }
  354. columnId = column.id = column.id || (isNaN(columnId) ? columnId : (prefix + columnId));
  355. if(isArray){ this.columns[columnId] = column; }
  356. // allow further base configuration in subclasses
  357. if(this._configColumn){
  358. this._configColumn(column, columnId, rowColumns, prefix);
  359. }
  360. // add grid reference to each column object for potential use by plugins
  361. column.grid = this;
  362. if(typeof column.init === "function"){ column.init(); }
  363. subRow.push(column); // make sure it can be iterated on
  364. }
  365. miscUtil.each(rowColumns, configColumn, this);
  366. return isArray ? rowColumns : subRow;
  367. },
  368. _destroyColumns: function(){
  369. // summary:
  370. // Iterates existing subRows looking for any column definitions with
  371. // destroy methods (defined by plugins) and calls them. This is called
  372. // immediately before configuring a new column structure.
  373. var subRows = this.subRows,
  374. // if we have column sets, then we don't need to do anything with the missing subRows, ColumnSet will handle it
  375. subRowsLength = subRows && subRows.length,
  376. i, j, column, len;
  377. // First remove rows (since they'll be refreshed after we're done),
  378. // so that anything aspected onto removeRow by plugins can run.
  379. // (cleanup will end up running again, but with nothing to iterate.)
  380. this.cleanup();
  381. for(i = 0; i < subRowsLength; i++){
  382. for(j = 0, len = subRows[i].length; j < len; j++){
  383. column = subRows[i][j];
  384. if(typeof column.destroy === "function"){ column.destroy(); }
  385. }
  386. }
  387. },
  388. configStructure: function(){
  389. // configure the columns and subRows
  390. var subRows = this.subRows,
  391. columns = this._columns = this.columns;
  392. // Reset this.columns unless it was already passed in as an object
  393. this.columns = !columns || columns instanceof Array ? {} : columns;
  394. if(subRows){
  395. // Process subrows, which will in turn populate the this.columns object
  396. for(var i = 0; i < subRows.length; i++){
  397. subRows[i] = this._configColumns(i + "-", subRows[i]);
  398. }
  399. }else{
  400. this.subRows = [this._configColumns("", columns)];
  401. }
  402. },
  403. _getColumns: function(){
  404. // _columns preserves what was passed to set("columns"), but if subRows
  405. // was set instead, columns contains the "object-ified" version, which
  406. // was always accessible in the past, so maintain that accessibility going
  407. // forward.
  408. return this._columns || this.columns;
  409. },
  410. _setColumns: function(columns){
  411. this._destroyColumns();
  412. // reset instance variables
  413. this.subRows = null;
  414. this.columns = columns;
  415. // re-run logic
  416. this._updateColumns();
  417. },
  418. _setSubRows: function(subrows){
  419. this._destroyColumns();
  420. this.subRows = subrows;
  421. this._updateColumns();
  422. },
  423. setColumns: function(columns){
  424. kernel.deprecated("setColumns(...)", 'use set("columns", ...) instead', "dgrid 0.4");
  425. this.set("columns", columns);
  426. },
  427. setSubRows: function(subrows){
  428. kernel.deprecated("setSubRows(...)", 'use set("subRows", ...) instead', "dgrid 0.4");
  429. this.set("subRows", subrows);
  430. },
  431. _updateColumns: function(){
  432. // summary:
  433. // Called when columns, subRows, or columnSets are reset
  434. this.configStructure();
  435. this.renderHeader();
  436. this.refresh();
  437. // re-render last collection if present
  438. this._lastCollection && this.renderArray(this._lastCollection);
  439. // After re-rendering the header, re-apply the sort arrow if needed.
  440. if(this._started){
  441. if(this._sort && this._sort.length){
  442. this.updateSortArrow(this._sort);
  443. } else {
  444. // Only call resize directly if we didn't call updateSortArrow,
  445. // since that calls resize itself when it updates.
  446. this.resize();
  447. }
  448. }
  449. }
  450. });
  451. function defaultRenderCell(object, data, td, options){
  452. if(this.formatter){
  453. // Support formatter, with or without formatterScope
  454. var formatter = this.formatter,
  455. formatterScope = this.grid.formatterScope;
  456. td.innerHTML = typeof formatter === "string" && formatterScope ?
  457. formatterScope[formatter](data, object) : this.formatter(data, object);
  458. }else if(data != null){
  459. td.appendChild(document.createTextNode(data));
  460. }
  461. }
  462. // expose appendIfNode and default implementation of renderCell,
  463. // e.g. for use by column plugins
  464. Grid.appendIfNode = appendIfNode;
  465. Grid.defaultRenderCell = defaultRenderCell;
  466. return Grid;
  467. });