├── jquery.tablebodyscroll.css ├── README.md └── jquery.tablebodyscroll.js /jquery.tablebodyscroll.css: -------------------------------------------------------------------------------- 1 | .tablebodyscroll-head { margin-bottom: 0 !important; border-bottom: none !important; } 2 | 3 | /* Hide the scrollbar on most platforms. Post-load Javascript adjusts the inner box a bit. */ 4 | .tablebodyscroll-scroller { position: relative; overflow: hidden; } 5 | .tablebodyscroll-scroller2 { position: absolute; overflow: scroll; top: 0; bottom: -17px; left: 0; right: -17px; } 6 | [dir="rtl"] .tablebodyscroll-scroller2 { left: -17px; right: 0; } 7 | .tablebodyscroll-scroller2::-webkit-scrollbar { display: none; } 8 | 9 | .tablebodyscroll-scroller3 { position: relative; } 10 | 11 | .tablebodyscroll-shadow-top .tablebodyscroll-scroller-shadow-top, .tablebodyscroll-shadow-both .tablebodyscroll-scroller-shadow-top { position: absolute; left: 0; right: 0; top: 0; height: 10px; background: rgba(0, 0, 0, 0.4); background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0)); } 12 | .tablebodyscroll-shadow-bottom .tablebodyscroll-scroller-shadow-bottom, .tablebodyscroll-shadow-both .tablebodyscroll-scroller-shadow-bottom { position: absolute; left: 0; right: 0; bottom: 0; height: 10px; background: rgba(0, 0, 0, 0); background: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); } 13 | 14 | .tablebodyscroll-scroller-indicator { position: absolute; right: 0; height: 25px; width: 3px; background: #CCCCCC; transition: top .25s; } 15 | [dir="rtl"] .tablebodyscroll-scroller-indicator { right: auto; left: 0; } 16 | .tablebodyscroll-scroller-indicator-hide { opacity: 0; transition: opacity .5s linear; } 17 | .tablebodyscroll-scroller-indicator-show { opacity: 1; } 18 | 19 | .tablebodyscroll-has-head .tablebodyscroll-scroller3 > table { margin-top: 0 !important; border-top: none !important; } 20 | .tablebodyscroll-scroller3 > table > thead > tr { height: 0; } 21 | .tablebodyscroll-has-foot .tablebodyscroll-scroller3 > table { margin-bottom: 0 !important; border-bottom: none !important; } 22 | .tablebodyscroll-scroller3 > table > tfoot > tr { height: 0; } 23 | 24 | .tablebodyscroll-scroller3 th.tablebodyscroll-body-hide-cell, .tablebodyscroll-scroller3 td.tablebodyscroll-body-hide-cell { padding-top: 0 !important; padding-bottom: 0 !important; } 25 | .tablebodyscroll-scroller3 div.tablebodyscroll-body-hide-cell { height: 0; overflow: hidden; } 26 | 27 | .tablebodyscroll-foot { margin-top: 0 !important; border-top: none !important; } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jQuery Table Body Scroll 2 | ======================== 3 | 4 | This jQuery plugin heavily modifies table bodies into slick vertical scrolling solutions. Ugly scrollbars are hidden out of sight, relying on visual indicators. Native keyboard, scroll wheel, and touch support. 5 | 6 | Combined with [TableCards](https://github.com/cubiclesoft/jquery-tablecards), traditional tables can be displayed on all devices in a neat, compact format. 7 | 8 | [![Donate](https://cubiclesoft.com/res/donate-shield.png)](https://cubiclesoft.com/donate/) [![Discord](https://img.shields.io/discord/777282089980526602?label=chat&logo=discord)](https://cubiclesoft.com/product-support/github/) 9 | 10 | Usage 11 | ----- 12 | 13 | ```html 14 | 15 | 16 | 40 | ``` 41 | 42 | There are several live examples under "Add Entry" in the [Admin Pack Demo](http://barebonescms.com/demos/admin_pack/admin.php). 43 | 44 | Options 45 | ------- 46 | 47 | The following options may be passed to TableBodyScroll: 48 | 49 | * height - An integer containing the height (Default is 60). 50 | * heightunit - A string containing one of the standard CSS units of measurement (Default is '%'). 51 | * percentelem - An object to reference for the height measurement when heightunit is '%' (Default is window). 52 | * postinit - A callback function to run after each TableCards initialization is run (Default is null). 53 | 54 | To destroy the instance and restore the table to its initial state, simply call: `$('#mytable').TableBodyScroll('destroy');` 55 | 56 | Events 57 | ------ 58 | 59 | The following custom events may be listened for: 60 | 61 | * tablebodyscroll:sizechanged - Notifies after resizing the table's most immediate parent. 62 | 63 | The following custom events may be manually triggered: 64 | 65 | * tablebodyscroll:resize - Notifies TableBodyScroll that the table has been resized. 66 | * tablebodyscroll:columnschanged - Notifies TableBodyScroll that the table columns have changed and to rebuild any headers and footers. 67 | -------------------------------------------------------------------------------- /jquery.tablebodyscroll.js: -------------------------------------------------------------------------------- 1 | // jQuery plugin to scroll the body of long tables so the table fits on a single screen. 2 | // (C) 2017 CubicleSoft. All Rights Reserved. 3 | 4 | (function($) { 5 | var debounce = function(func, wait) { 6 | var timeout = null; 7 | 8 | return function() { 9 | var context = this, args = arguments; 10 | var later = function() { 11 | timeout = null; 12 | 13 | func.apply(context, args); 14 | }; 15 | 16 | if (timeout) clearTimeout(timeout); 17 | timeout = setTimeout(later, wait); 18 | }; 19 | }; 20 | 21 | var debounce2 = function(func, wait, wait2) { 22 | var timeout = null, timeout2 = null; 23 | 24 | return function() { 25 | var context = this, args = arguments; 26 | var later = function() { 27 | clearTimeout(timeout); 28 | timeout = null; 29 | 30 | clearTimeout(timeout2); 31 | timeout2 = null; 32 | 33 | func.apply(context, args); 34 | }; 35 | 36 | if (timeout) clearTimeout(timeout); 37 | timeout = setTimeout(later, wait); 38 | 39 | if (!timeout2) timeout2 = setTimeout(later, wait2); 40 | }; 41 | }; 42 | 43 | $.fn.TableBodyScroll = function(options) { 44 | this.each(function() { 45 | var $this = $(this); 46 | 47 | if ($this.parent().hasClass('tablebodyscroll-scroller3')) 48 | { 49 | var scroller3 = $this.parent(); 50 | var scroller2 = scroller3.parent(); 51 | var scroller = scroller2.parent(); 52 | var wrapper = scroller.parent(); 53 | var origparent = wrapper.parent(); 54 | 55 | // Remove event handlers. 56 | scroller2.off('scroll.tablebodyscroll'); 57 | scroller2.off('mousemove.tablebodyscroll'); 58 | scroller2.off('keypress.tablebodyscroll'); 59 | $this.off('tablebodyscroll:resize'); 60 | $this.off('tablebodyscroll:columnschanged'); 61 | 62 | // Move the table back to its original parent in the DOM. 63 | wrapper.insertBefore($this); 64 | wrapper.remove(); 65 | 66 | // Clean up modified header/footer cells. 67 | $this.children('thead, tfoot').children('tr').children('th, td').each(function() { 68 | if ($(this).hasClass('tablebodyscroll-body-hide-cell')) 69 | { 70 | $(this).removeClass('tablebodyscroll-body-hide-cell'); 71 | 72 | var div = $(this).children('.tablebodyscroll-body-hide-cell'); 73 | div.insertBefore(div.contents()).remove(); 74 | } 75 | }); 76 | } 77 | }); 78 | 79 | if (typeof(options) === 'string' && options === 'destroy') return this; 80 | 81 | var settings = $.extend({}, $.fn.TableBodyScroll.defaults, options); 82 | 83 | return this.each(function() { 84 | var $this = $(this); 85 | 86 | // Wrap the table. 87 | var origparent = $this.parent(); 88 | var scrollerindicator = $('
').addClass('tablebodyscroll-scroller-indicator').addClass('tablebodyscroll-scroller-indicator-hide'); 89 | var scrollershadowtop = $('
').addClass('tablebodyscroll-scroller-shadow-top'); 90 | var scrollershadowbottom = $('
').addClass('tablebodyscroll-scroller-shadow-bottom'); 91 | var scroller3 = $('
').addClass('tablebodyscroll-scroller3').insertBefore($this).append($this); 92 | var scroller2 = $('
').addClass('tablebodyscroll-scroller2').insertBefore(scroller3).append(scroller3); 93 | var scroller = $('
').addClass('tablebodyscroll-scroller').insertBefore(scroller2).append(scroller2).append(scrollerindicator).append(scrollershadowtop).append(scrollershadowbottom); 94 | var wrapper = $('
').addClass('tablebodyscroll').insertBefore(scroller).append(scroller); 95 | 96 | // Generate header and footer tables. 97 | // Cloning has several mostly minor unresolveable issues but there is no other way to accurately make just the body of the table scroll. 98 | var origtheads = null; 99 | var newheadtable = null, newtheadcells = null, origtheadcells = null; 100 | 101 | var origtfoots = null; 102 | var newfoottable = null, newtfootcells = null, origtfootcells = null; 103 | 104 | var CloneHeadFoot = function() { 105 | origtheads = $this.children('thead'); 106 | origtheadcells = origtheads.children('tr').children('th, td'); 107 | if (origtheads.length) 108 | { 109 | origtheadcells.each(function() { 110 | if (!$(this).hasClass('tablebodyscroll-body-hide-cell')) 111 | { 112 | $(this).addClass('tablebodyscroll-body-hide-cell').append($('
').append($(this).contents())); 113 | } 114 | }); 115 | 116 | if (newheadtable) newheadtable.remove(); 117 | var newtheads = origtheads.clone(true, true); 118 | newtheads.find('id').removeAttr('id'); 119 | newheadtable = $('
').attr('class', $this.attr('class')).addClass('tablebodyscroll-head').insertBefore(scroller).append(newtheads); 120 | wrapper.addClass('tablebodyscroll-has-head'); 121 | newtheadcells = newtheads.children('tr').children('th, td'); 122 | } 123 | else 124 | { 125 | wrapper.removeClass('tablebodyscroll-has-head'); 126 | } 127 | 128 | origtfoots = $this.children('tfoot'); 129 | origtfootcells = origtfoots.children('tr').children('th, td'); 130 | if (origtfoots.length) 131 | { 132 | origtfootcells.each(function() { 133 | if (!$(this).hasClass('tablebodyscroll-body-hide-cell')) 134 | { 135 | $(this).addClass('tablebodyscroll-body-hide-cell').append($('
').append($(this).contents())); 136 | } 137 | }); 138 | 139 | if (newfoottable) newfoottable.remove(); 140 | var newtfoots = origtfoots.clone(true, true); 141 | newtfoots.find('id').removeAttr('id'); 142 | newfoottable = $('
').attr('class', $this.attr('class')).addClass('tablebodyscroll-foot').insertAfter(scroller).append(newtfoots); 143 | wrapper.addClass('tablebodyscroll-has-foot'); 144 | newtfootcells = newtfoots.children('tr').children('th, td'); 145 | } 146 | else 147 | { 148 | wrapper.removeClass('tablebodyscroll-has-foot'); 149 | } 150 | }; 151 | 152 | var scrollbarheight = 17; 153 | 154 | var HandleScroll = function() { 155 | var tempheight = $this.outerHeight(); 156 | if (tempheight < 1) return; 157 | 158 | // Calculate new shadow. 159 | var currshadow = scroller.attr('data-tablebodyscroll-shadow') || 'shadow-none'; 160 | var currpos = scroller2.scrollTop(); 161 | var scrollerheight = scroller2.height(); 162 | 163 | var newshadow; 164 | if (currpos > 1 && currpos + scrollerheight - scrollbarheight < tempheight - 1) newshadow = 'shadow-both'; 165 | else if (currpos > 1) newshadow = 'shadow-top'; 166 | else if (currpos + scrollerheight - scrollbarheight < tempheight - 1) newshadow = 'shadow-bottom'; 167 | else newshadow = 'shadow-none'; 168 | 169 | //console.log('currshadow = ' + currshadow + ', newshadow = ' + newshadow + ', currpos = ' + currpos + ', scrollerheight = ' + scrollerheight + ', scrollbarheight = ' + scrollbarheight + ', total = ' + (currpos + scrollerheight - scrollbarheight) + ', table height = ' + tempheight); 170 | 171 | if (currshadow !== newshadow) scroller.removeClass('tablebodyscroll-' + currshadow).addClass('tablebodyscroll-' + newshadow).attr('data-tablebodyscroll-shadow', newshadow); 172 | 173 | // Adjust scroll indicator. 174 | var child = scrollerindicator.get(0); 175 | 176 | child.style.top = ((currpos / (tempheight - scrollerheight + scrollbarheight)) * (scrollerheight - scrollbarheight - scrollerindicator.height())) + 'px'; 177 | } 178 | 179 | var lastparentwidth = 0, lasttablewidth = 0; 180 | 181 | var HandleResize = function() { 182 | var currpos = scroller2.scrollTop(); 183 | var tempheight = $this.outerHeight(); 184 | 185 | var maxheight = (settings.heightunit == '%' ? Math.floor($(settings.percentelem).height() * settings.height / 100) + 'px' : settings.height + settings.heightunit); 186 | scroller.css('height', maxheight); 187 | maxheight = scroller.height(); 188 | 189 | if (maxheight > tempheight) scroller.height(tempheight); 190 | 191 | // Move the table back to its original parent in the DOM, measure the width, and move it back. 192 | origparent.append($this); 193 | var tempwidth = $this.outerWidth(); 194 | scroller3.append($this); 195 | 196 | // Set the width of the scroller to the width of the table so that the inset shadows show properly and horizontal scrolling is correct. 197 | scroller.width(tempwidth); 198 | var origparentwidth = origparent.width(); 199 | var newparentwidth = (origparentwidth < tempwidth ? origparentwidth : tempwidth); 200 | scroller3.width(newparentwidth); 201 | 202 | //console.log('table height = ' + tempheight + ', max height = ' + maxheight + ', width = ' + tempwidth + ', origparentwidth = ' + origparentwidth); 203 | 204 | // Notify listeners. 205 | if (lastparentwidth != newparentwidth || lasttablewidth != tempwidth) 206 | { 207 | lastparentwidth = newparentwidth; 208 | lasttablewidth = tempwidth; 209 | 210 | setTimeout(function() { $this.trigger('tablebodyscroll:sizechanged'); }, 0); 211 | } 212 | 213 | // Resize thead and tfoot elements. 214 | if (origtheads.length) 215 | { 216 | for (var x = 0; x < origtheadcells.length; x++) 217 | { 218 | var origcell = origtheadcells.get(x); 219 | var newcell = newtheadcells.get(x); 220 | 221 | if (origcell && newcell) 222 | { 223 | var tempwidth2; 224 | 225 | // Deals with Google Chrome(!) + jQuery off-by-one errors. 226 | if (origcell.currentStyle) tempwidth2 = origcell.currentStyle.margin; 227 | else if (window.getComputedStyle) tempwidth2 = window.getComputedStyle(origcell, null).getPropertyValue('width'); 228 | else tempwidth2 = $(origcell).width(); 229 | 230 | $(newcell).css({ 'min-width': tempwidth2 }); 231 | } 232 | } 233 | } 234 | 235 | if (origtfoots.length) 236 | { 237 | for (var x = 0; x < origtfootcells.length; x++) 238 | { 239 | var origcell = origtfootcells.get(x); 240 | var newcell = newtfootcells.get(x); 241 | 242 | if (origcell && newcell) 243 | { 244 | var tempwidth2; 245 | 246 | // Deals with Google Chrome(!) + jQuery off-by-one errors. 247 | if (window.getComputedStyle) tempwidth2 = window.getComputedStyle(origcell, null).getPropertyValue('width'); 248 | else if (origcell.currentStyle) tempwidth2 = origcell.currentStyle.margin; 249 | else tempwidth2 = $(origcell).width(); 250 | 251 | $(newcell).css({ 'min-width': tempwidth2 }); 252 | } 253 | } 254 | } 255 | 256 | // Adjust scroller offsets. 257 | var parent = scroller.get(0); 258 | var child = scroller2.get(0); 259 | scrollbarheight = (child.offsetHeight - child.clientHeight); 260 | child.style.bottom = -scrollbarheight + "px"; 261 | 262 | var dir = (window.getComputedStyle ? window.getComputedStyle(parent, null).getPropertyValue('direction') : parent.currentStyle.direction); 263 | 264 | if (dir == 'ltr') child.style.right = -(child.offsetWidth - child.clientWidth) + "px"; 265 | else 266 | { 267 | child.style.left = -(child.offsetWidth - child.clientWidth) + "px"; 268 | child.style.right = '0px'; 269 | } 270 | 271 | // Update the scroller's shadows. 272 | scroller2.scrollTop(currpos); 273 | HandleScroll(); 274 | }; 275 | 276 | CloneHeadFoot(); 277 | 278 | var showingindicator = false; 279 | 280 | var DelayHideIndicator = debounce(function() { 281 | scrollerindicator.addClass('tablebodyscroll-scroller-indicator-hide'); 282 | scrollerindicator.removeClass('tablebodyscroll-scroller-indicator-show'); 283 | 284 | showingindicator = false; 285 | }, 1500); 286 | 287 | var ShowIndicator = function() { 288 | if (!showingindicator) 289 | { 290 | var currshadow = scroller.attr('data-tablebodyscroll-shadow') || 'shadow-none'; 291 | 292 | if (currshadow !== 'shadow-none') 293 | { 294 | scrollerindicator.addClass('tablebodyscroll-scroller-indicator-show'); 295 | scrollerindicator.removeClass('tablebodyscroll-scroller-indicator-hide'); 296 | 297 | showingindicator = true; 298 | } 299 | } 300 | 301 | if (showingindicator) DelayHideIndicator(); 302 | }; 303 | 304 | scroller2.on('scroll.tablebodyscroll', debounce2(function() { 305 | HandleScroll(); 306 | 307 | ShowIndicator(); 308 | }, 20, 50)); 309 | 310 | scroller2.on('mousemove.tablebodyscroll', debounce2(function() { 311 | ShowIndicator(); 312 | }, 20, 50)); 313 | 314 | scroller2.on('keypress.tablebodyscroll', debounce2(function() { 315 | ShowIndicator(); 316 | }, 20, 50)); 317 | 318 | $this.on('tablebodyscroll:resize', debounce2(function() { 319 | HandleResize(); 320 | }, 20, 100)); 321 | 322 | setTimeout(HandleResize, 0); 323 | 324 | $this.on('tablebodyscroll:columnschanged', function() { 325 | // Rebuild header and footer tables and resize. 326 | CloneHeadFoot(); 327 | 328 | HandleResize(); 329 | }); 330 | 331 | if (settings.postinit) settings.postinit(this, settings); 332 | }); 333 | } 334 | 335 | $.fn.TableBodyScroll.defaults = { 336 | 'height' : 60, 337 | 'heightunit' : '%', 338 | 'percentelem' : window, 339 | 'postinit' : null 340 | }; 341 | }(jQuery)); 342 | --------------------------------------------------------------------------------