ColumnResizer.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. define(["dojo/_base/declare", "dojo/on", "dojo/query", "dojo/_base/lang", "dojo/dom", "dojo/dom-geometry", "dojo/has", "../util/misc", "put-selector/put", "dojo/_base/html", "xstyle/css!../css/extensions/ColumnResizer.css"],
  2. function(declare, listen, query, lang, dom, geom, has, miscUtil, put){
  3. function addRowSpan(table, span, startRow, column, id){
  4. // loop through the rows of the table and add this column's id to
  5. // the rows' column
  6. for(var i=1; i<span; i++){
  7. table[startRow+i][column] = id;
  8. }
  9. }
  10. function subRowAssoc(subRows){
  11. // Take a sub-row structure and output an object with key=>value pairs
  12. // The keys will be the column id's; the values will be the first-row column
  13. // that column's resizer should be associated with.
  14. var i = subRows.length,
  15. l = i,
  16. numCols = subRows[0].length,
  17. table = new Array(i);
  18. // create table-like structure in an array so it can be populated
  19. // with row-spans and col-spans
  20. while(i--){
  21. table[i] = new Array(numCols);
  22. }
  23. var associations = {};
  24. for(i=0; i<l; i++){
  25. var row = table[i],
  26. subRow = subRows[i];
  27. // j: counter for table columns
  28. // js: counter for subrow structure columns
  29. for(var j=0, js=0; j<numCols; j++){
  30. var cell = subRow[js], k;
  31. // if something already exists in the table (row-span), skip this
  32. // spot and go to the next
  33. if(typeof row[j] != "undefined"){
  34. continue;
  35. }
  36. row[j] = cell.id;
  37. if(cell.rowSpan && cell.rowSpan > 1){
  38. addRowSpan(table, cell.rowSpan, i, j, cell.id);
  39. }
  40. // colSpans are only applicable in the second or greater rows
  41. // and only if the colSpan is greater than 1
  42. if(i>0 && cell.colSpan && cell.colSpan > 1){
  43. for(k=1; k<cell.colSpan; k++){
  44. // increment j and assign the id since this is a span
  45. row[++j] = cell.id;
  46. if(cell.rowSpan && cell.rowSpan > 1){
  47. addRowSpan(table, cell.rowSpan, i, j, cell.id);
  48. }
  49. }
  50. }
  51. associations[cell.id] = subRows[0][j].id;
  52. js++;
  53. }
  54. }
  55. return associations;
  56. }
  57. function resizeColumnWidth(grid, colId, width, parentType){
  58. // don't react to widths <= 0, e.g. for hidden columns
  59. if(width <= 0){ return; }
  60. var column = grid.columns[colId],
  61. event = {
  62. grid: grid,
  63. columnId: colId,
  64. width: width,
  65. bubbles: true,
  66. cancelable: true
  67. },
  68. rule;
  69. if(parentType){
  70. event.parentType = parentType;
  71. }
  72. if(!grid._resizedColumns || listen.emit(grid.headerNode, "dgrid-columnresize", event)){
  73. // Update width on column object, then convert value for CSS
  74. if(width === "auto"){
  75. delete column.width;
  76. }else{
  77. column.width = width;
  78. width += "px";
  79. }
  80. rule = grid._columnSizes[colId];
  81. if(rule){
  82. // Modify existing, rather than deleting + adding
  83. rule.set("width", width);
  84. }else{
  85. // Use miscUtil function directly, since we clean these up ourselves anyway
  86. rule = miscUtil.addCssRule(
  87. "#" + miscUtil.escapeCssIdentifier(grid.domNode.id) + " .dgrid-column-" + colId, "width: " + width + ";");
  88. }
  89. // keep a reference for future removal
  90. grid._columnSizes[colId] = rule;
  91. grid.resize();
  92. return true;
  93. }
  94. }
  95. // Functions for shared resizer node
  96. var resizerNode, // DOM node for resize indicator, reused between instances
  97. resizableCount = 0; // Number of ColumnResizer-enabled grid instances
  98. var resizer = {
  99. // This object contains functions for manipulating the shared resizerNode
  100. create: function(){
  101. resizerNode = put("div.dgrid-column-resizer");
  102. },
  103. destroy: function(){
  104. put(resizerNode, "!");
  105. resizerNode = null;
  106. },
  107. show: function(grid){
  108. var pos = geom.position(grid.domNode, true);
  109. resizerNode.style.top = pos.y + "px";
  110. resizerNode.style.height = pos.h + "px";
  111. put(document.body, resizerNode);
  112. },
  113. move: function(x){
  114. resizerNode.style.left = x + "px";
  115. },
  116. hide: function(){
  117. resizerNode.parentNode.removeChild(resizerNode);
  118. }
  119. };
  120. return declare(null, {
  121. resizeNode: null,
  122. // minWidth: Number
  123. // Minimum column width, in px.
  124. minWidth: 40,
  125. // adjustLastColumn: Boolean
  126. // If true, adjusts the last column's width to "auto" at times where the
  127. // browser would otherwise stretch all columns to span the grid.
  128. adjustLastColumn: true,
  129. _resizedColumns: false, // flag indicating if resizer has converted column widths to px
  130. buildRendering: function(){
  131. this.inherited(arguments);
  132. // Create resizerNode when first grid w/ ColumnResizer is created
  133. if(!resizableCount++){
  134. resizer.create();
  135. }
  136. },
  137. destroy: function(){
  138. this.inherited(arguments);
  139. // Remove any applied column size styles since we're tracking them directly
  140. for(var name in this._columnSizes){
  141. this._columnSizes[name].remove();
  142. }
  143. // If this is the last grid on the page with ColumnResizer, destroy the
  144. // shared resizerNode
  145. if(!--resizableCount){
  146. resizer.destroy();
  147. }
  148. },
  149. resizeColumnWidth: function(colId, width){
  150. // Summary:
  151. // calls grid's styleColumn function to add a style for the column
  152. // colId: String
  153. // column id
  154. // width: Integer
  155. // new width of the column
  156. return resizeColumnWidth(this, colId, width);
  157. },
  158. configStructure: function(){
  159. var oldSizes = this._oldColumnSizes = lang.mixin({}, this._columnSizes), // shallow clone
  160. k;
  161. this._resizedColumns = false;
  162. this._columnSizes = {};
  163. this.inherited(arguments);
  164. // Remove old column styles that are no longer relevant; this is specifically
  165. // done *after* calling inherited so that _columnSizes will contain keys
  166. // for all columns in the new structure that were assigned widths.
  167. for(k in oldSizes){
  168. if(!(k in this._columnSizes)){
  169. oldSizes[k].remove();
  170. }
  171. }
  172. delete this._oldColumnSizes;
  173. },
  174. _configColumn: function(column){
  175. this.inherited(arguments);
  176. var colId = column.id,
  177. rule;
  178. if("width" in column){
  179. // Update or add a style rule for the specified width
  180. if((rule = this._oldColumnSizes[colId])){
  181. rule.set("width", column.width + "px");
  182. }else{
  183. rule = miscUtil.addCssRule(
  184. "#" + this.domNode.id + " .dgrid-column-" + colId, "width: " + column.width + "px;");
  185. }
  186. this._columnSizes[colId] = rule;
  187. }
  188. },
  189. renderHeader: function(){
  190. this.inherited(arguments);
  191. var grid = this;
  192. var assoc;
  193. if(this.columnSets && this.columnSets.length){
  194. var csi = this.columnSets.length;
  195. while(csi--){
  196. assoc = lang.mixin(assoc||{}, subRowAssoc(this.columnSets[csi]));
  197. }
  198. }else if(this.subRows && this.subRows.length > 1){
  199. assoc = subRowAssoc(this.subRows);
  200. }
  201. var colNodes = query(".dgrid-cell", grid.headerNode),
  202. i = colNodes.length;
  203. while(i--){
  204. var colNode = colNodes[i],
  205. id = colNode.columnId,
  206. col = grid.columns[id],
  207. childNodes = colNode.childNodes;
  208. if(!col || col.resizable === false){ continue; }
  209. var headerTextNode = put("div.dgrid-resize-header-container");
  210. colNode.contents = headerTextNode;
  211. // move all the children to the header text node
  212. while(childNodes.length > 0){
  213. put(headerTextNode, childNodes[0]);
  214. }
  215. put(colNode, headerTextNode, "div.dgrid-resize-handle.resizeNode-"+id).columnId =
  216. assoc ? assoc[id] : id;
  217. }
  218. if(!grid.mouseMoveListen){
  219. // establish listeners for initiating, dragging, and finishing resize
  220. listen(grid.headerNode,
  221. ".dgrid-resize-handle:mousedown" +
  222. (has("touch") ? ",.dgrid-resize-handle:touchstart" : ""),
  223. function(e){
  224. grid._resizeMouseDown(e, this);
  225. grid.mouseMoveListen.resume();
  226. grid.mouseUpListen.resume();
  227. }
  228. );
  229. grid._listeners.push(grid.mouseMoveListen = listen.pausable(document,
  230. "mousemove" + (has("touch") ? ",touchmove" : ""),
  231. miscUtil.throttleDelayed(function(e){ grid._updateResizerPosition(e); })
  232. ));
  233. grid._listeners.push(grid.mouseUpListen = listen.pausable(document,
  234. "mouseup" + (has("touch") ? ",touchend" : ""),
  235. function(e){
  236. grid._resizeMouseUp(e);
  237. grid.mouseMoveListen.pause();
  238. grid.mouseUpListen.pause();
  239. }
  240. ));
  241. // initially pause the move/up listeners until a drag happens
  242. grid.mouseMoveListen.pause();
  243. grid.mouseUpListen.pause();
  244. }
  245. }, // end renderHeader
  246. _resizeMouseDown: function(e, target){
  247. // Summary:
  248. // called when mouse button is pressed on the header
  249. // e: Object
  250. // mousedown event object
  251. // preventDefault actually seems to be enough to prevent browser selection
  252. // in all but IE < 9. setSelectable works for those.
  253. e.preventDefault();
  254. dom.setSelectable(this.domNode, false);
  255. this._startX = this._getResizeMouseLocation(e); //position of the target
  256. this._targetCell = query(".dgrid-column-" + target.columnId, this.headerNode)[0];
  257. // Show resizerNode after initializing its x position
  258. this._updateResizerPosition(e);
  259. resizer.show(this);
  260. },
  261. _resizeMouseUp: function(e){
  262. // Summary:
  263. // called when mouse button is released
  264. // e: Object
  265. // mouseup event object
  266. var columnSizes = this._columnSizes,
  267. colNodes, colWidths, gridWidth;
  268. if(this.adjustLastColumn){
  269. // For some reason, total column width needs to be 1 less than this
  270. gridWidth = this.headerNode.clientWidth - 1;
  271. }
  272. //This is used to set all the column widths to a static size
  273. if(!this._resizedColumns){
  274. colNodes = query(".dgrid-cell", this.headerNode);
  275. if(this.columnSets && this.columnSets.length){
  276. colNodes = colNodes.filter(function(node){
  277. var idx = node.columnId.split("-");
  278. return idx[0] == "0" && !(node.columnId in columnSizes);
  279. });
  280. }else if(this.subRows && this.subRows.length > 1){
  281. colNodes = colNodes.filter(function(node){
  282. return node.columnId.charAt(0) == "0" && !(node.columnId in columnSizes);
  283. });
  284. }
  285. // Get a set of sizes before we start mutating, to avoid
  286. // weird disproportionate measures if the grid has set
  287. // column widths, but no full grid width set
  288. colWidths = colNodes.map(function(colNode){
  289. return colNode.offsetWidth;
  290. });
  291. // Set a baseline size for each column based on
  292. // its original measure
  293. colNodes.forEach(function(colNode, i){
  294. this.resizeColumnWidth(colNode.columnId, colWidths[i]);
  295. }, this);
  296. this._resizedColumns = true;
  297. }
  298. dom.setSelectable(this.domNode, true);
  299. var cell = this._targetCell,
  300. delta = this._getResizeMouseLocation(e) - this._startX, //final change in position of resizer
  301. newWidth = cell.offsetWidth + delta, //the new width after resize
  302. obj = this._getResizedColumnWidths(),//get current total column widths before resize
  303. totalWidth = obj.totalWidth,
  304. lastCol = obj.lastColId,
  305. lastColWidth = query(".dgrid-column-"+lastCol, this.headerNode)[0].offsetWidth;
  306. if(newWidth < this.minWidth){
  307. //enforce minimum widths
  308. newWidth = this.minWidth;
  309. }
  310. if(resizeColumnWidth(this, cell.columnId, newWidth, e.type)){
  311. if(cell.columnId != lastCol && this.adjustLastColumn){
  312. if(totalWidth + delta < gridWidth) {
  313. //need to set last column's width to auto
  314. resizeColumnWidth(this, lastCol, "auto", e.type);
  315. }else if(lastColWidth-delta <= this.minWidth) {
  316. //change last col width back to px, unless it is the last column itself being resized...
  317. resizeColumnWidth(this, lastCol, this.minWidth, e.type);
  318. }
  319. }
  320. }
  321. resizer.hide();
  322. // Clean up after the resize operation
  323. delete this._startX;
  324. delete this._targetCell;
  325. },
  326. _updateResizerPosition: function(e){
  327. // Summary:
  328. // updates position of resizer bar as mouse moves
  329. // e: Object
  330. // mousemove event object
  331. if(!this._targetCell){ return; } // Release event was already processed
  332. var mousePos = this._getResizeMouseLocation(e),
  333. delta = mousePos - this._startX, //change from where user clicked to where they drag
  334. width = this._targetCell.offsetWidth,
  335. left = mousePos;
  336. if(width + delta < this.minWidth){
  337. left = this._startX - (width - this.minWidth);
  338. }
  339. resizer.move(left);
  340. },
  341. _getResizeMouseLocation: function(e){
  342. //Summary:
  343. // returns position of mouse relative to the left edge
  344. // e: event object
  345. // mouse move event object
  346. var posX = 0;
  347. if(e.pageX){
  348. posX = e.pageX;
  349. }else if(e.clientX){
  350. posX = e.clientX + document.body.scrollLeft +
  351. document.documentElement.scrollLeft;
  352. }
  353. return posX;
  354. },
  355. _getResizedColumnWidths: function (){
  356. //Summary:
  357. // returns object containing new column width and column id
  358. var totalWidth = 0,
  359. colNodes = query(".dgrid-cell", this.headerNode);
  360. // For ColumnSets and subRows, only the top row of columns matters
  361. if(this.columnSets && this.columnSets.length){
  362. colNodes = colNodes.filter(function(node){
  363. var idx = node.columnId.split("-");
  364. return idx[1] == "0";
  365. });
  366. }else if(this.subRows && this.subRows.length > 1){
  367. colNodes = colNodes.filter(function(node){
  368. return node.columnId.charAt(0) == "0";
  369. });
  370. }
  371. var i = colNodes.length;
  372. if(!i){ return {}; }
  373. var lastColId = colNodes[i-1].columnId;
  374. while(i--){
  375. totalWidth += colNodes[i].offsetWidth;
  376. }
  377. return {totalWidth: totalWidth, lastColId: lastColId};
  378. }
  379. });
  380. });