// custom scrollbars module jcf.addModule({ name:'customscroll', selector:'div.scrollable-area', defaultOptions: { alwaysPreventWheel: false, enableMouseWheel: true, captureFocus: false, handleNested: true, alwaysKeepScrollbars: false, autoDetectWidth: false, scrollbarOptions: {}, focusClass:'scrollable-focus', wrapperTag: 'div', autoDetectWidthClass: 'autodetect-width', noHorizontalBarClass:'noscroll-horizontal', noVerticalBarClass:'noscroll-vertical', innerWrapperClass:'scrollable-inner-wrapper', outerWrapperClass:'scrollable-area-wrapper', horizontalClass: 'hscrollable', verticalClass: 'vscrollable', bothClass: 'anyscrollable' }, replaceObject: function(){ this.initStructure(); this.refreshState(); this.addEvents(); }, initStructure: function(){ // set scroll type this.realElement.jcf = this; if(jcf.lib.hasClass(this.realElement, this.options.bothClass) || jcf.lib.hasClass(this.realElement, this.options.horizontalClass) && jcf.lib.hasClass(this.realElement, this.options.verticalClass)) { this.scrollType = 'both'; } else if(jcf.lib.hasClass(this.realElement, this.options.horizontalClass)) { this.scrollType = 'horizontal'; } else { this.scrollType = 'vertical'; } // autodetect horizontal width if(jcf.lib.hasClass(this.realElement,this.options.autoDetectWidthClass)) { this.options.autoDetectWidth = true; } // init dimensions and build structure this.realElement.style.position = 'relative'; this.realElement.style.overflow = 'hidden'; // build content wrapper and scrollbar(s) this.buildWrapper(); this.buildScrollbars(); }, buildWrapper: function() { this.outerWrapper = document.createElement(this.options.wrapperTag); this.outerWrapper.className = this.options.outerWrapperClass; this.realElement.parentNode.insertBefore(this.outerWrapper, this.realElement); this.outerWrapper.appendChild(this.realElement); // autosize content if single child if(this.options.autoDetectWidth && (this.scrollType === 'both' || this.scrollType === 'horizontal') && this.realElement.children.length === 1) { var tmpWidth = 0; this.realElement.style.width = '99999px'; tmpWidth = this.realElement.children[0].offsetWidth; this.realElement.style.width = ''; if(tmpWidth) { this.realElement.children[0].style.width = tmpWidth+'px'; } } }, buildScrollbars: function() { if(this.scrollType === 'horizontal' || this.scrollType === 'both') { this.hScrollBar = new jcf.plugins.scrollbar(jcf.lib.extend(this.options.scrollbarOptions,{ vertical: false, spawnClass: this, holder: this.outerWrapper, range: this.realElement.scrollWidth - this.realElement.offsetWidth, size: this.realElement.offsetWidth, onScroll: jcf.lib.bind(function(v) { this.realElement.scrollLeft = v; },this) })); } if(this.scrollType === 'vertical' || this.scrollType === 'both') { this.vScrollBar = new jcf.plugins.scrollbar(jcf.lib.extend(this.options.scrollbarOptions,{ vertical: true, spawnClass: this, holder: this.outerWrapper, range: this.realElement.scrollHeight - this.realElement.offsetHeight, size: this.realElement.offsetHeight, onScroll: jcf.lib.bind(function(v) { this.realElement.scrollTop = v; },this) })); } this.outerWrapper.style.width = this.realElement.offsetWidth + 'px'; this.outerWrapper.style.height = this.realElement.offsetHeight + 'px'; this.resizeScrollContent(); }, resizeScrollContent: function() { var diffWidth = this.realElement.offsetWidth - jcf.lib.getInnerWidth(this.realElement); var diffHeight = this.realElement.offsetHeight - jcf.lib.getInnerHeight(this.realElement); this.realElement.style.width = Math.max(0, this.outerWrapper.offsetWidth - diffWidth - (this.vScrollBar ? this.vScrollBar.getScrollBarSize() : 0)) + 'px'; this.realElement.style.height = Math.max(0, this.outerWrapper.offsetHeight - diffHeight - (this.hScrollBar ? this.hScrollBar.getScrollBarSize() : 0)) + 'px'; }, addEvents: function() { // enable mouse wheel handling if(!jcf.isTouchDevice && this.options.enableMouseWheel) { jcf.lib.event.add(this.outerWrapper, 'mousewheel', this.onMouseWheel, this); } // add touch scroll on block body if(jcf.isTouchDevice || navigator.msPointerEnabled) { this.outerWrapper.style.msTouchAction = 'none'; jcf.lib.event.add(this.realElement, jcf.eventPress, this.onScrollablePress, this); } // handle nested scrollbars if(this.options.handleNested) { var el = this.realElement, name = this.name; while(el.parentNode) { if(el.parentNode.jcf && el.parentNode.jcf.name == name) { el.parentNode.jcf.refreshState(); } el = el.parentNode; } } }, onMouseWheel: function(e) { if(this.scrollType === 'vertical' || this.scrollType === 'both') { return this.vScrollBar.doScrollWheelStep(e.mWheelDelta) === false ? false : !this.options.alwaysPreventWheel; } else { return this.hScrollBar.doScrollWheelStep(e.mWheelDelta) === false ? false : !this.options.alwaysPreventWheel; } }, onScrollablePress: function(e) { if(e.pointerType !== e.MSPOINTER_TYPE_TOUCH) return; this.preventFlag = true; this.origWindowScrollTop = jcf.lib.getScrollTop(); this.origWindowScrollLeft = jcf.lib.getScrollLeft(); this.scrollableOffset = jcf.lib.getOffset(this.realElement); if(this.hScrollBar) { this.scrollableTouchX = (jcf.isTouchDevice ? e.changedTouches[0] : e).pageX; this.origValueX = this.hScrollBar.getScrollValue(); } if(this.vScrollBar) { this.scrollableTouchY = (jcf.isTouchDevice ? e.changedTouches[0] : e).pageY; this.origValueY = this.vScrollBar.getScrollValue(); } jcf.lib.event.add(this.realElement, jcf.eventMove, this.onScrollableMove, this); jcf.lib.event.add(this.realElement, jcf.eventRelease, this.onScrollableRelease, this); }, onScrollableMove: function(e) { if(this.vScrollBar) { var difY = (jcf.isTouchDevice ? e.changedTouches[0] : e).pageY - this.scrollableTouchY; var valY = this.origValueY-difY; this.vScrollBar.scrollTo(valY); if(valY < 0 || valY > this.vScrollBar.options.range) { this.preventFlag = false; } } if(this.hScrollBar) { var difX = (jcf.isTouchDevice ? e.changedTouches[0] : e).pageX - this.scrollableTouchX; var valX = this.origValueX-difX; this.hScrollBar.scrollTo(valX); if(valX < 0 || valX > this.hScrollBar.options.range) { this.preventFlag = false; } } if(this.preventFlag) { e.preventDefault(); } }, onScrollableRelease: function() { jcf.lib.event.remove(this.realElement, jcf.eventMove, this.onScrollableMove); jcf.lib.event.remove(this.realElement, jcf.eventRelease, this.onScrollableRelease); }, refreshState: function() { if(this.options.alwaysKeepScrollbars) { if(this.hScrollBar) this.hScrollBar.scrollBar.style.display = 'block'; if(this.vScrollBar) this.vScrollBar.scrollBar.style.display = 'block'; } else { if(this.hScrollBar) { if(this.getScrollRange(false)) { this.hScrollBar.scrollBar.style.display = 'block'; this.resizeScrollContent(); this.hScrollBar.setRange(this.getScrollRange(false)); } else { this.hScrollBar.scrollBar.style.display = 'none'; this.realElement.style.width = this.outerWrapper.style.width; } jcf.lib.toggleClass(this.outerWrapper, this.options.noHorizontalBarClass, this.hScrollBar.options.range === 0); } if(this.vScrollBar) { if(this.getScrollRange(true) > 0) { this.vScrollBar.scrollBar.style.display = 'block'; this.resizeScrollContent(); this.vScrollBar.setRange(this.getScrollRange(true)); } else { this.vScrollBar.scrollBar.style.display = 'none'; this.realElement.style.width = this.outerWrapper.style.width; } jcf.lib.toggleClass(this.outerWrapper, this.options.noVerticalBarClass, this.vScrollBar.options.range === 0); } } if(this.vScrollBar) { this.vScrollBar.setRange(this.realElement.scrollHeight - this.realElement.offsetHeight); this.vScrollBar.setSize(this.realElement.offsetHeight); this.vScrollBar.scrollTo(this.realElement.scrollTop); } if(this.hScrollBar) { this.hScrollBar.setRange(this.realElement.scrollWidth - this.realElement.offsetWidth); this.hScrollBar.setSize(this.realElement.offsetWidth); this.hScrollBar.scrollTo(this.realElement.scrollLeft); } }, getScrollRange: function(isVertical) { if(isVertical) { return this.realElement.scrollHeight - this.realElement.offsetHeight; } else { return this.realElement.scrollWidth - this.realElement.offsetWidth; } }, getCurrentRange: function(scrollInstance) { return this.getScrollRange(scrollInstance.isVertical); }, onCreateModule: function(){ if(jcf.modules.select) { this.extendSelect(); } if(jcf.modules.selectmultiple) { this.extendSelectMultiple(); } if(jcf.modules.textarea) { this.extendTextarea(); } }, onModuleAdded: function(module){ if(module.prototype.name == 'select') { this.extendSelect(); } if(module.prototype.name == 'selectmultiple') { this.extendSelectMultiple(); } if(module.prototype.name == 'textarea') { this.extendTextarea(); } }, extendSelect: function() { // add scrollable if needed on control ready jcf.modules.select.prototype.onControlReady = function(obj){ if(obj.selectList.scrollHeight > obj.selectList.offsetHeight) { obj.jcfScrollable = new jcf.modules.customscroll({ alwaysPreventWheel: true, replaces:obj.selectList }); } } // update scroll function var orig = jcf.modules.select.prototype.scrollToItem; jcf.modules.select.prototype.scrollToItem = function(){ orig.apply(this); if(this.jcfScrollable) { this.jcfScrollable.refreshState(); } } }, extendTextarea: function() { // add scrollable if needed on control ready jcf.modules.textarea.prototype.onControlReady = function(obj){ obj.jcfScrollable = new jcf.modules.customscroll({ alwaysKeepScrollbars: true, alwaysPreventWheel: true, replaces: obj.realElement }); } // update scroll function var orig = jcf.modules.textarea.prototype.refreshState; jcf.modules.textarea.prototype.refreshState = function(){ orig.apply(this); if(this.jcfScrollable) { this.jcfScrollable.refreshState(); } } }, extendSelectMultiple: function(){ // add scrollable if needed on control ready jcf.modules.selectmultiple.prototype.onControlReady = function(obj){ //if(obj.optionsHolder.scrollHeight > obj.optionsHolder.offsetHeight) { obj.jcfScrollable = new jcf.modules.customscroll({ alwaysPreventWheel: true, replaces:obj.optionsHolder }); //} } // update scroll function var orig = jcf.modules.selectmultiple.prototype.scrollToItem; jcf.modules.selectmultiple.prototype.scrollToItem = function(){ orig.apply(this); if(this.jcfScrollable) { this.jcfScrollable.refreshState(); } } // update scroll size? var orig2 = jcf.modules.selectmultiple.prototype.rebuildOptions; jcf.modules.selectmultiple.prototype.rebuildOptions = function(){ orig2.apply(this); if(this.jcfScrollable) { this.jcfScrollable.refreshState(); } } } }); // scrollbar plugin jcf.addPlugin({ name: 'scrollbar', defaultOptions: { size: 0, range: 0, moveStep: 6, moveDistance: 50, moveInterval: 10, trackHoldDelay: 900, holder: null, vertical: true, scrollTag: 'div', onScroll: function(){}, onScrollEnd: function(){}, onScrollStart: function(){}, disabledClass: 'btn-disabled', VscrollBarClass:'vscrollbar', VscrollStructure: '
', VscrollTrack: 'div.vscroll-line', VscrollBtnDecClass:'div.vscroll-up', VscrollBtnIncClass:'div.vscroll-down', VscrollSliderClass:'div.vscroll-slider', HscrollBarClass:'hscrollbar', HscrollStructure: '
', HscrollTrack: 'div.hscroll-line', HscrollBtnDecClass:'div.hscroll-left', HscrollBtnIncClass:'div.hscroll-right', HscrollSliderClass:'div.hscroll-slider' }, init: function(userOptions) { this.setOptions(userOptions); this.createScrollBar(); this.attachEvents(); this.setSize(); }, setOptions: function(extOptions) { // merge options this.options = jcf.lib.extend({}, this.defaultOptions, extOptions); this.isVertical = this.options.vertical; this.prefix = this.isVertical ? 'V' : 'H'; this.eventPageOffsetProperty = this.isVertical ? 'pageY' : 'pageX'; this.positionProperty = this.isVertical ? 'top' : 'left'; this.sizeProperty = this.isVertical ? 'height' : 'width'; this.dimenionsProperty = this.isVertical ? 'offsetHeight' : 'offsetWidth'; this.invertedDimenionsProperty = !this.isVertical ? 'offsetHeight' : 'offsetWidth'; // set corresponding classes for(var p in this.options) { if(p.indexOf(this.prefix) == 0) { this.options[p.substr(1)] = this.options[p]; } } }, createScrollBar: function() { // create dimensions this.scrollBar = document.createElement(this.options.scrollTag); this.scrollBar.className = this.options.scrollBarClass; this.scrollBar.innerHTML = this.options.scrollStructure; // get elements this.track = jcf.lib.queryBySelector(this.options.scrollTrack,this.scrollBar)[0]; this.btnDec = jcf.lib.queryBySelector(this.options.scrollBtnDecClass,this.scrollBar)[0]; this.btnInc = jcf.lib.queryBySelector(this.options.scrollBtnIncClass,this.scrollBar)[0]; this.slider = jcf.lib.queryBySelector(this.options.scrollSliderClass,this.scrollBar)[0]; this.slider.style.position = 'absolute'; this.track.style.position = 'relative'; }, attachEvents: function() { // append scrollbar to holder if provided if(this.options.holder) { this.options.holder.appendChild(this.scrollBar); } // attach listeners for slider and buttons jcf.lib.event.add(this.slider, jcf.eventPress, this.onSliderPressed, this); jcf.lib.event.add(this.btnDec, jcf.eventPress, this.onBtnDecPressed, this); jcf.lib.event.add(this.btnInc, jcf.eventPress, this.onBtnIncPressed, this); jcf.lib.event.add(this.track, jcf.eventPress, this.onTrackPressed, this); }, setSize: function(value) { if(typeof value === 'number') { this.options.size = value; } this.scrollOffset = this.scrollValue = this.sliderOffset = 0; this.scrollBar.style[this.sizeProperty] = this.options.size + 'px'; this.resizeControls(); this.refreshSlider(); }, setRange: function(r) { this.options.range = Math.max(r,0); this.resizeControls(); }, doScrollWheelStep: function(direction) { // 1 - scroll up, -1 scroll down this.startScroll(); if((direction < 0 && !this.isEndPosition()) || (direction > 0 && !this.isStartPosition())) { this.scrollTo(this.getScrollValue()-this.options.moveDistance * direction); this.moveScroll(); this.endScroll(); return false; } }, resizeControls: function() { // calculate dimensions this.barSize = this.scrollBar[this.dimenionsProperty]; this.btnDecSize = this.btnDec[this.dimenionsProperty]; this.btnIncSize = this.btnInc[this.dimenionsProperty]; this.trackSize = Math.max(0, this.barSize - this.btnDecSize - this.btnIncSize); // resize and reposition elements this.track.style[this.sizeProperty] = this.trackSize + 'px'; this.trackSize = this.track[this.dimenionsProperty]; this.sliderSize = this.getSliderSize(); this.slider.style[this.sizeProperty] = this.sliderSize + 'px'; this.sliderSize = this.slider[this.dimenionsProperty]; }, refreshSlider: function(complete) { // refresh dimensions if(complete) { this.resizeControls(); } // redraw slider and classes this.sliderOffset = isNaN(this.sliderOffset) ? 0 : this.sliderOffset; this.slider.style[this.positionProperty] = this.sliderOffset + 'px'; }, startScroll: function() { // refresh range if possible if(this.options.spawnClass && typeof this.options.spawnClass.getCurrentRange === 'function') { this.setRange(this.options.spawnClass.getCurrentRange(this)); } this.resizeControls(); this.scrollBarOffset = jcf.lib.getOffset(this.track)[this.positionProperty]; this.options.onScrollStart(); }, moveScroll: function() { this.options.onScroll(this.scrollValue); // add disabled classes jcf.lib.removeClass(this.btnDec, this.options.disabledClass); jcf.lib.removeClass(this.btnInc, this.options.disabledClass); if(this.scrollValue === 0) { jcf.lib.addClass(this.btnDec, this.options.disabledClass); } if(this.scrollValue === this.options.range) { jcf.lib.addClass(this.btnInc, this.options.disabledClass); } }, endScroll: function() { this.options.onScrollEnd(); }, startButtonMoveScroll: function(direction) { this.startScroll(); clearInterval(this.buttonScrollTimer); this.buttonScrollTimer = setInterval(jcf.lib.bind(function(){ this.scrollValue += this.options.moveStep * direction if(this.scrollValue > this.options.range) { this.scrollValue = this.options.range; this.endButtonMoveScroll(); } else if(this.scrollValue < 0) { this.scrollValue = 0; this.endButtonMoveScroll(); } this.scrollTo(this.scrollValue); },this),this.options.moveInterval); }, endButtonMoveScroll: function() { clearInterval(this.buttonScrollTimer); this.endScroll(); }, isStartPosition: function() { return this.scrollValue === 0; }, isEndPosition: function() { return this.scrollValue === this.options.range; }, getSliderSize: function() { return Math.round(this.getSliderSizePercent() * this.trackSize / 100); }, getSliderSizePercent: function() { return this.options.range === 0 ? 0 : this.barSize * 100 / (this.barSize + this.options.range); }, getSliderOffsetByScrollValue: function() { return (this.scrollValue * 100 / this.options.range) * (this.trackSize - this.sliderSize) / 100; }, getSliderOffsetPercent: function() { return this.sliderOffset * 100 / (this.trackSize - this.sliderSize); }, getScrollValueBySliderOffset: function() { return this.getSliderOffsetPercent() * this.options.range / 100; }, getScrollBarSize: function() { return this.scrollBar[this.invertedDimenionsProperty]; }, getScrollValue: function() { return this.scrollValue || 0; }, scrollOnePage: function(direction) { this.scrollTo(this.scrollValue + direction*this.barSize); }, scrollTo: function(x) { this.scrollValue = x < 0 ? 0 : x > this.options.range ? this.options.range : x; this.sliderOffset = this.getSliderOffsetByScrollValue(); this.refreshSlider(); this.moveScroll(); }, onSliderPressed: function(e){ jcf.lib.event.add(document.body, jcf.eventRelease, this.onSliderRelease, this); jcf.lib.event.add(document.body, jcf.eventMove, this.onSliderMove, this); jcf.lib.disableTextSelection(this.slider); // calculate offsets once this.sliderInnerOffset = (jcf.isTouchDevice ? e.changedTouches[0] : e)[this.eventPageOffsetProperty] - jcf.lib.getOffset(this.slider)[this.positionProperty]; this.startScroll(); return false; }, onSliderRelease: function(){ jcf.lib.event.remove(document.body, jcf.eventRelease, this.onSliderRelease); jcf.lib.event.remove(document.body, jcf.eventMove, this.onSliderMove); }, onSliderMove: function(e) { this.sliderOffset = (jcf.isTouchDevice ? e.changedTouches[0] : e)[this.eventPageOffsetProperty] - this.scrollBarOffset - this.sliderInnerOffset; if(this.sliderOffset < 0) { this.sliderOffset = 0; } else if(this.sliderOffset + this.sliderSize > this.trackSize) { this.sliderOffset = this.trackSize - this.sliderSize; } if(this.previousOffset != this.sliderOffset) { this.previousOffset = this.sliderOffset; this.scrollTo(this.getScrollValueBySliderOffset()); } }, onBtnIncPressed: function() { jcf.lib.event.add(document.body, jcf.eventRelease, this.onBtnIncRelease, this); jcf.lib.disableTextSelection(this.btnInc); this.startButtonMoveScroll(1); return false; }, onBtnIncRelease: function() { jcf.lib.event.remove(document.body, jcf.eventRelease, this.onBtnIncRelease); this.endButtonMoveScroll(); }, onBtnDecPressed: function() { jcf.lib.event.add(document.body, jcf.eventRelease, this.onBtnDecRelease, this); jcf.lib.disableTextSelection(this.btnDec); this.startButtonMoveScroll(-1); return false; }, onBtnDecRelease: function() { jcf.lib.event.remove(document.body, jcf.eventRelease, this.onBtnDecRelease); this.endButtonMoveScroll(); }, onTrackPressed: function(e) { var position = e[this.eventPageOffsetProperty] - jcf.lib.getOffset(this.track)[this.positionProperty]; var direction = position < this.sliderOffset ? -1 : position > this.sliderOffset + this.sliderSize ? 1 : 0; if(direction) { this.scrollOnePage(direction); } } });