floatingScroll

Lightweight jQuery plugin providing floating scrollbar functionality

Introductory remarks

Below, you will find some demos using floatingScroll, a jQuery plugin providing any lengthy containers on the page with a separate horizontal scrollbar, which does not get out of sight when the entire page is scrolled.

If you are looking for a guide on plugin usage and API please check out this README on GitHub.

#1: Table with collapsible columns

Description

Among other things this example demonstrates one of the use cases for the update method. Control column visibility by switching the states on corresponding checkboxes.

The table below was copied from whatwg.org for demonstration purposes only, the data presented here can be out of date.

Live demo

  Hidden Text, Search URL, Telephone E-mail Password Date and Time, Date, Month, Week, Time Local Date and Time Number Range Color Checkbox, Radio Button File Upload Submit Button Image Button Reset Button, Button
Content attributes
accept Yes
alt Yes
autocomplete Yes Yes Yes Yes Yes Yes Yes Yes Yes
checked Yes
dirname Yes
formaction Yes Yes
formenctype Yes Yes
formmethod Yes Yes
formnovalidate Yes Yes
formtarget Yes Yes
height Yes
list Yes Yes Yes Yes Yes Yes Yes Yes
max Yes Yes Yes Yes
maxlength Yes Yes Yes Yes
min Yes Yes Yes Yes
multiple Yes Yes
pattern Yes Yes Yes Yes
placeholder Yes Yes Yes Yes Yes
readonly Yes Yes Yes Yes Yes Yes Yes
required Yes Yes Yes Yes Yes Yes Yes Yes Yes
size Yes Yes Yes Yes
src Yes
step Yes Yes Yes Yes
width Yes
IDL attributes and methods
checked Yes
files Yes
value default value value value value value value value value value default/on filename default default default
valueAsDate Yes
valueAsNumber Yes Yes Yes Yes
list Yes Yes Yes Yes Yes Yes Yes Yes
selectedOption Yes Yes Yes† Yes Yes Yes Yes Yes
select() Yes Yes Yes
selectionStart Yes Yes Yes
selectionEnd Yes Yes Yes
selectionDirection Yes Yes Yes
setSelectionRange() Yes Yes Yes
stepDown() Yes Yes Yes Yes
stepUp() Yes Yes Yes Yes
Events
input event Yes Yes Yes Yes Yes Yes Yes Yes Yes
change event Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes

Demo code


// ⓘ Plugin initialization       
$(".fs-whatwg").floatingScroll();

// Handle checkbox state changes and update floating scrollbar
$(".fs-whatwg-cols").on("change", "input[type='checkbox']", function (e) {
    var $checkboxes = $(e.delegateTarget).find("input[type='checkbox']"),
        index;
    if ($checkboxes.filter(":checked").length) {
        index = $checkboxes.index(e.target);
        if (index > -1) {
            $("#fs-whatwg").find("th, td")
                .filter(":nth-child(" + (index + 2) + ")")
                .toggleClass("hidden");
        }
    } else {
        $checkboxes.prop("checked", true);
        $("#fs-whatwg").find("th.hidden, td.hidden").removeClass("hidden");
    }
    // ⓘ Force scrollbar update programmatically
    $(".fs-whatwg").floatingScroll("update");
});
            

#2: Code listing

Description

The listing below has long lines of code making horizontal scrolling necessary. This example also demonstrates one of the use cases for the destroy method.

Live demo

and turn on/off floating scrollbar


import $ from "jquery";

class FScroll {
    constructor(cont) {
        let inst = this;
        inst.cont = cont[0];
        let scrollBody = cont.closest(".fl-scrolls-body");
        if (scrollBody.length) {
            inst.scrollBody = scrollBody;
        }
        inst.sbar = inst.initScroll();
        inst.visible = true;
        inst.updateAPI(); // recalculate floating scrolls and hide those of them whose containers are out of sight
        inst.syncSbar(inst.cont);
        inst.addEventHandlers();
    }

    initScroll() {
        let flscroll = $("<div class='fl-scrolls'></div>");
        let {cont} = this;
        $("<div></div>").appendTo(flscroll).css({width: `${cont.scrollWidth}px`});
        return flscroll.appendTo(cont);
    }

    addEventHandlers() {
        let inst = this;
        let eventHandlers = inst.eventHandlers = [
            {
                $el: inst.scrollBody || $(window),
                handlers: {
                    // Don't use `$.proxy()` since it makes impossible event unbinding individually per instance (see the warning at http://api.jquery.com/unbind/)
                    scroll() {
                        inst.checkVisibility();
                    },
                    resize() {
                        inst.updateAPI();
                    }
                }
            },
            {
                $el: inst.sbar,
                handlers: {
                    scroll({target}) {
                        inst.visible && inst.syncCont(target, true);
                    }
                }
            },
            {
                $el: $(inst.cont),
                handlers: {
                    scroll({target}) {
                        inst.syncSbar(target, true);
                    },
                    focusin() {
                        setTimeout(inst.syncSbar.bind(inst, inst.cont), 0);
                    },
                    "update.fscroll"({namespace}) {
                        if (namespace === "fscroll") { // Check event namespace to ensure that this is not an extraneous event in a bubbling phase
                            inst.updateAPI();
                        }
                    },
                    "destroy.fscroll"({namespace}) {
                        if (namespace === "fscroll") {
                            inst.destroyAPI();
                        }
                    }
                }
            }
        ];
        eventHandlers.forEach(({$el, handlers}) => $el.bind(handlers));
    }

