/*
* 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:
*
*
* 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);