├── .npmrc ├── .travis.yml ├── src ├── in-viewport.module.js └── directives │ ├── enter.directive.js │ └── viewport.directive.js ├── CHANGELOG.md ├── .gitignore ├── .jshintrc ├── .editorconfig ├── examples ├── app.js ├── app.css ├── window-viewport.html └── index.html ├── LICENSE ├── package.json ├── gulpfile.js ├── README.md ├── dist ├── in-viewport.min.js └── in-viewport.js └── test ├── karma.conf.js └── spec └── directives ├── enter.directive.spec.js └── viewport.directive.spec.js /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install 6 | - bower install 7 | script: gulp test -------------------------------------------------------------------------------- /src/in-viewport.module.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular 5 | .module('in-viewport', []); 6 | 7 | })(window.angular); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 4 | * Allow to specify `window` as the viewport (thx @compwright) 5 | 6 | ## 1.0.1 7 | * Fix package.json main file 8 | * (Update some build settings) 9 | 10 | ## 1.0.0 11 | * Initial release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor/OS 2 | .DS_Store 3 | .idea 4 | .project 5 | .buildpath 6 | .settings 7 | nbproject 8 | *.sublime-project 9 | *.sublime-workspace 10 | 11 | 12 | # Dev dependencies 13 | node_modules 14 | bower_components 15 | npm-debug.log 16 | 17 | # Test output 18 | test/junit 19 | test/coverage -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | "camelcase": true, 5 | "eqeqeq": true, 6 | "eqnull": true, 7 | "indent": 4, 8 | "latedef": "function", 9 | "newcap": true, 10 | "quotmark": "single", 11 | "trailing": true, 12 | "undef": true, 13 | "unused": true, 14 | "maxlen": 200, 15 | "strict": true, 16 | "predef": ["angular"] 17 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | /** 5 | * Just a test controller 6 | * @param $scope 7 | * @constructor 8 | */ 9 | function TestCtrl($scope) 10 | { 11 | $scope.items = [{}, {}, {}, {}]; 12 | } 13 | 14 | TestCtrl.$inject = ['$scope']; 15 | 16 | // Bootstrap app 17 | angular 18 | .module('app', ['in-viewport']); 19 | 20 | 21 | // Add test controller 22 | angular 23 | .module('app') 24 | .controller('TestCtrl', TestCtrl); 25 | 26 | 27 | })(window.angular); -------------------------------------------------------------------------------- /examples/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | ul { 9 | list-style-type: none; 10 | padding: 0; 11 | } 12 | 13 | .viewport { 14 | width: 500px; 15 | height: 100%; 16 | 17 | margin: 0 auto; 18 | 19 | background: #ffffff; 20 | border: 1px solid #999; 21 | 22 | overflow: auto; 23 | } 24 | 25 | .item { 26 | margin: 20px 20px; 27 | padding: 50px 0; 28 | text-align: center; 29 | 30 | background: blue; 31 | color: #fff; 32 | } 33 | 34 | .in-viewport { 35 | background: limegreen; 36 | } 37 | .not-in-viewport { 38 | background: hotpink; 39 | } -------------------------------------------------------------------------------- /examples/window-viewport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | angular-in-view 8 | 9 | 10 | 11 | 12 | 13 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | angular-in-view 8 | 9 | 10 | 11 | 12 | 13 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2014 Showpad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-in-viewport", 3 | "version": "1.2.0", 4 | "description": "Directives to check if a DOM element is in a specified viewport", 5 | "main": "dist/in-viewport.js", 6 | "scripts": { 7 | "test": "gulp karma" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/showpad/angular-in-viewport.git" 12 | }, 13 | "keywords": [ 14 | "DOM", 15 | "viewport" 16 | ], 17 | "author": "Klaas Cuvelier", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/showpad/angular-in-viewport/issues" 21 | }, 22 | "homepage": "https://github.com/showpad/angularin-viewport", 23 | "devDependencies": { 24 | "angular-mocks": "1.2.32", 25 | "angular-scenario": "1.2.32", 26 | "gulp": "3.9.x", 27 | "gulp-concat": "2.4.2", 28 | "gulp-jshint": "1.6.x", 29 | "gulp-uglify": "1.0.x", 30 | "jasmine-core": "2.4.1", 31 | "jshint-stylish": "0.2.x", 32 | "karma": "0.13.19", 33 | "karma-chrome-launcher": "0.1.x", 34 | "karma-coverage": "0.5.3", 35 | "karma-jasmine": "0.3.6", 36 | "karma-junit-reporter": "0.3.8", 37 | "karma-phantomjs-launcher": "0.2.3", 38 | "phantomjs": "2.1.3" 39 | }, 40 | "dependencies": { 41 | "angular": "1.2.32" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/directives/enter.directive.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular 5 | .module('in-viewport') 6 | .directive('viewportEnter', viewportEnterDefinition); 7 | 8 | /** 9 | * Directive definition for viewport-enter 10 | */ 11 | function viewportEnterDefinition() 12 | { 13 | return { 14 | require: '^viewport', 15 | restrict: 'A', 16 | link: viewportEnterLinker 17 | }; 18 | } 19 | 20 | /** 21 | * Linker method for enter directive 22 | * @param $scope 23 | * @param iElement 24 | * @param iAttrs 25 | * @param viewportController 26 | */ 27 | function viewportEnterLinker($scope, iElement, iAttrs, controller) 28 | { 29 | if (iElement[0].nodeType !== 8 && iAttrs.viewportEnter) { 30 | controller.add('enter', iElement[0], function () { 31 | $scope.$apply(function () { 32 | $scope.$eval(iAttrs.viewportEnter); 33 | }); 34 | 35 | if (iAttrs.viewportLeave && iElement.attr('viewport-leave-registered') !== 'true') { 36 | iElement.attr('viewport-leave-registered', 'true'); 37 | 38 | // Lazy add leave callback 39 | controller.add('leave', iElement[0], function () { 40 | $scope.$apply(function () { 41 | $scope.$eval(iAttrs.viewportLeave); 42 | }); 43 | }); 44 | } 45 | }); 46 | } 47 | } 48 | 49 | })(window.angular); 50 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var karma = require('karma'); 5 | var jshint = require('gulp-jshint'); 6 | var uglify = require('gulp-uglify'); 7 | var concat = require('gulp-concat'); 8 | var stylish = require('jshint-stylish'); 9 | 10 | var config = { 11 | test: __dirname + '/test/karma.conf.js', 12 | src: [ 13 | './src/in-viewport.module.js', 14 | './src/directives/*.js' 15 | ], 16 | dist: './dist/' 17 | }; 18 | 19 | 20 | // Run karma unit tests 21 | gulp.task('karma', function (done) { 22 | var karmaConfig = { 23 | configFile: config.test, 24 | singleRun: true 25 | }; 26 | 27 | 28 | var server = new karma.Server(karmaConfig, karmaCallback); 29 | server.start(); 30 | 31 | function karmaCallback () 32 | { 33 | done(); 34 | } 35 | 36 | }); 37 | 38 | // JSHint the source files 39 | gulp.task('jshint', function () { 40 | return gulp.src(config.src) 41 | .pipe(jshint()) 42 | .pipe(jshint.reporter(stylish)); 43 | }); 44 | 45 | // Copy src to dist 46 | gulp.task('build-regular', ['test'], function () { 47 | return gulp 48 | .src(config.src) 49 | .pipe(concat('in-viewport.js')) 50 | .pipe(gulp.dest(config.dist)); 51 | }); 52 | 53 | // Copy minified src to dist 54 | gulp.task('build-minified', ['test'], function () { 55 | return gulp 56 | .src(config.src) 57 | .pipe(uglify()) 58 | .pipe(concat('in-viewport.min.js')) 59 | .pipe(gulp.dest(config.dist)); 60 | }); 61 | 62 | // Run tests 63 | gulp.task('test', ['jshint', 'karma']); 64 | 65 | // Build 66 | gulp.task('build', ['test', 'build-regular', 'build-minified']); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub version](https://badge.fury.io/gh/showpad%2Fangular-in-viewport.svg)](http://badge.fury.io/gh/showpad%2Fangular-in-viewport) 2 | [![NPM version](https://badge.fury.io/js/angular-in-viewport.svg)](http://badge.fury.io/js/angular-in-viewport) 3 | 4 | [![Build Status](https://travis-ci.org/showpad/angular-in-viewport.svg)](https://travis-ci.org/showpad/angular-in-viewport) 5 | 6 | # Unmaintained 7 | 8 | This library is no longer maintained by Showpad. 9 | Showpad has moved away from AngularJS and will not be publishing any new versions of this library. 10 | 11 | 12 | angular-in-viewport 13 | =================== 14 | 15 | Set of directives handling events when a DOM element enters or leaves a viewport 16 | 17 | ### viewport 18 | Directive (attribute) specifying the DOM element which should be used as viewport. 19 | 20 | To use `window` as the viewport element, set `viewport="window"` on any parent element. 21 | 22 | ### viewport-enter 23 | Directive (attribute) specifying a DOM element which should be watched. When the element enters the viewport the value of the attribute will be evaluated. 24 | 25 | ### viewport-leave 26 | Directive (attribute) specifying a DOM element which should be watched. When the element leaves the viewport the value of the attribute will be evaluated. 27 | The viewport-leave attribute needs a viewport-enter attribute with valid callback 28 | 29 | 30 | #Compatibility 31 | This plugin works with Angular 1.x (v1.2 and higher) 32 | 33 | #Example 34 | 35 | Viewport container element: 36 | 37 | ```HTML 38 | 41 | ``` 42 | 43 | Window viewport: 44 | 45 | ```HTML 46 | 49 | ``` 50 | 51 | # License 52 | This Angular module has been published under the [MIT license](LICENSE) 53 | -------------------------------------------------------------------------------- /dist/in-viewport.min.js: -------------------------------------------------------------------------------- 1 | !function(i){"use strict";i.module("in-viewport",[])}(window.angular); 2 | !function(e){"use strict";function t(){return{require:"^viewport",restrict:"A",link:r}}function r(e,t,r,i){8!==t[0].nodeType&&r.viewportEnter&&i.add("enter",t[0],function(){e.$apply(function(){e.$eval(r.viewportEnter)}),r.viewportLeave&&"true"!==t.attr("viewport-leave-registered")&&(t.attr("viewport-leave-registered","true"),i.add("leave",t[0],function(){e.$apply(function(){e.$eval(r.viewportLeave)})}))})}e.module("in-viewport").directive("viewportEnter",t)}(window.angular); 3 | !function(t){"use strict";function e(t){return{restrict:"A",scope:!0,controller:n,link:i(t)}}function n(e){function n(){var e,o,r;if(f){if(a)return void(w=!0);a=!0,e=f(),t.forEach(p,function(t){o=t.element.getBoundingClientRect(),r=i(o.left,o.top,e)||i(o.right,o.top,e)||i(o.left,o.bottom,e)||i(o.right,o.bottom,e),(null===t.state||t.state!==r)&&(r&&"function"==typeof t.enter?t.enter():r||"function"!=typeof t.leave||t.leave()),t.state=r}),a=!1,w&&(w=!1,n())}}function i(t,e,n){return t>=n.left&&t<=n.right&&e>=n.top&&e<=n.bottom}function o(t){f=t}function r(){return f}function u(){window.clearTimeout(d),d=window.setTimeout(function(){n()},100)}function l(t,e,n){var i;if(-1===["leave","enter"].indexOf(t))throw"invalid event specified";i=s.indexOf(e),-1===i&&(s.push(e),p.push({element:e,state:null,leave:null,enter:null}),i=s.length-1),p[i][t]=n}function c(){return p}var d,f=null,a=!1,w=!1,s=[],p=[];t.element(e).on("resize",u).on("orientationchange",u),this.setViewportFn=o,this.getViewportFn=r,this.add=l,this.getItems=c,this.updateDelayed=u}function i(e){var n=function(n,i,o,r){"window"===o.viewport?(r.setViewportFn(function(){return{top:0,left:0,bottom:window.innerHeight||document.documentElement.clientHeight,right:window.innerWidth||document.documentElement.clientWidth}}),t.element(e).on("scroll",r.updateDelayed)):(r.setViewportFn(function(){return i[0].getBoundingClientRect()}),i.on("scroll",r.updateDelayed)),n.$watch(function(){r.updateDelayed()})};return n.$inject=["$scope","iElement","iAttrs","viewport"],n}t.module("in-viewport").directive("viewport",e),e.$inject=["$window"],n.$inject=["$window"],i.$inject=["$window"]}(window.angular); -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-06-18 using 4 | // generator-karma 0.8.2 5 | 6 | module.exports = function (config) { 7 | config.set({ 8 | // enable / disable watching file and executing tests whenever any file changes 9 | autoWatch: false, 10 | 11 | // base path, that will be used to resolve files and exclude 12 | basePath: '../', 13 | 14 | // testing framework to use (jasmine/mocha/qunit/...) 15 | frameworks: ['jasmine'], 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'node_modules/angular/angular.js', 20 | 'node_modules/angular-mocks/angular-mocks.js', 21 | 22 | 'src/in-viewport.module.js', 23 | 'src/directives/*.js', 24 | 25 | 'test/spec/**/*.js' 26 | ], 27 | 28 | preprocessors: { 29 | 'src/**/*.js': ['coverage'] 30 | }, 31 | 32 | // web server port 33 | port: 8080, 34 | 35 | // Start these browsers, currently available: 36 | // - Chrome 37 | // - ChromeCanary 38 | // - Firefox 39 | // - Opera 40 | // - Safari (only Mac) 41 | // - PhantomJS 42 | // - IE (only Windows) 43 | browsers: [ 44 | 'PhantomJS' 45 | ], 46 | 47 | // Which plugins to enable 48 | plugins: [ 49 | 'karma-phantomjs-launcher', 50 | 'karma-jasmine', 51 | 'karma-coverage', 52 | 'karma-junit-reporter' 53 | ], 54 | 55 | reporters: ['junit', 'dots', 'coverage'], 56 | 57 | junitReporter: { 58 | outputDir: 'test/junit/', 59 | outputFile: 'test-results.xml' 60 | }, 61 | 62 | coverageReporter: { 63 | reporters:[ 64 | { 65 | type: 'cobertura', 66 | dir: 'test/coverage/xml/', 67 | file: 'coverage.xml' 68 | }, 69 | { 70 | type: 'html', 71 | dir: 'test/coverage/html/' 72 | } 73 | ] 74 | }, 75 | 76 | // Continuous Integration mode 77 | // if true, it capture browsers, run tests and exit 78 | singleRun: true, 79 | 80 | colors: false, 81 | 82 | // level of logging 83 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 84 | logLevel: config.LOG_ERROR 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /test/spec/directives/enter.directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('in-viewport: viewport-enter directive', function() { 4 | 5 | var $compile, 6 | $rootScope, 7 | $scope, 8 | 9 | viewportMockController; 10 | 11 | beforeEach(module('in-viewport')); 12 | 13 | beforeEach(inject(function (_$compile_, _$rootScope_, viewportDirective) { 14 | $compile = _$compile_; 15 | $rootScope = _$rootScope_; 16 | $scope = $rootScope.$new(); 17 | 18 | viewportDirective[0].controller = function () { 19 | this.items = []; 20 | this.add = function (event, element, callback) { 21 | this.items.push({ 22 | event: event, 23 | element: element, 24 | callback: callback 25 | }); 26 | }; 27 | this.updateDelayed = function () {}; 28 | this.setViewportFn = function () {}; 29 | 30 | viewportMockController = this; 31 | spyOn(viewportMockController, 'add').and.callThrough(); 32 | }; 33 | })); 34 | 35 | it('should empty attribute', function () { 36 | var element = angular.element('
'), 37 | elementScope; 38 | 39 | expect(function () { 40 | $compile(element)($scope); 41 | elementScope = element.scope(); 42 | $scope.$digest(); 43 | }).not.toThrow(); 44 | 45 | expect(viewportMockController.add.calls.count()).toBe(0); 46 | }); 47 | 48 | it('should require a viewport parent', function () { 49 | var element = angular.element('
'), 50 | elementScope; 51 | 52 | expect(function () { 53 | $compile(element)($scope); 54 | }).toThrow(); 55 | }); 56 | 57 | it('should add a listener to the viewport controller', function () { 58 | var element = angular.element('
'), 59 | elementScope; 60 | 61 | expect(function () { 62 | $compile(element)($scope); 63 | elementScope = element.scope(); 64 | $scope.$digest(); 65 | }).not.toThrow(); 66 | 67 | expect(viewportMockController.add.calls.count()).toBe(1); 68 | }); 69 | 70 | it('should add a listener to the viewport controller', function () { 71 | var element = angular.element('
'), 72 | elementScope; 73 | 74 | $compile(element)($scope); 75 | elementScope = element.scope(); 76 | $scope.$digest(); 77 | 78 | viewportMockController.items[0].callback(); 79 | $scope.$digest(); 80 | 81 | expect(elementScope.visible).toBeTruthy(); 82 | }); 83 | 84 | describe('given the callback is triggered', function() { 85 | var element, elementScope, callback; 86 | 87 | describe('given the element has a leave property', function() { 88 | 89 | describe('given the element\'s leave callback has not been registered yet', function () { 90 | beforeEach(function() { 91 | element = angular.element('
'); 92 | $compile(element)($scope); 93 | elementScope = element.scope(); 94 | $scope.$digest(); 95 | viewportMockController.items[0].callback(); 96 | }); 97 | 98 | 99 | it('should add a listener to the viewport controller', function () { 100 | expect(viewportMockController.add.calls.count()).toBe(2); 101 | expect(viewportMockController.items[1].event).toBe('leave'); 102 | }); 103 | }); 104 | 105 | describe('given the element\'s leave callback has been registered', function() { 106 | beforeEach(function() { 107 | element = angular.element('
'); 108 | $compile(element)($scope); 109 | elementScope = element.scope(); 110 | $scope.$digest(); 111 | viewportMockController.items[0].callback(); 112 | }); 113 | 114 | 115 | it('should not add a listener to the viewport controller', function () { 116 | expect(viewportMockController.add.calls.count()).toBe(1); 117 | }); 118 | }); 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /test/spec/directives/viewport.directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('in-viewport: viewport directive', function() { 4 | 5 | var $compile, 6 | $rootScope, 7 | $scope; 8 | 9 | beforeEach(module('in-viewport')); 10 | 11 | beforeEach(inject(function (_$compile_, _$rootScope_, viewportDirective) { 12 | $compile = _$compile_; 13 | $rootScope = _$rootScope_; 14 | $scope = $rootScope.$new(); 15 | 16 | })); 17 | 18 | function createEvent(name) 19 | { 20 | var event = document.createEvent("HTMLEvents"); 21 | event.initEvent(name, true, true) 22 | event.eventName = name; 23 | return event; 24 | } 25 | 26 | describe('Controller API', function () { 27 | var controller, element, elementScope; 28 | 29 | beforeEach(function () { 30 | element = angular.element('
'); 31 | 32 | $compile(element)($scope); 33 | elementScope = element.scope(); 34 | $scope.$digest(); 35 | controller = element.controller('viewport'); 36 | }); 37 | 38 | describe('setViewportFn/getViewportFn', function () { 39 | it('should set/get the current viewport box function', function () { 40 | var viewport = {}; 41 | controller.setViewportFn(viewport); 42 | expect(controller.getViewportFn()).toEqual(viewport); 43 | }); 44 | }); 45 | 46 | describe('add', function () { 47 | it('should throw an error when an invalid event is specified', function () { 48 | expect(function () { 49 | controller.add('foo', {}, function () {}); 50 | }).toThrow('invalid event specified'); 51 | }); 52 | 53 | it('should add the listeners to the list', function () { 54 | var element = {}, 55 | onEnter = function () {}, 56 | onLeave = function () {}, 57 | items; 58 | 59 | controller.add('enter', element, onEnter); 60 | controller.add('leave', element, onLeave); 61 | 62 | items = controller.getItems(); 63 | 64 | expect(items.length).toBe(1); 65 | expect(items[0].leave).toBe(onLeave); 66 | expect(items[0].enter).toBe(onEnter); 67 | expect(items[0].element).toBe(element); 68 | }); 69 | }); 70 | 71 | describe('updateDelayed', function () { 72 | it('should exist', function () { 73 | expect(controller.updateDelayed).toBeDefined(); 74 | }); 75 | 76 | it('should be called on window resize', function () { 77 | spyOn(window, 'setTimeout'); 78 | window.dispatchEvent(createEvent('resize')); 79 | expect(window.setTimeout.calls.count()).toBe(5); 80 | }); 81 | 82 | it('should be called on window orientationchange', function () { 83 | spyOn(window, 'setTimeout'); 84 | window.dispatchEvent(createEvent('orientationchange')); 85 | expect(window.setTimeout.calls.count()).toBe(6); 86 | }); 87 | 88 | it('should be called on viewport scroll', function () { 89 | spyOn(window, 'setTimeout'); 90 | element[0].dispatchEvent(createEvent('scroll')); 91 | expect(window.setTimeout.calls.count()).toBe(1); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('Update', function () { 97 | 98 | it('should call the on enter callback', function () { 99 | spyOn(window, 'setTimeout').and.callFake(function (callback) { 100 | callback(); 101 | }); 102 | 103 | var element = angular.element( 104 | '
' + 105 | '
' + 106 | '
' 107 | ), elementScope; 108 | 109 | document.body.appendChild(element[0]); 110 | 111 | $compile(element)($scope); 112 | elementScope = element.scope(); 113 | 114 | element[0].scrollTop = 550; 115 | element[0].dispatchEvent(createEvent('scroll')); 116 | expect(elementScope.entered).toBeTruthy(); 117 | }); 118 | 119 | it('should call the on leave callback', function () { 120 | spyOn(window, 'setTimeout').and.callFake(function (callback) { 121 | callback(); 122 | }); 123 | 124 | var element = angular.element( 125 | '
' + 126 | '
' + 127 | '
' + 128 | '
' 129 | ), elementScope; 130 | 131 | document.body.appendChild(element[0]); 132 | 133 | $compile(element)($scope); 134 | elementScope = element.scope(); 135 | 136 | element[0].scrollTop = 550; 137 | element[0].dispatchEvent(createEvent('scroll')); 138 | 139 | element[0].scrollTop = 0; 140 | element[0].dispatchEvent(createEvent('scroll')); 141 | expect(elementScope.leftIt).toBeTruthy(); 142 | }); 143 | 144 | }); 145 | 146 | 147 | 148 | }); 149 | -------------------------------------------------------------------------------- /src/directives/viewport.directive.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular 5 | .module('in-viewport') 6 | .directive('viewport', ViewportDefinition); 7 | 8 | /** 9 | * Directive Definition for Viewport 10 | */ 11 | function ViewportDefinition($window) 12 | { 13 | return { 14 | restrict: 'A', 15 | scope: true, 16 | controller: ViewportController, 17 | link: viewportLinking($window) 18 | }; 19 | } 20 | 21 | ViewportDefinition.$inject = ['$window']; 22 | 23 | /** 24 | * Controller for viewport directive 25 | * @constructor 26 | */ 27 | function ViewportController($window) 28 | { 29 | var viewportFn = null, 30 | isUpdating = false, 31 | updateAgain = false, 32 | elements = [], // keep elements in array for quick lookup 33 | items = [], 34 | updateTimeout; 35 | 36 | function update () 37 | { 38 | var viewportRect, 39 | elementRect, 40 | inViewport; 41 | 42 | if (!viewportFn) { 43 | return; 44 | } 45 | 46 | if (isUpdating) { 47 | updateAgain = true; 48 | return; 49 | } 50 | 51 | isUpdating = true; 52 | 53 | viewportRect = viewportFn(); 54 | 55 | angular.forEach(items, function (item) { 56 | elementRect = item.element.getBoundingClientRect(); 57 | 58 | inViewport = 59 | pointIsInsideBounds(elementRect.left, elementRect.top, viewportRect) || 60 | pointIsInsideBounds(elementRect.right, elementRect.top, viewportRect) || 61 | pointIsInsideBounds(elementRect.left, elementRect.bottom, viewportRect) || 62 | pointIsInsideBounds(elementRect.right, elementRect.bottom, viewportRect); 63 | 64 | // On first check and on change 65 | if (item.state === null || item.state !== inViewport) { 66 | if (inViewport && typeof item.enter === 'function') { 67 | item.enter(); 68 | } else if (!inViewport && typeof item.leave === 'function') { 69 | item.leave(); 70 | } 71 | } 72 | 73 | item.state = inViewport; 74 | }); 75 | 76 | isUpdating = false; 77 | 78 | if (updateAgain) { 79 | updateAgain = false; 80 | update(); 81 | } 82 | } 83 | 84 | /** 85 | * Check if a point is inside specified bounds 86 | * @param x 87 | * @param y 88 | * @param bounds 89 | * @returns {boolean} 90 | */ 91 | function pointIsInsideBounds(x, y, bounds) 92 | { 93 | return x >= bounds.left && x <= bounds.right && y >= bounds.top && y <= bounds.bottom; 94 | } 95 | 96 | /** 97 | * Set the viewport box function 98 | * @param function 99 | */ 100 | function setViewportFn(fn) 101 | { 102 | viewportFn = fn; 103 | } 104 | 105 | /** 106 | * Return the current viewport box function 107 | * @returns {*} 108 | */ 109 | function getViewportFn() 110 | { 111 | return viewportFn; 112 | } 113 | 114 | /** 115 | * trigger an update 116 | */ 117 | function updateDelayed() 118 | { 119 | window.clearTimeout(updateTimeout); 120 | updateTimeout = window.setTimeout(function () { 121 | update(); 122 | }, 100); 123 | } 124 | 125 | /** 126 | * Add listener for event 127 | * @param event 128 | * @param element 129 | * @param callback 130 | */ 131 | function add (event, element, callback) 132 | { 133 | var index; 134 | 135 | if (['leave', 'enter'].indexOf(event) === -1) { 136 | throw 'invalid event specified'; 137 | } 138 | 139 | index = elements.indexOf(element); 140 | 141 | if (index === -1) { 142 | elements.push(element); 143 | items.push({ 144 | element: element, 145 | state: null, 146 | leave: null, 147 | enter: null 148 | }); 149 | 150 | index = elements.length - 1; 151 | } 152 | 153 | items[index][event] = callback; 154 | } 155 | 156 | /** 157 | * Get list of items 158 | * @returns {Array} 159 | */ 160 | function getItems() 161 | { 162 | return items; 163 | } 164 | 165 | angular.element($window) 166 | .on('resize', updateDelayed) 167 | .on('orientationchange', updateDelayed); 168 | 169 | this.setViewportFn = setViewportFn; 170 | this.getViewportFn = getViewportFn; 171 | this.add = add; 172 | this.getItems = getItems; 173 | this.updateDelayed = updateDelayed; 174 | } 175 | 176 | // DI for controller 177 | ViewportController.$inject = ['$window']; 178 | 179 | /** 180 | * Linking method for viewport directive 181 | * @param $scope 182 | * @param iElement 183 | * @param controllers 184 | * @param controllers 185 | * @param $timeout 186 | * @constructor 187 | */ 188 | function viewportLinking($window) 189 | { 190 | var linkFn = function($scope, iElement, iAttrs, $ctrl) { 191 | if (iAttrs.viewport === 'window') { 192 | $ctrl.setViewportFn(function() { 193 | return { 194 | top: 0, 195 | left: 0, 196 | bottom: window.innerHeight || document.documentElement.clientHeight, 197 | right: window.innerWidth || document.documentElement.clientWidth 198 | }; 199 | }); 200 | angular.element($window).on('scroll', $ctrl.updateDelayed); 201 | } else { 202 | $ctrl.setViewportFn(function() { 203 | return iElement[0].getBoundingClientRect(); 204 | }); 205 | iElement.on('scroll', $ctrl.updateDelayed); 206 | } 207 | 208 | // Trick angular in calling this on digest 209 | $scope.$watch(function () { 210 | $ctrl.updateDelayed(); 211 | }); 212 | }; 213 | 214 | linkFn.$inject = ['$scope', 'iElement', 'iAttrs', 'viewport']; 215 | 216 | return linkFn; 217 | } 218 | 219 | viewportLinking.$inject = ['$window']; 220 | 221 | })(window.angular); -------------------------------------------------------------------------------- /dist/in-viewport.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular 5 | .module('in-viewport', []); 6 | 7 | })(window.angular); 8 | (function (angular) { 9 | 'use strict'; 10 | 11 | angular 12 | .module('in-viewport') 13 | .directive('viewportEnter', viewportEnterDefinition); 14 | 15 | /** 16 | * Directive definition for viewport-enter 17 | */ 18 | function viewportEnterDefinition() 19 | { 20 | return { 21 | require: '^viewport', 22 | restrict: 'A', 23 | link: viewportEnterLinker 24 | }; 25 | } 26 | 27 | /** 28 | * Linker method for enter directive 29 | * @param $scope 30 | * @param iElement 31 | * @param iAttrs 32 | * @param viewportController 33 | */ 34 | function viewportEnterLinker($scope, iElement, iAttrs, controller) 35 | { 36 | if (iElement[0].nodeType !== 8 && iAttrs.viewportEnter) { 37 | controller.add('enter', iElement[0], function () { 38 | $scope.$apply(function () { 39 | $scope.$eval(iAttrs.viewportEnter); 40 | }); 41 | 42 | if (iAttrs.viewportLeave && iElement.attr('viewport-leave-registered') !== 'true') { 43 | iElement.attr('viewport-leave-registered', 'true'); 44 | 45 | // Lazy add leave callback 46 | controller.add('leave', iElement[0], function () { 47 | $scope.$apply(function () { 48 | $scope.$eval(iAttrs.viewportLeave); 49 | }); 50 | }); 51 | } 52 | }); 53 | } 54 | } 55 | 56 | })(window.angular); 57 | 58 | (function (angular) { 59 | 'use strict'; 60 | 61 | angular 62 | .module('in-viewport') 63 | .directive('viewport', ViewportDefinition); 64 | 65 | /** 66 | * Directive Definition for Viewport 67 | */ 68 | function ViewportDefinition($window) 69 | { 70 | return { 71 | restrict: 'A', 72 | scope: true, 73 | controller: ViewportController, 74 | link: viewportLinking($window) 75 | }; 76 | } 77 | 78 | ViewportDefinition.$inject = ['$window']; 79 | 80 | /** 81 | * Controller for viewport directive 82 | * @constructor 83 | */ 84 | function ViewportController($window) 85 | { 86 | var viewportFn = null, 87 | isUpdating = false, 88 | updateAgain = false, 89 | elements = [], // keep elements in array for quick lookup 90 | items = [], 91 | updateTimeout; 92 | 93 | function update () 94 | { 95 | var viewportRect, 96 | elementRect, 97 | inViewport; 98 | 99 | if (!viewportFn) { 100 | return; 101 | } 102 | 103 | if (isUpdating) { 104 | updateAgain = true; 105 | return; 106 | } 107 | 108 | isUpdating = true; 109 | 110 | viewportRect = viewportFn(); 111 | 112 | angular.forEach(items, function (item) { 113 | elementRect = item.element.getBoundingClientRect(); 114 | 115 | inViewport = 116 | pointIsInsideBounds(elementRect.left, elementRect.top, viewportRect) || 117 | pointIsInsideBounds(elementRect.right, elementRect.top, viewportRect) || 118 | pointIsInsideBounds(elementRect.left, elementRect.bottom, viewportRect) || 119 | pointIsInsideBounds(elementRect.right, elementRect.bottom, viewportRect); 120 | 121 | // On first check and on change 122 | if (item.state === null || item.state !== inViewport) { 123 | if (inViewport && typeof item.enter === 'function') { 124 | item.enter(); 125 | } else if (!inViewport && typeof item.leave === 'function') { 126 | item.leave(); 127 | } 128 | } 129 | 130 | item.state = inViewport; 131 | }); 132 | 133 | isUpdating = false; 134 | 135 | if (updateAgain) { 136 | updateAgain = false; 137 | update(); 138 | } 139 | } 140 | 141 | /** 142 | * Check if a point is inside specified bounds 143 | * @param x 144 | * @param y 145 | * @param bounds 146 | * @returns {boolean} 147 | */ 148 | function pointIsInsideBounds(x, y, bounds) 149 | { 150 | return x >= bounds.left && x <= bounds.right && y >= bounds.top && y <= bounds.bottom; 151 | } 152 | 153 | /** 154 | * Set the viewport box function 155 | * @param function 156 | */ 157 | function setViewportFn(fn) 158 | { 159 | viewportFn = fn; 160 | } 161 | 162 | /** 163 | * Return the current viewport box function 164 | * @returns {*} 165 | */ 166 | function getViewportFn() 167 | { 168 | return viewportFn; 169 | } 170 | 171 | /** 172 | * trigger an update 173 | */ 174 | function updateDelayed() 175 | { 176 | window.clearTimeout(updateTimeout); 177 | updateTimeout = window.setTimeout(function () { 178 | update(); 179 | }, 100); 180 | } 181 | 182 | /** 183 | * Add listener for event 184 | * @param event 185 | * @param element 186 | * @param callback 187 | */ 188 | function add (event, element, callback) 189 | { 190 | var index; 191 | 192 | if (['leave', 'enter'].indexOf(event) === -1) { 193 | throw 'invalid event specified'; 194 | } 195 | 196 | index = elements.indexOf(element); 197 | 198 | if (index === -1) { 199 | elements.push(element); 200 | items.push({ 201 | element: element, 202 | state: null, 203 | leave: null, 204 | enter: null 205 | }); 206 | 207 | index = elements.length - 1; 208 | } 209 | 210 | items[index][event] = callback; 211 | } 212 | 213 | /** 214 | * Get list of items 215 | * @returns {Array} 216 | */ 217 | function getItems() 218 | { 219 | return items; 220 | } 221 | 222 | angular.element($window) 223 | .on('resize', updateDelayed) 224 | .on('orientationchange', updateDelayed); 225 | 226 | this.setViewportFn = setViewportFn; 227 | this.getViewportFn = getViewportFn; 228 | this.add = add; 229 | this.getItems = getItems; 230 | this.updateDelayed = updateDelayed; 231 | } 232 | 233 | // DI for controller 234 | ViewportController.$inject = ['$window']; 235 | 236 | /** 237 | * Linking method for viewport directive 238 | * @param $scope 239 | * @param iElement 240 | * @param controllers 241 | * @param controllers 242 | * @param $timeout 243 | * @constructor 244 | */ 245 | function viewportLinking($window) 246 | { 247 | var linkFn = function($scope, iElement, iAttrs, $ctrl) { 248 | if (iAttrs.viewport === 'window') { 249 | $ctrl.setViewportFn(function() { 250 | return { 251 | top: 0, 252 | left: 0, 253 | bottom: window.innerHeight || document.documentElement.clientHeight, 254 | right: window.innerWidth || document.documentElement.clientWidth 255 | }; 256 | }); 257 | angular.element($window).on('scroll', $ctrl.updateDelayed); 258 | } else { 259 | $ctrl.setViewportFn(function() { 260 | return iElement[0].getBoundingClientRect(); 261 | }); 262 | iElement.on('scroll', $ctrl.updateDelayed); 263 | } 264 | 265 | // Trick angular in calling this on digest 266 | $scope.$watch(function () { 267 | $ctrl.updateDelayed(); 268 | }); 269 | }; 270 | 271 | linkFn.$inject = ['$scope', 'iElement', 'iAttrs', 'viewport']; 272 | 273 | return linkFn; 274 | } 275 | 276 | viewportLinking.$inject = ['$window']; 277 | 278 | })(window.angular); --------------------------------------------------------------------------------