From 29769457a578367815c698404998685e74fabd5c Mon Sep 17 00:00:00 2001 From: Baptiste Wojtkowski Date: Thu, 26 Sep 2024 10:31:44 +0200 Subject: [PATCH] Bug 36291: Update fixedHeader to v4.0.1 On testing bug 36130, I found that scrolling had some glitch (at least on my computer, Firefox on ARM64 macOS). I used the following command to fill the DB with several batches: ``` PERL5LIB=$PERL5LIB:./lib perl fake_data.pl --how-many 100 --entity ill for x in {1..21}; do echo "INSERT INTO illbatches (name, backend, library_id, status_code, patron_id) VALUES ('$x', 'FreeForm', 'CPL', 'NEW', 52);"; done | koha-mysql kohadev ``` In order to solve this bug, we decided to update fixedHeader to v4.0.1 TEST PLAN: 1. Have a fresh KTD 2. Run: $ ktd --shell k$ bash <(curl -s https://raw.githubusercontent.com/ammopt/koha-ill-dev/master/start-ill-dev.sh) k$ cd k$ git clone https://github.com/ammopt/koha-ill-dev.git k$ cd koha-ill-dev/ k$ sudo apt install libdata-faker-perl libtext-lorem-perl k$ PERL5LIB=$PERL5LIB:./lib perl fake_data.pl --how-many 200 --entity ill k$ for x in {1..21}; do echo "INSERT INTO illbatches (name, backend, library_id, status_code, patron_id) VALUES ('$x', 'FreeForm', 'CPL', 'NEW', 52);"; done | koha-mysql kohadev 3. Go to /cgi-bin/koha/ill/ill-requests.pl?method=batch_list 4. Scroll down => FAIL: Koha is your enemy! 5. Apply patch and refresh cache from server 6. Go to /cgi-bin/koha/ill/ill-requests.pl?method=batch_list Koha and you signed a peace treaty! --- .../lib/datatables/datatables.css | 23 +- .../lib/datatables/datatables.js | 443 ++++++++++++------ 2 files changed, 304 insertions(+), 162 deletions(-) diff --git a/koha-tmpl/intranet-tmpl/lib/datatables/datatables.css b/koha-tmpl/intranet-tmpl/lib/datatables/datatables.css index b828164..f2ddac2 100644 --- a/koha-tmpl/intranet-tmpl/lib/datatables/datatables.css +++ b/koha-tmpl/intranet-tmpl/lib/datatables/datatables.css @@ -1235,18 +1235,12 @@ html.dark div.dt-button-collection { box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.8); } - -table.fixedHeader-floating { - background-color: white; -} - -table.fixedHeader-floating.no-footer { - border-bottom-width: 0; -} - +/* fixedHeader 4.0.1 */ +table.fixedHeader-floating, table.fixedHeader-locked { - position: absolute !important; - background-color: white; + position: relative !important; + background-color: var(--dt-html-background); + background-color: var(--dt-html-background); } @media print { @@ -1254,13 +1248,8 @@ table.fixedHeader-locked { display: none; } } -html.dark table.fixedHeader-floating { - background-color: var(--dt-html-background); -} -html.dark table.fixedHeader-locked { - background-color: var(--dt-html-background); -} +/* End of fixedHeader 4.0.1 */ table.dataTable.dtr-inline.collapsed > tbody > tr > td.child, table.dataTable.dtr-inline.collapsed > tbody > tr > th.child, diff --git a/koha-tmpl/intranet-tmpl/lib/datatables/datatables.js b/koha-tmpl/intranet-tmpl/lib/datatables/datatables.js index d5de7e7..5e26229 100644 --- a/koha-tmpl/intranet-tmpl/lib/datatables/datatables.js +++ b/koha-tmpl/intranet-tmpl/lib/datatables/datatables.js @@ -105931,7 +105931,7 @@ return DataTable; })); -/*! FixedHeader 3.4.0 +/*! FixedHeader 4.0.1 * © SpryMedia Ltd - datatables.net/license */ @@ -105976,7 +105976,7 @@ return DataTable; // Browser factory( jQuery, window, document ); } -}(function( $, window, document, undefined ) { +}(function( $, window, document ) { 'use strict'; var DataTable = $.fn.dataTable; @@ -105986,10 +105986,9 @@ var DataTable = $.fn.dataTable; * @summary FixedHeader * @description Fix a table's header or footer, so it is always visible while * scrolling - * @version 3.4.0 - * @author SpryMedia Ltd (www.sprymedia.co.uk) - * @contact www.sprymedia.co.uk - * @copyright SpryMedia Ltd. + * @version 4.0.1 + * @author SpryMedia Ltd + * @contact datatables.net * * This source file is free software, available under the following license: * MIT license - http://datatables.net/license/mit @@ -106004,6 +106003,10 @@ var DataTable = $.fn.dataTable; var _instCounter = 0; var FixedHeader = function (dt, config) { + if (!DataTable.versionCheck('2')) { + throw 'Warning: FixedHeader requires DataTables 2 or newer'; + } + // Sanity check - you just know it will happen if (!(this instanceof FixedHeader)) { throw "FixedHeader must be initialised with the 'new' keyword."; @@ -106052,13 +106055,13 @@ var FixedHeader = function (dt, config) { header: { host: null, floating: null, - floatingParent: $('
'), + floatingParent: $('
'), placeholder: null }, footer: { host: null, floating: null, - floatingParent: $('
'), + floatingParent: $('
'), placeholder: null } }; @@ -106068,7 +106071,9 @@ var FixedHeader = function (dt, config) { var dtSettings = dt.settings()[0]; if (dtSettings._fixedHeader) { - throw 'FixedHeader already initialised on table ' + dtSettings.nTable.id; + throw ( + 'FixedHeader already initialised on table ' + dtSettings.nTable.id + ); } dtSettings._fixedHeader = this; @@ -106197,6 +106202,8 @@ $.extend(FixedHeader.prototype, { this._positions(); this._scroll(force !== undefined ? force : true); + this._widths(this.dom.header); + this._widths(this.dom.footer); }, /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * @@ -106281,14 +106288,16 @@ $.extend(FixedHeader.prototype, { if (!force && itemDom.floating) { // existing floating element - reuse it - itemDom.floating.removeClass('fixedHeader-floating fixedHeader-locked'); + itemDom.floating.removeClass( + 'fixedHeader-floating fixedHeader-locked' + ); } else { if (itemDom.floating) { if (itemDom.placeholder !== null) { itemDom.placeholder.remove(); } - this._unsize(item); + itemDom.floating.children().detach(); itemDom.floating.remove(); } @@ -106300,35 +106309,41 @@ $.extend(FixedHeader.prototype, { itemDom.floating = $(dt.table().node().cloneNode(false)) .attr('aria-hidden', 'true') .css({ - 'table-layout': 'fixed', top: 0, left: 0 }) - .removeAttr('id') - .append(itemElement); + .removeAttr('id'); itemDom.floatingParent .css({ - width: scrollBody.width(), + width: scrollBody[0].offsetWidth, overflow: 'hidden', height: 'fit-content', position: 'fixed', - left: scrollEnabled ? tableNode.offset().left + scrollBody.scrollLeft() : 0 + left: scrollEnabled + ? tableNode.offset().left + scrollBody.scrollLeft() + : 0 }) .css( item === 'header' ? { top: this.c.headerOffset, bottom: '' - } + } : { top: '', bottom: this.c.footerOffset - } + } + ) + .addClass( + item === 'footer' + ? 'dtfh-floatingparent-foot' + : 'dtfh-floatingparent-head' ) - .addClass(item === 'footer' ? 'dtfh-floatingparentfoot' : 'dtfh-floatingparenthead') - .append(itemDom.floating) - .appendTo('body'); + .appendTo('body') + .children() + .eq(0) + .append(itemDom.floating); this._stickyPosition(itemDom.floating, '-'); @@ -106341,14 +106356,42 @@ $.extend(FixedHeader.prototype, { scrollLeftUpdate(); scrollBody.off('scroll.dtfh').on('scroll.dtfh', scrollLeftUpdate); + // Need padding on the header's container to allow for a scrollbar, + // just like how DataTables handles it + itemDom.floatingParent.children().css({ + width: 'fit-content', + paddingRight: that.s.dt.settings()[0].oBrowser.barWidth + }); + + // Blocker to hide the table behind the scrollbar - this needs to use + // fixed positioning in the container since we don't have an outer wrapper + let blocker = $( + item === 'footer' + ? 'div.dtfc-bottom-blocker' + : 'div.dtfc-top-blocker', + dt.table().container() + ); + + if (blocker.length) { + blocker + .clone() + .appendTo(itemDom.floatingParent) + .css({ + position: 'fixed', + right: blocker.width() + }); + } + // Insert a fake thead/tfoot into the DataTable to stop it jumping around itemDom.placeholder = itemElement.clone(false); itemDom.placeholder.find('*[id]').removeAttr('id'); + // Move the thead / tfoot elements around - original into the floating + // element and clone into the original table itemDom.host.prepend(itemDom.placeholder); + itemDom.floating.append(itemElement); - // Clone widths - this._matchWidths(itemDom.placeholder, itemDom.floating); + this._widths(itemDom); } }, @@ -106367,17 +106410,16 @@ $.extend(FixedHeader.prototype, { if ($(this).css('position') === 'sticky') { var right = $(this).css('right'); var left = $(this).css('left'); + var potential; + if (right !== 'auto' && !rtl) { - // New position either adds or dismisses the barWidth - var potential = - +right.replace(/px/g, '') + - (sign === '-' ? -1 : 1) * that.s.dt.settings()[0].oBrowser.barWidth; + potential = +right.replace(/px/g, '') + $(this).css('right', potential > 0 ? potential : 0); } else if (left !== 'auto' && rtl) { - var potential = - +left.replace(/px/g, '') + - (sign === '-' ? -1 : 1) * that.s.dt.settings()[0].oBrowser.barWidth; + potential = +left.replace(/px/g, ''); + $(this).css('left', potential > 0 ? potential : 0); } } @@ -106385,69 +106427,6 @@ $.extend(FixedHeader.prototype, { } }, - /** - * Copy widths from the cells in one element to another. This is required - * for the footer as the footer in the main table takes its sizes from the - * header columns. That isn't present in the footer so to have it still - * align correctly, the sizes need to be copied over. It is also required - * for the header when auto width is not enabled - * - * @param {jQuery} from Copy widths from - * @param {jQuery} to Copy widths to - * @private - */ - _matchWidths: function (from, to) { - var get = function (name) { - return $(name, from) - .map(function () { - return ( - $(this) - .css('width') - .replace(/[^\d\.]/g, '') * 1 - ); - }) - .toArray(); - }; - - var set = function (name, toWidths) { - $(name, to).each(function (i) { - $(this).css({ - width: toWidths[i], - minWidth: toWidths[i] - }); - }); - }; - - var thWidths = get('th'); - var tdWidths = get('td'); - - set('th', thWidths); - set('td', tdWidths); - }, - - /** - * Remove assigned widths from the cells in an element. This is required - * when inserting the footer back into the main table so the size is defined - * by the header columns and also when auto width is disabled in the - * DataTable. - * - * @param {string} item The `header` or `footer` - * @private - */ - _unsize: function (item) { - var el = this.dom[item].floating; - - if (el && (item === 'footer' || (item === 'header' && !this.s.autoWidth))) { - $('th, td', el).css({ - width: '', - minWidth: '' - }); - } - else if (el && item === 'header') { - $('th, td', el).css('min-width', ''); - } - }, - /** * Reposition the floating elements to take account of horizontal page * scroll @@ -106458,13 +106437,14 @@ $.extend(FixedHeader.prototype, { */ _horizontal: function (item, scrollLeft) { var itemDom = this.dom[item]; - var position = this.s.position; var lastScrollLeft = this.s.scrollLeft; if (itemDom.floating && lastScrollLeft[item] !== scrollLeft) { // If scrolling is enabled we need to match the floating header to the body if (this._scrollEnabled()) { - var newScrollLeft = $($(this.s.dt.table().node()).parent()).scrollLeft(); + var newScrollLeft = $( + $(this.s.dt.table().node()).parent() + ).scrollLeft(); itemDom.floating.scrollLeft(newScrollLeft); itemDom.floatingParent.scrollLeft(newScrollLeft); } @@ -106489,7 +106469,6 @@ $.extend(FixedHeader.prototype, { * @private */ _modeChange: function (mode, item, forceChange) { - var dt = this.s.dt; var itemDom = this.dom[item]; var position = this.s.position; @@ -106504,15 +106483,11 @@ $.extend(FixedHeader.prototype, { // It isn't trivial to add a !important css attribute... var importantWidth = function (w) { - itemDom.floating.attr('style', function (i, s) { - return (s || '') + 'width: ' + w + 'px !important;'; - }); + itemDom.floating[0].style.setProperty('width', w + 'px', 'important'); // If not scrolling also have to update the floatingParent if (!scrollEnabled) { - itemDom.floatingParent.attr('style', function (i, s) { - return (s || '') + 'width: ' + w + 'px !important;'; - }); + itemDom.floatingParent[0].style.setProperty('width', w + 'px', 'important'); } }; @@ -106531,8 +106506,6 @@ $.extend(FixedHeader.prototype, { itemDom.placeholder = null; } - this._unsize(item); - if (item === 'header') { itemDom.host.prepend(tablePart); } @@ -106547,13 +106520,16 @@ $.extend(FixedHeader.prototype, { } if (itemDom.floatingParent) { + itemDom.floatingParent.find('div.dtfc-top-blocker').remove(); itemDom.floatingParent.remove(); } - $($(itemDom.host.parent()).parent()).scrollLeft(scrollBody.scrollLeft()); + $($(itemDom.host.parent()).parent()).scrollLeft( + scrollBody.scrollLeft() + ); } else if (mode === 'in') { - // Remove the header from the read header and insert into a fixed + // Remove the header from the real table and insert into a fixed // positioned floating table clone this._clone(item, forceChange); @@ -106568,17 +106544,23 @@ $.extend(FixedHeader.prototype, { : position.tfootTop; // Calculate the amount that the footer or header needs to be shuffled - var shuffle = - item === 'footer' - ? // footer and top of body isn't on screen - bodyTop > windowBottom - ? // Yes - push the footer below - position.tfootHeight - : // No - bottom set to the gap between the top of the body and the bottom of the window - bodyTop + position.tfootHeight - windowBottom - : // Otherwise must be a header so get the difference from the bottom of the - // desired floating header and the bottom of the table body - windowTop + this.c.headerOffset + position.theadHeight - bodyBottom; + var shuffle; + + if (item === 'footer') { + shuffle = + bodyTop > windowBottom + ? position.tfootHeight // Yes - push the footer below + : bodyTop + position.tfootHeight - windowBottom; // No + } + else { + // Otherwise must be a header so get the difference from the bottom of the + // desired floating header and the bottom of the table body + shuffle = + windowTop + + this.c.headerOffset + + position.theadHeight - + bodyBottom; + } // Set the top or bottom based off of the offset and the shuffle value var prop = item === 'header' ? 'top' : 'bottom'; @@ -106589,10 +106571,8 @@ $.extend(FixedHeader.prototype, { .css(prop, val) .css({ left: position.left, - height: item === 'header' ? position.theadHeight : position.tfootHeight, - 'z-index': 2 - }) - .append(itemDom.floating); + 'z-index': 3 + }); importantWidth(position.width); @@ -106667,20 +106647,23 @@ $.extend(FixedHeader.prototype, { position.width = tableNode.outerWidth(); position.left = tableNode.offset().left; position.theadTop = thead.offset().top; - position.tbodyTop = scrollEnabled ? scrollBody.offset().top : tbody.offset().top; - position.tbodyHeight = scrollEnabled ? scrollBody.outerHeight() : tbody.outerHeight(); + position.tbodyTop = scrollEnabled + ? scrollBody.offset().top + : tbody.offset().top; + position.tbodyHeight = scrollEnabled + ? scrollBody.outerHeight() + : tbody.outerHeight(); position.theadHeight = thead.outerHeight(); position.theadBottom = position.theadTop + position.theadHeight; + position.tfootTop = position.tbodyTop + position.tbodyHeight; //tfoot.offset().top; if (tfoot.length) { - position.tfootTop = position.tbodyTop + position.tbodyHeight; //tfoot.offset().top; position.tfootBottom = position.tfootTop + tfoot.outerHeight(); position.tfootHeight = tfoot.outerHeight(); } else { - position.tfootTop = position.tbodyTop + tbody.outerHeight(); position.tfootBottom = position.tfootTop; - position.tfootHeight = position.tfootTop; + position.tfootHeight = 0; } }, @@ -106715,10 +106698,12 @@ $.extend(FixedHeader.prototype, { // Body Details var bodyTop = scrollEnabled ? scrollOffset.top : position.tbodyTop; var bodyLeft = scrollEnabled ? scrollOffset.left : position.left; - var bodyBottom = scrollEnabled ? scrollOffset.top + scrollHeight : position.tfootTop; - var bodyWidth = scrollEnabled ? scrollBody.outerWidth() : position.tbodyWidth; - - var windowBottom = windowTop + windowHeight; + var bodyBottom = scrollEnabled + ? scrollOffset.top + scrollHeight + : position.tfootTop; + var bodyWidth = scrollEnabled + ? scrollBody.outerWidth() + : position.tbodyWidth; if (this.c.header) { if (!this.s.enable) { @@ -106728,24 +106713,27 @@ $.extend(FixedHeader.prototype, { // the scroll of the window plus the headerOffset and the height of the header else if ( !position.visible || - windowTop + this.c.headerOffset + position.theadHeight <= bodyTop + windowTop + this.c.headerOffset + position.theadHeight <= + bodyTop ) { headerMode = 'in-place'; } // The header should be floated if else if ( // The scrolling plus the header offset plus the height of the header is lower than the top of the body - windowTop + this.c.headerOffset + position.theadHeight > bodyTop && + windowTop + this.c.headerOffset + position.theadHeight > + bodyTop && // And the scrolling at the top plus the header offset is above the bottom of the body - windowTop + this.c.headerOffset + position.theadHeight < bodyBottom + windowTop + this.c.headerOffset + position.theadHeight < + bodyBottom ) { headerMode = 'in'; - var scrollBody = $($(this.s.dt.table().node()).parent()); // Further to the above, If the scrolling plus the header offset plus the header height is lower // than the bottom of the table a shuffle is required so have to force the calculation if ( - windowTop + this.c.headerOffset + position.theadHeight > bodyBottom || + windowTop + this.c.headerOffset + position.theadHeight > + bodyBottom || this.dom.header.floatingParent === undefined ) { forceChange = true; @@ -106756,6 +106744,8 @@ $.extend(FixedHeader.prototype, { top: this.c.headerOffset, position: 'fixed' }) + .children() + .eq(0) .append(this.dom.header.floating); } } @@ -106780,7 +106770,11 @@ $.extend(FixedHeader.prototype, { height: 0 }; - if (this.c.footer && this.dom.tfoot.length) { + if ( + this.c.footer && + this.dom.tfoot.length && + this.dom.tfoot.find('th, td').length + ) { if (!this.s.enable) { footerMode = 'in-place'; } @@ -106791,7 +106785,8 @@ $.extend(FixedHeader.prototype, { footerMode = 'in-place'; } else if ( - bodyBottom + position.tfootHeight + this.c.footerOffset > windowBottom && + bodyBottom + position.tfootHeight + this.c.footerOffset > + windowBottom && bodyTop + this.c.footerOffset < windowBottom ) { footerMode = 'in'; @@ -106852,12 +106847,17 @@ $.extend(FixedHeader.prototype, { // Need some rounding here as sometimes very small decimal places are encountered // If the actual height is bigger or equal to the height we just applied then the footer is "Floating" - if (Math.round(scrollBody.outerHeight()) >= Math.round(newHeight)) { + if ( + Math.round(scrollBody.outerHeight()) >= + Math.round(newHeight) + ) { $(this.dom.tfoot.parent()).addClass('fixedHeader-floating'); } // Otherwise max-width has kicked in so it is not floating else { - $(this.dom.tfoot.parent()).removeClass('fixedHeader-floating'); + $(this.dom.tfoot.parent()).removeClass( + 'fixedHeader-floating' + ); } } } @@ -106875,16 +106875,27 @@ $.extend(FixedHeader.prototype, { if (this.s.dt.settings()[0]._fixedColumns !== undefined) { var adjustBlocker = function (side, end, el) { if (el === undefined) { - var blocker = $('div.dtfc-' + side + '-' + end + '-blocker'); + var blocker = $( + 'div.dtfc-' + side + '-' + end + '-blocker' + ); - el = blocker.length === 0 ? null : blocker.clone().css('z-index', 1); + el = + blocker.length === 0 + ? null + : blocker.clone().css('z-index', 1); } if (el !== null) { if (headerMode === 'in' || headerMode === 'below') { el.appendTo('body').css({ - top: end === 'top' ? header.offset.top : footer.offset.top, - left: side === 'right' ? bodyLeft + bodyWidth - el.width() : bodyLeft + top: + end === 'top' + ? header.offset.top + : footer.offset.top, + left: + side === 'right' + ? bodyLeft + bodyWidth - el.width() + : bodyLeft }); } else { @@ -106901,7 +106912,11 @@ $.extend(FixedHeader.prototype, { 'top', this.dom.header.rightBlocker ); - this.dom.header.leftBlocker = adjustBlocker('left', 'top', this.dom.header.leftBlocker); + this.dom.header.leftBlocker = adjustBlocker( + 'left', + 'top', + this.dom.header.leftBlocker + ); this.dom.footer.rightBlocker = adjustBlocker( 'right', 'bottom', @@ -106925,6 +106940,46 @@ $.extend(FixedHeader.prototype, { return true; } return false; + }, + + /** + * Realign columns by using the colgroup tag and + * checking column widths + */ + _widths: function (itemDom) { + if (! itemDom || ! itemDom.placeholder) { + return; + } + + // Match the table overall width + var tableNode = $(this.s.dt.table().node()); + var scrollBody = $(tableNode.parent()); + + itemDom.floatingParent.css('width', scrollBody[0].offsetWidth); + itemDom.floating.css('width', tableNode[0].offsetWidth); + + // Strip out the old colgroup + $('colgroup', itemDom.floating).remove(); + + // Copy the `colgroup` element to define the number of columns - needed + // for complex header cases where a column might not have a unique + // header + var cols = itemDom.placeholder + .parent() + .find('colgroup') + .clone() + .appendTo(itemDom.floating) + .find('col'); + + // However, the widths defined in the colgroup from the DataTable might + // not exactly reflect the actual widths of the columns (content can + // force it to stretch). So we need to copy the actual widths into the + // colgroup / col's used for the floating header. + var widths = this.s.dt.columns(':visible').widths(); + + for (var i=0 ; i