clipbucket/upload/styles/cb_28/theme/js/jcf_new/jcf.scrollable.js

668 lines
20 KiB
JavaScript

/*!
* 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: '<div class="jcf-scrollable-wrapper"></div>'
},
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: '<div class="jcf-scrollbar"><div class="jcf-scrollbar-dec"></div><div class="jcf-scrollbar-slider"><div class="jcf-scrollbar-handle"></div></div><div class="jcf-scrollbar-inc"></div></div>',
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));