├── .travis.yml ├── .gitignore ├── .editorconfig ├── bower.json ├── .jshintrc ├── test ├── .jshintrc └── spec │ └── ng-scrollbar.js ├── LICENSE ├── dist ├── ng-scrollbar.min.css ├── ng-scrollbar.css ├── ng-scrollbar.min.js └── ng-scrollbar.js ├── example └── index.html ├── package.json ├── karma.conf.js ├── CONTRIBUTING.md ├── src ├── ng-scrollbar.less └── ng-scrollbar.js ├── README.md └── Gruntfile.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.8' 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | .idea 4 | .tmp 5 | 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-scrollbar", 3 | "version": "0.0.8", 4 | "dependencies": { 5 | }, 6 | "main": [ 7 | "./dist/ng-scrollbar.js", 8 | "./dist/ng-scrollbar.css" 9 | ], 10 | "devDependencies": { 11 | "angular": "latest", 12 | "angular-mocks": "latest", 13 | "jquery": "latest" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": false, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "globalstrict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "predef": [ 23 | "angular" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": false, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "globalstrict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "predef": [ 23 | "$", 24 | "angular", 25 | "describe", 26 | "beforeEach", 27 | "afterEach", 28 | "inject", 29 | "it", 30 | "expect" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Asaf David 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/spec/ng-scrollbar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Module: ngScrollbar', function () { 4 | var scope, $sandbox, $compile, $timeout; 5 | 6 | // load the controller's module 7 | beforeEach(module('ngScrollbar')); 8 | 9 | beforeEach(inject(function ($injector, $rootScope, _$compile_, _$timeout_) { 10 | scope = $rootScope; 11 | $compile = _$compile_; 12 | $timeout = _$timeout_; 13 | 14 | $sandbox = $('
').appendTo($('body')); 15 | })); 16 | 17 | afterEach(function() { 18 | $sandbox.remove(); 19 | scope.$destroy(); 20 | }); 21 | 22 | var templates = { 23 | 'default': { 24 | scope: { 25 | helloWorld: 'hello world' 26 | }, 27 | element: '
{{ helloWorld }}
' 28 | } 29 | }; 30 | 31 | function compileDirective(template) { 32 | template = template ? templates[template] : templates['default']; 33 | angular.extend(scope, template.scope || templates['default'].scope); 34 | var $element = $(template.element).appendTo($sandbox); 35 | $element = $compile($element)(scope); 36 | scope.$digest(); 37 | return $element; 38 | } 39 | 40 | it('should correctly display hello world', function () { 41 | var elm = compileDirective(); 42 | expect(elm.text()).toBe('hello world'); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /dist/ng-scrollbar.min.css: -------------------------------------------------------------------------------- 1 | .ngsb-wrap{-ms-touch-action:none}.ngsb-wrap .ngsb-container{width:auto;overflow:hidden;transition:.5s all}.ngsb-wrap:hover .ngsb-scrollbar{opacity:1;filter:"alpha(opacity=100)";-ms-filter:"alpha(opacity=100)"}.ngsb-wrap .ngsb-scrollbar{width:16px;height:100%;top:0;right:0;opacity:.75;filter:"alpha(opacity=75)";-ms-filter:"alpha(opacity=75)"}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-container{position:absolute;top:0;left:0;bottom:0;right:0;height:auto}.ngsb-wrap .ngsb-scrollbar a.ngsb-thumb-container{margin:20px 0}.ngsb-wrap .ngsb-scrollbar .ngsb-track{height:100%;margin:0 auto;width:6px;background:#000;background:rgba(0,0,0,.4);-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;filter:"alpha(opacity=40)";-ms-filter:"alpha(opacity=40)";box-shadow:1px 1px 1px rgba(255,255,255,.1)}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos{cursor:pointer;width:100%;height:30px}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos .ngsb-thumb{transition:.5s all;width:4px;height:100%;margin:0 auto;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;text-align:center;background:#fff;background:rgba(255,255,255,.4);filter:"alpha(opacity=40)";-ms-filter:"alpha(opacity=40)"}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos:hover .ngsb-thumb{background:rgba(255,255,255,.5);filter:"alpha(opacity=50)";-ms-filter:"alpha(opacity=50)"}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos:active{background:rgba(255,255,255,.6);filter:"alpha(opacity=60)";-ms-filter:"alpha(opacity=60)"} -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 |
19 |
20 | 21 | 24 | Bar is shown: {{barShown}} 25 |
26 |
27 |

Scroll me down!

28 |

...

29 |

...

30 |

...

31 |

...

32 |

...

33 |

...

34 |

Learn more

35 |
36 |
37 |
38 |
39 | 40 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-scrollbar", 3 | "version": "0.0.8", 4 | "main": "dist/ng-scrollbar.min.js", 5 | "description": "A custom scrollbar written in pure AngularJS", 6 | "keywords": [ 7 | "angular", 8 | "angularjs", 9 | "scrollbar" 10 | ], 11 | "homepage": "https://github.com/asafdav/ng-scrollbar", 12 | "bugs": "https://github.com/asafdav/ng-scrollbar/issues", 13 | "author": { 14 | "name": "Asaf David", 15 | "email": "asafdav@gmail.com", 16 | "url": "http://about.me/asafdavid" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/asafdav/ng-scrollbar.git" 21 | }, 22 | "licenses": [ 23 | { 24 | "type": "MIT" 25 | } 26 | ], 27 | "devDependencies": { 28 | "grunt": "~0.4.1", 29 | "grunt-contrib-jshint": "~0.6.4", 30 | "grunt-contrib-concat": "~0.3.0", 31 | "grunt-contrib-uglify": "~0.2.4", 32 | "grunt-contrib-watch": "~0.5.3", 33 | "grunt-contrib-clean": "~0.5.0", 34 | "grunt-contrib-connect": "~0.5.0", 35 | "grunt-contrib-less": "~0.9.0", 36 | "grunt-contrib-cssmin": "~0.7.0", 37 | "connect-livereload": "~0.3.0", 38 | "grunt-open": "~0.2.2", 39 | "grunt-karma": "~0.6.2", 40 | "grunt-ngmin": "0.0.3", 41 | "grunt-bump": "0.0.11", 42 | "matchdep": "~0.1.2", 43 | "karma-script-launcher": "~0.1.0", 44 | "karma-chrome-launcher": "~0.1.0", 45 | "karma-firefox-launcher": "~0.1.0", 46 | "karma-html2js-preprocessor": "~0.1.0", 47 | "karma-jasmine": "~0.1.3", 48 | "karma-requirejs": "~0.2.0", 49 | "karma-coffee-preprocessor": "~0.1.0", 50 | "karma-phantomjs-launcher": "~0.1.0", 51 | "karma": "~0.10.2" 52 | }, 53 | "dependencies": { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | 3 | 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | // list of files / patterns to load in the browser 11 | files: [ 12 | 'bower_components/jquery/dist/jquery.js', 13 | 'bower_components/angular/angular.js', 14 | 'bower_components/angular-mocks/angular-mocks.js', 15 | 'src/*.js', 16 | 'test/spec/*.js' 17 | ], 18 | 19 | // list of files to exclude 20 | exclude: [], 21 | 22 | // test results reporter to use 23 | // possible values: dots || progress || growl 24 | reporters: ['progress'], 25 | 26 | frameworks: ['jasmine'], 27 | 28 | // web server port 29 | port: 8080, 30 | 31 | // cli runner port 32 | runnerPort: 9100, 33 | 34 | // enable / disable colors in the output (reporters and logs) 35 | colors: true, 36 | 37 | // level of logging 38 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 39 | logLevel: config.LOG_INFO, 40 | 41 | // enable / disable watching file and executing tests whenever any file changes 42 | autoWatch: false, 43 | 44 | // Start these browsers, currently available: 45 | // - Chrome 46 | // - ChromeCanary 47 | // - Firefox 48 | // - Opera 49 | // - Safari (only Mac) 50 | // - PhantomJS 51 | // - IE (only Windows) 52 | browsers: ['Chrome'], 53 | 54 | // If browser does not capture in given timeout [ms], kill it 55 | captureTimeout: 5000, 56 | 57 | // Continuous Integration mode 58 | // if true, it capture browsers, run tests and exit 59 | singleRun: false 60 | 61 | }); 62 | }; -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Important notes 4 | Please don't edit files in the `dist` subdirectory as they are generated via Grunt. You'll find source code in the `src` subdirectory! 5 | 6 | ### Code style 7 | Regarding code style like indentation and whitespace, **follow the conventions you see used in the source already.** 8 | 9 | ### PhantomJS 10 | While Grunt can run the included unit tests via [PhantomJS](http://phantomjs.org/), this shouldn't be considered a substitute for the real thing. Please be sure to test the `test/*.html` unit test file(s) in _actual_ browsers. 11 | 12 | ## Modifying the code 13 | First, ensure that you have the latest [Node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed. 14 | 15 | Test that Grunt's CLI and Bower are installed by running `grunt --version` and `bower --version`. If the commands aren't found, run `npm install -g grunt-cli bower`. For more information about installing the tools, see the [getting started with Grunt guide](http://gruntjs.com/getting-started) or [bower.io](http://bower.io/) respectively. 16 | 17 | 1. Fork and clone the repo. 18 | 1. Run `npm install` to install all build dependencies (including Grunt). 19 | 1. Run `bower install` to install the front-end dependencies. 20 | 1. Run `grunt` to grunt this project. 21 | 22 | Assuming that you don't see any red, you're ready to go. Just be sure to run `grunt` after making any changes, to ensure that nothing is broken. 23 | 24 | ## Submitting pull requests 25 | 26 | 1. Create a new branch, please don't work in your `master` branch directly. 27 | 1. Add failing tests for the change you want to make. Run `grunt` to see the tests fail. 28 | 1. Fix stuff. 29 | 1. Run `grunt` to see if the tests pass. Repeat steps 2-4 until done. 30 | 1. Open `test/*.html` unit test file(s) in actual browser to ensure tests pass everywhere. 31 | 1. Update the documentation to reflect any changes. 32 | 1. Push to your fork and submit a pull request. 33 | -------------------------------------------------------------------------------- /dist/ng-scrollbar.css: -------------------------------------------------------------------------------- 1 | .ngsb-wrap { 2 | -ms-touch-action: none; 3 | } 4 | .ngsb-wrap .ngsb-container { 5 | width: auto; 6 | overflow: hidden; 7 | transition: 0.5s all; 8 | } 9 | .ngsb-wrap:hover .ngsb-scrollbar { 10 | opacity: 1; 11 | filter: "alpha(opacity=100)"; 12 | -ms-filter: "alpha(opacity=100)"; 13 | /* old ie */ 14 | } 15 | .ngsb-wrap .ngsb-scrollbar { 16 | width: 16px; 17 | height: 100%; 18 | top: 0; 19 | right: 0; 20 | opacity: 0.75; 21 | filter: "alpha(opacity=75)"; 22 | -ms-filter: "alpha(opacity=75)"; 23 | /* old ie */ 24 | } 25 | .ngsb-wrap .ngsb-scrollbar .ngsb-thumb-container { 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | bottom: 0; 30 | right: 0; 31 | height: auto; 32 | } 33 | .ngsb-wrap .ngsb-scrollbar a.ngsb-thumb-container { 34 | margin: 20px 0; 35 | } 36 | .ngsb-wrap .ngsb-scrollbar .ngsb-track { 37 | height: 100%; 38 | margin: 0 auto; 39 | width: 6px; 40 | background: #000; 41 | background: rgba(0, 0, 0, 0.4); 42 | -webkit-border-radius: 2px; 43 | -moz-border-radius: 2px; 44 | border-radius: 2px; 45 | filter: "alpha(opacity=40)"; 46 | -ms-filter: "alpha(opacity=40)"; 47 | /* old ie */ 48 | box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.1); 49 | } 50 | .ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos { 51 | cursor: pointer; 52 | width: 100%; 53 | height: 30px; 54 | } 55 | .ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos .ngsb-thumb { 56 | transition: 0.5s all; 57 | width: 4px; 58 | height: 100%; 59 | margin: 0 auto; 60 | -webkit-border-radius: 10px; 61 | -moz-border-radius: 10px; 62 | border-radius: 10px; 63 | text-align: center; 64 | background: #fff; 65 | /* rgba fallback */ 66 | background: rgba(255, 255, 255, 0.4); 67 | filter: "alpha(opacity=40)"; 68 | -ms-filter: "alpha(opacity=40)"; 69 | /* old ie */ 70 | } 71 | .ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos:hover .ngsb-thumb { 72 | background: rgba(255, 255, 255, 0.5); 73 | filter: "alpha(opacity=50)"; 74 | -ms-filter: "alpha(opacity=50)"; 75 | /* old ie */ 76 | } 77 | .ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos:active { 78 | background: rgba(255, 255, 255, 0.6); 79 | filter: "alpha(opacity=60)"; 80 | -ms-filter: "alpha(opacity=60)"; 81 | /* old ie */ 82 | } 83 | -------------------------------------------------------------------------------- /src/ng-scrollbar.less: -------------------------------------------------------------------------------- 1 | .ngsb-wrap { 2 | -ms-touch-action:none; 3 | 4 | .ngsb-container{ 5 | width:auto; 6 | overflow:hidden; 7 | transition: 0.5s all; 8 | } 9 | 10 | &:hover { 11 | .ngsb-scrollbar { 12 | opacity:1; 13 | filter:"alpha(opacity=100)"; -ms-filter:"alpha(opacity=100)"; /* old ie */ 14 | } 15 | } 16 | 17 | .ngsb-scrollbar { 18 | width:16px; 19 | height:100%; 20 | top:0; 21 | right:0; 22 | opacity:0.75; 23 | filter:"alpha(opacity=75)"; -ms-filter:"alpha(opacity=75)"; /* old ie */ 24 | 25 | .ngsb-thumb-container{ 26 | position:absolute; 27 | top:0; 28 | left:0; 29 | bottom:0; 30 | right:0; 31 | height:auto; 32 | } 33 | 34 | a+ { 35 | &.ngsb-thumb-container{ 36 | margin:20px 0; 37 | } 38 | } 39 | 40 | .ngsb-track{ 41 | height:100%; 42 | margin:0 auto; 43 | width:6px; 44 | background: #000; 45 | background: rgba(0, 0, 0, 0.4); 46 | -webkit-border-radius:2px; 47 | -moz-border-radius:2px; 48 | border-radius:2px; 49 | filter:"alpha(opacity=40)"; -ms-filter:"alpha(opacity=40)"; /* old ie */ 50 | box-shadow:1px 1px 1px rgba(255,255,255,0.1); 51 | } 52 | 53 | .ngsb-thumb-pos { 54 | cursor:pointer; 55 | width:100%; 56 | height:30px; 57 | 58 | .ngsb-thumb { 59 | transition: 0.5s all; 60 | width:4px; 61 | height:100%; 62 | margin:0 auto; 63 | -webkit-border-radius:10px; 64 | -moz-border-radius:10px; 65 | border-radius:10px; 66 | text-align:center; 67 | background:#fff; /* rgba fallback */ 68 | background:rgba(255,255,255,0.4); 69 | filter:"alpha(opacity=40)"; 70 | -ms-filter:"alpha(opacity=40)"; /* old ie */ 71 | } 72 | 73 | &:hover { 74 | .ngsb-thumb { 75 | background:rgba(255,255,255,0.5); 76 | filter:"alpha(opacity=50)"; 77 | -ms-filter:"alpha(opacity=50)"; /* old ie */ 78 | } 79 | } 80 | 81 | &:active { 82 | background:rgba(255,255,255,0.6); 83 | filter:"alpha(opacity=60)"; -ms-filter:"alpha(opacity=60)"; /* old ie */ 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng-scrollbar 2 | A custom scrollbar written in pure AngularJS. 3 | 4 | Works with the mouse and on touch screen. 5 | 6 | Tired of using jquery for a stupid scrollbar? well, this directive is just for you. 7 | 8 | 9 | ## Usage 10 | 11 | 1. Add ng-scrollbar.min.js to you main file (index.html) 12 | 13 | you can download this by: 14 | * using bower and running `bower install ng-scrollbar` 15 | * Download the [production version][min] or the [development version][max]. 16 | 17 | [min]: https://raw.github.com/asafdav/ng-scrollbar/master/dist/angular-ng-scrollbar.min.js 18 | [max]: https://raw.github.com/asafdav/ng-scrollbar/master/dist/angular-ng-scrollbar.js 19 | 20 | In your web page: 21 | 22 | ```html 23 | 24 | 25 | 26 | ``` 27 | 28 | 2. Set `ngScrollbar` as a dependency in your module 29 | ```javascript 30 | var myapp = angular.module('myapp', ['ngScrollbar']) 31 | ``` 32 | 33 | 3. Add ng-scrollbar directive to the wanted element, example: 34 | ```html 35 |
....
36 | ``` 37 | 38 | ## Rebuild the scrollbar 39 | In case you need to rebuild the scrollbar, you may tell ng-scrollbar to rebuild it for you by broadcasting an event. 40 | It's useful to use this option when the size or visibility of the container is dynamic and during the link phase the size can't be determined. 41 | ```html 42 |
....
43 | ``` 44 | 45 | ```javascript 46 | // rebuild the scrollbar 47 | $scope.$broadcast('rebuild:me'); 48 | ``` 49 | 50 | In case you need to rebuild the scrollbar on every window's resize, you may use "rebuild-on-resize" option. 51 | ```html 52 |
....
53 | ``` 54 | 55 | In case you need to stick content to bottom (chat or something) use "bottom" option. 56 | ```html 57 |
....
58 | ``` 59 | 60 | ## Events and Flags 61 | On rebuilding the scrollbar you can get notified by 2 events 62 | ```javascript 63 | $scope.$on('scrollbar.show', function(){ 64 | console.log('Scrollbar show'); 65 | }); 66 | $scope.$on('scrollbar.hide', function(){ 67 | console.log('Scrollbar hide'); 68 | }); 69 | ``` 70 | 71 | Or you can use "is-bar-shown" option. It should be read-only 72 | ```html 73 |
....
74 |
Bar shown: {{flag}}
75 | ``` 76 | 77 | ## Examples 78 | See the example in the respository. 79 | [example/index.html](https://htmlpreview.github.io/?https://github.com/asafdav/ng-scrollbar/blob/master/example/index.html) 80 | 81 | 82 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/asafdav/ng-scrollbar/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 83 | 84 | -------------------------------------------------------------------------------- /dist/ng-scrollbar.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ng-scrollbar 3 | * @version v0.0.7 - 2015-06-19 4 | * @link https://github.com/asafdav/ng-scrollbar 5 | * @author Asaf David 6 | * @license MIT License, http://www.opensource.org/licenses/MIT 7 | */ 8 | "use strict";angular.module("ngScrollbar",[]).directive("ngScrollbar",["$parse","$window",function(a,b){return{restrict:"A",replace:!0,transclude:!0,scope:{showYScrollbar:"=?isBarShown"},link:function(a,c,d){var e,g,h,i,j,k,l,m,n,o,p,q={bottom:d.hasOwnProperty("bottom")},r=angular.element(b),s=!!r[0].addEventListener,t=!!r[0].removeEventListener,u={top:0},v={top:0},w=function(){l={position:"relative",overflow:"hidden","max-width":"100%",height:"100%"},v.height&&(l.height=v.height+"px"),m={position:"absolute",height:u.height+"px",top:u.top+"px"},n={position:"relative","line-height":u.height+"px"},o={position:"relative",top:v.top+"px",overflow:"hidden"}},x=function(){i.css("top",u.top+"px");var a=u.top/v.height;v.top=-Math.round(v.scrollHeight*a),g.css("top",v.top+"px")},y=function(a){var b=a.hasOwnProperty("offsetY")?a.offsetY:a.layerY,c=Math.max(0,Math.min(parseInt(u.trackHeight,10)-parseInt(u.height,10),b));u.top=c,x(),a.stopPropagation()},z=function(a){var b=40,c=a,d=c.detail,e=c.wheelDelta,g=225,h=g-1;return d=d?e&&(f=e/d)?d/f:-d/1.35:e/120,d=1>d?-1>d?(-Math.pow(d,2)-h)/g:d:(Math.pow(d,2)+h)/g,a.delta=Math.min(Math.max(d/2,-1),1),a.delta=a.delta*b,u.top=Math.max(0,Math.min(parseInt(v.height,10)-parseInt(u.height,10),parseInt(u.top,10)-a.delta)),x(),a.preventDefault?void a.preventDefault():!1},A=0,B=function(a,b,c){u.top=Math.max(0,Math.min(parseInt(u.trackHeight,10)-parseInt(u.height,10),c)),a.stopPropagation()},C=function(a){var b=0,c=a.pageY-i[0].scrollTop-A;B(a,b,c),x()},D=function(a){r.off("mousemove",C),r.off("mouseup",D),a.stopPropagation()},E=function(a){var b=0,c=a.originalEvent.changedTouches[0].pageY-i[0].scrollTop-A;B(a,b,c),x()},F=function(a){r.off("touchmove",E),r.off("touchend",F),a.stopPropagation()},G=function(a){var b=void 0!==r[0].onmousewheel?"mousewheel":"DOMMouseScroll";s?a.addEventListener(b,z,!1):a.attachEvent("onmousewheel",z)},H=function(a){var b=void 0!==r[0].onmousewheel?"mousewheel":"DOMMouseScroll";t?a.removeEventListener(b,z,!1):a.detachEvent("onmousewheel",z)},I=function(b){b=q.bottom||b,e=angular.element(c.children()[0]),g=angular.element(e.children()[0]),h=angular.element(e.children()[1]),i=angular.element(angular.element(h.children()[0]).children()[0]),j=angular.element(i.children()[0]),k=angular.element(angular.element(h.children()[0]).children()[1]),v.height=c[0].offsetHeight,v.scrollHeight=g[0].scrollHeight,v.height
'}}]); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-01-17 using generator-angular-component 0.2.3 2 | 'use strict'; 3 | 4 | module.exports = function(grunt) { 5 | 6 | // Configurable paths 7 | var yoConfig = { 8 | livereload: 35729, 9 | src: 'src', 10 | dist: 'dist', 11 | name: 'ng-scrollbar' 12 | }; 13 | 14 | // Livereload setup 15 | var lrSnippet = require('connect-livereload')({port: yoConfig.livereload}); 16 | var mountFolder = function (connect, dir) { 17 | return connect.static(require('path').resolve(dir)); 18 | }; 19 | 20 | // Load all grunt tasks 21 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 22 | 23 | // Project configuration 24 | grunt.initConfig({ 25 | pkg: grunt.file.readJSON('package.json'), 26 | yo: yoConfig, 27 | meta: { 28 | banner: '/**\n' + 29 | ' * <%= pkg.name %>\n' + 30 | ' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 31 | ' * @link <%= pkg.homepage %>\n' + 32 | ' * @author <%= pkg.author.name %> <<%= pkg.author.email %>>\n' + 33 | ' * @license MIT License, http://www.opensource.org/licenses/MIT\n' + 34 | ' */\n' 35 | }, 36 | open: { 37 | server: { 38 | path: 'http://localhost:<%= connect.options.port %>' 39 | } 40 | }, 41 | clean: { 42 | dist: { 43 | files: [{ 44 | dot: true, 45 | src: [ 46 | '.tmp', 47 | '<%= yo.dist %>/*', 48 | '!<%= yo.dist %>/.git*' 49 | ] 50 | }] 51 | }, 52 | server: '.tmp' 53 | }, 54 | watch: { 55 | gruntfile: { 56 | files: '<%= jshint.gruntfile.src %>', 57 | tasks: ['jshint:gruntfile'] 58 | }, 59 | less: { 60 | files: ['<%= yo.src %>/{,*/}*.less'], 61 | tasks: ['less:dist'] 62 | }, 63 | app: { 64 | files: [ 65 | '<%= yo.src %>/{,*/}*.html', 66 | '{.tmp,<%= yo.src %>}/{,*/}*.css', 67 | '{.tmp,<%= yo.src %>}/{,*/}*.js' 68 | ], 69 | options: { 70 | livereload: yoConfig.livereload 71 | } 72 | }, 73 | test: { 74 | files: '<%= jshint.test.src %>', 75 | tasks: ['jshint:test', 'qunit'] 76 | } 77 | }, 78 | connect: { 79 | options: { 80 | port: 9000, 81 | hostname: '0.0.0.0' // Change this to '0.0.0.0' to access the server from outside. 82 | }, 83 | livereload: { 84 | options: { 85 | middleware: function (connect) { 86 | return [ 87 | lrSnippet, 88 | mountFolder(connect, '.tmp'), 89 | mountFolder(connect, yoConfig.src) 90 | ]; 91 | } 92 | } 93 | } 94 | }, 95 | less: { 96 | options: { 97 | // dumpLineNumbers: 'all', 98 | paths: ['<%= yo.src %>'] 99 | }, 100 | dist: { 101 | files: { 102 | '<%= yo.dist %>/<%= yo.name %>.css': '<%= yo.src %>/<%= yo.name %>.less' 103 | } 104 | } 105 | }, 106 | cssmin: { 107 | minify: { 108 | expand: true, 109 | cwd: '<%= yo.dist %>', 110 | src: ['*.css', '!*.min.css'], 111 | dest: '<%= yo.dist %>', 112 | ext: '.min.css' 113 | } 114 | }, 115 | jshint: { 116 | gruntfile: { 117 | options: { 118 | jshintrc: '.jshintrc' 119 | }, 120 | src: 'Gruntfile.js' 121 | }, 122 | src: { 123 | options: { 124 | jshintrc: '.jshintrc' 125 | }, 126 | src: ['<%= yo.src %>/{,*/}*.js'] 127 | }, 128 | test: { 129 | options: { 130 | jshintrc: 'test/.jshintrc' 131 | }, 132 | src: ['test/**/*.js'] 133 | } 134 | }, 135 | karma: { 136 | options: { 137 | configFile: 'karma.conf.js', 138 | browsers: ['PhantomJS'] 139 | }, 140 | unit: { 141 | singleRun: true 142 | }, 143 | server: { 144 | autoWatch: true 145 | } 146 | }, 147 | ngmin: { 148 | options: { 149 | banner: '<%= meta.banner %>' 150 | }, 151 | dist: { 152 | src: ['<%= yo.src %>/<%= pkg.name %>.js'], 153 | dest: '<%= yo.dist %>/<%= pkg.name %>.js' 154 | } 155 | // dist: { 156 | // files: { 157 | // '/.js': '/.js' 158 | // } 159 | // } 160 | }, 161 | concat: { 162 | options: { 163 | banner: '<%= meta.banner %>', 164 | stripBanners: true 165 | }, 166 | dist: { 167 | src: ['<%= yo.src %>/<%= pkg.name %>.js'], 168 | dest: '<%= yo.dist %>/<%= pkg.name %>.js' 169 | } 170 | }, 171 | uglify: { 172 | options: { 173 | banner: '<%= meta.banner %>' 174 | }, 175 | dist: { 176 | src: '<%= concat.dist.dest %>', 177 | dest: '<%= yo.dist %>/<%= pkg.name %>.min.js' 178 | } 179 | } 180 | }); 181 | 182 | grunt.registerTask('test', [ 183 | 'jshint', 184 | 'karma:unit' 185 | ]); 186 | 187 | grunt.registerTask('build', [ 188 | 'clean:dist', 189 | 'less:dist', 190 | 'cssmin:minify', 191 | 'ngmin:dist', 192 | 'uglify:dist' 193 | ]); 194 | 195 | grunt.registerTask('release', [ 196 | 'test', 197 | 'bump-only', 198 | 'build', 199 | 'bump-commit' 200 | ]); 201 | 202 | grunt.registerTask('default', ['build']); 203 | 204 | }; 205 | -------------------------------------------------------------------------------- /dist/ng-scrollbar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('ngScrollbar', []).directive('ngScrollbar', [ 3 | '$parse', 4 | '$window', 5 | function ($parse, $window) { 6 | return { 7 | restrict: 'A', 8 | replace: true, 9 | transclude: true, 10 | scope: { 'showYScrollbar': '=?isBarShown' }, 11 | link: function (scope, element, attrs) { 12 | var mainElm, transculdedContainer, tools, thumb, thumbLine, track; 13 | var flags = { bottom: attrs.hasOwnProperty('bottom') }; 14 | var win = angular.element($window); 15 | var hasAddEventListener = !!win[0].addEventListener; 16 | var hasRemoveEventListener = !!win[0].removeEventListener; 17 | // Elements 18 | var dragger = { top: 0 }, page = { top: 0 }; 19 | // Styles 20 | var scrollboxStyle, draggerStyle, draggerLineStyle, pageStyle; 21 | var calcStyles = function () { 22 | scrollboxStyle = { 23 | position: 'relative', 24 | overflow: 'hidden', 25 | 'max-width': '100%', 26 | height: '100%' 27 | }; 28 | if (page.height) { 29 | scrollboxStyle.height = page.height + 'px'; 30 | } 31 | draggerStyle = { 32 | position: 'absolute', 33 | height: dragger.height + 'px', 34 | top: dragger.top + 'px' 35 | }; 36 | draggerLineStyle = { 37 | position: 'relative', 38 | 'line-height': dragger.height + 'px' 39 | }; 40 | pageStyle = { 41 | position: 'relative', 42 | top: page.top + 'px', 43 | overflow: 'hidden' 44 | }; 45 | }; 46 | var redraw = function () { 47 | thumb.css('top', dragger.top + 'px'); 48 | var draggerOffset = dragger.top / page.height; 49 | page.top = -Math.round(page.scrollHeight * draggerOffset); 50 | transculdedContainer.css('top', page.top + 'px'); 51 | }; 52 | var trackClick = function (event) { 53 | var offsetY = event.hasOwnProperty('offsetY') ? event.offsetY : event.layerY; 54 | var newTop = Math.max(0, Math.min(parseInt(dragger.trackHeight, 10) - parseInt(dragger.height, 10), offsetY)); 55 | dragger.top = newTop; 56 | redraw(); 57 | event.stopPropagation(); 58 | }; 59 | var wheelHandler = function (event) { 60 | var wheelSpeed = 40; 61 | // Mousewheel speed normalization approach adopted from 62 | // http://stackoverflow.com/a/13650579/1427418 63 | var o = event, d = o.detail, w = o.wheelDelta, n = 225, n1 = n - 1; 64 | // Normalize delta 65 | d = d ? w && (f = w / d) ? d / f : -d / 1.35 : w / 120; 66 | // Quadratic scale if |d| > 1 67 | d = d < 1 ? d < -1 ? (-Math.pow(d, 2) - n1) / n : d : (Math.pow(d, 2) + n1) / n; 68 | // Delta *should* not be greater than 2... 69 | event.delta = Math.min(Math.max(d / 2, -1), 1); 70 | event.delta = event.delta * wheelSpeed; 71 | dragger.top = Math.max(0, Math.min(parseInt(page.height, 10) - parseInt(dragger.height, 10), parseInt(dragger.top, 10) - event.delta)); 72 | redraw(); 73 | if (!!event.preventDefault) { 74 | event.preventDefault(); 75 | } else { 76 | return false; 77 | } 78 | }; 79 | var lastOffsetY = 0; 80 | var thumbDrag = function (event, offsetX, offsetY) { 81 | dragger.top = Math.max(0, Math.min(parseInt(dragger.trackHeight, 10) - parseInt(dragger.height, 10), offsetY)); 82 | event.stopPropagation(); 83 | }; 84 | var dragHandler = function (event) { 85 | var newOffsetX = 0; 86 | var newOffsetY = event.pageY - thumb[0].scrollTop - lastOffsetY; 87 | thumbDrag(event, newOffsetX, newOffsetY); 88 | redraw(); 89 | }; 90 | var _mouseUp = function (event) { 91 | win.off('mousemove', dragHandler); 92 | win.off('mouseup', _mouseUp); 93 | event.stopPropagation(); 94 | }; 95 | var _touchDragHandler = function (event) { 96 | var newOffsetX = 0; 97 | var newOffsetY = event.originalEvent.changedTouches[0].pageY - thumb[0].scrollTop - lastOffsetY; 98 | thumbDrag(event, newOffsetX, newOffsetY); 99 | redraw(); 100 | }; 101 | var _touchEnd = function (event) { 102 | win.off('touchmove', _touchDragHandler); 103 | win.off('touchend', _touchEnd); 104 | event.stopPropagation(); 105 | }; 106 | var registerEvent = function (elm) { 107 | var wheelEvent = win[0].onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll'; 108 | if (hasAddEventListener) { 109 | elm.addEventListener(wheelEvent, wheelHandler, false); 110 | } else { 111 | elm.attachEvent('onmousewheel', wheelHandler); 112 | } 113 | }; 114 | var removeEvent = function (elm) { 115 | var wheelEvent = win[0].onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll'; 116 | if (hasRemoveEventListener) { 117 | elm.removeEventListener(wheelEvent, wheelHandler, false); 118 | } else { 119 | elm.detachEvent('onmousewheel', wheelHandler); 120 | } 121 | }; 122 | var buildScrollbar = function (rollToBottom) { 123 | rollToBottom = flags.bottom || rollToBottom; 124 | mainElm = angular.element(element.children()[0]); 125 | transculdedContainer = angular.element(mainElm.children()[0]); 126 | tools = angular.element(mainElm.children()[1]); 127 | thumb = angular.element(angular.element(tools.children()[0]).children()[0]); 128 | thumbLine = angular.element(thumb.children()[0]); 129 | track = angular.element(angular.element(tools.children()[0]).children()[1]); 130 | page.height = element[0].offsetHeight; 131 | page.scrollHeight = transculdedContainer[0].scrollHeight; 132 | if (page.height < page.scrollHeight) { 133 | scope.showYScrollbar = true; 134 | scope.$emit('scrollbar.show'); 135 | // Calculate the dragger height 136 | dragger.height = Math.round(page.height / page.scrollHeight * page.height); 137 | dragger.trackHeight = page.height; 138 | // update the transcluded content style and clear the parent's 139 | calcStyles(); 140 | element.css({ overflow: 'hidden' }); 141 | mainElm.css(scrollboxStyle); 142 | transculdedContainer.css(pageStyle); 143 | thumb.css(draggerStyle); 144 | thumbLine.css(draggerLineStyle); 145 | // Bind scroll bar events 146 | track.bind('click', trackClick); 147 | // Handle mousewheel 148 | registerEvent(transculdedContainer[0]); 149 | // Drag the scroller with the mouse 150 | thumb.on('mousedown', function (event) { 151 | lastOffsetY = event.pageY - thumb[0].offsetTop; 152 | win.on('mouseup', _mouseUp); 153 | win.on('mousemove', dragHandler); 154 | event.preventDefault(); 155 | }); 156 | // Drag the scroller by touch 157 | thumb.on('touchstart', function (event) { 158 | lastOffsetY = event.originalEvent.changedTouches[0].pageY - thumb[0].offsetTop; 159 | win.on('touchend', _touchEnd); 160 | win.on('touchmove', _touchDragHandler); 161 | event.preventDefault(); 162 | }); 163 | if (rollToBottom) { 164 | flags.bottom = false; 165 | dragger.top = parseInt(page.height, 10) - parseInt(dragger.height, 10); 166 | } else { 167 | dragger.top = Math.max(0, Math.min(parseInt(page.height, 10) - parseInt(dragger.height, 10), parseInt(dragger.top, 10))); 168 | } 169 | redraw(); 170 | } else { 171 | scope.showYScrollbar = false; 172 | scope.$emit('scrollbar.hide'); 173 | thumb.off('mousedown'); 174 | removeEvent(transculdedContainer[0]); 175 | transculdedContainer.attr('style', 'position:relative;top:0'); 176 | // little hack to remove other inline styles 177 | mainElm.css({ height: '100%' }); 178 | } 179 | }; 180 | var rebuildTimer; 181 | var rebuild = function (e, data) { 182 | /* jshint -W116 */ 183 | if (rebuildTimer != null) { 184 | clearTimeout(rebuildTimer); 185 | } 186 | /* jshint +W116 */ 187 | var rollToBottom = !!data && !!data.rollToBottom; 188 | rebuildTimer = setTimeout(function () { 189 | page.height = null; 190 | buildScrollbar(rollToBottom); 191 | if (!scope.$$phase) { 192 | scope.$digest(); 193 | } 194 | // update parent for flag update 195 | if (!scope.$parent.$$phase) { 196 | scope.$parent.$digest(); 197 | } 198 | }, 72); 199 | }; 200 | buildScrollbar(); 201 | if (!!attrs.rebuildOn) { 202 | attrs.rebuildOn.split(' ').forEach(function (eventName) { 203 | scope.$on(eventName, rebuild); 204 | }); 205 | } 206 | if (attrs.hasOwnProperty('rebuildOnResize')) { 207 | win.on('resize', rebuild); 208 | } 209 | }, 210 | template: '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' 211 | }; 212 | } 213 | ]); -------------------------------------------------------------------------------- /src/ng-scrollbar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('ngScrollbar', []).directive('ngScrollbar', [ 4 | '$parse', 5 | '$window', 6 | function ($parse, $window) { 7 | return { 8 | restrict: 'A', 9 | replace: true, 10 | transclude: true, 11 | scope:{ 12 | 'showYScrollbar': '=?isBarShown' 13 | }, 14 | link: function(scope, element, attrs) { 15 | 16 | var mainElm, transculdedContainer, tools, thumb, thumbLine, track; 17 | 18 | var flags = { 19 | bottom: attrs.hasOwnProperty('bottom') 20 | }; 21 | 22 | var win = angular.element($window); 23 | 24 | var hasAddEventListener = !!win[0].addEventListener; 25 | var hasRemoveEventListener = !!win[0].removeEventListener; 26 | 27 | // Elements 28 | var dragger = { 29 | top: 0 30 | }, page = { 31 | top: 0 32 | }; 33 | 34 | // Styles 35 | var scrollboxStyle, draggerStyle, draggerLineStyle, pageStyle; 36 | 37 | var calcStyles = function () { 38 | scrollboxStyle = { 39 | position: 'relative', 40 | overflow: 'hidden', 41 | 'max-width': '100%', 42 | height: '100%' 43 | }; 44 | 45 | if (page.height) { 46 | scrollboxStyle.height = page.height + 'px'; 47 | } 48 | 49 | draggerStyle = { 50 | position: 'absolute', 51 | height: dragger.height + 'px', 52 | top: dragger.top + 'px' 53 | }; 54 | 55 | draggerLineStyle = { 56 | position: 'relative', 57 | 'line-height': dragger.height + 'px' 58 | }; 59 | 60 | pageStyle = { 61 | position: 'relative', 62 | top: page.top + 'px', 63 | overflow: 'hidden' 64 | }; 65 | }; 66 | 67 | var redraw = function() { 68 | thumb.css('top', dragger.top + 'px'); 69 | var draggerOffset = dragger.top / page.height; 70 | page.top = -Math.round(page.scrollHeight * draggerOffset); 71 | transculdedContainer.css('top', page.top + 'px'); 72 | }; 73 | 74 | var trackClick = function(event) { 75 | var offsetY = event.hasOwnProperty('offsetY') ? event.offsetY : event.layerY; 76 | var newTop = Math.max(0, Math.min(parseInt(dragger.trackHeight, 10) - parseInt(dragger.height, 10), offsetY)); 77 | 78 | dragger.top = newTop; 79 | redraw(); 80 | 81 | event.stopPropagation(); 82 | }; 83 | 84 | var wheelHandler = function(event) { 85 | 86 | var wheelSpeed = 40; 87 | 88 | // Mousewheel speed normalization approach adopted from 89 | // http://stackoverflow.com/a/13650579/1427418 90 | var o = event, d = o.detail, w = o.wheelDelta, n = 225, n1 = n-1; 91 | 92 | // Normalize delta 93 | d = d ? w && (f = w/d) ? d/f : -d/1.35 : w/120; 94 | // Quadratic scale if |d| > 1 95 | d = d < 1 ? d < -1 ? (-Math.pow(d, 2) - n1) / n : d : (Math.pow(d, 2) + n1) / n; 96 | // Delta *should* not be greater than 2... 97 | event.delta = Math.min(Math.max(d / 2, -1), 1); 98 | 99 | event.delta = event.delta * wheelSpeed; 100 | 101 | dragger.top = Math.max(0, Math.min(parseInt(page.height, 10) - parseInt(dragger.height, 10), parseInt(dragger.top, 10) - event.delta)); 102 | redraw(); 103 | 104 | if (!!event.preventDefault) { 105 | event.preventDefault(); 106 | } else { 107 | return false; 108 | } 109 | }; 110 | 111 | var lastOffsetY = 0; 112 | 113 | var thumbDrag = function (event, offsetX, offsetY) { 114 | dragger.top = Math.max(0, Math.min(parseInt(dragger.trackHeight, 10) - parseInt(dragger.height, 10), offsetY)); 115 | event.stopPropagation(); 116 | }; 117 | 118 | var dragHandler = function (event) { 119 | var newOffsetX = 0; 120 | var newOffsetY = event.pageY - thumb[0].scrollTop - lastOffsetY; 121 | thumbDrag(event, newOffsetX, newOffsetY); 122 | redraw(); 123 | }; 124 | 125 | var _mouseUp = function (event) { 126 | win.off('mousemove', dragHandler); 127 | win.off('mouseup', _mouseUp); 128 | event.stopPropagation(); 129 | }; 130 | 131 | var _touchDragHandler = function (event) { 132 | var newOffsetX = 0; 133 | var newOffsetY = event.originalEvent.changedTouches[0].pageY - thumb[0].scrollTop - lastOffsetY; 134 | thumbDrag(event, newOffsetX, newOffsetY); 135 | redraw(); 136 | }; 137 | 138 | var _touchEnd = function (event) { 139 | win.off('touchmove', _touchDragHandler); 140 | win.off('touchend', _touchEnd); 141 | event.stopPropagation(); 142 | }; 143 | 144 | var registerEvent = function(elm) { 145 | var wheelEvent = win[0].onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll'; 146 | if (hasAddEventListener) { 147 | elm.addEventListener(wheelEvent, wheelHandler, false); 148 | } else { 149 | elm.attachEvent('onmousewheel', wheelHandler); 150 | } 151 | }; 152 | 153 | var removeEvent = function(elm) { 154 | var wheelEvent = win[0].onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll'; 155 | if (hasRemoveEventListener) { 156 | elm.removeEventListener(wheelEvent, wheelHandler, false); 157 | } else { 158 | elm.detachEvent('onmousewheel', wheelHandler); 159 | } 160 | }; 161 | 162 | var buildScrollbar = function (rollToBottom) { 163 | 164 | rollToBottom = flags.bottom || rollToBottom; 165 | mainElm = angular.element(element.children()[0]); 166 | transculdedContainer = angular.element(mainElm.children()[0]); 167 | tools = angular.element(mainElm.children()[1]); 168 | thumb = angular.element(angular.element(tools.children()[0]).children()[0]); 169 | thumbLine = angular.element(thumb.children()[0]); 170 | track = angular.element(angular.element(tools.children()[0]).children()[1]); 171 | 172 | page.height = element[0].offsetHeight; 173 | page.scrollHeight = transculdedContainer[0].scrollHeight; 174 | 175 | if (page.height < page.scrollHeight) { 176 | scope.showYScrollbar = true; 177 | scope.$emit('scrollbar.show'); 178 | 179 | // Calculate the dragger height 180 | dragger.height = Math.round(page.height / page.scrollHeight * page.height); 181 | dragger.trackHeight = page.height; 182 | 183 | // update the transcluded content style and clear the parent's 184 | calcStyles(); 185 | element.css({overflow: 'hidden'}); 186 | mainElm.css(scrollboxStyle); 187 | transculdedContainer.css(pageStyle); 188 | thumb.css(draggerStyle); 189 | thumbLine.css(draggerLineStyle); 190 | 191 | // Bind scroll bar events 192 | track.bind('click', trackClick); 193 | 194 | // Handle mousewheel 195 | registerEvent(transculdedContainer[0]); 196 | 197 | // Drag the scroller with the mouse 198 | thumb.on('mousedown', function (event) { 199 | lastOffsetY = event.pageY - thumb[0].offsetTop; 200 | win.on('mouseup', _mouseUp); 201 | win.on('mousemove', dragHandler); 202 | event.preventDefault(); 203 | }); 204 | 205 | // Drag the scroller by touch 206 | thumb.on('touchstart', function (event) { 207 | lastOffsetY = event.originalEvent.changedTouches[0].pageY - thumb[0].offsetTop; 208 | win.on('touchend', _touchEnd); 209 | win.on('touchmove', _touchDragHandler); 210 | event.preventDefault(); 211 | }); 212 | 213 | if (rollToBottom) { 214 | flags.bottom = false; 215 | dragger.top = parseInt(page.height, 10) - parseInt(dragger.height, 10); 216 | } else { 217 | dragger.top = Math.max(0, Math.min(parseInt(page.height, 10) - parseInt(dragger.height, 10), parseInt(dragger.top, 10))); 218 | } 219 | 220 | redraw(); 221 | } else { 222 | scope.showYScrollbar = false; 223 | scope.$emit('scrollbar.hide'); 224 | 225 | thumb.off('mousedown'); 226 | removeEvent(transculdedContainer[0]); 227 | transculdedContainer.attr('style', 'position:relative;top:0'); // little hack to remove other inline styles 228 | mainElm.css({height: '100%'}); 229 | } 230 | }; 231 | 232 | var rebuildTimer; 233 | 234 | var rebuild = function(e, data) { 235 | /* jshint -W116 */ 236 | if (rebuildTimer != null) { 237 | clearTimeout(rebuildTimer); 238 | } 239 | /* jshint +W116 */ 240 | var rollToBottom = !!data && !!data.rollToBottom; 241 | rebuildTimer = setTimeout(function () { 242 | page.height = null; 243 | buildScrollbar(rollToBottom); 244 | if (!scope.$$phase) { 245 | scope.$digest(); 246 | } 247 | // update parent for flag update 248 | if(!scope.$parent.$$phase){ 249 | scope.$parent.$digest(); 250 | } 251 | }, 72); 252 | }; 253 | 254 | buildScrollbar(); 255 | 256 | if (!!attrs.rebuildOn) { 257 | attrs.rebuildOn.split(' ').forEach(function (eventName) { 258 | scope.$on(eventName, rebuild); 259 | }); 260 | } 261 | 262 | if (attrs.hasOwnProperty('rebuildOnResize')) { 263 | win.on('resize', rebuild); 264 | } 265 | }, 266 | template: '
' + 267 | '
' + 268 | '
' + 269 | '
' + 270 | '
' + 271 | '
' + 272 | '
' + 273 | '
' + 274 | '
' + 275 | '
' + 276 | '
' + 277 | '
' + 278 | '
' 279 | }; 280 | } 281 | ]); 282 | --------------------------------------------------------------------------------