angular-inview.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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: **2.2.0**
  6. (function() {
  7. 'use strict';
  8. // An [angular.js](https://angularjs.org) directive to evaluate an expression if
  9. // a DOM element is or not in the current visible browser viewport.
  10. // Use it in your AngularJS app by including the javascript and requireing it:
  11. //
  12. // `angular.module('myApp', ['angular-inview'])`
  13. var angularInviewModule = angular.module('angular-inview', [])
  14. // ## in-view directive
  15. //
  16. // ### Usage
  17. // ```html
  18. // <any in-view="{expression}" [in-view-options="{object}"]></any>
  19. // ```
  20. .directive('inView', ['$parse', inViewDirective])
  21. // ## in-view-container directive
  22. .directive('inViewContainer', inViewContainerDirective);
  23. // ## Implementation
  24. function inViewDirective ($parse) {
  25. return {
  26. // Evaluate the expression passet to the attribute `in-view` when the DOM
  27. // element is visible in the viewport.
  28. restrict: 'A',
  29. require: '?^^inViewContainer',
  30. link: function inViewDirectiveLink (scope, element, attrs, container) {
  31. // in-view-options attribute can be specified with an object expression
  32. // containing:
  33. // - `offset`: An array of values to offset the element position.
  34. // Offsets are expressed as arrays of 4 numbers [top, right, bottom, left].
  35. // Like CSS, you can also specify only 2 numbers [top/bottom, left/right].
  36. // Instead of numbers, some array elements can be a string with a percentage.
  37. // Positive numbers are offsets outside the element rectangle and
  38. // negative numbers are offsets to the inside.
  39. // - `viewportOffset`: Like the element offset but appied to the viewport.
  40. // - `generateDirection`: Indicate if the `direction` information should
  41. // be included in `$inviewInfo` (default false).
  42. // - `generateParts`: Indicate if the `parts` information should
  43. // be included in `$inviewInfo` (default false).
  44. // - `throttle`: Specify a number of milliseconds by which to limit the
  45. // number of incoming events.
  46. var options = {};
  47. if (attrs.inViewOptions) {
  48. options = scope.$eval(attrs.inViewOptions);
  49. }
  50. if (options.offset) {
  51. options.offset = normalizeOffset(options.offset);
  52. }
  53. if (options.viewportOffset) {
  54. options.viewportOffset = normalizeOffset(options.viewportOffset);
  55. }
  56. // Build reactive chain from an initial event
  57. var viewportEventSignal = signalSingle({ type: 'initial' })
  58. // Merged with the window events
  59. .merge(signalFromEvent(window, 'checkInView click ready wheel mousewheel DomMouseScroll MozMousePixelScroll resize scroll touchmove mouseup keydown'));
  60. // Merged with the page visibility events
  61. if (options.considerPageVisibility) {
  62. viewportEventSignal = viewportEventSignal.merge(signalFromEvent(document, 'visibilitychange'));
  63. }
  64. // Merged with the page focus/blur events
  65. if (options.considerPageFocus) {
  66. viewportEventSignal = viewportEventSignal.merge(signalFromEvent(window, 'focus blur'));
  67. }
  68. // Merge with container's events signal
  69. if (container) {
  70. viewportEventSignal = viewportEventSignal.merge(container.eventsSignal);
  71. }
  72. // Throttle if option specified
  73. if (options.throttle) {
  74. viewportEventSignal = viewportEventSignal.throttle(options.throttle);
  75. }
  76. // Map to viewport intersection and in-view informations
  77. var inviewInfoSignal = viewportEventSignal
  78. // Inview information structure contains:
  79. // - `inView`: a boolean value indicating if the element is
  80. // visible in the viewport;
  81. // - `changed`: a boolean value indicating if the inview status
  82. // changed after the last event;
  83. // - `event`: the event that initiated the in-view check;
  84. .map(function(event) {
  85. var viewportRect;
  86. if (container) {
  87. viewportRect = container.getViewportRect();
  88. // TODO merge with actual window!
  89. } else {
  90. viewportRect = getViewportRect();
  91. }
  92. viewportRect = offsetRect(viewportRect, options.viewportOffset);
  93. var elementRect = offsetRect(element[0].getBoundingClientRect(), options.offset);
  94. var isVisible = !!(element[0].offsetWidth || element[0].offsetHeight || element[0].getClientRects().length);
  95. var documentVisible = !options.considerPageVisibility || document.visibilityState === 'visible' || document.hidden === false;
  96. var documentFocussed = !options.considerPageFocus || document.hasFocus();
  97. var info = {
  98. inView: documentVisible && documentFocussed && isVisible && intersectRect(elementRect, viewportRect),
  99. event: event,
  100. element: element,
  101. elementRect: elementRect,
  102. viewportRect: viewportRect
  103. };
  104. // Add inview parts
  105. if (options.generateParts && info.inView) {
  106. info.parts = {};
  107. info.parts.top = elementRect.top >= viewportRect.top;
  108. info.parts.left = elementRect.left >= viewportRect.left;
  109. info.parts.bottom = elementRect.bottom <= viewportRect.bottom;
  110. info.parts.right = elementRect.right <= viewportRect.right;
  111. }
  112. return info;
  113. })
  114. // Add the changed information to the inview structure.
  115. .scan({}, function (lastInfo, newInfo) {
  116. // Add inview direction info
  117. if (options.generateDirection && newInfo.inView && lastInfo.elementRect) {
  118. newInfo.direction = {
  119. horizontal: newInfo.elementRect.left - lastInfo.elementRect.left,
  120. vertical: newInfo.elementRect.top - lastInfo.elementRect.top
  121. };
  122. }
  123. // Calculate changed flag
  124. newInfo.changed =
  125. newInfo.inView !== lastInfo.inView ||
  126. !angular.equals(newInfo.parts, lastInfo.parts) ||
  127. !angular.equals(newInfo.direction, lastInfo.direction);
  128. return newInfo;
  129. })
  130. // Filters only informations that should be forwarded to the callback
  131. .filter(function (info) {
  132. // Don't forward if no relevant infomation changed
  133. if (!info.changed) {
  134. return false;
  135. }
  136. // Don't forward if not initially in-view
  137. if (info.event.type === 'initial' && !info.inView) {
  138. return false;
  139. }
  140. return true;
  141. });
  142. // Execute in-view callback
  143. var inViewExpression = $parse(attrs.inView);
  144. var dispose = inviewInfoSignal.subscribe(function (info) {
  145. scope.$applyAsync(function () {
  146. inViewExpression(scope, {
  147. '$inview': info.inView,
  148. '$inviewInfo': info
  149. });
  150. });
  151. });
  152. // Dispose of reactive chain
  153. scope.$on('$destroy', dispose);
  154. }
  155. }
  156. }
  157. function inViewContainerDirective () {
  158. return {
  159. restrict: 'A',
  160. controller: ['$element', function ($element) {
  161. this.element = $element;
  162. this.eventsSignal = signalFromEvent($element, 'scroll');
  163. this.getViewportRect = function () {
  164. return $element[0].getBoundingClientRect();
  165. };
  166. }]
  167. }
  168. }
  169. // ## Utilities
  170. function getViewportRect () {
  171. var result = {
  172. top: 0,
  173. left: 0,
  174. width: window.innerWidth,
  175. right: window.innerWidth,
  176. height: window.innerHeight,
  177. bottom: window.innerHeight
  178. };
  179. if (result.height) {
  180. return result;
  181. }
  182. var mode = document.compatMode;
  183. if (mode === 'CSS1Compat') {
  184. result.width = result.right = document.documentElement.clientWidth;
  185. result.height = result.bottom = document.documentElement.clientHeight;
  186. } else {
  187. result.width = result.right = document.body.clientWidth;
  188. result.height = result.bottom = document.body.clientHeight;
  189. }
  190. return result;
  191. }
  192. function intersectRect (r1, r2) {
  193. return !(r2.left > r1.right ||
  194. r2.right < r1.left ||
  195. r2.top > r1.bottom ||
  196. r2.bottom < r1.top);
  197. }
  198. function normalizeOffset (offset) {
  199. if (!angular.isArray(offset)) {
  200. return [offset, offset, offset, offset];
  201. }
  202. if (offset.length == 2) {
  203. return offset.concat(offset);
  204. }
  205. else if (offset.length == 3) {
  206. return offset.concat([offset[1]]);
  207. }
  208. return offset;
  209. }
  210. function offsetRect (rect, offset) {
  211. if (!offset) {
  212. return rect;
  213. }
  214. var offsetObject = {
  215. top: isPercent(offset[0]) ? (parseFloat(offset[0]) * rect.height) : offset[0],
  216. right: isPercent(offset[1]) ? (parseFloat(offset[1]) * rect.width) : offset[1],
  217. bottom: isPercent(offset[2]) ? (parseFloat(offset[2]) * rect.height) : offset[2],
  218. left: isPercent(offset[3]) ? (parseFloat(offset[3]) * rect.width) : offset[3]
  219. };
  220. // Note: ClientRect object does not allow its properties to be written to therefore a new object has to be created.
  221. return {
  222. top: rect.top - offsetObject.top,
  223. left: rect.left - offsetObject.left,
  224. bottom: rect.bottom + offsetObject.bottom,
  225. right: rect.right + offsetObject.right,
  226. height: rect.height + offsetObject.top + offsetObject.bottom,
  227. width: rect.width + offsetObject.left + offsetObject.right
  228. };
  229. }
  230. function isPercent (n) {
  231. return angular.isString(n) && n.indexOf('%') > 0;
  232. }
  233. // ## QuickSignal FRP
  234. // A quick and dirty implementation of Rx to have a streamlined code in the
  235. // directives.
  236. // ### QuickSignal
  237. //
  238. // - `didSubscribeFunc`: a function receiving a `subscriber` as described below
  239. //
  240. // Usage:
  241. // var mySignal = new QuickSignal(function(subscriber) { ... })
  242. function QuickSignal (didSubscribeFunc) {
  243. this.didSubscribeFunc = didSubscribeFunc;
  244. }
  245. // Subscribe to a signal and consume the steam of data.
  246. //
  247. // Returns a function that can be called to stop the signal stream of data and
  248. // perform cleanup.
  249. //
  250. // A `subscriber` is a function that will be called when a new value arrives.
  251. // a `subscriber.$dispose` property can be set to a function to be called uppon
  252. // disposal. When setting the `$dispose` function, the previously set function
  253. // should be chained.
  254. QuickSignal.prototype.subscribe = function (subscriber) {
  255. this.didSubscribeFunc(subscriber);
  256. var dispose = function () {
  257. if (subscriber.$dispose) {
  258. subscriber.$dispose();
  259. subscriber.$dispose = null;
  260. }
  261. }
  262. return dispose;
  263. }
  264. QuickSignal.prototype.map = function (f) {
  265. var s = this;
  266. return new QuickSignal(function (subscriber) {
  267. subscriber.$dispose = s.subscribe(function (nextValue) {
  268. subscriber(f(nextValue));
  269. });
  270. });
  271. };
  272. QuickSignal.prototype.filter = function (f) {
  273. var s = this;
  274. return new QuickSignal(function (subscriber) {
  275. subscriber.$dispose = s.subscribe(function (nextValue) {
  276. if (f(nextValue)) {
  277. subscriber(nextValue);
  278. }
  279. });
  280. });
  281. };
  282. QuickSignal.prototype.scan = function (initial, scanFunc) {
  283. var s = this;
  284. return new QuickSignal(function (subscriber) {
  285. var last = initial;
  286. subscriber.$dispose = s.subscribe(function (nextValue) {
  287. last = scanFunc(last, nextValue);
  288. subscriber(last);
  289. });
  290. });
  291. }
  292. QuickSignal.prototype.merge = function (signal) {
  293. return signalMerge(this, signal);
  294. };
  295. QuickSignal.prototype.throttle = function (threshhold) {
  296. var s = this, last, deferTimer;
  297. return new QuickSignal(function (subscriber) {
  298. var chainDisposable = s.subscribe(function () {
  299. var now = +new Date,
  300. args = arguments;
  301. if (last && now < last + threshhold) {
  302. clearTimeout(deferTimer);
  303. deferTimer = setTimeout(function () {
  304. last = now;
  305. subscriber.apply(null, args);
  306. }, threshhold);
  307. } else {
  308. last = now;
  309. subscriber.apply(null, args);
  310. }
  311. });
  312. subscriber.$dispose = function () {
  313. clearTimeout(deferTimer);
  314. if (chainDisposable) chainDisposable();
  315. };
  316. });
  317. };
  318. function signalMerge () {
  319. var signals = arguments;
  320. return new QuickSignal(function (subscriber) {
  321. var disposables = [];
  322. for (var i = signals.length - 1; i >= 0; i--) {
  323. disposables.push(signals[i].subscribe(function () {
  324. subscriber.apply(null, arguments);
  325. }));
  326. }
  327. subscriber.$dispose = function () {
  328. for (var i = disposables.length - 1; i >= 0; i--) {
  329. if (disposables[i]) disposables[i]();
  330. }
  331. }
  332. });
  333. }
  334. // Returns a signal from DOM events of a target.
  335. function signalFromEvent (target, event) {
  336. return new QuickSignal(function (subscriber) {
  337. var handler = function (e) {
  338. subscriber(e);
  339. };
  340. var el = angular.element(target);
  341. el.on(event, handler);
  342. subscriber.$dispose = function () {
  343. el.off(event, handler);
  344. };
  345. });
  346. }
  347. function signalSingle (value) {
  348. return new QuickSignal(function (subscriber) {
  349. setTimeout(function() { subscriber(value); });
  350. });
  351. }
  352. // Module loaders exports
  353. if (typeof define === 'function' && define.amd) {
  354. define(['angular'], angularInviewModule);
  355. } else if (typeof module !== 'undefined' && module && module.exports) {
  356. module.exports = angularInviewModule;
  357. }
  358. })();