/* * ScrollPad plugin. * * This plugin creates a scrollable area with features similar (but more limited) * to october.scrollbar.js, with virtual scroll bars. This plugin is more lightweight * in terms of calculations and more responsive. It doesn't use scripting for scrolling, * instead it uses the native scrolling and listens for the onscroll event to update * the virtual scroll bars. * * The plugin is partially based on Trackpad Scroll Emulator * https://github.com/jnicol/trackpad-scroll-emulator, cleaned up for the better CPU and * memory (DOM references) management. * * Expected markup: *
*
*
* The content goes here. The two wrapping * DIV elements are required. *
*
*
* * Data attributes: * - data-control="scrollpad" - enables the plugin. * - data-direction="vertical|horizontal" - sets the scrolling direction. * * JavaScript API: * $('#area').scrollpad({direction: 'vertical'}) * $('#area').scrollpad('dispose') * $('#area').scrollpad('scrollToStart') * * TODO: In FireFox the control in the horizontal mode displays the native scrollbars, * because negative margin-bottom in the scrollable element doesn't work for some reason. * Try to align the scrollable element with absolute positioning (negative right and bottom) * instead of negative margins. * */ +function ($) { "use strict"; var Base = $.oc.foundation.base, BaseProto = Base.prototype // SCROLLPAD CLASS DEFINITION // ============================ var Scrollpad = function(element, options) { this.$el = $(element) this.scrollbarElement = null this.dragHandleElement = null this.scrollContentElement = null this.contentElement = null this.options = options this.scrollbarSize = null this.updateScrollbarTimer = null this.dragOffset = null Base.call(this) // // Initialization // this.init() $.oc.foundation.controlUtils.markDisposable(element) } Scrollpad.prototype = Object.create(BaseProto) Scrollpad.prototype.constructor = Scrollpad Scrollpad.prototype.dispose = function() { this.unregisterHandlers() this.$el.get(0).removeChild(this.scrollbarElement) this.$el.removeData('oc.scrollpad') this.$el = null this.scrollbarElement = null this.dragHandleElement = null this.scrollContentElement = null this.contentElement = null BaseProto.dispose.call(this) } Scrollpad.prototype.scrollToStart = function() { var scrollAttr = this.options.direction == 'vertical' ? 'scrollTop' : 'scrollLeft' this.scrollContentElement[scrollAttr] = 0 } Scrollpad.prototype.update = function() { this.updateScrollbarSize() } // SCROLLPAD INTERNAL METHODS // ============================ Scrollpad.prototype.init = function() { this.build() this.setScrollContentSize() this.registerHandlers() } Scrollpad.prototype.build = function() { var el = this.$el.get(0) this.scrollContentElement = el.children[0] this.contentElement = this.scrollContentElement.children[0] this.$el.prepend('
') this.scrollbarElement = el.querySelector('.scrollpad-scrollbar') this.dragHandleElement = el.querySelector('.scrollpad-scrollbar > .drag-handle') } Scrollpad.prototype.registerHandlers = function() { this.$el.on('mouseenter', this.proxy(this.onMouseEnter)) this.$el.on('mouseleave', this.proxy(this.onMouseLeave)) this.$el.one('dispose-control', this.proxy(this.dispose)) this.scrollContentElement.addEventListener('scroll', this.proxy(this.onScroll)) this.dragHandleElement.addEventListener('mousedown', this.proxy(this.onStartDrag)) } Scrollpad.prototype.unregisterHandlers = function() { this.$el.off('mouseenter', this.proxy(this.onMouseEnter)) this.$el.off('mouseleave', this.proxy(this.onMouseLeave)) this.$el.off('dispose-control', this.proxy(this.dispose)) this.scrollContentElement.removeEventListener('scroll', this.proxy(this.onScroll)) this.dragHandleElement.removeEventListener('mousedown', this.proxy(this.onStartDrag)) document.removeEventListener('mousemove', this.proxy(this.onMouseMove)) document.removeEventListener('mouseup', this.proxy(this.onEndDrag)) } Scrollpad.prototype.setScrollContentSize = function() { var scrollbarSize = this.getScrollbarSize() if (this.options.direction == 'vertical') this.scrollContentElement.setAttribute('style', 'margin-right: -' + scrollbarSize + 'px') else this.scrollContentElement.setAttribute('style', 'margin-bottom: -' + scrollbarSize + 'px') } Scrollpad.prototype.getScrollbarSize = function() { if (this.scrollbarSize !== null) return this.scrollbarSize var testerElement = document.createElement('div') testerElement.setAttribute('class', 'scrollpad-scrollbar-size-tester') testerElement.appendChild(document.createElement('div')) document.body.appendChild(testerElement) var width = testerElement.offsetWidth, innerWidth = testerElement.querySelector('div').offsetWidth document.body.removeChild(testerElement) // Some magic for FireFox, see // https://github.com/jnicol/trackpad-scroll-emulator/blob/master/jquery.trackpad-scroll-emulator.js if (width === innerWidth && navigator.userAgent.toLowerCase().indexOf('firefox') > -1) return this.scrollbarSize = 17 return this.scrollbarSize = width - innerWidth } Scrollpad.prototype.updateScrollbarSize = function() { this.scrollbarElement.removeAttribute('data-hidden') var contentSize = this.options.direction == 'vertical' ? this.contentElement.scrollHeight : this.contentElement.scrollWidth, scrollOffset = this.options.direction == 'vertical' ? this.scrollContentElement.scrollTop : this.scrollContentElement.scrollLeft, scrollbarSize = this.options.direction == 'vertical' ? this.scrollbarElement.offsetHeight : this.scrollbarElement.offsetWidth, scrollbarRatio = scrollbarSize / contentSize, handleOffset = Math.round(scrollbarRatio * scrollOffset) + 2, handleSize = Math.floor(scrollbarRatio * (scrollbarSize - 2)) - 2; if (scrollbarSize < contentSize) { if (this.options.direction == 'vertical') this.dragHandleElement.setAttribute('style', 'top: ' + handleOffset + 'px; height: ' + handleSize + 'px') else this.dragHandleElement.setAttribute('style', 'left: ' + handleOffset + 'px; width: ' + handleSize + 'px') this.scrollbarElement.removeAttribute('data-hidden') } else this.scrollbarElement.setAttribute('data-hidden', true) } Scrollpad.prototype.displayScrollbar = function() { this.clearUpdateScrollbarTimer() this.updateScrollbarSize() this.scrollbarElement.setAttribute('data-visible', 'true') } Scrollpad.prototype.hideScrollbar = function() { this.scrollbarElement.removeAttribute('data-visible') } Scrollpad.prototype.clearUpdateScrollbarTimer = function() { if (this.updateScrollbarTimer === null) return clearTimeout(this.updateScrollbarTimer) this.updateScrollbarTimer = null } // EVENT HANDLERS // ============================ Scrollpad.prototype.onMouseEnter = function() { this.displayScrollbar() } Scrollpad.prototype.onMouseLeave = function() { this.hideScrollbar() } Scrollpad.prototype.onScroll = function() { if (this.updateScrollbarTimer !== null) return this.updateScrollbarTimer = setTimeout(this.proxy(this.displayScrollbar), 10) } Scrollpad.prototype.onStartDrag = function(ev) { $.oc.foundation.event.stop(ev) var pageCoords = $.oc.foundation.event.pageCoordinates(ev), eventOffset = this.options.direction == 'vertical' ? pageCoords.y : pageCoords.x, handleCoords = $.oc.foundation.element.absolutePosition(this.dragHandleElement), handleOffset = this.options.direction == 'vertical' ? handleCoords.top : handleCoords.left this.dragOffset = eventOffset - handleOffset document.addEventListener('mousemove', this.proxy(this.onMouseMove)) document.addEventListener('mouseup', this.proxy(this.onEndDrag)) } Scrollpad.prototype.onMouseMove = function(ev) { $.oc.foundation.event.stop(ev) var eventCoordsAttr = this.options.direction == 'vertical' ? 'y' : 'x', elementCoordsAttr = this.options.direction == 'vertical' ? 'top' : 'left', offsetAttr = this.options.direction == 'vertical' ? 'offsetHeight' : 'offsetWidth', scrollAttr = this.options.direction == 'vertical' ? 'scrollTop' : 'scrollLeft' var eventOffset = $.oc.foundation.event.pageCoordinates(ev)[eventCoordsAttr], scrollbarOffset = $.oc.foundation.element.absolutePosition(this.scrollbarElement)[elementCoordsAttr], dragPos = eventOffset - scrollbarOffset - this.dragOffset, scrollbarSize = this.scrollbarElement[offsetAttr], contentSize = this.contentElement[offsetAttr], dragPerc = dragPos / scrollbarSize if (dragPerc > 1) dragPerc = 1 var scrollPos = dragPerc * contentSize; this.scrollContentElement[scrollAttr] = scrollPos } Scrollpad.prototype.onEndDrag = function(ev) { document.removeEventListener('mousemove', this.proxy(this.onMouseMove)) document.removeEventListener('mouseup', this.proxy(this.onEndDrag)) } // SCROLLPAD PLUGIN DEFINITION // ============================ Scrollpad.DEFAULTS = { direction: 'vertical' } var old = $.fn.scrollpad $.fn.scrollpad = function (option) { var args = Array.prototype.slice.call(arguments, 1), result = undefined this.each(function () { var $this = $(this) var data = $this.data('oc.scrollpad') var options = $.extend({}, Scrollpad.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data) $this.data('oc.scrollpad', (data = new Scrollpad(this, options))) if (typeof option == 'string') result = data[option].apply(data, args) if (typeof result != 'undefined') return false }) return result ? result : this } $.fn.scrollpad.Constructor = Scrollpad // SCROLLPAD NO CONFLICT // ================= $.fn.scrollpad.noConflict = function () { $.fn.scrollpad = old return this } // SCROLLPAD DATA-API // =============== $(document).on('render', function(){ $('div[data-control=scrollpad]').scrollpad() }) }(window.jQuery);