/*! * JavaScript Custom Forms : Scrollbar Module * * Copyright 2014-2015 PSD2HTML - http://psd2html.com/jcf * Released under the MIT license (LICENSE.txt) * * Version: 1.2.1 */ (function(jcf) { jcf.addModule(function($, window) { 'use strict'; var module = { name: 'Scrollable', selector: '.jcf-scrollable', plugins: { ScrollBar: ScrollBar }, options: { mouseWheelStep: 150, handleResize: true, alwaysShowScrollbars: false, alwaysPreventMouseWheel: false, scrollAreaStructure: '
' }, matchElement: function(element) { return element.is('.jcf-scrollable'); }, init: function() { this.initStructure(); this.attachEvents(); this.rebuildScrollbars(); }, initStructure: function() { // prepare structure this.doc = $(document); this.win = $(window); this.realElement = $(this.options.element); this.scrollWrapper = $(this.options.scrollAreaStructure).insertAfter(this.realElement); // set initial styles this.scrollWrapper.css('position', 'relative'); this.realElement.css('overflow', 'hidden'); this.vBarEdge = 0; }, attachEvents: function() { // create scrollbars var self = this; this.vBar = new ScrollBar({ holder: this.scrollWrapper, vertical: true, onScroll: function(scrollTop) { self.realElement.scrollTop(scrollTop); } }); this.hBar = new ScrollBar({ holder: this.scrollWrapper, vertical: false, onScroll: function(scrollLeft) { self.realElement.scrollLeft(scrollLeft); } }); // add event handlers this.realElement.on('scroll', this.onScroll); if (this.options.handleResize) { this.win.on('resize orientationchange load', this.onResize); } // add pointer/wheel event handlers this.realElement.on('jcf-mousewheel', this.onMouseWheel); this.realElement.on('jcf-pointerdown', this.onTouchBody); }, onScroll: function() { this.redrawScrollbars(); }, onResize: function() { // do not rebuild scrollbars if form field is in focus if (!$(document.activeElement).is(':input')) { this.rebuildScrollbars(); } }, onTouchBody: function(e) { if (e.pointerType === 'touch') { this.touchData = { scrollTop: this.realElement.scrollTop(), scrollLeft: this.realElement.scrollLeft(), left: e.pageX, top: e.pageY }; this.doc.on({ 'jcf-pointermove': this.onMoveBody, 'jcf-pointerup': this.onReleaseBody }); } }, onMoveBody: function(e) { var targetScrollTop, targetScrollLeft, verticalScrollAllowed = this.verticalScrollActive, horizontalScrollAllowed = this.horizontalScrollActive; if (e.pointerType === 'touch') { targetScrollTop = this.touchData.scrollTop - e.pageY + this.touchData.top; targetScrollLeft = this.touchData.scrollLeft - e.pageX + this.touchData.left; // check that scrolling is ended and release outer scrolling if (this.verticalScrollActive && (targetScrollTop < 0 || targetScrollTop > this.vBar.maxValue)) { verticalScrollAllowed = false; } if (this.horizontalScrollActive && (targetScrollLeft < 0 || targetScrollLeft > this.hBar.maxValue)) { horizontalScrollAllowed = false; } this.realElement.scrollTop(targetScrollTop); this.realElement.scrollLeft(targetScrollLeft); if (verticalScrollAllowed || horizontalScrollAllowed) { e.preventDefault(); } else { this.onReleaseBody(e); } } }, onReleaseBody: function(e) { if (e.pointerType === 'touch') { delete this.touchData; this.doc.off({ 'jcf-pointermove': this.onMoveBody, 'jcf-pointerup': this.onReleaseBody }); } }, onMouseWheel: function(e) { var currentScrollTop = this.realElement.scrollTop(), currentScrollLeft = this.realElement.scrollLeft(), maxScrollTop = this.realElement.prop('scrollHeight') - this.embeddedDimensions.innerHeight, maxScrollLeft = this.realElement.prop('scrollWidth') - this.embeddedDimensions.innerWidth, extraLeft, extraTop, preventFlag; // check edge cases if (!this.options.alwaysPreventMouseWheel) { if (this.verticalScrollActive && e.deltaY) { if (!(currentScrollTop <= 0 && e.deltaY < 0) && !(currentScrollTop >= maxScrollTop && e.deltaY > 0)) { preventFlag = true; } } if (this.horizontalScrollActive && e.deltaX) { if (!(currentScrollLeft <= 0 && e.deltaX < 0) && !(currentScrollLeft >= maxScrollLeft && e.deltaX > 0)) { preventFlag = true; } } if (!this.verticalScrollActive && !this.horizontalScrollActive) { return; } } // prevent default action and scroll item if (preventFlag || this.options.alwaysPreventMouseWheel) { e.preventDefault(); } else { return; } extraLeft = e.deltaX / 100 * this.options.mouseWheelStep; extraTop = e.deltaY / 100 * this.options.mouseWheelStep; this.realElement.scrollTop(currentScrollTop + extraTop); this.realElement.scrollLeft(currentScrollLeft + extraLeft); }, setScrollBarEdge: function(edgeSize) { this.vBarEdge = edgeSize || 0; this.redrawScrollbars(); }, saveElementDimensions: function() { this.savedDimensions = { top: this.realElement.width(), left: this.realElement.height() }; return this; }, restoreElementDimensions: function() { if (this.savedDimensions) { this.realElement.css({ width: this.savedDimensions.width, height: this.savedDimensions.height }); } return this; }, saveScrollOffsets: function() { this.savedOffsets = { top: this.realElement.scrollTop(), left: this.realElement.scrollLeft() }; return this; }, restoreScrollOffsets: function() { if (this.savedOffsets) { this.realElement.scrollTop(this.savedOffsets.top); this.realElement.scrollLeft(this.savedOffsets.left); } return this; }, getContainerDimensions: function() { // save current styles var desiredDimensions, currentStyles, currentHeight, currentWidth; if (this.isModifiedStyles) { desiredDimensions = { width: this.realElement.innerWidth() + this.vBar.getThickness(), height: this.realElement.innerHeight() + this.hBar.getThickness() }; } else { // unwrap real element and measure it according to CSS this.saveElementDimensions().saveScrollOffsets(); this.realElement.insertAfter(this.scrollWrapper); this.scrollWrapper.detach(); // measure element currentStyles = this.realElement.prop('style'); currentWidth = parseFloat(currentStyles.width); currentHeight = parseFloat(currentStyles.height); // reset styles if needed if (this.embeddedDimensions && currentWidth && currentHeight) { this.isModifiedStyles |= (currentWidth !== this.embeddedDimensions.width || currentHeight !== this.embeddedDimensions.height); this.realElement.css({ overflow: '', width: '', height: '' }); } // calculate desired dimensions for real element desiredDimensions = { width: this.realElement.outerWidth(), height: this.realElement.outerHeight() }; // restore structure and original scroll offsets this.scrollWrapper.insertAfter(this.realElement); this.realElement.css('overflow', 'hidden').prependTo(this.scrollWrapper); this.restoreElementDimensions().restoreScrollOffsets(); } return desiredDimensions; }, getEmbeddedDimensions: function(dimensions) { // handle scrollbars cropping var fakeBarWidth = this.vBar.getThickness(), fakeBarHeight = this.hBar.getThickness(), paddingWidth = this.realElement.outerWidth() - this.realElement.width(), paddingHeight = this.realElement.outerHeight() - this.realElement.height(), resultDimensions; if (this.options.alwaysShowScrollbars) { // simply return dimensions without custom scrollbars this.verticalScrollActive = true; this.horizontalScrollActive = true; resultDimensions = { innerWidth: dimensions.width - fakeBarWidth, innerHeight: dimensions.height - fakeBarHeight }; } else { // detect when to display each scrollbar this.saveElementDimensions(); this.verticalScrollActive = false; this.horizontalScrollActive = false; // fill container with full size this.realElement.css({ width: dimensions.width - paddingWidth, height: dimensions.height - paddingHeight }); this.horizontalScrollActive = this.realElement.prop('scrollWidth') > this.containerDimensions.width; this.verticalScrollActive = this.realElement.prop('scrollHeight') > this.containerDimensions.height; this.restoreElementDimensions(); resultDimensions = { innerWidth: dimensions.width - (this.verticalScrollActive ? fakeBarWidth : 0), innerHeight: dimensions.height - (this.horizontalScrollActive ? fakeBarHeight : 0) }; } $.extend(resultDimensions, { width: resultDimensions.innerWidth - paddingWidth, height: resultDimensions.innerHeight - paddingHeight }); return resultDimensions; }, rebuildScrollbars: function() { // resize wrapper according to real element styles this.containerDimensions = this.getContainerDimensions(); this.embeddedDimensions = this.getEmbeddedDimensions(this.containerDimensions); // resize wrapper to desired dimensions this.scrollWrapper.css({ width: this.containerDimensions.width, height: this.containerDimensions.height }); // resize element inside wrapper excluding scrollbar size this.realElement.css({ overflow: 'hidden', width: this.embeddedDimensions.width, height: this.embeddedDimensions.height }); // redraw scrollbar offset this.redrawScrollbars(); }, redrawScrollbars: function() { var viewSize, maxScrollValue; // redraw vertical scrollbar if (this.verticalScrollActive) { viewSize = this.vBarEdge ? this.containerDimensions.height - this.vBarEdge : this.embeddedDimensions.innerHeight; maxScrollValue = Math.max(this.realElement.prop('offsetHeight'), this.realElement.prop('scrollHeight')) - this.vBarEdge; this.vBar.show().setMaxValue(maxScrollValue - viewSize).setRatio(viewSize / maxScrollValue).setSize(viewSize); this.vBar.setValue(this.realElement.scrollTop()); } else { this.vBar.hide(); } // redraw horizontal scrollbar if (this.horizontalScrollActive) { viewSize = this.embeddedDimensions.innerWidth; maxScrollValue = this.realElement.prop('scrollWidth'); if (maxScrollValue === viewSize) { this.horizontalScrollActive = false; } this.hBar.show().setMaxValue(maxScrollValue - viewSize).setRatio(viewSize / maxScrollValue).setSize(viewSize); this.hBar.setValue(this.realElement.scrollLeft()); } else { this.hBar.hide(); } // set "touch-action" style rule var touchAction = ''; if (this.verticalScrollActive && this.horizontalScrollActive) { touchAction = 'none'; } else if (this.verticalScrollActive) { touchAction = 'pan-x'; } else if (this.horizontalScrollActive) { touchAction = 'pan-y'; } this.realElement.css('touchAction', touchAction); }, refresh: function() { this.rebuildScrollbars(); }, destroy: function() { // remove event listeners this.win.off('resize orientationchange load', this.onResize); this.realElement.off({ 'jcf-mousewheel': this.onMouseWheel, 'jcf-pointerdown': this.onTouchBody }); this.doc.off({ 'jcf-pointermove': this.onMoveBody, 'jcf-pointerup': this.onReleaseBody }); // restore structure this.saveScrollOffsets(); this.vBar.destroy(); this.hBar.destroy(); this.realElement.insertAfter(this.scrollWrapper).css({ touchAction: '', overflow: '', width: '', height: '' }); this.scrollWrapper.remove(); this.restoreScrollOffsets(); } }; // custom scrollbar function ScrollBar(options) { this.options = $.extend({ holder: null, vertical: true, inactiveClass: 'jcf-inactive', verticalClass: 'jcf-scrollbar-vertical', horizontalClass: 'jcf-scrollbar-horizontal', scrollbarStructure: '
', btnDecSelector: '.jcf-scrollbar-dec', btnIncSelector: '.jcf-scrollbar-inc', sliderSelector: '.jcf-scrollbar-slider', handleSelector: '.jcf-scrollbar-handle', scrollInterval: 300, scrollStep: 400 // px/sec }, options); this.init(); } $.extend(ScrollBar.prototype, { init: function() { this.initStructure(); this.attachEvents(); }, initStructure: function() { // define proporties this.doc = $(document); this.isVertical = !!this.options.vertical; this.sizeProperty = this.isVertical ? 'height' : 'width'; this.fullSizeProperty = this.isVertical ? 'outerHeight' : 'outerWidth'; this.invertedSizeProperty = this.isVertical ? 'width' : 'height'; this.thicknessMeasureMethod = 'outer' + this.invertedSizeProperty.charAt(0).toUpperCase() + this.invertedSizeProperty.substr(1); this.offsetProperty = this.isVertical ? 'top' : 'left'; this.offsetEventProperty = this.isVertical ? 'pageY' : 'pageX'; // initialize variables this.value = this.options.value || 0; this.maxValue = this.options.maxValue || 0; this.currentSliderSize = 0; this.handleSize = 0; // find elements this.holder = $(this.options.holder); this.scrollbar = $(this.options.scrollbarStructure).appendTo(this.holder); this.btnDec = this.scrollbar.find(this.options.btnDecSelector); this.btnInc = this.scrollbar.find(this.options.btnIncSelector); this.slider = this.scrollbar.find(this.options.sliderSelector); this.handle = this.slider.find(this.options.handleSelector); // set initial styles this.scrollbar.addClass(this.isVertical ? this.options.verticalClass : this.options.horizontalClass).css({ touchAction: this.isVertical ? 'pan-x' : 'pan-y', position: 'absolute' }); this.slider.css({ position: 'relative' }); this.handle.css({ touchAction: 'none', position: 'absolute' }); }, attachEvents: function() { this.bindHandlers(); this.handle.on('jcf-pointerdown', this.onHandlePress); this.slider.add(this.btnDec).add(this.btnInc).on('jcf-pointerdown', this.onButtonPress); }, onHandlePress: function(e) { if (e.pointerType === 'mouse' && e.button > 1) { return; } else { e.preventDefault(); this.handleDragActive = true; this.sliderOffset = this.slider.offset()[this.offsetProperty]; this.innerHandleOffset = e[this.offsetEventProperty] - this.handle.offset()[this.offsetProperty]; this.doc.on('jcf-pointermove', this.onHandleDrag); this.doc.on('jcf-pointerup', this.onHandleRelease); } }, onHandleDrag: function(e) { e.preventDefault(); this.calcOffset = e[this.offsetEventProperty] - this.sliderOffset - this.innerHandleOffset; this.setValue(this.calcOffset / (this.currentSliderSize - this.handleSize) * this.maxValue); this.triggerScrollEvent(this.value); }, onHandleRelease: function() { this.handleDragActive = false; this.doc.off('jcf-pointermove', this.onHandleDrag); this.doc.off('jcf-pointerup', this.onHandleRelease); }, onButtonPress: function(e) { var direction, clickOffset; if (e.pointerType === 'mouse' && e.button > 1) { return; } else { e.preventDefault(); if (!this.handleDragActive) { if (this.slider.is(e.currentTarget)) { // slider pressed direction = this.handle.offset()[this.offsetProperty] > e[this.offsetEventProperty] ? -1 : 1; clickOffset = e[this.offsetEventProperty] - this.slider.offset()[this.offsetProperty]; this.startPageScrolling(direction, clickOffset); } else { // scrollbar buttons pressed direction = this.btnDec.is(e.currentTarget) ? -1 : 1; this.startSmoothScrolling(direction); } this.doc.on('jcf-pointerup', this.onButtonRelease); } } }, onButtonRelease: function() { this.stopPageScrolling(); this.stopSmoothScrolling(); this.doc.off('jcf-pointerup', this.onButtonRelease); }, startPageScrolling: function(direction, clickOffset) { var self = this, stepValue = direction * self.currentSize; // limit checker var isFinishedScrolling = function() { var handleTop = (self.value / self.maxValue) * (self.currentSliderSize - self.handleSize); if (direction > 0) { return handleTop + self.handleSize >= clickOffset; } else { return handleTop <= clickOffset; } }; // scroll by page when track is pressed var doPageScroll = function() { self.value += stepValue; self.setValue(self.value); self.triggerScrollEvent(self.value); if (isFinishedScrolling()) { clearInterval(self.pageScrollTimer); } }; // start scrolling this.pageScrollTimer = setInterval(doPageScroll, this.options.scrollInterval); doPageScroll(); }, stopPageScrolling: function() { clearInterval(this.pageScrollTimer); }, startSmoothScrolling: function(direction) { var self = this, dt; this.stopSmoothScrolling(); // simple animation functions var raf = window.requestAnimationFrame || function(func) { setTimeout(func, 16); }; var getTimestamp = function() { return Date.now ? Date.now() : new Date().getTime(); }; // set animation limit var isFinishedScrolling = function() { if (direction > 0) { return self.value >= self.maxValue; } else { return self.value <= 0; } }; // animation step var doScrollAnimation = function() { var stepValue = (getTimestamp() - dt) / 1000 * self.options.scrollStep; if (self.smoothScrollActive) { self.value += stepValue * direction; self.setValue(self.value); self.triggerScrollEvent(self.value); if (!isFinishedScrolling()) { dt = getTimestamp(); raf(doScrollAnimation); } } }; // start animation self.smoothScrollActive = true; dt = getTimestamp(); raf(doScrollAnimation); }, stopSmoothScrolling: function() { this.smoothScrollActive = false; }, triggerScrollEvent: function(scrollValue) { if (this.options.onScroll) { this.options.onScroll(scrollValue); } }, getThickness: function() { return this.scrollbar[this.thicknessMeasureMethod](); }, setSize: function(size) { // resize scrollbar var btnDecSize = this.btnDec[this.fullSizeProperty](), btnIncSize = this.btnInc[this.fullSizeProperty](); // resize slider this.currentSize = size; this.currentSliderSize = size - btnDecSize - btnIncSize; this.scrollbar.css(this.sizeProperty, size); this.slider.css(this.sizeProperty, this.currentSliderSize); this.currentSliderSize = this.slider[this.sizeProperty](); // resize handle this.handleSize = Math.round(this.currentSliderSize * this.ratio); this.handle.css(this.sizeProperty, this.handleSize); this.handleSize = this.handle[this.fullSizeProperty](); return this; }, setRatio: function(ratio) { this.ratio = ratio; return this; }, setMaxValue: function(maxValue) { this.maxValue = maxValue; this.setValue(Math.min(this.value, this.maxValue)); return this; }, setValue: function(value) { this.value = value; if (this.value < 0) { this.value = 0; } else if (this.value > this.maxValue) { this.value = this.maxValue; } this.refresh(); }, setPosition: function(styles) { this.scrollbar.css(styles); return this; }, hide: function() { this.scrollbar.detach(); return this; }, show: function() { this.scrollbar.appendTo(this.holder); return this; }, refresh: function() { // recalculate handle position if (this.value === 0 || this.maxValue === 0) { this.calcOffset = 0; } else { this.calcOffset = (this.value / this.maxValue) * (this.currentSliderSize - this.handleSize); } this.handle.css(this.offsetProperty, this.calcOffset); // toggle inactive classes this.btnDec.toggleClass(this.options.inactiveClass, this.value === 0); this.btnInc.toggleClass(this.options.inactiveClass, this.value === this.maxValue); this.scrollbar.toggleClass(this.options.inactiveClass, this.maxValue === 0); }, destroy: function() { // remove event handlers and scrollbar block itself this.btnDec.add(this.btnInc).off('jcf-pointerdown', this.onButtonPress); this.handle.off('jcf-pointerdown', this.onHandlePress); this.doc.off('jcf-pointermove', this.onHandleDrag); this.doc.off('jcf-pointerup', this.onHandleRelease); this.doc.off('jcf-pointerup', this.onButtonRelease); this.stopSmoothScrolling(); this.stopPageScrolling(); this.scrollbar.remove(); } }); return module; }); }(jcf));