colResizable-1.6.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. /**
  2. _ _____ _ _ _
  3. | | __ \ (_) | | | |
  4. ___ ___ | | |__) |___ ___ _ ______ _| |__ | | ___
  5. / __/ _ \| | _ // _ \/ __| |_ / _` | '_ \| |/ _ \
  6. | (_| (_) | | | \ \ __/\__ \ |/ / (_| | |_) | | __/
  7. \___\___/|_|_| \_\___||___/_/___\__,_|_.__/|_|\___|
  8. v1.6 - jQuery plugin created by Alvaro Prieto Lauroba
  9. Licences: MIT & GPL
  10. Feel free to use or modify this plugin as far as my full name is kept
  11. If you are going to use this plug-in in production environments it is
  12. strongly recommended to use its minified version: colResizable.min.js
  13. */
  14. (function($){
  15. var d = $(document); //window object
  16. var h = $("head"); //head object
  17. var drag = null; //reference to the current grip that is being dragged
  18. var tables = {}; //object of the already processed tables (table.id as key)
  19. var count = 0; //internal count to create unique IDs when needed.
  20. //common strings for packing
  21. var ID = "id";
  22. var PX = "px";
  23. var SIGNATURE ="JColResizer";
  24. var FLEX = "JCLRFlex";
  25. //short-cuts
  26. var I = parseInt;
  27. var M = Math;
  28. var ie = navigator.userAgent.indexOf('Trident/4.0')>0;
  29. var S;
  30. try{S = sessionStorage;}catch(e){} //Firefox crashes when executed as local file system
  31. //append required CSS rules
  32. h.append("<style type='text/css'> .JColResizer{table-layout:fixed;} .JColResizer > tbody > tr > td, .JColResizer > tbody > tr > th{overflow:hidden;padding-left:.3rem; padding-right:.3rem} .JCLRgrips{ height:0px; position:relative;} .JCLRgrip{margin-left:-5px; position:absolute; z-index:5; } .JCLRgrip .JColResizer{position:absolute;background-color:red;filter:alpha(opacity=1);opacity:0;width:10px;height:100%;cursor: e-resize;top:0px} .JCLRLastGrip{position:absolute; width:1px; } .JCLRgripDrag{ border-left:1px dotted black; } .JCLRFlex{width:auto!important;} .JCLRgrip.JCLRdisabledGrip .JColResizer{cursor:default; display:none;}</style>");
  33. /**
  34. * Function to allow column resizing for table objects. It is the starting point to apply the plugin.
  35. * @param {DOM node} tb - reference to the DOM table object to be enhanced
  36. * @param {Object} options - some customization values
  37. */
  38. var init = function( tb, options){
  39. var t = $(tb); //the table object is wrapped
  40. t.opt = options; //each table has its own options available at anytime
  41. t.mode = options.resizeMode; //shortcuts
  42. t.dc = t.opt.disabledColumns;
  43. if(t.opt.disable) return destroy(t); //the user is asking to destroy a previously colResized table
  44. var id = t.id = t.attr(ID) || SIGNATURE+count++; //its id is obtained, if null new one is generated
  45. t.p = t.opt.postbackSafe; //short-cut to detect postback safe
  46. if(!t.is("table") || tables[id] && !t.opt.partialRefresh) return; //if the object is not a table or if it was already processed then it is ignored.
  47. if (t.opt.hoverCursor !== 'e-resize') h.append("<style type='text/css'>.JCLRgrip .JColResizer:hover{cursor:"+ t.opt.hoverCursor +"!important}</style>"); //if hoverCursor has been set, append the style
  48. t.addClass(SIGNATURE).attr(ID, id).before('<div class="JCLRgrips"/>'); //the grips container object is added. Signature class forces table rendering in fixed-layout mode to prevent column's min-width
  49. t.g = []; t.c = []; t.w = t.width(); t.gc = t.prev(); t.f=t.opt.fixed; //t.c and t.g are arrays of columns and grips respectively
  50. if(options.marginLeft) t.gc.css("marginLeft", options.marginLeft); //if the table contains margins, it must be specified
  51. if(options.marginRight) t.gc.css("marginRight", options.marginRight); //since there is no (direct) way to obtain margin values in its original units (%, em, ...)
  52. t.cs = I(ie? tb.cellSpacing || tb.currentStyle.borderSpacing :t.css('border-spacing'))||2; //table cellspacing (not even jQuery is fully cross-browser)
  53. t.b = I(ie? tb.border || tb.currentStyle.borderLeftWidth :t.css('border-left-width'))||1; //outer border width (again cross-browser issues)
  54. // if(!(tb.style.width || tb.width)) t.width(t.width()); //I am not an IE fan at all, but it is a pity that only IE has the currentStyle attribute working as expected. For this reason I can not check easily if the table has an explicit width or if it is rendered as "auto"
  55. tables[id] = t; //the table object is stored using its id as key
  56. createGrips(t); //grips are created
  57. };
  58. /**
  59. * This function allows to remove any enhancements performed by this plugin on a previously processed table.
  60. * @param {jQuery ref} t - table object
  61. */
  62. var destroy = function(t){
  63. var id=t.attr(ID), t=tables[id]; //its table object is found
  64. if(!t||!t.is("table")) return; //if none, then it wasn't processed
  65. t.removeClass(SIGNATURE+" "+FLEX).gc.remove(); //class and grips are removed
  66. delete tables[id]; //clean up data
  67. };
  68. /**
  69. * Function to create all the grips associated with the table given by parameters
  70. * @param {jQuery ref} t - table object
  71. */
  72. var createGrips = function(t){
  73. var th = t.find(">thead>tr:first>th,>thead>tr:first>td"); //table headers are obtained
  74. if(!th.length) th = t.find(">tbody>tr:first>th,>tr:first>th,>tbody>tr:first>td, >tr:first>td"); //but headers can also be included in different ways
  75. th = th.filter(":visible"); //filter invisible columns
  76. t.cg = t.find("col"); //a table can also contain a colgroup with col elements
  77. t.ln = th.length; //table length is stored
  78. if(t.p && S && S[t.id])memento(t,th); //if 'postbackSafe' is enabled and there is data for the current table, its coloumn layout is restored
  79. th.each(function(i){ //iterate through the table column headers
  80. var c = $(this); //jquery wrap for the current column
  81. var dc = t.dc.indexOf(i)!=-1; //is this a disabled column?
  82. var g = $(t.gc.append('<div class="JCLRgrip"></div>')[0].lastChild); //add the visual node to be used as grip
  83. g.append(dc ? "": t.opt.gripInnerHtml).append('<div class="'+SIGNATURE+'"></div>');
  84. if(i == t.ln-1){ //if the current grip is the las one
  85. g.addClass("JCLRLastGrip"); //add a different css class to stlye it in a different way if needed
  86. if(t.f) g.html(""); //if the table resizing mode is set to fixed, the last grip is removed since table with can not change
  87. }
  88. g.bind('touchstart mousedown', onGripMouseDown); //bind the mousedown event to start dragging
  89. if (!dc){
  90. //if normal column bind the mousedown event to start dragging, if disabled then apply its css class
  91. g.removeClass('JCLRdisabledGrip').bind('touchstart mousedown', onGripMouseDown);
  92. }else{
  93. g.addClass('JCLRdisabledGrip');
  94. }
  95. g.t = t; g.i = i; g.c = c; c.w =c.width(); //some values are stored in the grip's node data as shortcut
  96. t.g.push(g); t.c.push(c); //the current grip and column are added to its table object
  97. c.width(c.w).removeAttr("width"); //the width of the column is converted into pixel-based measurements
  98. g.data(SIGNATURE, {i:i, t:t.attr(ID), last: i == t.ln-1}); //grip index and its table name are stored in the HTML
  99. });
  100. t.cg.removeAttr("width"); //remove the width attribute from elements in the colgroup
  101. t.find('td, th').not(th).not('table th, table td').each(function(){
  102. $(this).removeAttr('width'); //the width attribute is removed from all table cells which are not nested in other tables and dont belong to the header
  103. });
  104. if(!t.f){
  105. t.removeAttr('width').addClass(FLEX); //if not fixed, let the table grow as needed
  106. }
  107. syncGrips(t); //the grips are positioned according to the current table layout
  108. //there is a small problem, some cells in the table could contain dimension values interfering with the
  109. //width value set by this plugin. Those values are removed
  110. };
  111. /**
  112. * Function to allow the persistence of columns dimensions after a browser postback. It is based in
  113. * the HTML5 sessionStorage object, which can be emulated for older browsers using sessionstorage.js
  114. * @param {jQuery ref} t - table object
  115. * @param {jQuery ref} th - reference to the first row elements (only set in deserialization)
  116. */
  117. var memento = function(t, th){
  118. var w,m=0,i=0,aux =[],tw;
  119. if(th){ //in deserialization mode (after a postback)
  120. t.cg.removeAttr("width");
  121. if(t.opt.flush){ S[t.id] =""; return;} //if flush is activated, stored data is removed
  122. w = S[t.id].split(";"); //column widths is obtained
  123. tw = w[t.ln+1];
  124. if(!t.f && tw){ //if not fixed and table width data available its size is restored
  125. t.width(tw*=1);
  126. if(t.opt.overflow) { //if overfolw flag is set, restore table width also as table min-width
  127. t.css('min-width', tw + PX);
  128. t.w = tw;
  129. }
  130. }
  131. for(;i<t.ln;i++){ //for each column
  132. aux.push(100*w[i]/w[t.ln]+"%"); //width is stored in an array since it will be required again a couple of lines ahead
  133. th.eq(i).css("width", aux[i] ); //each column width in % is restored
  134. }
  135. for(i=0;i<t.ln;i++)
  136. t.cg.eq(i).css("width", aux[i]); //this code is required in order to create an inline CSS rule with higher precedence than an existing CSS class in the "col" elements
  137. }else{ //in serialization mode (after resizing a column)
  138. S[t.id] =""; //clean up previous data
  139. for(;i < t.c.length; i++){ //iterate through columns
  140. w = t.c[i].width(); //width is obtained
  141. S[t.id] += w+";"; //width is appended to the sessionStorage object using ID as key
  142. m+=w; //carriage is updated to obtain the full size used by columns
  143. }
  144. S[t.id]+=m; //the last item of the serialized string is the table's active area (width),
  145. //to be able to obtain % width value of each columns while deserializing
  146. if(!t.f) S[t.id] += ";"+t.width(); //if not fixed, table width is stored
  147. }
  148. };
  149. /**
  150. * Function that places each grip in the correct position according to the current table layout
  151. * @param {jQuery ref} t - table object
  152. */
  153. var syncGrips = function (t){
  154. t.gc.width(t.w); //the grip's container width is updated
  155. for(var i=0; i<t.ln; i++){ //for each column
  156. var c = t.c[i];
  157. t.g[i].css({ //height and position of the grip is updated according to the table layout
  158. left: c.offset().left - t.offset().left + c.outerWidth(false) + t.cs / 2 + PX,
  159. height: t.opt.headerOnly? t.c[0].outerHeight(false) : t.outerHeight(false)
  160. });
  161. }
  162. };
  163. /**
  164. * This function updates column's width according to the horizontal position increment of the grip being
  165. * dragged. The function can be called while dragging if liveDragging is enabled and also from the onGripDragOver
  166. * event handler to synchronize grip's position with their related columns.
  167. * @param {jQuery ref} t - table object
  168. * @param {number} i - index of the grip being dragged
  169. * @param {bool} isOver - to identify when the function is being called from the onGripDragOver event
  170. */
  171. var syncCols = function(t,i,isOver){
  172. var inc = drag.x-drag.l, c = t.c[i], c2 = t.c[i+1];
  173. var w = c.w + inc; var w2= c2.w- inc; //their new width is obtained
  174. c.width( w + PX);
  175. t.cg.eq(i).width( w + PX);
  176. if(t.f){ //if fixed mode
  177. c2.width(w2 + PX);
  178. t.cg.eq(i+1).width( w2 + PX);
  179. }else if(t.opt.overflow) { //if overflow is set, incriment min-width to force overflow
  180. t.css('min-width', t.w + inc);
  181. }
  182. if(isOver){
  183. c.w=w;
  184. c2.w= t.f ? w2 : c2.w;
  185. }
  186. };
  187. /**
  188. * This function updates all columns width according to its real width. It must be taken into account that the
  189. * sum of all columns can exceed the table width in some cases (if fixed is set to false and table has some kind
  190. * of max-width).
  191. * @param {jQuery ref} t - table object
  192. */
  193. var applyBounds = function(t){
  194. var w = $.map(t.c, function(c){ //obtain real widths
  195. return c.width();
  196. });
  197. t.width(t.w = t.width()).removeClass(FLEX); //prevent table width changes
  198. $.each(t.c, function(i,c){
  199. c.width(w[i]).w = w[i]; //set column widths applying bounds (table's max-width)
  200. });
  201. t.addClass(FLEX); //allow table width changes
  202. };
  203. /**
  204. * Event handler used while dragging a grip. It checks if the next grip's position is valid and updates it.
  205. * @param {event} e - mousemove event binded to the window object
  206. */
  207. var onGripDrag = function(e){
  208. if(!drag) return;
  209. var t = drag.t; //table object reference
  210. var oe = e.originalEvent.touches;
  211. var ox = oe ? oe[0].pageX : e.pageX; //original position (touch or mouse)
  212. var x = ox - drag.ox + drag.l; //next position according to horizontal mouse position increment
  213. var mw = t.opt.minWidth, i = drag.i ; //cell's min width
  214. var l = t.cs*1.5 + mw + t.b;
  215. var last = i == t.ln-1; //check if it is the last column's grip (usually hidden)
  216. var min = i? t.g[i-1].position().left+t.cs+mw: l; //min position according to the contiguous cells
  217. var max = t.f ? //fixed mode?
  218. i == t.ln-1?
  219. t.w-l:
  220. t.g[i+1].position().left-t.cs-mw:
  221. Infinity; //max position according to the contiguous cells
  222. x = M.max(min, M.min(max, x)); //apply bounding
  223. drag.x = x; drag.css("left", x + PX); //apply position increment
  224. if(last){ //if it is the last grip
  225. var c = t.c[drag.i]; //width of the last column is obtained
  226. drag.w = c.w + x- drag.l;
  227. }
  228. if(t.opt.liveDrag){ //if liveDrag is enabled
  229. if(last){
  230. c.width(drag.w);
  231. if(!t.f && t.opt.overflow){ //if overflow is set, incriment min-width to force overflow
  232. t.css('min-width', t.w + x - drag.l);
  233. }else {
  234. t.w = t.width();
  235. }
  236. }else{
  237. syncCols(t,i); //columns are synchronized
  238. }
  239. syncGrips(t);
  240. var cb = t.opt.onDrag; //check if there is an onDrag callback
  241. if (cb) { e.currentTarget = t[0]; cb(e); } //if any, it is fired
  242. }
  243. return false; //prevent text selection while dragging
  244. };
  245. /**
  246. * Event handler fired when the dragging is over, updating table layout
  247. * @param {event} e - grip's drag over event
  248. */
  249. var onGripDragOver = function(e){
  250. d.unbind('touchend.'+SIGNATURE+' mouseup.'+SIGNATURE).unbind('touchmove.'+SIGNATURE+' mousemove.'+SIGNATURE);
  251. $("head :last-child").remove(); //remove the dragging cursor style
  252. if(!drag) return;
  253. drag.removeClass(drag.t.opt.draggingClass); //remove the grip's dragging css-class
  254. if (!(drag.x - drag.l == 0)) {
  255. var t = drag.t;
  256. var cb = t.opt.onResize; //get some values
  257. var i = drag.i; //column index
  258. var last = i == t.ln-1; //check if it is the last column's grip (usually hidden)
  259. var c = t.g[i].c; //the column being dragged
  260. if(last){
  261. c.width(drag.w);
  262. c.w = drag.w;
  263. }else{
  264. syncCols(t, i, true); //the columns are updated
  265. }
  266. if(!t.f) applyBounds(t); //if not fixed mode, then apply bounds to obtain real width values
  267. syncGrips(t); //the grips are updated
  268. if (cb) { e.currentTarget = t[0]; cb(e); } //if there is a callback function, it is fired
  269. if(t.p && S) memento(t); //if postbackSafe is enabled and there is sessionStorage support, the new layout is serialized and stored
  270. }
  271. drag = null; //since the grip's dragging is over
  272. };
  273. /**
  274. * Event handler fired when the grip's dragging is about to start. Its main goal is to set up events
  275. * and store some values used while dragging.
  276. * @param {event} e - grip's mousedown event
  277. */
  278. var onGripMouseDown = function(e){
  279. var o = $(this).data(SIGNATURE); //retrieve grip's data
  280. var t = tables[o.t], g = t.g[o.i]; //shortcuts for the table and grip objects
  281. var oe = e.originalEvent.touches; //touch or mouse event?
  282. g.ox = oe? oe[0].pageX: e.pageX; //the initial position is kept
  283. g.l = g.position().left;
  284. g.x = g.l;
  285. d.bind('touchmove.'+SIGNATURE+' mousemove.'+SIGNATURE, onGripDrag ).bind('touchend.'+SIGNATURE+' mouseup.'+SIGNATURE, onGripDragOver); //mousemove and mouseup events are bound
  286. h.append("<style type='text/css'>*{cursor:"+ t.opt.dragCursor +"!important}</style>"); //change the mouse cursor
  287. g.addClass(t.opt.draggingClass); //add the dragging class (to allow some visual feedback)
  288. drag = g; //the current grip is stored as the current dragging object
  289. if(t.c[o.i].l) for(var i=0,c; i<t.ln; i++){ c=t.c[i]; c.l = false; c.w= c.width(); } //if the colum is locked (after browser resize), then c.w must be updated
  290. return false; //prevent text selection
  291. };
  292. /**
  293. * Event handler fired when the browser is resized. The main purpose of this function is to update
  294. * table layout according to the browser's size synchronizing related grips
  295. */
  296. var onResize = function(){
  297. for(var t in tables){
  298. if( tables.hasOwnProperty( t ) ) {
  299. t = tables[t];
  300. var i, mw=0;
  301. t.removeClass(SIGNATURE); //firefox doesn't like layout-fixed in some cases
  302. if (t.f) { //in fixed mode
  303. t.w = t.width(); //its new width is kept
  304. for(i=0; i<t.ln; i++) mw+= t.c[i].w;
  305. //cell rendering is not as trivial as it might seem, and it is slightly different for
  306. //each browser. In the beginning i had a big switch for each browser, but since the code
  307. //was extremely ugly now I use a different approach with several re-flows. This works
  308. //pretty well but it's a bit slower. For now, lets keep things simple...
  309. for(i=0; i<t.ln; i++) t.c[i].css("width", M.round(1000*t.c[i].w/mw)/10 + "%").l=true;
  310. //c.l locks the column, telling us that its c.w is outdated
  311. }else{ //in non fixed-sized tables
  312. applyBounds(t); //apply the new bounds
  313. if(t.mode == 'flex' && t.p && S){ //if postbackSafe is enabled and there is sessionStorage support,
  314. memento(t); //the new layout is serialized and stored for 'flex' tables
  315. }
  316. }
  317. syncGrips(t.addClass(SIGNATURE));
  318. }
  319. }
  320. };
  321. //bind resize event, to update grips position
  322. $(window).bind('resize.'+SIGNATURE, onResize);
  323. /**
  324. * The plugin is added to the jQuery library
  325. * @param {Object} options - an object that holds some basic customization values
  326. */
  327. $.fn.extend({
  328. colResizable: function(options) {
  329. var defaults = {
  330. //attributes:
  331. resizeMode: 'fit', //mode can be 'fit', 'flex' or 'overflow'
  332. draggingClass: 'JCLRgripDrag', //css-class used when a grip is being dragged (for visual feedback purposes)
  333. gripInnerHtml: '', //if it is required to use a custom grip it can be done using some custom HTML
  334. liveDrag: false, //enables table-layout updating while dragging
  335. minWidth: 15, //minimum width value in pixels allowed for a column
  336. headerOnly: false, //specifies that the size of the the column resizing anchors will be bounded to the size of the first row
  337. hoverCursor: "e-resize", //cursor to be used on grip hover
  338. dragCursor: "e-resize", //cursor to be used while dragging
  339. postbackSafe: false, //when it is enabled, table layout can persist after postback or page refresh. It requires browsers with sessionStorage support (it can be emulated with sessionStorage.js).
  340. flush: false, //when postbakSafe is enabled, and it is required to prevent layout restoration after postback, 'flush' will remove its associated layout data
  341. marginLeft: null, //in case the table contains any margins, colResizable needs to know the values used, e.g. "10%", "15em", "5px" ...
  342. marginRight: null, //in case the table contains any margins, colResizable needs to know the values used, e.g. "10%", "15em", "5px" ...
  343. disable: false, //disables all the enhancements performed in a previously colResized table
  344. partialRefresh: false, //can be used in combination with postbackSafe when the table is inside of an updatePanel,
  345. disabledColumns: [], //column indexes to be excluded
  346. //events:
  347. onDrag: null, //callback function to be fired during the column resizing process if liveDrag is enabled
  348. onResize: null //callback function fired when the dragging process is over
  349. }
  350. var options = $.extend(defaults, options);
  351. //since now there are 3 different ways of resizing columns, I changed the external interface to make it clear
  352. //calling it 'resizeMode' but also to remove the "fixed" attribute which was confusing for many people
  353. options.fixed = true;
  354. options.overflow = false;
  355. switch(options.resizeMode){
  356. case 'flex': options.fixed = false; break;
  357. case 'overflow': options.fixed = false; options.overflow = true; break;
  358. }
  359. return this.each(function() {
  360. init( this, options);
  361. });
  362. }
  363. });
  364. })(jQuery);