angular-inview.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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. // Merge with container's events signal
  65. if (container) {
  66. viewportEventSignal = viewportEventSignal.merge(container.eventsSignal);
  67. }
  68. // Throttle if option specified
  69. if (options.throttle) {
  70. viewportEventSignal = viewportEventSignal.throttle(options.throttle);
  71. }
  72. // Map to viewport intersection and in-view informations
  73. var inviewInfoSignal = viewportEventSignal
  74. // Inview information structure contains:
  75. // - `inView`: a boolean value indicating if the element is
  76. // visible in the viewport;
  77. // - `changed`: a boolean value indicating if the inview status
  78. // changed after the last event;
  79. // - `event`: the event that initiated the in-view check;
  80. .map(function(event) {
  81. var viewportRect;
  82. if (container) {
  83. viewportRect = container.getViewportRect();
  84. // TODO merge with actual window!
  85. } else {
  86. viewportRect = getViewportRect();
  87. }
  88. viewportRect = offsetRect(viewportRect, options.viewportOffset);
  89. var elementRect = offsetRect(element[0].getBoundingClientRect(), options.offset);
  90. var isVisible = !!(element[0].offsetWidth || element[0].offsetHeight || element[0].getClientRects().length);
  91. var documentVisible = !options.considerPageVisibility || document.visibilityState === 'visible' || document.hidden === false;
  92. var info = {
  93. inView: documentVisible && isVisible && intersectRect(elementRect, viewportRect),
  94. event: event,
  95. element: element,
  96. elementRect: elementRect,
  97. viewportRect: viewportRect
  98. };
  99. // Add inview parts
  100. if (options.generateParts && info.inView) {
  101. info.parts = {};
  102. info.parts.top = elementRect.top >= viewportRect.top;
  103. info.parts.left = elementRect.left >= viewportRect.left;
  104. info.parts.bottom = elementRect.bottom <= viewportRect.bottom;
  105. info.parts.right = elementRect.right <= viewportRect.right;
  106. }
  107. return info;
  108. })
  109. // Add the changed information to the inview structure.
  110. .scan({}, function (lastInfo, newInfo) {
  111. // Add inview direction info
  112. if (options.generateDirection && newInfo.inView && lastInfo.elementRect) {
  113. newInfo.direction = {
  114. horizontal: newInfo.elementRect.left - lastInfo.elementRect.left,
  115. vertical: newInfo.elementRect.top - lastInfo.elementRect.top
  116. };
  117. }
  118. // Calculate changed flag
  119. newInfo.changed =
  120. newInfo.inView !== lastInfo.inView ||
  121. !angular.equals(newInfo.parts, lastInfo.parts) ||
  122. !angular.equals(newInfo.direction, lastInfo.direction);
  123. return newInfo;
  124. })
  125. // Filters only informations that should be forwarded to the callback
  126. .filter(function (info) {
  127. // Don't forward if no relevant infomation changed
  128. if (!info.changed) {
  129. return false;
  130. }
  131. // Don't forward if not initially in-view
  132. if (info.event.type === 'initial' && !info.inView) {
  133. return false;
  134. }
  135. return true;
  136. });
  137. // Execute in-view callback
  138. var inViewExpression = $parse(attrs.inView);
  139. var dispose = inviewInfoSignal.subscribe(function (info) {
  140. scope.$applyAsync(function () {
  141. inViewExpression(scope, {
  142. '$inview': info.inView,
  143. '$inviewInfo': info
  144. });
  145. });
  146. });
  147. // Dispose of reactive chain
  148. scope.$on('$destroy', dispose);
  149. }
  150. }
  151. }
  152. function inViewContainerDirective () {
  153. return {
  154. restrict: 'A',
  155. controller: ['$element', function ($element) {
  156. this.element = $element;
  157. this.eventsSignal = signalFromEvent($element, 'scroll');
  158. this.getViewportRect = function () {
  159. return $element[0].getBoundingClientRect();
  160. };
  161. }]
  162. }
  163. }
  164. // ## Utilities
  165. function getViewportRect () {
  166. var result = {
  167. top: 0,
  168. left: 0,
  169. width: window.innerWidth,
  170. right: window.innerWidth,
  171. height: window.innerHeight,
  172. bottom: window.innerHeight
  173. };
  174. if (result.height) {
  175. return result;
  176. }
  177. var mode = document.compatMode;
  178. if (mode === 'CSS1Compat') {
  179. result.width = result.right = document.documentElement.clientWidth;
  180. result.height = result.bottom = document.documentElement.clientHeight;
  181. } else {
  182. result.width = result.right = document.body.clientWidth;
  183. result.height = result.bottom = document.body.clientHeight;
  184. }
  185. return result;
  186. }
  187. function intersectRect (r1, r2) {
  188. return !(r2.left > r1.right ||
  189. r2.right < r1.left ||
  190. r2.top > r1.bottom ||
  191. r2.bottom < r1.top);
  192. }
  193. function normalizeOffset (offset) {
  194. if (!angular.isArray(offset)) {
  195. return [offset, offset, offset, offset];
  196. }
  197. if (offset.length == 2) {
  198. return offset.concat(offset);
  199. }
  200. else if (offset.length == 3) {
  201. return offset.concat([offset[1]]);
  202. }
  203. return offset;
  204. }
  205. function offsetRect (rect, offset) {
  206. if (!offset) {
  207. return rect;
  208. }
  209. var offsetObject = {
  210. top: isPercent(offset[0]) ? (parseFloat(offset[0]) * rect.height) : offset[0],
  211. right: isPercent(offset[1]) ? (parseFloat(offset[1]) * rect.width) : offset[1],
  212. bottom: isPercent(offset[2]) ? (parseFloat(offset[2]) * rect.height) : offset[2],
  213. left: isPercent(offset[3]) ? (parseFloat(offset[3]) * rect.width) : offset[3]
  214. };
  215. // Note: ClientRect object does not allow its properties to be written to therefore a new object has to be created.
  216. return {
  217. top: rect.top - offsetObject.top,
  218. left: rect.left - offsetObject.left,
  219. bottom: rect.bottom + offsetObject.bottom,
  220. right: rect.right + offsetObject.right,
  221. height: rect.height + offsetObject.top + offsetObject.bottom,
  222. width: rect.width + offsetObject.left + offsetObject.right
  223. };
  224. }
  225. function isPercent (n) {
  226. return angular.isString(n) && n.indexOf('%') > 0;
  227. }
  228. // ## QuickSignal FRP
  229. // A quick and dirty implementation of Rx to have a streamlined code in the
  230. // directives.
  231. // ### QuickSignal
  232. //
  233. // - `didSubscribeFunc`: a function receiving a `subscriber` as described below
  234. //
  235. // Usage:
  236. // var mySignal = new QuickSignal(function(subscriber) { ... })
  237. function QuickSignal (didSubscribeFunc) {
  238. this.didSubscribeFunc = didSubscribeFunc;
  239. }
  240. // Subscribe to a signal and consume the steam of data.
  241. //
  242. // Returns a function that can be called to stop the signal stream of data and
  243. // perform cleanup.
  244. //
  245. // A `subscriber` is a function that will be called when a new value arrives.
  246. // a `subscriber.$dispose` property can be set to a function to be called uppon
  247. // disposal. When setting the `$dispose` function, the previously set function
  248. // should be chained.
  249. QuickSignal.prototype.subscribe = function (subscriber) {
  250. this.didSubscribeFunc(subscriber);
  251. var dispose = function () {
  252. if (subscriber.$dispose) {
  253. subscriber.$dispose();
  254. subscriber.$dispose = null;
  255. }
  256. }
  257. return dispose;
  258. }
  259. QuickSignal.prototype.map = function (f) {
  260. var s = this;
  261. return new QuickSignal(function (subscriber) {
  262. subscriber.$dispose = s.subscribe(function (nextValue) {
  263. subscriber(f(nextValue));
  264. });
  265. });
  266. };
  267. QuickSignal.prototype.filter = function (f) {
  268. var s = this;
  269. return new QuickSignal(function (subscriber) {
  270. subscriber.$dispose = s.subscribe(function (nextValue) {
  271. if (f(nextValue)) {
  272. subscriber(nextValue);
  273. }
  274. });
  275. });
  276. };
  277. QuickSignal.prototype.scan = function (initial, scanFunc) {
  278. var s = this;
  279. return new QuickSignal(function (subscriber) {
  280. var last = initial;
  281. subscriber.$dispose = s.subscribe(function (nextValue) {
  282. last = scanFunc(last, nextValue);
  283. subscriber(last);
  284. });
  285. });
  286. }
  287. QuickSignal.prototype.merge = function (signal) {
  288. return signalMerge(this, signal);
  289. };
  290. QuickSignal.prototype.throttle = function (threshhold) {
  291. var s = this, last, deferTimer;
  292. return new QuickSignal(function (subscriber) {
  293. var chainDisposable = s.subscribe(function () {
  294. var now = +new Date,
  295. args = arguments;
  296. if (last && now < last + threshhold) {
  297. clearTimeout(deferTimer);
  298. deferTimer = setTimeout(function () {
  299. last = now;
  300. subscriber.apply(null, args);
  301. }, threshhold);
  302. } else {
  303. last = now;
  304. subscriber.apply(null, args);
  305. }
  306. });
  307. subscriber.$dispose = function () {
  308. clearTimeout(deferTimer);
  309. if (chainDisposable) chainDisposable();
  310. };
  311. });
  312. };
  313. function signalMerge () {
  314. var signals = arguments;
  315. return new QuickSignal(function (subscriber) {
  316. var disposables = [];
  317. for (var i = signals.length - 1; i >= 0; i--) {
  318. disposables.push(signals[i].subscribe(function () {
  319. subscriber.apply(null, arguments);
  320. }));
  321. }
  322. subscriber.$dispose = function () {
  323. for (var i = disposables.length - 1; i >= 0; i--) {
  324. if (disposables[i]) disposables[i]();
  325. }
  326. }
  327. });
  328. }
  329. // Returns a signal from DOM events of a target.
  330. function signalFromEvent (target, event) {
  331. return new QuickSignal(function (subscriber) {
  332. var handler = function (e) {
  333. subscriber(e);
  334. };
  335. var el = angular.element(target);
  336. el.on(event, handler);
  337. subscriber.$dispose = function () {
  338. el.off(event, handler);
  339. };
  340. });
  341. }
  342. function signalSingle (value) {
  343. return new QuickSignal(function (subscriber) {
  344. setTimeout(function() { subscriber(value); });
  345. });
  346. }
  347. // Module loaders exports
  348. if (typeof define === 'function' && define.amd) {
  349. define(['angular'], angularInviewModule);
  350. } else if (typeof module !== 'undefined' && module && module.exports) {
  351. module.exports = angularInviewModule;
  352. }
  353. })();