/** ace custom scroller. it is not as feature-rich as plugins such as nicescroll but it's good enough for most cases. */ (function($ , undefined) { var ace_scroll = function(element , _settings) { var self = this; var attrib_values = ace.helper.getattrsettings(element, $.fn.ace_scroll.defaults); var settings = $.extend({}, $.fn.ace_scroll.defaults, _settings, attrib_values); this.size = 0; this.lock = false; this.lock_anyway = false; this.$element = $(element); this.element = element; var vertical = true; var disabled = false; var active = false; var created = false; var $content_wrap = null, content_wrap = null; var $track = null, $bar = null, track = null, bar = null; var bar_style = null; var bar_size = 0, bar_pos = 0, bar_max_pos = 0, bar_size_2 = 0, move_bar = true; var reset_once = false; var styleclass = ''; var trackflip = false;//vertical on left or horizontal on top var tracksize = 0; var css_pos, css_size, max_css_size, client_size, scroll_direction, scroll_size; var ratio = 1; var inline_style = false; var mouse_track = false; var mouse_release_target = 'onmouseup' in window ? window : 'html'; var dragevent = settings.dragevent || false; var trigger_scroll = _settings.scrollevent || false; var detached = settings.detached || false;//when detached, hideonidle as well? var updatepos = settings.updatepos || false;//default is true var hideonidle = settings.hideonidle || false; var hidedelay = settings.hidedelay || 1500; var insidetrack = false;//used to hide scroll track when mouse is up and outside of track var observecontent = settings.observecontent || false; var prevcontentsize = 0; var is_dirty = true;//to prevent consecutive 'reset' calls this.create = function(_settings) { if(created) return; //if(disabled) return; if(_settings) settings = $.extend({}, $.fn.ace_scroll.defaults, _settings); this.size = parseint(this.$element.attr('data-size')) || settings.size || 200; vertical = !settings['horizontal']; css_pos = vertical ? 'top' : 'left';//'left' for horizontal css_size = vertical ? 'height' : 'width';//'width' for horizontal max_css_size = vertical ? 'maxheight' : 'maxwidth'; client_size = vertical ? 'clientheight' : 'clientwidth'; scroll_direction = vertical ? 'scrolltop' : 'scrollleft'; scroll_size = vertical ? 'scrollheight' : 'scrollwidth'; this.$element.addclass('ace-scroll'); if(this.$element.css('position') == 'static') { inline_style = this.element.style.position; this.element.style.position = 'relative'; } else inline_style = false; var scroll_bar = null; if(!detached) { this.$element.wrapinner('
'); this.$element.prepend('
'); } else { scroll_bar = $('
').appendto('body'); } $content_wrap = this.$element; if(!detached) $content_wrap = this.$element.find('.scroll-content').eq(0); if(!vertical) $content_wrap.wrapinner('
'); content_wrap = $content_wrap.get(0); if(detached) { //set position for detached scrollbar $track = scroll_bar; settrackpos(); } else $track = this.$element.find('.scroll-track').eq(0); $bar = $track.find('.scroll-bar').eq(0); track = $track.get(0); bar = $bar.get(0); bar_style = bar.style; //add styling classes and horizontalness if(!vertical) $track.addclass('scroll-hz'); if(settings.styleclass) { styleclass = settings.styleclass; $track.addclass(styleclass); trackflip = !!styleclass.match(/scroll\-left|scroll\-top/); } //calculate size of track! if(tracksize == 0) { $track.show(); gettracksize(); } $track.hide(); //if(!touchdrag) { $track.on('mousedown', mouse_down_track); $bar.on('mousedown', mouse_down_bar); //} $content_wrap.on('scroll', function() { if(move_bar) { bar_pos = parseint(math.round(this[scroll_direction] * ratio)); bar_style[css_pos] = bar_pos + 'px'; } move_bar = false; if(trigger_scroll) this.$element.trigger('scroll', [content_wrap]); }) if(settings.mousewheel) { this.lock = settings.mousewheellock; this.lock_anyway = settings.lockanyway; //mousewheel library available? this.$element.on(!!$.event.special.mousewheel ? 'mousewheel.ace_scroll' : 'mousewheel.ace_scroll dommousescroll.ace_scroll', function(event) { if(disabled) return; checkcontentchanges(true); if(!active) return !self.lock_anyway; if(mouse_track) { mouse_track = false; $('html').off('.ace_scroll') $(mouse_release_target).off('.ace_scroll'); if(dragevent) self.$element.trigger('drag.end'); } event.deltay = event.deltay || 0; var delta = (event.deltay > 0 || event.originalevent.detail < 0 || event.originalevent.wheeldelta > 0) ? 1 : -1 var scrollend = false//have we reached the end of scrolling? var clientsize = content_wrap[client_size], scrollamount = content_wrap[scroll_direction]; if( !self.lock ) { if(delta == -1) scrollend = (content_wrap[scroll_size] <= scrollamount + clientsize); else scrollend = (scrollamount == 0); } self.move_bar(true); //var step = parseint( math.min(math.max(parseint(clientsize / 8) , 80) , self.size) ) + 1; var step = parseint(clientsize / 8); if(step < 80) step = 80; if(step > self.size) step = self.size; step += 1; content_wrap[scroll_direction] = scrollamount - (delta * step); return scrollend && !self.lock_anyway; }) } //swipe not available yet var touchdrag = ace.vars['touch'] && 'ace_drag' in $.event.special && settings.touchdrag //&& !settings.touchswipe; //add drag event for touch devices to scroll if(touchdrag/** || ($.fn.swipe && settings.touchswipe)*/) { var dir = '', event_name = touchdrag ? 'ace_drag' : 'swipe'; this.$element.on(event_name + '.ace_scroll', function(event) { if(disabled) { event.retval.cancel = true; return; } checkcontentchanges(true); if(!active) { event.retval.cancel = this.lock_anyway; return; } dir = event.direction; if( (vertical && (dir == 'up' || dir == 'down')) || (!vertical && (dir == 'left' || dir == 'right')) ) { var distance = vertical ? event.dy : event.dx; if(distance != 0) { if(math.abs(distance) > 20 && touchdrag) distance = distance * 2; self.move_bar(true); content_wrap[scroll_direction] = content_wrap[scroll_direction] + distance; } } }) } ///////////////////////////////// if(hideonidle) { $track.addclass('idle-hide'); } if(observecontent) { $track.on('mouseenter.ace_scroll', function() { insidetrack = true; checkcontentchanges(false); }).on('mouseleave.ace_scroll', function() { insidetrack = false; if(mouse_track == false) hidescrollbars(); }); } //some mobile browsers don't have mouseenter this.$element.on('mouseenter.ace_scroll touchstart.ace_scroll', function(e) { //if(ace.vars['old_ie']) return;//ie8 has a problem triggering event two times and strangely wrong values for this.size especially in fullscreen widget! is_dirty = true; if(observecontent) checkcontentchanges(true); else if(settings.hoverreset) self.reset(true); $track.addclass('scroll-hover'); }).on('mouseleave.ace_scroll touchend.ace_scroll', function() { $track.removeclass('scroll-hover'); }); // if(!vertical) $content_wrap.children(0).css(css_size, this.size);//the extra wrapper $content_wrap.css(max_css_size , this.size); disabled = false; created = true; } this.is_active = function() { return active; } this.is_enabled = function() { return !disabled; } this.move_bar = function($move) { move_bar = $move; } this.get_track = function() { return track; } this.reset = function(innert_call) { if(disabled) return;// this; if(!created) this.create(); ///////////////////// var size = this.size; if(innert_call && !is_dirty) { return; } is_dirty = false; if(detached) { var border_size = parseint(math.round( (parseint($content_wrap.css('border-top-width')) + parseint($content_wrap.css('border-bottom-width'))) / 2.5 ));//(2.5 from trial?!) size -= border_size;//only if detached } var content_size = vertical ? content_wrap[scroll_size] : size; if( (vertical && content_size == 0) || (!vertical && this.element.scrollwidth == 0) ) { //element is hidden //this.$element.addclass('scroll-hidden'); $track.removeclass('scroll-active') return;// this; } var available_space = vertical ? size : content_wrap.clientwidth; if(!vertical) $content_wrap.children(0).css(css_size, size);//the extra wrapper $content_wrap.css(max_css_size , this.size); if(content_size > available_space) { active = true; $track.css(css_size, available_space).show(); ratio = parsefloat((available_space / content_size).tofixed(5)) bar_size = parseint(math.round(available_space * ratio)); bar_size_2 = parseint(math.round(bar_size / 2)); bar_max_pos = available_space - bar_size; bar_pos = parseint(math.round(content_wrap[scroll_direction] * ratio)); bar_style[css_size] = bar_size + 'px'; bar_style[css_pos] = bar_pos + 'px'; $track.addclass('scroll-active'); if(tracksize == 0) { gettracksize(); } if(!reset_once) { //this.$element.removeclass('scroll-hidden'); if(settings.reset) { //reset scrollbar to zero position at first content_wrap[scroll_direction] = 0; bar_style[css_pos] = 0; } reset_once = true; } if(detached) settrackpos(); } else { active = false; $track.hide(); $track.removeclass('scroll-active'); $content_wrap.css(max_css_size , ''); } return;// this; } this.disable = function() { content_wrap[scroll_direction] = 0; bar_style[css_pos] = 0; disabled = true; active = false; $track.hide(); this.$element.addclass('scroll-disabled'); $track.removeclass('scroll-active'); $content_wrap.css(max_css_size , ''); } this.enable = function() { disabled = false; this.$element.removeclass('scroll-disabled'); } this.destroy = function() { active = false; disabled = false; created = false; this.$element.removeclass('ace-scroll scroll-disabled scroll-active'); this.$element.off('.ace_scroll') if(!detached) { if(!vertical) { //remove the extra wrapping div $content_wrap.find('> div').children().unwrap(); } $content_wrap.children().unwrap(); $content_wrap.remove(); } $track.remove(); if(inline_style !== false) this.element.style.position = inline_style; if(idletimer != null) { cleartimeout(idletimer); idletimer = null; } } this.modify = function(_settings) { if(_settings) settings = $.extend({}, settings, _settings); this.destroy(); this.create(); is_dirty = true; this.reset(true); } this.update = function(_settings) { if(_settings) settings = $.extend({}, settings, _settings); this.size = _settings.size || this.size; this.lock = _settings.mousewheellock || this.lock; this.lock_anyway = _settings.lockanyway || this.lock_anyway; if(_settings.styleclass != undefined) { if(styleclass) $track.removeclass(styleclass); styleclass = _settings.styleclass; if(styleclass) $track.addclass(styleclass); trackflip = !!styleclass.match(/scroll\-left|scroll\-top/); } } this.start = function() { content_wrap[scroll_direction] = 0; } this.end = function() { content_wrap[scroll_direction] = content_wrap[scroll_size]; } this.hide = function() { $track.hide(); } this.show = function() { $track.show(); } this.update_scroll = function() { move_bar = false; bar_style[css_pos] = bar_pos + 'px'; content_wrap[scroll_direction] = parseint(math.round(bar_pos / ratio)); } function mouse_down_track(e) { e.preventdefault(); e.stoppropagation(); var track_offset = $track.offset(); var track_pos = track_offset[css_pos];//top for vertical, left for horizontal var mouse_pos = vertical ? e.pagey : e.pagex; if(mouse_pos > track_pos + bar_pos) { bar_pos = mouse_pos - track_pos - bar_size + bar_size_2; if(bar_pos > bar_max_pos) { bar_pos = bar_max_pos; } } else { bar_pos = mouse_pos - track_pos - bar_size_2; if(bar_pos < 0) bar_pos = 0; } self.update_scroll() } var mouse_pos1 = -1, mouse_pos2 = -1; function mouse_down_bar(e) { e.preventdefault(); e.stoppropagation(); if(vertical) { mouse_pos2 = mouse_pos1 = e.pagey; } else { mouse_pos2 = mouse_pos1 = e.pagex; } mouse_track = true; $('html').off('mousemove.ace_scroll').on('mousemove.ace_scroll', mouse_move_bar) $(mouse_release_target).off('mouseup.ace_scroll').on('mouseup.ace_scroll', mouse_up_bar); $track.addclass('active'); if(dragevent) self.$element.trigger('drag.start'); } function mouse_move_bar(e) { e.preventdefault(); e.stoppropagation(); if(vertical) { mouse_pos2 = e.pagey; } else { mouse_pos2 = e.pagex; } if(mouse_pos2 - mouse_pos1 + bar_pos > bar_max_pos) { mouse_pos2 = mouse_pos1 + bar_max_pos - bar_pos; } else if(mouse_pos2 - mouse_pos1 + bar_pos < 0) { mouse_pos2 = mouse_pos1 - bar_pos; } bar_pos = bar_pos + (mouse_pos2 - mouse_pos1); mouse_pos1 = mouse_pos2; if(bar_pos < 0) { bar_pos = 0; } else if(bar_pos > bar_max_pos) { bar_pos = bar_max_pos; } self.update_scroll() } function mouse_up_bar(e) { e.preventdefault(); e.stoppropagation(); mouse_track = false; $('html').off('.ace_scroll') $(mouse_release_target).off('.ace_scroll'); $track.removeclass('active'); if(dragevent) self.$element.trigger('drag.end'); if(active && hideonidle && !insidetrack) hidescrollbars(); } var idletimer = null; var prevchecktime = 0; function checkcontentchanges(hidesoon) { //check if content size has been modified since last time? //and with at least 1s delay var newcheck = +new date(); if(observecontent && newcheck - prevchecktime > 1000) { var newsize = content_wrap[scroll_size]; if(prevcontentsize != newsize) { prevcontentsize = newsize; is_dirty = true; self.reset(true); } prevchecktime = newcheck; } //show scrollbars when not idle anymore i.e. triggered by mousewheel, dragging, etc if(active && hideonidle) { if(idletimer != null) { cleartimeout(idletimer); idletimer = null; } $track.addclass('not-idle'); if(!insidetrack && hidesoon == true) { //hidesoon is false when mouse enters track hidescrollbars(); } } } function hidescrollbars() { if(idletimer != null) { cleartimeout(idletimer); idletimer = null; } idletimer = settimeout(function() { idletimer = null; $track.removeclass('not-idle'); } , hidedelay); } //for detached scrollbars function gettracksize() { $track.css('visibility', 'hidden').addclass('scroll-hover'); if(vertical) tracksize = parseint($track.outerwidth()) || 0; else tracksize = parseint($track.outerheight()) || 0; $track.css('visibility', '').removeclass('scroll-hover'); } this.track_size = function() { if(tracksize == 0) gettracksize(); return tracksize; } //for detached scrollbars function settrackpos() { if(updatepos === false) return; var off = $content_wrap.offset();//because we want it relative to parent not document var left = off.left; var top = off.top; if(vertical) { if(!trackflip) { left += ($content_wrap.outerwidth() - tracksize) } } else { if(!trackflip) { top += ($content_wrap.outerheight() - tracksize) } } if(updatepos === true) $track.css({top: parseint(top), left: parseint(left)}); else if(updatepos === 'left') $track.css('left', parseint(left)); else if(updatepos === 'top') $track.css('top', parseint(top)); } this.create(); is_dirty = true; this.reset(true); prevcontentsize = content_wrap[scroll_size]; return this; } $.fn.ace_scroll = function (option,value) { var retval; var $set = this.each(function () { var $this = $(this); var data = $this.data('ace_scroll'); var options = typeof option === 'object' && option; if (!data) $this.data('ace_scroll', (data = new ace_scroll(this, options))); //else if(typeof options == 'object') data['modify'](options); if (typeof option === 'string') retval = data[option](value); }); return (retval === undefined) ? $set : retval; }; $.fn.ace_scroll.defaults = { 'size' : 200, 'horizontal': false, 'mousewheel': true, 'mousewheellock': false, 'lockanyway': false, 'styleclass' : false, 'observecontent': false, 'hideonidle': false, 'hidedelay': 1500, 'hoverreset': true //reset scrollbar sizes on mouse hover because of possible sizing changes , 'reset': false //true= set scrolltop = 0 , 'dragevent': false , 'touchdrag': true , 'touchswipe': false , 'scrollevent': false //trigger scroll event , 'detached': false , 'updatepos': true /** , 'track' : true, 'show' : false, 'dark': false, 'alwaysvisible': false, 'margin': false, 'thin': false, 'position': 'right' */ } /** $(document).on('ace.settings.ace_scroll', function(e, name) { if(name == 'sidebar_collapsed') $('.ace-scroll').scroller('reset'); }); $(window).on('resize.ace_scroll', function() { $('.ace-scroll').scroller('reset'); }); */ })(window.jquery);