/*
* slimscroll
* http://github.com/yawetse/slimscroll
*
* Copyright (c) 2014 Yaw Joseph Etse. All rights reserved.
*/
'use strict';
var classie = require('classie'),
extend = require('util-extend'),
domhelper = require('domhelper');
/**
* Slimscroll is a small commonjs module with no library dependencies (sans jquery) that transforms any div into a scrollable area with a nice scrollbar
* @{@link https://github.com/yawetse/slimscroll}
* @author Yaw Joseph Etse
* @copyright Copyright (c) 2014 Yaw Joseph Etse. All rights reserved.
* @license MIT
* @module slimscroll
* @requires module:classie
* @requires module:util-extent
* @requires module:util
* @requires module:domhelper
* @requires module:events
* @todo need to switch to node events
*/
var slimscroll = function(options,elementsArray){
/** module default configuration */
var defaults = {
idSelector: 'body',
width : 'auto',// width in pixels of the visible scroll area
height : '250px',// height in pixels of the visible scroll area
size : '7px',// width in pixels of the scrollbar and rail
color: '#000',// scrollbar color, accepts any hex/color value
position : 'right',// scrollbar position - left/right
distance : '1px',// distance in pixels between the side edge and the scrollbar
start : 'top',// default scroll position on load - top / bottom / $('selector')
opacity : 0.4,// sets scrollbar opacity
alwaysVisible : false,// enables always-on mode for the scrollbar
disableFadeOut : false,// check if we should hide the scrollbar when user is hovering over
railVisible : false,// sets visibility of the rail
railColor : '#333',// sets rail color
railOpacity : 0.2,// sets rail opacity
railDraggable : true,// whether we should use jQuery UI Draggable to enable bar dragging
railClass : 'slimScrollRail',// defautlt CSS class of the slimscroll rail
barClass : 'slimScrollBar',// defautlt CSS class of the slimscroll bar
wrapperClass : 'slimScrollDiv',// defautlt CSS class of the slimscroll wrapper
allowPageScroll : false,// check if mousewheel should scroll the window if we reach top/bottom
wheelStep : 20,// scroll amount applied to each mouse wheel step
touchScrollStep : 200,// scroll amount applied when user is using gestures
addedOriginalClass: 'originalScrollableElement',
borderRadius: '7px',// sets border radius
railBorderRadius : '7px'// sets border radius of the rail
},
o = extend( defaults,options ),
thisElements = (elementsArray) ? elementsArray : document.querySelectorAll(options.idSelector),
me,
rail,
bar,
barHeight,
minBarHeight = 30,
mousedownPageY,
mousedownT,
isDragg,
currentBar,
currentTouchDif,
releaseScroll,
isOverBar,
percentScroll,
queueHide,
lastScroll,
isOverPanel;
/**
* creates new slimscrolls
*/
this.init = function(){
// do it for every element that matches selector
for(var x=0; x<thisElements.length;x++){
var touchDif,
barHeight,
divS = '<div></div>';
releaseScroll = false;
// used in event handlers and for better minification
me = thisElements[x];
classie.addClass(me,o.addedOriginalClass);
// ensure we are not binding it again
if( classie.hasClass(me.parentNode,o.wrapperClass) ){
// start from last bar position
var offset = me.scrollTop;
bar = me.parentNode.querSelector('.' + o.barClass),// find bar and rail,
rail = me.parentNode.querSelector('.' + o.railClass);
getBarHeight();
// check if we should scroll existing instance
if (typeof options==='object'){
// Pass height: auto to an existing slimscroll object to force a resize after contents have changed
if ( 'height' in options && options.height === 'auto' ) {
me.parentNode.style.height='auto';
me.style.height='auto';
var height = me.parentNode.parentNode.scrollHeight;
me.parent.style.height=height;
me.style.height=height;
}
if ('scrollTo' in options){
// jump to a static point
offset = parseInt(o.scrollTo,10);
}
else if ('scrollBy' in options){
// jump by value pixels
offset += parseInt(o.scrollBy,10);
}
else if ('destroy' in options){
// remove slimscroll elements
domhelper.removeElement(bar);
domhelper.removeElement(rail);
domhelper.unwrapElement(me);
return;
}
// scroll content by the given offset
// console.log("add scrollContent");
scrollContent(offset, false, true,me);
}
return;
}
// optionally set height to the parent's height
o.height = (options.height === 'auto') ? me.parentNode.offsetHeight : options.height;
// wrap content
var wrapper = document.createElement("div");
classie.addClass(wrapper,o.wrapperClass);
wrapper.style.position= 'relative';
wrapper.style.overflow= 'hidden';
wrapper.style.width= o.width;
wrapper.style.height= o.height;
// update style for the div
me.style.overflow= 'hidden';
me.style.width= o.width;
me.style.height= o.height;
// create scrollbar rail
rail = document.createElement("div");
classie.addClass(rail,o.railClass);
rail.style.width= o.size;
rail.style.height= '100%';
rail.style.position= 'absolute';
rail.style.top= 0;
rail.style.display= (o.alwaysVisible && o.railVisible) ? 'block' : 'none';
rail.style['border-radius']= o.railBorderRadius;
rail.style.background= o.railColor;
rail.style.opacity= o.railOpacity;
rail.style.zIndex= 90;
// create scrollbar
bar = document.createElement("div");
classie.addClass(bar,o.barClass);
bar.style.background= o.color;
bar.style.width= o.size;
bar.style.position= 'absolute';
bar.style.top= 0;
bar.style.opacity= o.opacity;
bar.style.display= o.alwaysVisible ? 'block' : 'none';
bar.style['border-radius'] = o.borderRadius;
bar.style.BorderRadius= o.borderRadius;
bar.style.MozBorderRadius= o.borderRadius;
bar.style.WebkitBorderRadius= o.borderRadius;
bar.style.zIndex= 99;
// set position
if(o.position === 'right'){
rail.style.right = o.distance;
bar.style.right = o.distance;
}
else{
rail.style.left = o.distance;
bar.style.left = o.distance;
}
// wrap it
domhelper.elementWrap(me,wrapper);
// append to parent div
me.parentNode.appendChild(bar);
me.parentNode.appendChild(rail);
// set up initial height
getBarHeight();
// make it draggable and no longer dependent on the jqueryUI
bar.addEventListener("mousedown",mousedownEventHandler);
document.addEventListener("mouseup",mouseupEventHandler);
bar.addEventListener("selectstart",selectstartEventHandler);
bar.addEventListener("mouseover",mouseoverEventHandler);
bar.addEventListener("mouseleave",mouseleaveEventHandler);
bar.addEventListener('touchstart',scrollContainerTouchStartEventHandler);
rail.addEventListener("mouseover",railMouseOverEventHandler);
rail.addEventListener("mouseleave",railMouseLeaveEventHandler);
me.addEventListener("mouseover",scrollContainerMouseOverEventHandler);
me.addEventListener("mouseleave",scrollContainerMouseLeaveEventHandler);
me.addEventListener('DOMMouseScroll', mouseWheelEventHandler, false );
me.addEventListener('mousewheel', mouseWheelEventHandler, false );
}
// check start position
if (o.start === 'bottom'){
// scroll content to bottom
bar.style.top= me.offsetHeight - bar.offsetHeight;
scrollContent(0, true);
}
else if (o.start !== 'top'){
// assume jQuery selector
scrollContent( domhelper.getPosition(document.querSelector(o.start).top, null, true));
// make sure bar stays hidden
if (!o.alwaysVisible) {
domhelper.elementHideCss(bar);
}
}
document.addEventListener('touchmove',scrollContainerTouchMoveEventHandler);
}.bind(this);
/**
* Removes the auto scrolling for touch devices.
* @memberOf slimscroll
* @private
*/
function getBarHeight(){
if(!bar){
bar = currentBar;
}
// calculate scrollbar height and make sure it is not too small
barHeight = Math.max((me.offsetHeight / me.scrollHeight) * me.offsetHeight, minBarHeight);
bar.style.height= barHeight + 'px' ;
// hide scrollbar if content is not long enough
var display = (me.offsetHeight === barHeight) ? 'none' : 'block';
bar.style.display= display;
}
/**
* creates new slimscrollsprivate functnio
*/
function _privateFunction(){
console.log("pf");
}
/**
* Removes the auto scrolling for touch devices.
* @function
*/
function scrollContent(y, isWheel, isJump,element,bar,isTouch){
releaseScroll = false;
var delta = y;
me = element;
bar = (bar)? bar : me.parentNode.querySelector('.'+o.barClass);
var maxTop = me.offsetHeight - bar.offsetHeight;
if (isWheel){
// move bar with mouse wheel
delta = parseInt(bar.style.
top,10) + y * parseInt(o.wheelStep,10) / 100 * bar.offsetHeight;
// move bar, make sure it doesn't go out
delta = Math.min(Math.max(delta, 0), maxTop);
// if scrolling down, make sure a fractional change to the
// scroll position isn't rounded away when the scrollbar's CSS is set
// this flooring of delta would happened automatically when
// bar.css is set below, but we floor here for clarity
delta = (y > 0) ? Math.ceil(delta) : Math.floor(delta);
// scroll the scrollbar
bar.style.top= delta + 'px';
}
else if(isTouch){
// calculate actual scroll amount
percentScroll = parseInt(bar.style.top,10) / (me.offsetHeight - bar.offsetHeight);
delta = percentScroll * (me.scrollHeight - me.offsetHeight);
// scroll the scrollbar
bar.style.top= delta + 'px';
}
// calculate actual scroll amount
percentScroll = parseInt(bar.style.top,10) / (me.offsetHeight - bar.offsetHeight);
delta = percentScroll * (me.scrollHeight - me.offsetHeight);
if (isJump){
delta = y;
var offsetTop = delta / me.scrollHeight * me.offsetHeight;
offsetTop = Math.min(Math.max(offsetTop, 0), maxTop);
bar.style.top= offsetTop + 'px';
}
// scroll content
me.scrollTop=delta;
// console.log("delta",delta,"~~delta",~~delta);
// fire scrolling event
// me.dispatchEvent(slimScrollEvent)
var newevent = document.createEvent("Event");
newevent.initEvent('slimscrolling',true,true,"blah");
me.dispatchEvent(newevent, ~~delta);
// me.trigger('slimscrolling', ~~delta);
// ensure bar is visible
showBar();
// trigger hide when scroll is stopped
hideBar();
}
function showBar(){
// recalculate bar height
getBarHeight();
clearTimeout(queueHide);
// when bar reached top or bottom
if (percentScroll === ~~percentScroll){
//release wheel
releaseScroll = o.allowPageScroll;
// publish approporiate event
if (lastScroll !== percentScroll){
var msg = (~~percentScroll === 0) ? 'top' : 'bottom';
var newevent = document.createEvent("Event");
newevent.initEvent('slimscroll',true,true);
me.dispatchEvent(newevent, msg);
}
}
else{
releaseScroll = false;
}
lastScroll = percentScroll;
// show only when required
if(barHeight >= me.offsetHeight) {
//allow window scroll
releaseScroll = true;
return;
}
bar.style.transition="opacity .5s";
bar.style.opacity=o.opacity;
if (o.railVisible) {
rail.style.transform="opacity .5s";
rail.style.opacity=1;
}
}
function hideBar(){
// only hide when options allow it
if (!o.alwaysVisible){
queueHide = setTimeout(function(){
if (!(o.disableFadeOut && isOverPanel) && !isOverBar && !isDragg){
bar.style.transition="opacity 1s";
bar.style.opacity=0;
rail.style.transition="opacity 1s";
rail.style.opacity=0;
}
}, 500);
}
}
function mouseWheelEventHandler(e){
// use mouse wheel only when mouse is over
if (!isOverPanel) { return; }
var delta = 0;
if (e.wheelDelta) {
delta = -e.wheelDelta/120;
}
if (e.detail) {
delta = e.detail / 3;
}
var target = e.target;
var parentWrapper = domhelper.getParentElement(target,o.wrapperClass);
if (parentWrapper /* && parentWrapper.isEqualNode(me.parentNode)*/ ){
// scroll content
scrollContent(delta, true,null,parentWrapper.querySelector('.'+o.addedOriginalClass));
}
else{
console.log("not the right parent node");
}
// stop window scroll
if (!releaseScroll) {
e.preventDefault();
}
}
function mousedownEventHandler(e){
var eTarget = e.target;
currentBar = eTarget;
isDragg = true;
mousedownT = parseInt(eTarget.style.top,10);
mousedownPageY = e.pageY;
if(currentBar){
currentBar.addEventListener("mousemove",mousemoveEventHandler);
}
e.preventDefault();
return false;
}
function mousemoveEventHandler(e){
var currTop = mousedownT + e.pageY - mousedownPageY;
if(currentBar){
currentBar.style.top=currTop;
scrollContent(0, domhelper.getPosition(currentBar).top, false,me,currentBar);// scroll content
}
}
function mouseupEventHandler(e){
isDragg = false;
if(currentBar){
hideBar(currentBar);
currentBar.removeEventListener('mousemove',mousemoveEventHandler);
}
}
function mouseoverEventHandler(e){ isOverBar = true; }
function mouseleaveEventHandler(e){ isOverBar = false; }
function selectstartEventHandler(e){
// e.stopPropagation();
// e.preventDefault();
return false;
}
function railMouseOverEventHandler(e){ showBar(); }
function railMouseLeaveEventHandler(e){ hideBar(); }
function scrollContainerMouseOverEventHandler(e){
isOverPanel = true;
showBar(bar);
hideBar(bar);
}
function scrollContainerMouseLeaveEventHandler(e){
isOverPanel = true;
showBar(bar);
hideBar(bar);
}
function scrollContainerTouchStartEventHandler(e){
// console.log(e.target);
if (e.touches.length){
// record where touch started
currentTouchDif = e.touches[0].pageY;
}
}
function scrollContainerTouchMoveEventHandler(e){
// prevent scrolling the page if necessary
if(!releaseScroll){
e.preventDefault();
}
if(e.touches.length){
// see how far user swiped
var diff = (currentTouchDif - e.touches[0].pageY) / o.touchScrollStep;
// scroll content
scrollContent(diff, true,null,me,currentBar,true);
currentTouchDif = e.touches[0].pageY;
}
}
};
module.exports = slimscroll;
// If there is a window object, that at least has a document property,
// define linotype
if ( typeof window === "object" && typeof window.document === "object" ) {
window.slimscroll = slimscroll;
}