');
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 | '
'
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);
--------------------------------------------------------------------------------