'use strict'; angular.module('ngScrollbar', []).directive('ngScrollbar', [ '$parse', '$window', function ($parse, $window) { return { restrict: 'A', replace: true, transclude: true, scope: { 'showYScrollbar': '=?isBarShown' }, link: function (scope, element, attrs) { var mainElm, transculdedContainer, tools, thumb, thumbLine, track; var flags = { bottom: attrs.hasOwnProperty('bottom') }; var win = angular.element($window); var hasAddEventListener = !!win[0].addEventListener; var hasRemoveEventListener = !!win[0].removeEventListener; // Elements var dragger = { top: 0 }, page = { top: 0 }; // Styles var scrollboxStyle, draggerStyle, draggerLineStyle, pageStyle; var calcStyles = function () { scrollboxStyle = { position: 'relative', overflow: 'hidden', 'max-width': '100%', height: '100%' }; if (page.height) { scrollboxStyle.height = page.height + 'px'; } draggerStyle = { position: 'absolute', height: dragger.height + 'px', top: dragger.top + 'px' }; draggerLineStyle = { position: 'relative', 'line-height': dragger.height + 'px' }; pageStyle = { position: 'relative', top: page.top + 'px', overflow: 'hidden' }; }; var redraw = function () { thumb.css('top', dragger.top + 'px'); var draggerOffset = dragger.top / page.height; page.top = -Math.round(page.scrollHeight * draggerOffset); transculdedContainer.css('top', page.top + 'px'); }; var trackClick = function (event) { var offsetY = event.hasOwnProperty('offsetY') ? event.offsetY : event.layerY; var newTop = Math.max(0, Math.min(parseInt(dragger.trackHeight, 10) - parseInt(dragger.height, 10), offsetY)); dragger.top = newTop; redraw(); event.stopPropagation(); }; var wheelHandler = function (event) { var wheelSpeed = 40; // Mousewheel speed normalization approach adopted from // http://stackoverflow.com/a/13650579/1427418 var o = event, d = o.detail, w = o.wheelDelta, n = 225, n1 = n - 1; // Normalize delta d = d ? w && (f = w / d) ? d / f : -d / 1.35 : w / 120; // Quadratic scale if |d| > 1 d = d < 1 ? d < -1 ? (-Math.pow(d, 2) - n1) / n : d : (Math.pow(d, 2) + n1) / n; // Delta *should* not be greater than 2... event.delta = Math.min(Math.max(d / 2, -1), 1); event.delta = event.delta * wheelSpeed; dragger.top = Math.max(0, Math.min(parseInt(page.height, 10) - parseInt(dragger.height, 10), parseInt(dragger.top, 10) - event.delta)); redraw(); if (!!event.preventDefault) { event.preventDefault(); } else { return false; } }; var lastOffsetY = 0; var thumbDrag = function (event, offsetX, offsetY) { dragger.top = Math.max(0, Math.min(parseInt(dragger.trackHeight, 10) - parseInt(dragger.height, 10), offsetY)); event.stopPropagation(); }; var dragHandler = function (event) { var newOffsetX = 0; var newOffsetY = event.pageY - thumb[0].scrollTop - lastOffsetY; thumbDrag(event, newOffsetX, newOffsetY); redraw(); }; var _mouseUp = function (event) { win.off('mousemove', dragHandler); win.off('mouseup', _mouseUp); event.stopPropagation(); }; var _touchDragHandler = function (event) { var newOffsetX = 0; var newOffsetY = event.originalEvent.changedTouches[0].pageY - thumb[0].scrollTop - lastOffsetY; thumbDrag(event, newOffsetX, newOffsetY); redraw(); }; var _touchEnd = function (event) { win.off('touchmove', _touchDragHandler); win.off('touchend', _touchEnd); event.stopPropagation(); }; var registerEvent = function (elm) { var wheelEvent = win[0].onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll'; if (hasAddEventListener) { elm.addEventListener(wheelEvent, wheelHandler, false); } else { elm.attachEvent('onmousewheel', wheelHandler); } }; var removeEvent = function (elm) { var wheelEvent = win[0].onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll'; if (hasRemoveEventListener) { elm.removeEventListener(wheelEvent, wheelHandler, false); } else { elm.detachEvent('onmousewheel', wheelHandler); } }; var buildScrollbar = function (rollToBottom) { rollToBottom = flags.bottom || rollToBottom; mainElm = angular.element(element.children()[0]); transculdedContainer = angular.element(mainElm.children()[0]); tools = angular.element(mainElm.children()[1]); thumb = angular.element(angular.element(tools.children()[0]).children()[0]); thumbLine = angular.element(thumb.children()[0]); track = angular.element(angular.element(tools.children()[0]).children()[1]); page.height = element[0].offsetHeight; page.scrollHeight = transculdedContainer[0].scrollHeight; if (page.height < page.scrollHeight) { scope.showYScrollbar = true; scope.$emit('scrollbar.show'); // Calculate the dragger height dragger.height = Math.round(page.height / page.scrollHeight * page.height); dragger.trackHeight = page.height; // update the transcluded content style and clear the parent's calcStyles(); element.css({ overflow: 'hidden' }); mainElm.css(scrollboxStyle); transculdedContainer.css(pageStyle); thumb.css(draggerStyle); thumbLine.css(draggerLineStyle); // Bind scroll bar events track.bind('click', trackClick); // Handle mousewheel registerEvent(transculdedContainer[0]); // Drag the scroller with the mouse thumb.on('mousedown', function (event) { lastOffsetY = event.pageY - thumb[0].offsetTop; win.on('mouseup', _mouseUp); win.on('mousemove', dragHandler); event.preventDefault(); }); // Drag the scroller by touch thumb.on('touchstart', function (event) { lastOffsetY = event.originalEvent.changedTouches[0].pageY - thumb[0].offsetTop; win.on('touchend', _touchEnd); win.on('touchmove', _touchDragHandler); event.preventDefault(); }); if (rollToBottom) { flags.bottom = false; dragger.top = parseInt(page.height, 10) - parseInt(dragger.height, 10); } else { dragger.top = Math.max(0, Math.min(parseInt(page.height, 10) - parseInt(dragger.height, 10), parseInt(dragger.top, 10))); } redraw(); } else { scope.showYScrollbar = false; scope.$emit('scrollbar.hide'); thumb.off('mousedown'); removeEvent(transculdedContainer[0]); transculdedContainer.attr('style', 'position:relative;top:0'); // little hack to remove other inline styles mainElm.css({ height: '100%' }); } }; var rebuildTimer; var rebuild = function (e, data) { /* jshint -W116 */ if (rebuildTimer != null) { clearTimeout(rebuildTimer); } /* jshint +W116 */ var rollToBottom = !!data && !!data.rollToBottom; rebuildTimer = setTimeout(function () { page.height = null; buildScrollbar(rollToBottom); if (!scope.$$phase) { scope.$digest(); } // update parent for flag update if (!scope.$parent.$$phase) { scope.$parent.$digest(); } }, 72); }; buildScrollbar(); if (!!attrs.rebuildOn) { attrs.rebuildOn.split(' ').forEach(function (eventName) { scope.$on(eventName, rebuild); }); } if (attrs.hasOwnProperty('rebuildOnResize')) { win.on('resize', rebuild); } }, template: '<div>' + '<div class="ngsb-wrap">' + '<div class="ngsb-container" ng-transclude></div>' + '<div class="ngsb-scrollbar" style="position: absolute; display: block;" ng-show="showYScrollbar">' + '<div class="ngsb-thumb-container">' + '<div class="ngsb-thumb-pos" oncontextmenu="return false;">' + '<div class="ngsb-thumb" ></div>' + '</div>' + '<div class="ngsb-track"></div>' + '</div>' + '</div>' + '</div>' + '</div>' }; } ]);