Friday, July 22, 2016

(scroll)height of children influences scrollheight of page

This is a follow-up on earlier post ‘Freeze pane on SharePoint list
Business users very much appreciate the inserted functionality to freeze the listview header in visible top of screen, while one scrolls through the content. However, they reported a problem in case of smaller display size: it was not possible to scroll completely to the bottom, and also the page navigation controls cannot be reached.
The cause is that in my initial code I decided to unconditional hide the page scrollbar. I decided for this as otherwise the page scrollbar would remain in state to scroll through a large list, despite my changes to reduce the height of that list and give it a scrollbar for itself.
The simple way out to resolve the reported issue would be to just not hide the page scrollbar. But from User Experience point of view I dislike that, the result with 2 scrollbars and the page scrollbar over full initial height, is confusing for end users. I instead opted for only displaying the scrollbar when needed (screen smaller as minimal height for listview), and in such case reduce the scrollheight of page to only scroll to the bottom of the height-reduced listview. Thus not for the full initial height.
At first (html) code attempts this was not that simple to achieve, whatever reduction I made still the page scrollheight remained at the initial large value. Via debugger I identified the cause. The scrollheight of the listview remained at the large value, despite that I reduced the visible height. And the scrollheight of page as parent is directly influenced by the sum of scrollheights of the child elements in page. The scrollheight value is readonly, thus not possible to also reduce this to match the reduced height of the listview Div element.
Post ‘Making elements not affect page height (thus scrolling)’ put me on track to break out of this: It appears that scrollheight of relative positioned children has impact; but not that of absolute positioned children. So what I needed to change in my initial FreezePane code was to break the relative parent-child positioning relation for the listview with respect to page. This is accomplished by following changes:
// Set visible height, with minimum of 300 var visibleTableHeight = Math.max(availableHeight, 300); // $table.wrap("<DIV class='FreezedLV' style='OVERFLOW: auto; HEIGHT: 500px;'></DIV>"); $table.wrap("<DIV class='FreezedLV' style='position:absolute; left:0; top: 0; width:100%; OVERFLOW: auto; HEIGHT: " + visibleTableHeight + "px;'></DIV>"); // $(".FreezedLV").wrap("<DIV class='FreezedLVContainer'></DIV>"); $(".FreezedLV").wrap("<DIV class='FreezedLVContainer' style='position:relative; margin:0px; height:" + (visibleTableHeight + freezedTRHeight) + "px;'></DIV>");
The end-result: now truly happy users, some of which forced to view the page on small(er) laptop screensizes.
The complete 'FreezePane' method:
function FreezePane() { var $table = $(".ms-listviewtable").first().data("summary", "list name"); // Determine the available height for table to render in visible screen. var origWorkspaceHeight = $("#s4-workspace").height(); var origTableHeight = $table.height(); var spaceInWorkspaceWithoutTable = origWorkspaceHeight - origTableHeight; var windowHeight = window.innerHeight; if (windowHeight == undefined) { windowHeight = document.documentElement.clientHeight; } var topPos = $("#s4-workspace").offset().top; var freezedTRHeight = 25; var availableHeight = windowHeight - topPos - spaceInWorkspaceWithoutTable - freezedTRHeight; // Set visible height, with minimum of 300 var visibleTableHeight = Math.max(availableHeight, 300); // WRAP TABLE IN SCROLL PANE $("#s4-workspace").css( { 'overflow-y': 'auto' } ); $table.wrap("<DIV class='FreezedLV' style='position:absolute; left:0; top: 20; width:100%; OVERFLOW: auto; HEIGHT: " + visibleTableHeight + "px;'></DIV>"); // FROZEN HEADER ROW $(".FreezedLV").wrap("<DIV class='FreezedLVContainer' style='position:relative; margin:0px; height:" + (visibleTableHeight + freezedTRHeight) + "px;'></DIV>"); $("<table id='FreezedTR' class='ms-listviewtable' cellPadding='1' cellSpacing='0'></table>").insertBefore(".FreezedLV"); $("#FreezedTR").width($table.width() + "px"); var $origHeader = $("", $table); var $firstRowTable = $("", $table); var $freezeHeader = $origHeader.clone(); // Propagate computed width of columns of origheader to freezeheader $origHeader.children("th").each(function() { var width = $(this).width(); var ownerIndex = $(this).index(); $($freezeHeader.children("th")[ownerIndex]).width(width + "px"); }); $("#FreezedTR").append($freezeHeader); $("#FreezedTR").append($firstRowTable.clone()); $("#FreezedTR").wrap("<DIV style='OVERFLOW: hidden; HEIGHT: " + freezedTRHeight + "px;'></DIV>"); // Visualize "hide" the orig header, make sure it still is rendered as otherwise the alignment of 'body' rows is altered $origHeader.children("th").each(function() { $(this).css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px' , 'padding-top' : '0px', 'padding-bottom' : '0px' } ); $(this).find("div").css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px', 'margin-top' : '0px', 'margin-bottom' : '0px' } ); $(this).find("input").css( { 'height' : '0px' , 'max-height' : '0px', 'min-height' : '0px' } ); }); $origHeader.css( { 'max-height' : '1px', 'visibility' : 'hidden' } ); // Delegate eventhandlers from the copied+freezed header to the actual header of the listview. var $inputFH = $freezeHeader.find("input[title='Select or deselect all items']"); $inputFH[0].onclick = null; $inputFH[0].onfocus = null; $table.onmouseover = null; $ { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-viewheadertr").find("input[title='Select or deselect all items']").trigger('click'); }); $inputFH.focus(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-viewheadertr").find("input[title='Select or deselect all items']").trigger('focus'); }); $("#FreezedTR").mouseover(function() { $(this).closest(".FreezedLVContainer").find(".FreezedLV .ms-listviewtable").trigger('mouseover'); }); }


  1. This is really slick William - I'm glad I stumbled upon your blog! However, when I implement this code I can select multiple items, but the "select all" option at the top no longer works. Does that happen to you too or is that due to some setting on my end? Thanks for the great post!

    1. Thanks for the compliments and thanks for informing me on the issue; neither myself nor end-users were aware of this. I've update the code-snippet to restore the 'selectAll/deselectAll' functionality also in the frozen header.

  2. That worked like a charm. Please add a donation link to your site - I'd like to buy you a beer!

    1. Glad you like.
      My intend to blog is for sharing knowledge, and win appreciation by peers acknowledgement. Thanks for the offer though ;-)

  3. Ah, spied one more little quirk: if you go directly to select/deselect without first giving focus to the list rows or otherwise interacting directly with the table it will still not register.

    1. You're a thorough tester, thank you. Fixed this one also, code-snippet updated.

  4. Ok, so as the thorough code tester: now if you mouse over the table and then select all it checks everything *except* for the last row. So close!

    1. Tried to reproduce this, but works in my context; in all browsers.

      Regarding the code addition / tweek you suggested:
      $("#FreezedTR").mouseover(function() {
      .find(".FreezedLV .ms-listviewtable")
      .find("input[title='Select or deselect all items']")

      I see no use in that. The standard HTML returned for ListView does not include a 'onmouseover' event-handler on the (de)select-all checkbox:
      <INPUT title="Select or deselect all items"
      class=s4-selectAllCbx style="HEIGHT: 0px; MIN-HEIGHT: 0px; MAX-HEIGHT: 0px"
      onclick=ToggleAllItems(event,this,2801) type=checkbox value="">

      Propagating the mouseover event from the freezed header to this checkbox therefore will not do anything.

    2. Strange, but for some reason I had that issue and that line of code resolved it - maybe it was some conflict with another script. In any event, I'm all squared away now. Much obliged!