    checkVisibility() {
        let inst = this;
        let mustHide = (inst.sbar[0].scrollWidth <= inst.sbar[0].offsetWidth);
        if (!mustHide) {
            let contRect = inst.cont.getBoundingClientRect();
            let maxVisibleY = inst.scrollBody ?
                inst.scrollBody[0].getBoundingClientRect().bottom :
                window.innerHeight || document.documentElement.clientHeight;
            mustHide = ((contRect.bottom <= maxVisibleY) || (contRect.top > maxVisibleY));
        }
        if (inst.visible === mustHide) {
            inst.visible = !mustHide;
            // we cannot simply hide a floating scrollbar since its scrollLeft property will not update in that case
            inst.sbar.toggleClass("fl-scrolls-hidden");
        }
    }

    syncCont(sender, preventSyncSbar) {
        let inst = this;
        // Prevents next syncSbar function from changing scroll position
        if (inst.preventSyncCont === true) {
            inst.preventSyncCont = false;
            return;
        }
        inst.preventSyncSbar = !!preventSyncSbar;
        inst.cont.scrollLeft = sender.scrollLeft;
    }

    syncSbar(sender, preventSyncCont) {
        let inst = this;
        // Prevents next syncCont function from changing scroll position
        if (inst.preventSyncSbar === true) {
            inst.preventSyncSbar = false;
            return;
        }
        inst.preventSyncCont = !!preventSyncCont;
        inst.sbar[0].scrollLeft = sender.scrollLeft;
    }

    // Recalculate scroll width and container boundaries
    updateAPI() {
        let inst = this;
        let {cont} = inst;
        inst.sbar.width($(cont).outerWidth());
        if (!inst.scrollBody) {
            inst.sbar.css("left", `${cont.getBoundingClientRect().left}px`);
        }
        $("div", inst.sbar).width(cont.scrollWidth);
        inst.checkVisibility(); // fixes issue #2
    }

    // Remove a scrollbar and all related event handlers
    destroyAPI() {
        this.eventHandlers.forEach(({$el, handlers}) => $el.unbind(handlers));
        this.eventHandlers = null;
        this.sbar.remove();
    }
}

$.fn.floatingScroll = function (method = "init") {
    if (method === "init") {
        this.each((index, el) => new FScroll($(el)));
    } else if (FScroll.prototype.hasOwnProperty(`${method}API`)) {
        this.trigger(`${method}.fscroll`);
    }
    return this;
};

$(document).ready(() => {
    $("body [data-fl-scrolls]").floatingScroll();
});
                

Demo code


// ⓘ Plugin initialization        
$(".fs-listing").floatingScroll();

// Destroy or re-create floating scrollbar clicking the button
$("#fs-listing-toggle").on("click", function () {
    var $scrollOwner = $(".fs-listing");
    if ($scrollOwner.hasClass("fs-listing-collapsed")) {
        $scrollOwner
            .removeClass("fs-listing-collapsed")
            // ⓘ Reinitialize       
            .floatingScroll("init");
    } else {
        $scrollOwner
            .addClass("fs-listing-collapsed")
            // ⓘ Destroy and cleanup   
            .floatingScroll("destroy");
    }
});
            

#3: Floating scrollbar in a popup

Description

This example demonstrates two special use cases:

  1. floating scrollbar inside a positioned popup;
  2. vertical floating scrollbar.

Please find out the details of the “custom viewport” mode in the plugin’s docs.

Live demo

Click here to open a popup

×
Floating scrollbar orientation:

Try to unthread the labyrinth!

labyrinth

How is your progress? :)

Demo code (key parts)

HTML:


<!-- Apply the "fl-scrolls-viewport" class to the popup block -->
<div class="fs-popup fs-popup-hidden fl-scrolls-viewport">
    <!-- Apply the "fl-scrolls-body" class to the popup contents block -->
    <div class="fl-scrolls-body">
        <!-- Our horizontally scrollable container -->
        <div id="fs-maze" class="fs-demo fs-maze">
            <!-- Horizontally wide contents -->
        </div>
    </div>
</div>
            

CSS:


.fs-popup {
    height:550px;
    padding:10px;
    position:fixed; /* must be positioned (relative is the default) */
    /* other styles */
}
.fs-popup .fl-scrolls-body {
    height:550px; /* same as for parent (the latter should not be scrollable) */
    width:100%;
}
.fs-popup .fl-scrolls[data-orientation="horizontal"]:not(.fl-scrolls-hidden) {
    bottom:10px; /* same value as the parent’s bottom padding */
    left:10px; /* same value as the parent’s left padding */
}
            

JavaScript:


var orientation = "horizontal"; // or "vertical"
$("#fs-maze").floatingScroll("init", {orientation: orientation});

$("#fs-open-popup").on("click", function (e) {
    // Show the popup
    $(".fs-popup").toggleClass("fs-popup-hidden");
    // ...
    // You may need to force scrollbar update when the popup becomes visible
    $("#fs-maze").floatingScroll("update");
});
            

#4 “Unobtrusive” floating scrollbar

Description

This feature makes a floating scrollbar appear only when the mouse pointer hovers over the scrollable container. Refer the plugin’s docs for more details.

Live demo