import $ from "jquery"; const HORIZONTAL = "horizontal"; const VERTICAL = "vertical"; let getOrientationProps = orientation => { let horizontal = orientation === HORIZONTAL; // Properties with “X” are for cross orientation (relative to the scrollbar orientation) return { ORIENTATION: orientation, SIZE: horizontal ? "width" : "height", X_SIZE: horizontal ? "height" : "width", OFFSET_SIZE: horizontal ? "offsetWidth" : "offsetHeight", OFFSET_X_SIZE: horizontal ? "offsetHeight" : "offsetWidth", CLIENT_SIZE: horizontal ? "clientWidth" : "clientHeight", CLIENT_X_SIZE: horizontal ? "clientHeight" : "clientWidth", INNER_X_SIZE: horizontal ? "innerHeight" : "innerWidth", SCROLL_SIZE: horizontal ? "scrollWidth" : "scrollHeight", SCROLL_POS: horizontal ? "scrollLeft" : "scrollTop", START: horizontal ? "left" : "top", X_START: horizontal ? "top" : "left", X_END: horizontal ? "bottom" : "right" }; }; let floatingScrollProto = { init(container, orientation) { let instance = this; instance.orientationProps = getOrientationProps(orientation); let scrollBody = container.closest(".fl-scrolls-body"); if (scrollBody.length) { instance.scrollBody = scrollBody; } instance.container = container[0]; instance.visible = true; instance.initWidget(); instance.updateAPI(); // recalculate scrollbar parameters and set its visibility instance.addEventHandlers(); // Set skipSync flags to their initial values (because update() above calls syncWidget()) instance.skipSyncContainer = instance.skipSyncWidget = false; }, initWidget() { let instance = this; const {ORIENTATION, SIZE, SCROLL_SIZE} = instance.orientationProps; let widget = instance.widget = $(`
`); $("
").appendTo(widget)[SIZE](instance.container[SCROLL_SIZE]); widget.appendTo(instance.container); }, addEventHandlers() { let instance = this; let eventHandlers = instance.eventHandlers = [ { $el: $(window), handlers: { "destroyDetached.fscroll"({namespace}) { if (namespace === "fscroll") { instance.destroyDetachedAPI(); } } } }, { $el: instance.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() { instance.updateAPI(); }, resize() { instance.updateAPI(); } } }, { $el: instance.widget, handlers: { scroll() { if (instance.visible && !instance.skipSyncContainer) { instance.syncContainer(); } // Resume widget->container syncing after the widget scrolling has finished // (it might be temporally disabled by the container while syncing the widget) instance.skipSyncContainer = false; } } }, { $el: $(instance.container), handlers: { scroll() { if (!instance.skipSyncWidget) { instance.syncWidget(); } // Resume container->widget syncing after the container scrolling has finished // (it might be temporally disabled by the widget while syncing the container) instance.skipSyncWidget = false; }, focusin() { setTimeout(() => { // The widget might be destroyed before the timer is triggered (Amphiluke/handy-scroll#14) if (instance.widget) { instance.syncWidget(); } }, 0); }, "update.fscroll"({namespace}) { // Check event namespace to ensure that this is not an extraneous event in a bubbling phase if (namespace === "fscroll") { instance.updateAPI(); } }, "destroy.fscroll"({namespace}) { if (namespace === "fscroll") { instance.destroyAPI(); } } } } ]; eventHandlers.forEach(({$el, handlers}) => $el.bind(handlers)); }, checkVisibility() { let instance = this; let {widget, container, scrollBody} = instance; const {SCROLL_SIZE, OFFSET_SIZE, X_START, X_END, INNER_X_SIZE, CLIENT_X_SIZE} = instance.orientationProps; let mustHide = (widget[0][SCROLL_SIZE] <= widget[0][OFFSET_SIZE]); if (!mustHide) { let containerRect = container.getBoundingClientRect(); let maxVisibleCrossEnd = scrollBody ? scrollBody[0].getBoundingClientRect()[X_END] : window[INNER_X_SIZE] || document.documentElement[CLIENT_X_SIZE]; mustHide = ((containerRect[X_END] <= maxVisibleCrossEnd) || (containerRect[X_START] > maxVisibleCrossEnd)); } if (instance.visible === mustHide) { instance.visible = !mustHide; // We cannot simply hide the scrollbar since its scroll position won’t update in that case widget.toggleClass("fl-scrolls-hidden"); } }, syncContainer() { let instance = this; const {SCROLL_POS} = instance.orientationProps; let scrollPos = instance.widget[0][SCROLL_POS]; if (instance.container[SCROLL_POS] !== scrollPos) { // Prevents container’s “scroll” event handler from syncing back again widget scroll position instance.skipSyncWidget = true; // Note that this makes container’s “scroll” event handlers execute instance.container[SCROLL_POS] = scrollPos; } }, syncWidget() { let instance = this; const {SCROLL_POS} = instance.orientationProps; let scrollPos = instance.container[SCROLL_POS]; if (instance.widget[0][SCROLL_POS] !== scrollPos) { // Prevents widget’s “scroll” event handler from syncing back again container scroll position instance.skipSyncContainer = true; // Note that this makes widget’s “scroll” event handlers execute instance.widget[0][SCROLL_POS] = scrollPos; } }, // Recalculate scroll width/height and container boundaries updateAPI() { let instance = this; const {SIZE, X_SIZE, OFFSET_X_SIZE, CLIENT_SIZE, CLIENT_X_SIZE, SCROLL_SIZE, START} = instance.orientationProps; let {widget, container, scrollBody} = instance; let clientSize = container[CLIENT_SIZE]; let scrollSize = container[SCROLL_SIZE]; widget[SIZE](clientSize); if (!scrollBody) { widget.css(START, `${container.getBoundingClientRect()[START]}px`); } $("div", widget)[SIZE](scrollSize); // Fit widget size to the native scrollbar size if needed if (scrollSize > clientSize) { widget[X_SIZE](widget[0][OFFSET_X_SIZE] - widget[0][CLIENT_X_SIZE] + 1); // +1px JIC } instance.syncWidget(); instance.checkVisibility(); // fixes issue #2 }, // Remove a scrollbar and all related event handlers destroyAPI() { let instance = this; instance.eventHandlers.forEach(({$el, handlers}) => $el.unbind(handlers)); instance.widget.remove(); instance.eventHandlers = instance.widget = instance.container = instance.scrollBody = null; }, destroyDetachedAPI() { if (!$.contains(document.body, this.container)) { this.destroyAPI(); } } }; $.fn.floatingScroll = function (method = "init", options = {}) { if (method === "init") { let {orientation = HORIZONTAL} = options; if (orientation !== HORIZONTAL && orientation !== VERTICAL) { throw new Error(`Scrollbar orientation should be either “${HORIZONTAL}” or “${VERTICAL}”`); } this.each((index, el) => Object.create(floatingScrollProto).init($(el), orientation)); } else if (Object.prototype.hasOwnProperty.call(floatingScrollProto, `${method}API`)) { this.trigger(`${method}.fscroll`); } return this; }; $(() => { $("body [data-fl-scrolls]").each((index, el) => { let $el = $(el); $el.floatingScroll("init", $el.data("flScrolls") || {}); }); });