├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── angular-inview.js
├── angular-inview.spec.js
├── bower.json
├── examples
├── basic.html
├── container.html
├── fixed.html
└── throttle.html
├── karma.conf.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components/
2 | node_modules/
3 | libpeerconnection.log
4 | npm-debug.log
5 | .idea
6 | docs
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | docs
2 | examples
3 | angular-inview.spec.js
4 | bower.json
5 | karma.conf.js
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Nicola Peduzzi
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # InView Directive for AngularJS [](https://circleci.com/gh/thenikso/angular-inview)
2 |
3 | Check if a DOM element is or not in the browser current visible viewport.
4 |
5 | ```html
6 |
7 | ```
8 |
9 | **This is a directive for AngularJS 1, support for Angular 2 is not in the works yet (PRs are welcome!)**
10 |
11 | > Version 2 of this directive uses a lightwight embedded reactive framework and
12 | it is a complete rewrite of v1
13 |
14 | ## Installation
15 |
16 | ### With npm
17 |
18 | ```
19 | npm install angular-inview
20 | ```
21 |
22 | ### With bower
23 |
24 | ```
25 | bower install angular-inview
26 | ```
27 |
28 | ## Setup
29 |
30 | In your document include this scripts:
31 |
32 | ```html
33 |
34 |
35 | ```
36 |
37 | In your AngularJS app, you'll need to import the `angular-inview` module:
38 |
39 | ```javascript
40 | angular.module('myModule', ['angular-inview']);
41 | ```
42 |
43 | Or with a module loader setup like Webpack/Babel you can do:
44 |
45 | ```javascript
46 | import angularInview from 'angular-inview';
47 |
48 | angular.module('myModule', [angularInview]);
49 | ```
50 |
51 | ## Usage
52 |
53 | This module will define two directives: `in-view` and `in-view-container`.
54 |
55 | ### InView
56 |
57 | ```html
58 |
59 | ```
60 |
61 | The `in-view` attribute must contain a valid [AngularJS expression](http://docs.angularjs.org/guide/expression)
62 | to work. When the DOM element enters or exits the viewport, the expression will
63 | be evaluated. To actually check if the element is in view, the following data is
64 | available in the expression:
65 |
66 | - `$inview` is a boolean value indicating if the DOM element is in view.
67 | If using this directive for infinite scrolling, you may want to use this like
68 | ``.
69 |
70 | - `$inviewInfo` is an object containing extra info regarding the event
71 |
72 | ```
73 | {
74 | changed: ,
75 | event: ,
76 | element: ,
77 | elementRect: {
78 | top: ,
79 | left: ,
80 | bottom: ,
81 | right: ,
82 | },
83 | viewportRect: {
84 | top: ,
85 | left: ,
86 | bottom: ,
87 | right: ,
88 | },
89 | direction: { // if generateDirection option is true
90 | vertical: ,
91 | horizontal: ,
92 | },
93 | parts: { // if generateParts option is true
94 | top: ,
95 | left: ,
96 | bottom: ,
97 | right: ,
98 | },
99 | }
100 | ```
101 |
102 | - `changed` indicates if the inview value changed with this event
103 | - `event` the DOM event that triggered the inview check
104 | - `element` the DOM element subject of the inview check
105 | - `elementRect` a rectangle with the virtual (considering offset) position of
106 | the element used for the inview check
107 | - `viewportRect` a rectangle with the virtual (considering offset) viewport
108 | dimensions used for the inview check
109 | - `direction` an indication of how the element has moved from the last event
110 | relative to the viewport. Ie. if you scoll the page down by 100 pixels, the
111 | value of `direction.vertical` will be `-100`
112 | - `parts` an indication of which side of the element are fully visible. Ie. if
113 | `parts.top=false` and `parts.bottom=true` it means that the bottom part of
114 | the element is visible at the top of the viewport (but its top part is
115 | hidden behind the browser bar)
116 |
117 | An additional attribute `in-view-options` can be specified with an object value containing:
118 |
119 | - `offset`: An expression returning an array of values to offset the element position.
120 |
121 | Offsets are expressed as arrays of 4 values `[top, right, bottom, left]`.
122 | Like CSS, you can also specify only 2 values `[top/bottom, left/right]`.
123 |
124 | Values can be either a string with a percentage or numbers (in pixel).
125 | Positive values are offsets outside the element rectangle and
126 | negative values are offsets to the inside.
127 |
128 | Example valid values for the offset are: `100`, `[200, 0]`,
129 | `[100, 0, 200, 50]`, `'20%'`, `['50%', 30]`
130 |
131 | - `viewportOffset`: Like the element offset but appied to the viewport. You may
132 | want to use this to shrink the virtual viewport effectivelly checking if your
133 | element is visible (i.e.) in the bottom part of the screen `['-50%', 0, 0]`.
134 |
135 | - `generateDirection`: Indicate if the `direction` information should
136 | be included in `$inviewInfo` (default false).
137 |
138 | - `generateParts`: Indicate if the `parts` information should
139 | be included in `$inviewInfo` (default false).
140 |
141 | - `throttle`: a number indicating a millisecond value of throttle which will
142 | limit the in-view event firing rate to happen every that many milliseconds
143 |
144 | ### InViewContainer
145 |
146 | Use `in-view-container` when you have a scrollable container that contains `in-view`
147 | elements. When an `in-view` element is inside such container, it will properly
148 | trigger callbacks when the container scrolls as well as when the window scrolls.
149 |
150 | ```html
151 |
154 | ```
155 |
156 | ## Examples
157 |
158 | The following triggers the `lineInView` when the line comes in view:
159 |
160 | ```html
161 | This is test line #{{$index}}
162 | ```
163 |
164 | **See more examples in the [`examples` folder](./examples).**
165 |
166 | ## Migrate from v1
167 |
168 | Version 1 of this directive can still be installed with
169 | `npm install angular-inview@1.5.7`. If you already have v1 and want to
170 | upgrade to v2 here are some tips:
171 |
172 | - `throttle` option replaces `debounce`. You can just change the name. Notice that
173 | the functioning has changed as well, a debounce waits until there are no more
174 | events for the given amount of time before triggering; throttle instead stabilizes
175 | the event triggering only once every amount of time. In practival terms this
176 | should not affect negativelly your app.
177 | - `offset` and `viewportOffset` replace the old offset options in a more structured
178 | and flexible way. `offsetTop: 100` becomes `offset: [100, 0, 0, 0]`.
179 | - `$inviewInfo.event` replaces `$event` in the expression.
180 | - `generateParts` in the options has now to be set to `true` to have
181 | `$inviewInfo.parts` available.
182 |
183 | ## Contribute
184 |
185 | 1. Fork this repo
186 | 2. Setup your new repo with `npm install` and `npm install angular`
187 | 3. Edit `angular-inview.js` and `angular-inview.spec.js` to add your feature
188 | 4. Run `npm test` to check that all is good
189 | 5. Create a [PR](https://github.com/thenikso/angular-inview/pulls)
190 |
191 | If you want to become a contributor with push access open an issue asking that
192 | or contact the author directly.
193 |
--------------------------------------------------------------------------------
/angular-inview.js:
--------------------------------------------------------------------------------
1 | // # Angular-Inview
2 | // - Author: [Nicola Peduzzi](https://github.com/thenikso)
3 | // - Repository: https://github.com/thenikso/angular-inview
4 | // - Install with: `npm install angular-inview`
5 | // - Version: **3.0.0**
6 | (function() {
7 | 'use strict';
8 |
9 | // An [angular.js](https://angularjs.org) directive to evaluate an expression if
10 | // a DOM element is or not in the current visible browser viewport.
11 | // Use it in your AngularJS app by including the javascript and requireing it:
12 | //
13 | // `angular.module('myApp', ['angular-inview'])`
14 | var moduleName = 'angular-inview';
15 | angular.module(moduleName, [])
16 |
17 | // ## in-view directive
18 | //
19 | // ### Usage
20 | // ```html
21 | //
22 | // ```
23 | .directive('inView', ['$parse', inViewDirective])
24 |
25 | // ## in-view-container directive
26 | .directive('inViewContainer', inViewContainerDirective);
27 |
28 | // ## Implementation
29 | function inViewDirective ($parse) {
30 | return {
31 | // Evaluate the expression passed to the attribute `in-view` when the DOM
32 | // element is visible in the viewport.
33 | restrict: 'A',
34 | require: '?^^inViewContainer',
35 | link: function inViewDirectiveLink (scope, element, attrs, container) {
36 | // in-view-options attribute can be specified with an object expression
37 | // containing:
38 | // - `offset`: An array of values to offset the element position.
39 | // Offsets are expressed as arrays of 4 numbers [top, right, bottom, left].
40 | // Like CSS, you can also specify only 2 numbers [top/bottom, left/right].
41 | // Instead of numbers, some array elements can be a string with a percentage.
42 | // Positive numbers are offsets outside the element rectangle and
43 | // negative numbers are offsets to the inside.
44 | // - `viewportOffset`: Like the element offset but appied to the viewport.
45 | // - `generateDirection`: Indicate if the `direction` information should
46 | // be included in `$inviewInfo` (default false).
47 | // - `generateParts`: Indicate if the `parts` information should
48 | // be included in `$inviewInfo` (default false).
49 | // - `throttle`: Specify a number of milliseconds by which to limit the
50 | // number of incoming events.
51 | var options = {};
52 | if (attrs.inViewOptions) {
53 | options = scope.$eval(attrs.inViewOptions);
54 | }
55 | if (options.offset) {
56 | options.offset = normalizeOffset(options.offset);
57 | }
58 | if (options.viewportOffset) {
59 | options.viewportOffset = normalizeOffset(options.viewportOffset);
60 | }
61 |
62 | // Build reactive chain from an initial event
63 | var viewportEventSignal = signalSingle({ type: 'initial' })
64 |
65 | // Merged with the window events
66 | .merge(signalFromEvent(window, 'checkInView click ready wheel mousewheel DomMouseScroll MozMousePixelScroll resize scroll touchmove mouseup keydown'))
67 |
68 | // Merge with container's events signal
69 | if (container) {
70 | viewportEventSignal = viewportEventSignal.merge(container.eventsSignal);
71 | }
72 |
73 | // Throttle if option specified
74 | if (options.throttle) {
75 | viewportEventSignal = viewportEventSignal.throttle(options.throttle);
76 | }
77 |
78 | // Map to viewport intersection and in-view informations
79 | var inviewInfoSignal = viewportEventSignal
80 |
81 | // Inview information structure contains:
82 | // - `inView`: a boolean value indicating if the element is
83 | // visible in the viewport;
84 | // - `changed`: a boolean value indicating if the inview status
85 | // changed after the last event;
86 | // - `event`: the event that initiated the in-view check;
87 | .map(function(event) {
88 | var viewportRect;
89 | if (container) {
90 | viewportRect = container.getViewportRect();
91 | // TODO merge with actual window!
92 | } else {
93 | viewportRect = getViewportRect();
94 | }
95 | viewportRect = offsetRect(viewportRect, options.viewportOffset);
96 | var elementRect = offsetRect(element[0].getBoundingClientRect(), options.offset);
97 | var isVisible = !!(element[0].offsetWidth || element[0].offsetHeight || element[0].getClientRects().length);
98 | var info = {
99 | inView: isVisible && intersectRect(elementRect, viewportRect),
100 | event: event,
101 | element: element,
102 | elementRect: elementRect,
103 | viewportRect: viewportRect
104 | };
105 | // Add inview parts
106 | if (options.generateParts && info.inView) {
107 | info.parts = {};
108 | info.parts.top = elementRect.top >= viewportRect.top;
109 | info.parts.left = elementRect.left >= viewportRect.left;
110 | info.parts.bottom = elementRect.bottom <= viewportRect.bottom;
111 | info.parts.right = elementRect.right <= viewportRect.right;
112 | }
113 | return info;
114 | })
115 |
116 | // Add the changed information to the inview structure.
117 | .scan({}, function (lastInfo, newInfo) {
118 | // Add inview direction info
119 | if (options.generateDirection && newInfo.inView && lastInfo.elementRect) {
120 | newInfo.direction = {
121 | horizontal: newInfo.elementRect.left - lastInfo.elementRect.left,
122 | vertical: newInfo.elementRect.top - lastInfo.elementRect.top
123 | };
124 | }
125 | // Calculate changed flag
126 | newInfo.changed =
127 | newInfo.inView !== lastInfo.inView ||
128 | !angular.equals(newInfo.parts, lastInfo.parts) ||
129 | !angular.equals(newInfo.direction, lastInfo.direction);
130 | return newInfo;
131 | })
132 |
133 | // Filters only informations that should be forwarded to the callback
134 | .filter(function (info) {
135 | // Don't forward if no relevant infomation changed
136 | if (!info.changed) {
137 | return false;
138 | }
139 | // Don't forward if not initially in-view
140 | if (info.event.type === 'initial' && !info.inView) {
141 | return false;
142 | }
143 | return true;
144 | });
145 |
146 | // Execute in-view callback
147 | var inViewExpression = $parse(attrs.inView);
148 | var dispose = inviewInfoSignal.subscribe(function (info) {
149 | scope.$applyAsync(function () {
150 | inViewExpression(scope, {
151 | '$inview': info.inView,
152 | '$inviewInfo': info
153 | });
154 | });
155 | });
156 |
157 | // Dispose of reactive chain
158 | scope.$on('$destroy', dispose);
159 | }
160 | }
161 | }
162 |
163 | function inViewContainerDirective () {
164 | return {
165 | restrict: 'A',
166 | controller: ['$element', function ($element) {
167 | this.element = $element;
168 | this.eventsSignal = signalFromEvent($element, 'scroll');
169 | this.getViewportRect = function () {
170 | return $element[0].getBoundingClientRect();
171 | };
172 | }]
173 | }
174 | }
175 |
176 | // ## Utilities
177 |
178 | function getViewportRect () {
179 | var result = {
180 | top: 0,
181 | left: 0,
182 | width: window.innerWidth,
183 | right: window.innerWidth,
184 | height: window.innerHeight,
185 | bottom: window.innerHeight
186 | };
187 | if (result.height) {
188 | return result;
189 | }
190 | var mode = document.compatMode;
191 | if (mode === 'CSS1Compat') {
192 | result.width = result.right = document.documentElement.clientWidth;
193 | result.height = result.bottom = document.documentElement.clientHeight;
194 | } else {
195 | result.width = result.right = document.body.clientWidth;
196 | result.height = result.bottom = document.body.clientHeight;
197 | }
198 | return result;
199 | }
200 |
201 | function intersectRect (r1, r2) {
202 | return !(r2.left > r1.right ||
203 | r2.right < r1.left ||
204 | r2.top > r1.bottom ||
205 | r2.bottom < r1.top);
206 | }
207 |
208 | function normalizeOffset (offset) {
209 | if (!angular.isArray(offset)) {
210 | return [offset, offset, offset, offset];
211 | }
212 | if (offset.length == 2) {
213 | return offset.concat(offset);
214 | }
215 | else if (offset.length == 3) {
216 | return offset.concat([offset[1]]);
217 | }
218 | return offset;
219 | }
220 |
221 | function offsetRect (rect, offset) {
222 | if (!offset) {
223 | return rect;
224 | }
225 | var offsetObject = {
226 | top: isPercent(offset[0]) ? (parseFloat(offset[0]) * rect.height / 100) : offset[0],
227 | right: isPercent(offset[1]) ? (parseFloat(offset[1]) * rect.width / 100) : offset[1],
228 | bottom: isPercent(offset[2]) ? (parseFloat(offset[2]) * rect.height / 100) : offset[2],
229 | left: isPercent(offset[3]) ? (parseFloat(offset[3]) * rect.width / 100) : offset[3]
230 | };
231 | // Note: ClientRect object does not allow its properties to be written to therefore a new object has to be created.
232 | return {
233 | top: rect.top - offsetObject.top,
234 | left: rect.left - offsetObject.left,
235 | bottom: rect.bottom + offsetObject.bottom,
236 | right: rect.right + offsetObject.right,
237 | height: rect.height + offsetObject.top + offsetObject.bottom,
238 | width: rect.width + offsetObject.left + offsetObject.right
239 | };
240 | }
241 |
242 | function isPercent (n) {
243 | return angular.isString(n) && n.indexOf('%') > 0;
244 | }
245 |
246 | // ## QuickSignal FRP
247 | // A quick and dirty implementation of Rx to have a streamlined code in the
248 | // directives.
249 |
250 | // ### QuickSignal
251 | //
252 | // - `didSubscribeFunc`: a function receiving a `subscriber` as described below
253 | //
254 | // Usage:
255 | // var mySignal = new QuickSignal(function(subscriber) { ... })
256 | function QuickSignal (didSubscribeFunc) {
257 | this.didSubscribeFunc = didSubscribeFunc;
258 | }
259 |
260 | // Subscribe to a signal and consume the steam of data.
261 | //
262 | // Returns a function that can be called to stop the signal stream of data and
263 | // perform cleanup.
264 | //
265 | // A `subscriber` is a function that will be called when a new value arrives.
266 | // a `subscriber.$dispose` property can be set to a function to be called uppon
267 | // disposal. When setting the `$dispose` function, the previously set function
268 | // should be chained.
269 | QuickSignal.prototype.subscribe = function (subscriber) {
270 | this.didSubscribeFunc(subscriber);
271 | var dispose = function () {
272 | if (subscriber.$dispose) {
273 | subscriber.$dispose();
274 | subscriber.$dispose = null;
275 | }
276 | }
277 | return dispose;
278 | }
279 |
280 | QuickSignal.prototype.map = function (f) {
281 | var s = this;
282 | return new QuickSignal(function (subscriber) {
283 | subscriber.$dispose = s.subscribe(function (nextValue) {
284 | subscriber(f(nextValue));
285 | });
286 | });
287 | };
288 |
289 | QuickSignal.prototype.filter = function (f) {
290 | var s = this;
291 | return new QuickSignal(function (subscriber) {
292 | subscriber.$dispose = s.subscribe(function (nextValue) {
293 | if (f(nextValue)) {
294 | subscriber(nextValue);
295 | }
296 | });
297 | });
298 | };
299 |
300 | QuickSignal.prototype.scan = function (initial, scanFunc) {
301 | var s = this;
302 | return new QuickSignal(function (subscriber) {
303 | var last = initial;
304 | subscriber.$dispose = s.subscribe(function (nextValue) {
305 | last = scanFunc(last, nextValue);
306 | subscriber(last);
307 | });
308 | });
309 | }
310 |
311 | QuickSignal.prototype.merge = function (signal) {
312 | return signalMerge(this, signal);
313 | };
314 |
315 | QuickSignal.prototype.throttle = function (threshhold) {
316 | var s = this, last, deferTimer;
317 | return new QuickSignal(function (subscriber) {
318 | var chainDisposable = s.subscribe(function () {
319 | var now = +new Date,
320 | args = arguments;
321 | if (last && now < last + threshhold) {
322 | clearTimeout(deferTimer);
323 | deferTimer = setTimeout(function () {
324 | last = now;
325 | subscriber.apply(null, args);
326 | }, threshhold);
327 | } else {
328 | last = now;
329 | subscriber.apply(null, args);
330 | }
331 | });
332 | subscriber.$dispose = function () {
333 | clearTimeout(deferTimer);
334 | if (chainDisposable) chainDisposable();
335 | };
336 | });
337 | };
338 |
339 | function signalMerge () {
340 | var signals = arguments;
341 | return new QuickSignal(function (subscriber) {
342 | var disposables = [];
343 | for (var i = signals.length - 1; i >= 0; i--) {
344 | disposables.push(signals[i].subscribe(function () {
345 | subscriber.apply(null, arguments);
346 | }));
347 | }
348 | subscriber.$dispose = function () {
349 | for (var i = disposables.length - 1; i >= 0; i--) {
350 | if (disposables[i]) disposables[i]();
351 | }
352 | }
353 | });
354 | }
355 |
356 | // Returns a signal from DOM events of a target.
357 | function signalFromEvent (target, event) {
358 | return new QuickSignal(function (subscriber) {
359 | var handler = function (e) {
360 | subscriber(e);
361 | };
362 | var el = angular.element(target);
363 | event.split(' ').map(function (e) {
364 | el[0].addEventListener(e, handler, true);
365 | });
366 | subscriber.$dispose = function () {
367 | event.split(' ').map(function (e) {
368 | el[0].removeEventListener(e, handler, true);
369 | });
370 | };
371 | });
372 | }
373 |
374 | function signalSingle (value) {
375 | return new QuickSignal(function (subscriber) {
376 | setTimeout(function() { subscriber(value); });
377 | });
378 | }
379 |
380 | // Module loaders exports
381 | if (typeof define === 'function' && define.amd) {
382 | define(['angular'], moduleName);
383 | } else if (typeof module !== 'undefined' && module && module.exports) {
384 | module.exports = moduleName;
385 | }
386 |
387 | })();
388 |
--------------------------------------------------------------------------------
/angular-inview.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe("angular-inview", function() {
4 |
5 | var $rootScope, $compile, $q;
6 |
7 | beforeEach(function () {
8 | module('angular-inview');
9 |
10 | inject(function (_$rootScope_, _$compile_, _$q_) {
11 | $rootScope = _$rootScope_;
12 | $compile = _$compile_;
13 | $q = _$q_;
14 | });
15 | });
16 |
17 | describe("in-view directive", function() {
18 |
19 | it("should trigger in-view expression with `$inview` local", function(done) {
20 | makeTestForHtml(
21 | ''
22 | )
23 | .then(function (test) {
24 | expect(test.spy.calls.count()).toBe(1);
25 | expect(test.spy).toHaveBeenCalledWith(true);
26 | })
27 | .then(done);
28 | });
29 |
30 | it("should not trigger in-view expression if out of viewport", function(done) {
31 | makeTestForHtml(
32 | ''
33 | )
34 | .then(function (test) {
35 | expect(test.spy.calls.count()).toBe(0);
36 | })
37 | .then(done);
38 | });
39 |
40 | it("should change inview status when scrolling out of view", function(done) {
41 | makeTestForHtml(
42 | '' +
43 | ''
44 | )
45 | .then(lazyScrollTo(100))
46 | .then(function (test) {
47 | expect(test.spy.calls.count()).toBe(2);
48 | expect(test.spy).toHaveBeenCalledWith(true);
49 | expect(test.spy).toHaveBeenCalledWith(false);
50 | })
51 | .then(done);
52 | });
53 |
54 | describe("informations object", function() {
55 |
56 | it("should return an info object with additional informations", function(done) {
57 | makeTestForHtml(
58 | ''
59 | )
60 | .then(function (test) {
61 | expect(test.spy.calls.count()).toBe(1);
62 | var info = test.spy.calls.mostRecent().args[0];
63 | expect(info.inView).toEqual(true);
64 | expect(info.changed).toEqual(true);
65 | expect(info.elementRect).toBeDefined();
66 | expect(info.viewportRect).toBeDefined();
67 | expect(info.direction).not.toBeDefined();
68 | expect(info.parts).not.toBeDefined();
69 | })
70 | .then(done);
71 | });
72 |
73 | it("should return proper `parts` informations", function(done) {
74 | makeTestForHtml(
75 | '' +
76 | ''
77 | )
78 | .then(function (test) {
79 | expect(test.spy.calls.count()).toBe(1);
80 | var info = test.spy.calls.argsFor(0)[0];
81 | expect(info.parts).toEqual({
82 | top: true,
83 | left: true,
84 | bottom: true,
85 | right: true
86 | });
87 | return test;
88 | })
89 | .then(lazyScrollTo([400, 400]))
90 | .then(function (test) {
91 | var info = test.spy.calls.argsFor(1)[0];
92 | expect(test.spy.calls.count()).toBe(2);
93 | expect(info.parts).toEqual(undefined);
94 | return test;
95 | })
96 | .then(lazyScrollTo([100, 100]))
97 | .then(function (test) {
98 | var info = test.spy.calls.argsFor(2)[0];
99 | expect(test.spy.calls.count()).toBe(3);
100 | expect(info.parts).toEqual({
101 | top: false,
102 | left: false,
103 | bottom: true,
104 | right: true
105 | });
106 | return test;
107 | })
108 | .then(lazyScrollTo([0, 0]))
109 | .then(function (test) {
110 | var info = test.spy.calls.argsFor(3)[0];
111 | expect(test.spy.calls.count()).toBe(4);
112 | expect(info.parts).toEqual({
113 | top: true,
114 | left: true,
115 | bottom: true,
116 | right: true
117 | });
118 | })
119 | .then(done);
120 | });
121 |
122 | it("should return proper `direction` informations", function(done) {
123 | makeTestForHtml(
124 | '' +
125 | ''
126 | )
127 | .then(function (test) {
128 | var info = test.spy.calls.argsFor(0)[0];
129 | expect(info.direction).toEqual(undefined);
130 | return test;
131 | })
132 | .then(lazyScrollTo([100, 100]))
133 | .then(function (test) {
134 | var info = test.spy.calls.argsFor(1)[0];
135 | expect(info.direction).toEqual({
136 | horizontal: -100,
137 | vertical: -100
138 | });
139 | return test;
140 | })
141 | .then(lazyScrollTo([50, 50]))
142 | .then(function (test) {
143 | var info = test.spy.calls.argsFor(2)[0];
144 | expect(info.direction).toEqual({
145 | horizontal: 50,
146 | vertical: 50
147 | });
148 | })
149 | .then(done);
150 | });
151 |
152 | });
153 |
154 | describe("offset options", function() {
155 |
156 | it("should consider element offset option", function(done) {
157 | makeTestForHtml(
158 | '' +
159 | ''
160 | )
161 | .then(function (test) {
162 | var info = test.spy.calls.argsFor(0)[0];
163 | expect(info.inView).toEqual(true);
164 | expect(info.parts).toEqual({
165 | top: false,
166 | left: true,
167 | bottom: true,
168 | right: true
169 | });
170 | return test;
171 | })
172 | .then(done);
173 | });
174 |
175 | it("should consider negative offsets", function(done) {
176 | makeTestForHtml(
177 | '' +
178 | ''
179 | )
180 | .then(function (test) {
181 | var info = test.spy.calls.argsFor(0)[0];
182 | expect(info.parts).toEqual({
183 | top: true,
184 | left: true,
185 | bottom: true,
186 | right: true
187 | });
188 | return test;
189 | })
190 | .then(lazyScrollTo(100))
191 | .then(function (test) {
192 | var info = test.spy.calls.argsFor(1)[0];
193 | expect(info.parts).toEqual({
194 | top: false,
195 | left: true,
196 | bottom: true,
197 | right: true
198 | });
199 | return test;
200 | })
201 | .then(lazyScrollTo(50))
202 | .then(function (test) {
203 | var info = test.spy.calls.argsFor(2)[0];
204 | expect(info.parts).toEqual({
205 | top: true,
206 | left: true,
207 | bottom: true,
208 | right: true
209 | });
210 | return test;
211 | })
212 | .then(done);
213 | });
214 |
215 | it("should consider percent offset option", function(done) {
216 | makeTestForHtml(
217 | '' +
218 | ''
219 | )
220 | .then(function (test) {
221 | var info = test.spy.calls.argsFor(0)[0];
222 | expect(info.inView).toEqual(true);
223 | expect(info.parts).toEqual({
224 | top: false,
225 | left: true,
226 | bottom: true,
227 | right: true
228 | });
229 | return test;
230 | })
231 | .then(done);
232 | });
233 |
234 | it("should consider viewport offset options", function(done) {
235 | makeTestForHtml(
236 | '' +
237 | ''
238 | )
239 | .then(function (test) {
240 | var info = test.spy.calls.argsFor(0)[0];
241 | expect(info.parts).toEqual({
242 | top: true,
243 | left: true,
244 | bottom: true,
245 | right: true
246 | });
247 | return test;
248 | })
249 | .then(lazyScrollTo(200))
250 | .then(function (test) {
251 | var info = test.spy.calls.argsFor(1)[0];
252 | expect(info.parts).toEqual({
253 | top: false,
254 | left: true,
255 | bottom: true,
256 | right: true
257 | });
258 | return test;
259 | })
260 | .then(done);
261 | });
262 |
263 | });
264 |
265 | it("should accept a `throttle` option", function(done) {
266 | makeTestForHtml(
267 | '' +
268 | ''
269 | )
270 | .then(function (test) {
271 | expect(test.spy.calls.count()).toBe(1);
272 | expect(test.spy.calls.mostRecent().args[0]).toBe(true);
273 | return test;
274 | })
275 | .then(lazyScrollTo(200))
276 | .then(lazyWait(100))
277 | .then(function (test) {
278 | expect(test.spy.calls.count()).toBe(1);
279 | return test;
280 | })
281 | .then(lazyScrollTo(0))
282 | .then(lazyWait(100))
283 | .then(function (test) {
284 | expect(test.spy.calls.count()).toBe(1);
285 | return test;
286 | })
287 | .then(lazyScrollTo(200))
288 | .then(lazyWait(100))
289 | .then(function (test) {
290 | expect(test.spy.calls.count()).toBe(2);
291 | expect(test.spy.calls.mostRecent().args[0]).toBe(false);
292 | return test;
293 | })
294 | .then(lazyScrollTo(0))
295 | .then(done);
296 | });
297 |
298 | });
299 |
300 | describe("in-view-container directive", function() {
301 |
302 | it("should trigger in-view when scrolling a container", function(done) {
303 | makeTestForHtml(
304 | '' +
305 | '
' +
306 | '
' +
307 | '
'
308 | )
309 | .then(function (test) {
310 | expect(test.spy.calls.count()).toBe(1);
311 | expect(test.spy).toHaveBeenCalledWith(true);
312 | return test;
313 | })
314 | .then(lazyScrollTestElementTo(100))
315 | .then(function (test) {
316 | expect(test.spy.calls.count()).toBe(2);
317 | expect(test.spy).toHaveBeenCalledWith(false);
318 | })
319 | .then(done);
320 | });
321 |
322 | });
323 |
324 | // A test object has the properties:
325 | //
326 | // - `element`: An angular element inserted in the test page
327 | // - `scope`: a new isolated scope that can be referenced in the element
328 | // - `spy`: a conveninence jasmine spy attached to the scope as `spy`
329 | function makeTestForHtml(html) {
330 | var test = {};
331 | // Prepare test elements
332 | window.document.body.style.height = '100%';
333 | window.document.body.parentElement.style.height = '100%';
334 | test.element = angular.element(html);
335 | angular.element(window.document.body).empty().append(test.element);
336 | // Prepare test scope
337 | test.scope = $rootScope.$new(true);
338 | test.spy = test.scope.spy = jasmine.createSpy('spy');
339 | // Compile the element
340 | $compile(test.element)(test.scope);
341 | test.scope.$digest();
342 | return scrollTo(window, [0, 0], true).then(function () {
343 | return test;
344 | });
345 | }
346 |
347 | // Scrolls the element to the given x, y position and waits a bit before
348 | // resolving the returned promise.
349 | function scrollTo(element, position, useTimeout) {
350 | if (!angular.isDefined(position)) {
351 | position = element;
352 | element = window;
353 | }
354 | if (!angular.isArray(position)) {
355 | position = [0, position];
356 | }
357 | // Prepare promise resolution
358 | var deferred = $q.defer(), timeout;
359 | var scrollOnceHandler = function () {
360 | var check = (element === window) ?
361 | [element.scrollX, element.scrollY] :
362 | [element.scrollLeft, element.scrollTop];
363 | if (check[0] != position[0] || check[1] != position[1]) {
364 | return;
365 | }
366 | if (timeout) {
367 | clearTimeout(timeout);
368 | timeout = null;
369 | }
370 | angular.element(element).off('scroll', scrollOnceHandler);
371 | deferred.resolve();
372 | $rootScope.$digest();
373 | };
374 | angular.element(element).on('scroll', scrollOnceHandler);
375 | // Actual scrolling
376 | if (element === window) {
377 | element.scrollTo.apply(element, position);
378 | }
379 | else {
380 | element.scrollLeft = position[0];
381 | element.scrollTop = position[1];
382 | }
383 | // Backup resolver
384 | if (useTimeout) timeout = setTimeout(function () {
385 | angular.element(element).off('scroll', scrollOnceHandler);
386 | var check = (element === window) ?
387 | [element.scrollX, element.scrollY] :
388 | [element.scrollLeft, element.scrollTop];
389 | if (check[0] != position[0] || check[1] != position[1]) {
390 | deferred.reject();
391 | }
392 | else {
393 | deferred.resolve();
394 | }
395 | $rootScope.$digest();
396 | }, 100);
397 | return deferred.promise;
398 | }
399 |
400 | function lazyScrollTo () {
401 | var args = arguments;
402 | return function (x) {
403 | return scrollTo.apply(null, args).then(function () {
404 | return x;
405 | });
406 | }
407 | }
408 |
409 | function lazyScrollTestElementTo (pos) {
410 | return function (test) {
411 | return scrollTo(test.element[0], pos, true).then(function () {
412 | return test;
413 | });
414 | }
415 | }
416 |
417 | function lazyWait (millisec) {
418 | return function (x) {
419 | return $q(function (resolve) {
420 | setTimeout(function () {
421 | resolve(x);
422 | $rootScope.$digest();
423 | }, millisec);
424 | });
425 | }
426 | }
427 |
428 | });
429 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-inview",
3 | "version": "2.0.0",
4 | "main": "./angular-inview.js",
5 | "dependencies": {
6 | "angular": "~1"
7 | },
8 | "readmeFilename": "README.md",
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/thenikso/angular-inview.git"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | angular-inview basic example
6 |
34 |
35 |
36 |
37 |
42 |
43 |
44 | - This is test line #{{$index}}
45 |
46 |
47 |
48 |
49 |
74 |
75 |
--------------------------------------------------------------------------------
/examples/container.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | angular-inview container example
6 |
44 |
45 |
46 |
47 |
52 |
53 |
54 |
55 | - This is test contained-line #{{$index}}
56 |
57 |
58 |
59 | - This is test line #{{$index}}
60 |
61 |
62 |
63 |
64 |
88 |
89 |
--------------------------------------------------------------------------------
/examples/fixed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | angular-inview basic example
6 |
34 |
35 |
36 |
37 |
42 |
43 |
44 | - This is test line #{{$index}}
45 |
46 |
47 |
48 |
49 |
66 |
67 |
--------------------------------------------------------------------------------
/examples/throttle.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | angular-inview debounce option example
6 |
34 |
35 |
36 |
37 |
38 |
39 | - In-view events will be fired only after 1 second of scroll inactivity.
40 |
41 |
42 |
43 |
44 |
45 | - This is test line #{{$index}}
48 |
49 |
50 |
51 |
52 |
71 |
72 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Wed Jul 30 2014 11:18:46 GMT+0200 (CEST)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'node_modules/angular/angular.js',
19 | 'node_modules/angular-mocks/angular-mocks.js',
20 | 'angular-inview.js',
21 | 'angular-inview.spec.js'
22 | ],
23 |
24 |
25 | // list of files to exclude
26 | exclude: [],
27 |
28 |
29 | // preprocess matching files before serving them to the browser
30 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
31 | preprocessors: {
32 | },
33 |
34 |
35 | // test results reporter to use
36 | // possible values: 'dots', 'progress'
37 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
38 | reporters: ['progress'],
39 |
40 |
41 | // web server port
42 | port: 9876,
43 |
44 |
45 | // enable / disable colors in the output (reporters and logs)
46 | colors: true,
47 |
48 |
49 | // level of logging
50 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
51 | logLevel: config.LOG_INFO,
52 |
53 |
54 | // enable / disable watching file and executing tests whenever any file changes
55 | autoWatch: true,
56 |
57 |
58 | // start these browsers
59 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
60 | browsers: ['Chrome'],
61 |
62 |
63 | // Continuous Integration mode
64 | // if true, Karma captures browsers, runs the tests and exits
65 | singleRun: true
66 | });
67 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-inview",
3 | "version": "3.1.0",
4 | "description": "AngularJS directive to check if a DOM element is in the viewport",
5 | "main": "angular-inview.js",
6 | "scripts": {
7 | "test": "karma start",
8 | "publish-docs": "docco -o docs angular-inview.js && mv docs/angular-inview.html docs/index.html && gh-pages -d docs"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/thenikso/angular-inview.git"
13 | },
14 | "keywords": [
15 | "angualr",
16 | "inview"
17 | ],
18 | "author": "Nicola Peduzzi (http://nikso.net)",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/thenikso/angular-inview/issues"
22 | },
23 | "homepage": "https://github.com/thenikso/angular-inview#readme",
24 | "peerDependencies": {
25 | "angular": ">=1.0.0 < 2.0.0"
26 | },
27 | "devDependencies": {
28 | "angular": ">=1.0.0 < 2.0.0",
29 | "angular-mocks": "1.6.8",
30 | "docco": "^0.7.0",
31 | "gh-pages": "^0.11.0",
32 | "jasmine-core": "2.9.1",
33 | "karma": "2.0.0",
34 | "karma-chrome-launcher": "2.2.0",
35 | "karma-jasmine": "1.1.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------