/*! nanoScrollerJS - v0.8.0 - 2014 * http://jamesflorentino.github.com/nanoScrollerJS/ * Copyright (c) 2014 James Florentino; Licensed MIT */ (function($, window, document) { "use strict"; var BROWSER_IS_IE7, BROWSER_SCROLLBAR_WIDTH, DOMSCROLL, DOWN, DRAG, KEYDOWN, KEYUP, MOUSEDOWN, MOUSEMOVE, MOUSEUP, MOUSEWHEEL, NanoScroll, PANEDOWN, RESIZE, SCROLL, SCROLLBAR, TOUCHMOVE, UP, WHEEL, cAF, defaults, getBrowserScrollbarWidth, hasTransform, isFFWithBuggyScrollbar, rAF, transform, _elementStyle, _prefixStyle, _vendor; defaults = { /** a classname for the pane element. @property paneClass @type String @default 'nano-pane' */ paneClass: 'nano-pane', /** a classname for the slider element. @property sliderClass @type String @default 'nano-slider' */ sliderClass: 'nano-slider', /** a classname for the content element. @property contentClass @type String @default 'nano-content' */ contentClass: 'nano-content', /** a setting to enable native scrolling in iOS devices. @property iOSNativeScrolling @type Boolean @default false */ iOSNativeScrolling: false, /** a setting to prevent the rest of the page being scrolled when user scrolls the `.content` element. @property preventPageScrolling @type Boolean @default false */ preventPageScrolling: false, /** a setting to disable binding to the resize event. @property disableResize @type Boolean @default false */ disableResize: false, /** a setting to make the scrollbar always visible. @property alwaysVisible @type Boolean @default false */ alwaysVisible: false, /** a default timeout for the `flash()` method. @property flashDelay @type Number @default 1500 */ flashDelay: 1500, /** a minimum height for the `.slider` element. @property sliderMinHeight @type Number @default 20 */ sliderMinHeight: 20, /** a maximum height for the `.slider` element. @property sliderMaxHeight @type Number @default null */ sliderMaxHeight: null, /** an alternate document context. @property documentContext @type Document @default null */ documentContext: null, /** an alternate window context. @property windowContext @type Window @default null */ windowContext: null }; /** @property SCROLLBAR @type String @static @final @private */ SCROLLBAR = 'scrollbar'; /** @property SCROLL @type String @static @final @private */ SCROLL = 'scroll'; /** @property MOUSEDOWN @type String @final @private */ MOUSEDOWN = 'mousedown'; /** @property MOUSEMOVE @type String @static @final @private */ MOUSEMOVE = 'mousemove'; /** @property MOUSEWHEEL @type String @final @private */ MOUSEWHEEL = 'mousewheel'; /** @property MOUSEUP @type String @static @final @private */ MOUSEUP = 'mouseup'; /** @property RESIZE @type String @final @private */ RESIZE = 'resize'; /** @property DRAG @type String @static @final @private */ DRAG = 'drag'; /** @property UP @type String @static @final @private */ UP = 'up'; /** @property PANEDOWN @type String @static @final @private */ PANEDOWN = 'panedown'; /** @property DOMSCROLL @type String @static @final @private */ DOMSCROLL = 'DOMMouseScroll'; /** @property DOWN @type String @static @final @private */ DOWN = 'down'; /** @property WHEEL @type String @static @final @private */ WHEEL = 'wheel'; /** @property KEYDOWN @type String @static @final @private */ KEYDOWN = 'keydown'; /** @property KEYUP @type String @static @final @private */ KEYUP = 'keyup'; /** @property TOUCHMOVE @type String @static @final @private */ TOUCHMOVE = 'touchmove'; /** @property BROWSER_IS_IE7 @type Boolean @static @final @private */ BROWSER_IS_IE7 = window.navigator.appName === 'Microsoft Internet Explorer' && /msie 7./i.test(window.navigator.appVersion) && window.ActiveXObject; /** @property BROWSER_SCROLLBAR_WIDTH @type Number @static @default null @private */ BROWSER_SCROLLBAR_WIDTH = null; rAF = window.requestAnimationFrame; cAF = window.cancelAnimationFrame; _elementStyle = document.createElement('div').style; _vendor = (function() { var i, transform, vendor, vendors, _i, _len; vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT']; for (i = _i = 0, _len = vendors.length; _i < _len; i = ++_i) { vendor = vendors[i]; transform = vendors[i] + 'ransform'; if (transform in _elementStyle) { return vendors[i].substr(0, vendors[i].length - 1); } } return false; })(); _prefixStyle = function(style) { if (_vendor === false) { return false; } if (_vendor === '') { return style; } return _vendor + style.charAt(0).toUpperCase() + style.substr(1); }; transform = _prefixStyle('transform'); hasTransform = transform !== false; /** Returns browser's native scrollbar width @method getBrowserScrollbarWidth @return {Number} the scrollbar width in pixels @static @private */ getBrowserScrollbarWidth = function() { var outer, outerStyle, scrollbarWidth; outer = document.createElement('div'); outerStyle = outer.style; outerStyle.position = 'absolute'; outerStyle.width = '100px'; outerStyle.height = '100px'; outerStyle.overflow = SCROLL; outerStyle.top = '-9999px'; document.body.appendChild(outer); scrollbarWidth = outer.offsetWidth - outer.clientWidth; document.body.removeChild(outer); return scrollbarWidth; }; isFFWithBuggyScrollbar = function() { var isOSXFF, ua, version; ua = window.navigator.userAgent; isOSXFF = /(?=.+Mac OS X)(?=.+Firefox)/.test(ua); if (!isOSXFF) { return false; } version = /Firefox\/\d{2}\./.exec(ua); if (version) { version = version[0].replace(/\D+/g, ''); } return isOSXFF && +version > 23; }; /** @class NanoScroll @param element {HTMLElement|Node} the main element @param options {Object} nanoScroller's options @constructor */ NanoScroll = (function() { function NanoScroll(el, options) { this.el = el; this.options = options; BROWSER_SCROLLBAR_WIDTH || (BROWSER_SCROLLBAR_WIDTH = getBrowserScrollbarWidth()); this.$el = $(this.el); this.doc = $(this.options.documentContext || document); this.win = $(this.options.windowContext || window); this.$content = this.$el.children("." + options.contentClass); this.$content.attr('tabindex', this.options.tabIndex || 0); this.content = this.$content[0]; this.previousPosition = 0; if (this.options.iOSNativeScrolling && (this.el.style.WebkitOverflowScrolling != null || navigator.userAgent.match(/mobi.+Gecko/i))) { this.nativeScrolling(); } else { this.generate(); } this.createEvents(); this.addEvents(); this.reset(); } /** Prevents the rest of the page being scrolled when user scrolls the `.nano-content` element. @method preventScrolling @param event {Event} @param direction {String} Scroll direction (up or down) @private */ NanoScroll.prototype.preventScrolling = function(e, direction) { if (!this.isActive) { return; } if (e.type === DOMSCROLL) { if (direction === DOWN && e.originalEvent.detail > 0 || direction === UP && e.originalEvent.detail < 0) { e.preventDefault(); } } else if (e.type === MOUSEWHEEL) { if (!e.originalEvent || !e.originalEvent.wheelDelta) { return; } if (direction === DOWN && e.originalEvent.wheelDelta < 0 || direction === UP && e.originalEvent.wheelDelta > 0) { e.preventDefault(); } } }; /** Enable iOS native scrolling @method nativeScrolling @private */ NanoScroll.prototype.nativeScrolling = function() { this.$content.css({ WebkitOverflowScrolling: 'touch' }); this.iOSNativeScrolling = true; this.isActive = true; }; /** Updates those nanoScroller properties that are related to current scrollbar position. @method updateScrollValues @private */ NanoScroll.prototype.updateScrollValues = function() { var content, direction; content = this.content; this.maxScrollTop = content.scrollHeight - content.clientHeight; this.prevScrollTop = this.contentScrollTop || 0; this.contentScrollTop = content.scrollTop; direction = this.contentScrollTop > this.previousPosition ? "down" : this.contentScrollTop < this.previousPosition ? "up" : "same"; this.previousPosition = this.contentScrollTop; if (direction !== "same") { this.$el.trigger('update', { position: this.contentScrollTop, maximum: this.maxScrollTop, direction: direction }); } if (!this.iOSNativeScrolling) { this.maxSliderTop = this.paneHeight - this.sliderHeight; this.sliderTop = this.maxScrollTop === 0 ? 0 : this.contentScrollTop * this.maxSliderTop / this.maxScrollTop; } }; /** Updates CSS styles for current scroll position. Uses CSS 2d transfroms and `window.requestAnimationFrame` if available. @method setOnScrollStyles @private */ NanoScroll.prototype.setOnScrollStyles = function() { var cssValue; if (hasTransform) { cssValue = {}; cssValue[transform] = "translate(0, " + this.sliderTop + "px)"; } else { cssValue = { top: this.sliderTop }; } if (rAF) { if (!this.scrollRAF) { this.scrollRAF = rAF((function(_this) { return function() { _this.scrollRAF = null; _this.slider.css(cssValue); }; })(this)); } } else { this.slider.css(cssValue); } }; /** Creates event related methods @method createEvents @private */ NanoScroll.prototype.createEvents = function() { this.events = { down: (function(_this) { return function(e) { _this.isBeingDragged = true; _this.offsetY = e.pageY - _this.slider.offset().top; _this.pane.addClass('active'); _this.doc.bind(MOUSEMOVE, _this.events[DRAG]).bind(MOUSEUP, _this.events[UP]); return false; }; })(this), drag: (function(_this) { return function(e) { _this.sliderY = e.pageY - _this.$el.offset().top - _this.offsetY; _this.scroll(); if (_this.contentScrollTop >= _this.maxScrollTop && _this.prevScrollTop !== _this.maxScrollTop) { _this.$el.trigger('scrollend'); } else if (_this.contentScrollTop === 0 && _this.prevScrollTop !== 0) { _this.$el.trigger('scrolltop'); } return false; }; })(this), up: (function(_this) { return function(e) { _this.isBeingDragged = false; _this.pane.removeClass('active'); _this.doc.unbind(MOUSEMOVE, _this.events[DRAG]).unbind(MOUSEUP, _this.events[UP]); return false; }; })(this), resize: (function(_this) { return function(e) { _this.reset(); }; })(this), panedown: (function(_this) { return function(e) { _this.sliderY = (e.offsetY || e.originalEvent.layerY) - (_this.sliderHeight * 0.5); _this.scroll(); _this.events.down(e); return false; }; })(this), scroll: (function(_this) { return function(e) { _this.updateScrollValues(); if (_this.isBeingDragged) { return; } if (!_this.iOSNativeScrolling) { _this.sliderY = _this.sliderTop; _this.setOnScrollStyles(); } if (e == null) { return; } if (_this.contentScrollTop >= _this.maxScrollTop) { if (_this.options.preventPageScrolling) { _this.preventScrolling(e, DOWN); } if (_this.prevScrollTop !== _this.maxScrollTop) { _this.$el.trigger('scrollend'); } } else if (_this.contentScrollTop === 0) { if (_this.options.preventPageScrolling) { _this.preventScrolling(e, UP); } if (_this.prevScrollTop !== 0) { _this.$el.trigger('scrolltop'); } } }; })(this), wheel: (function(_this) { return function(e) { var delta; if (e == null) { return; } delta = e.delta || e.wheelDelta || (e.originalEvent && e.originalEvent.wheelDelta) || -e.detail || (e.originalEvent && -e.originalEvent.detail); if (delta) { _this.sliderY += -delta / 3; } _this.scroll(); return false; }; })(this) }; }; /** Adds event listeners with jQuery. @method addEvents @private */ NanoScroll.prototype.addEvents = function() { var events; this.removeEvents(); events = this.events; if (!this.options.disableResize) { this.win.bind(RESIZE, events[RESIZE]); } if (!this.iOSNativeScrolling) { this.slider.bind(MOUSEDOWN, events[DOWN]); this.pane.bind(MOUSEDOWN, events[PANEDOWN]).bind("" + MOUSEWHEEL + " " + DOMSCROLL, events[WHEEL]); } this.$content.bind("" + SCROLL + " " + MOUSEWHEEL + " " + DOMSCROLL + " " + TOUCHMOVE, events[SCROLL]); }; /** Removes event listeners with jQuery. @method removeEvents @private */ NanoScroll.prototype.removeEvents = function() { var events; events = this.events; this.win.unbind(RESIZE, events[RESIZE]); if (!this.iOSNativeScrolling) { this.slider.unbind(); this.pane.unbind(); } this.$content.unbind("" + SCROLL + " " + MOUSEWHEEL + " " + DOMSCROLL + " " + TOUCHMOVE, events[SCROLL]); }; /** Generates nanoScroller's scrollbar and elements for it. @method generate @chainable @private */ NanoScroll.prototype.generate = function() { var contentClass, cssRule, currentPadding, options, pane, paneClass, sliderClass; options = this.options; paneClass = options.paneClass, sliderClass = options.sliderClass, contentClass = options.contentClass; if (!(pane = this.$el.children("." + paneClass)).length && !pane.children("." + sliderClass).length) { this.$el.append("