Tuesday, April 16, 2013

"Alway in view" paradigm in web

I was thinking about writing the next post in my blog and remembered one well-known paradigm I used in my completed web projects. The paradigm is to show current element(s) to be navigated always in view, that means visible. You know, a classic web application can have a long content causing scrollbars in browser. For instance, a vertical scrollbar means there is a hidden content - above or below the current viewport. Not only the browser, a div element can have scrollbars and a viewport as well (as long as the style overflow:hidden is not set). Keeping important element(s) always in view increases the usability. If an element, which is expected to be shown, doesn't stay visible, the user is often confused and forced to navigate the element manually. Just two examples with screenshots and jQuery based solutions.

Assume we have two tables. The first table on the left side shows users in a system. Rows are clickable. User's details for a clicked row are shown in the second table on the right side.


The left table is big and when we're scrolling down, at certain point we can not see the right table at all. Now, a row click in the left table updates the right table, but it might be hidden due to its small length.


Is it user friendly? No. To overcome this problem we will implement a jQuery function which checks if a specified element is beyond the visible area.
$.fn.showTopInView = function () {
    var winTop = $(window).scrollTop();
    var bounds = this.offset();

    if (winTop > bounds.top) {
        $('html, body').animate({scrollTop: 0}, 'fast');
    }
};
The .offset() method allows us to retrieve the current position of an element relative to the document. We compare element's top position with the amount we have scrolled from the topmost part of the page (calculated by $(window).scrollTop()). If the element (referenced with this) is located in the hidden area, its top position is less than this scrolled amount. In this case, we simple scroll to topmost part of the page with an animation. Using is simple. In our example, we should force execution of the following script when row clicking:
 
$('#IdOfTheRightTableContainer').showTopInView();
 
This can happen in onclick callback directly or streamed down as an JS script in response to the browser (if clicks cause AJAX requests). IdOfTheRightTableContainer is an Id of the div element the right table is placed in.

Another example is more complicated. As I said, scrollbars can also appear in div and hide inner elements. Assume again, we have a table included in such div container. The table displays some search results. e.g. documents and folders. Furthermore, assume we have thumbnails for every entry in this result table. User clicks on a thumbnail and want to see the related table's entry. The requirement thus - the table's entry of the selected thumbnail should be selected (highlighted) as well and should scroll to the div's viewport in order to always stay visible. The picture below demonstrates this behavior.


The script for solving this task uses the jQuery's method .position(). Other as .offset(), .position() retrieves the current position relative to the offset parent. The offset parent should be positioned (position: absolute or relative). In our example the offset parent is a relative positioned div element with the style class ui-layout-content (note: the web app used the jQuery Layout plugin). First, we need to write a function which will find the table's entry by some given identifier (here pid argument). This function gets invoked when user clicks on a thumbnail.
function selectResultEntryOnCurrentPage (pid) {
    var $resultTable = $("#searchResultTable");
    var $resultEntry = $resultTable.find(".resultEntry[data-pid='" + pid + "']");
    $resultTable.find(".resultEntry").removeClass("selected");
    $resultEntry.addClass("selected");
    
    scrollToResultEntry ($resultTable, $resultEntry);
}
What we did here, was only a row selection. The table and the found table's entry are passed as parameters into the next function scrollToResultEntry. This function is responsible for keeping the selected entry visible.
function scrollToResultEntry ($resultTable, $resultEntry) {
    // scroll to the entry with animation if it's not completely visible
    var $scrollPane = $resultTable.closest(".ui-layout-content");
    var entryPos = $resultEntry.position();
    if (entryPos.top < 0) {
        // element has parts in the hidden top area of the scrollable layout pane ==> scroll up
        $scrollPane.animate({scrollTop: $scrollPane.scrollTop() + entryPos.top - 5}, "fast");
    } else if ($scrollPane.height() < entryPos.top + $resultEntry.outerHeight()) {
        // element has parts in the hidden bottom area of the scrollable layout pane ==> scroll down
        $scrollPane.animate({
            scrollTop: $scrollPane.scrollTop() + $resultEntry.outerHeight() + (entryPos.top - $scrollPane.height()) + 5}, "fast");
    }    
}
We measure the top position of the selected table's entry. Negative value means the entry is not completely visible and has some parts in the top area of the scrollable div (the mentioned offset parent). The second condition $scrollPane.height() < entryPos.top + $resultEntry.outerHeight() means the entry is not completely visible and has some parts in the bottom area of the scrollable div. In both cases, the entry is scrolled to the div's viewport with an animation. The new vertical scroll position is calculated (a little bit tricky) and set in the scrollTop option.