on-screen.es6.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. /**
  2. * Attaches the scroll event handler
  3. *
  4. * @return {void}
  5. */
  6. function attach() {
  7. var container = this.options.container;
  8. if (container instanceof HTMLElement) {
  9. var style = window.getComputedStyle(container);
  10. if (style.position === 'static') {
  11. container.style.position = 'relative';
  12. }
  13. }
  14. container.addEventListener('scroll', this._scroll);
  15. window.addEventListener('resize', this._scroll);
  16. this._scroll();
  17. this.attached = true;
  18. }
  19. /**
  20. * Checks an element's position in respect to the viewport
  21. * and determines wether it's inside the viewport.
  22. *
  23. * @param {node} element The DOM node you want to check
  24. * @return {boolean} A boolean value that indicates wether is on or off the viewport.
  25. */
  26. function inViewport(el) {
  27. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
  28. tolerance: 0
  29. };
  30. if (!el) {
  31. throw new Error('You should specify the element you want to test');
  32. }
  33. if (typeof el === 'string') {
  34. el = document.querySelector(el);
  35. }
  36. var elRect = el.getBoundingClientRect();
  37. return (
  38. // Check bottom boundary
  39. elRect.bottom - options.tolerance > 0 &&
  40. // Check right boundary
  41. elRect.right - options.tolerance > 0 &&
  42. // Check left boundary
  43. elRect.left + options.tolerance < (window.innerWidth || document.documentElement.clientWidth) &&
  44. // Check top boundary
  45. elRect.top + options.tolerance < (window.innerHeight || document.documentElement.clientHeight)
  46. );
  47. }
  48. /**
  49. * Checks an element's position in respect to a HTMLElement
  50. * and determines wether it's within its boundaries.
  51. *
  52. * @param {node} element The DOM node you want to check
  53. * @return {boolean} A boolean value that indicates wether is on or off the container.
  54. */
  55. function inContainer(el) {
  56. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
  57. tolerance: 0,
  58. container: ''
  59. };
  60. if (!el) {
  61. throw new Error('You should specify the element you want to test');
  62. }
  63. if (typeof el === 'string') {
  64. el = document.querySelector(el);
  65. }
  66. if (typeof options === 'string') {
  67. options = {
  68. tolerance: 0,
  69. container: document.querySelector(options)
  70. };
  71. }
  72. if (typeof options.container === 'string') {
  73. options.container = document.querySelector(options.container);
  74. }
  75. if (options instanceof HTMLElement) {
  76. options = {
  77. tolerance: 0,
  78. container: options
  79. };
  80. }
  81. if (!options.container) {
  82. throw new Error('You should specify a container element');
  83. }
  84. var containerRect = options.container.getBoundingClientRect();
  85. return (
  86. // // Check bottom boundary
  87. el.offsetTop + el.clientHeight - options.tolerance > options.container.scrollTop &&
  88. // Check right boundary
  89. el.offsetLeft + el.clientWidth - options.tolerance > options.container.scrollLeft &&
  90. // Check left boundary
  91. el.offsetLeft + options.tolerance < containerRect.width + options.container.scrollLeft &&
  92. // // Check top boundary
  93. el.offsetTop + options.tolerance < containerRect.height + options.container.scrollTop
  94. );
  95. }
  96. // TODO: Refactor this so it can be easily tested
  97. /* istanbul ignore next */
  98. function eventHandler() {
  99. var trackedElements = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  100. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
  101. tolerance: 0
  102. };
  103. var selectors = Object.keys(trackedElements);
  104. var testVisibility = void 0;
  105. if (!selectors.length) return;
  106. if (options.container === window) {
  107. testVisibility = inViewport;
  108. } else {
  109. testVisibility = inContainer;
  110. }
  111. selectors.forEach(function(selector) {
  112. trackedElements[selector].nodes.forEach(function(item) {
  113. if (testVisibility(item.node, options)) {
  114. item.wasVisible = item.isVisible;
  115. item.isVisible = true;
  116. } else {
  117. item.wasVisible = item.isVisible;
  118. item.isVisible = false;
  119. }
  120. if (item.isVisible === true && item.wasVisible === false) {
  121. if (!trackedElements[selector].enter) return;
  122. Object.keys(trackedElements[selector].enter).forEach(function(callback) {
  123. if (typeof trackedElements[selector].enter[callback] === 'function') {
  124. trackedElements[selector].enter[callback](item.node, 'enter');
  125. }
  126. });
  127. }
  128. if (item.isVisible === false && item.wasVisible === true) {
  129. if (!trackedElements[selector].leave) return;
  130. Object.keys(trackedElements[selector].leave).forEach(function(callback) {
  131. if (typeof trackedElements[selector].leave[callback] === 'function') {
  132. trackedElements[selector].leave[callback](item.node, 'leave');
  133. }
  134. });
  135. }
  136. });
  137. });
  138. }
  139. /**
  140. * Debounces the scroll event to avoid performance issues
  141. *
  142. * @return {void}
  143. */
  144. function debouncedScroll() {
  145. var _this = this;
  146. var timeout = void 0;
  147. return function() {
  148. clearTimeout(timeout);
  149. timeout = setTimeout(function() {
  150. eventHandler(_this.trackedElements, _this.options);
  151. }, _this.options.debounce);
  152. };
  153. }
  154. /**
  155. * Removes the scroll event handler
  156. *
  157. * @return {void}
  158. */
  159. function destroy() {
  160. this.options.container.removeEventListener('scroll', this._scroll);
  161. window.removeEventListener('resize', this._scroll);
  162. this.attached = false;
  163. }
  164. /**
  165. * Stops tracking elements matching a CSS selector. If a selector has no
  166. * callbacks it gets removed.
  167. *
  168. * @param {string} event The event you want to stop tracking (enter or leave)
  169. * @param {string} selector The CSS selector you want to stop tracking
  170. * @return {void}
  171. */
  172. function off(event, selector, handler) {
  173. var enterCallbacks = Object.keys(this.trackedElements[selector].enter || {});
  174. var leaveCallbacks = Object.keys(this.trackedElements[selector].leave || {});
  175. if ({}.hasOwnProperty.call(this.trackedElements, selector)) {
  176. if (handler) {
  177. if (this.trackedElements[selector][event]) {
  178. var callbackName = typeof handler === 'function' ? handler.name : handler;
  179. delete this.trackedElements[selector][event][callbackName];
  180. }
  181. } else {
  182. delete this.trackedElements[selector][event];
  183. }
  184. }
  185. if (!enterCallbacks.length && !leaveCallbacks.length) {
  186. delete this.trackedElements[selector];
  187. }
  188. }
  189. /**
  190. * Starts tracking elements matching a CSS selector
  191. *
  192. * @param {string} event The event you want to track (enter or leave)
  193. * @param {string} selector The element you want to track
  194. * @param {function} callback The callback function to handle the event
  195. * @return {void}
  196. */
  197. function on(event, selector, callback) {
  198. var allowed = ['enter', 'leave'];
  199. if (!event) throw new Error('No event given. Choose either enter or leave');
  200. if (!selector) throw new Error('No selector to track');
  201. if (allowed.indexOf(event) < 0) throw new Error(event + ' event is not supported');
  202. if (!{}.hasOwnProperty.call(this.trackedElements, selector)) {
  203. this.trackedElements[selector] = {};
  204. }
  205. this.trackedElements[selector].nodes = [];
  206. for (var i = 0, elems = document.querySelectorAll(selector); i < elems.length; i++) {
  207. var item = {
  208. isVisible: false,
  209. wasVisible: false,
  210. node: elems[i]
  211. };
  212. this.trackedElements[selector].nodes.push(item);
  213. }
  214. if (typeof callback === 'function') {
  215. if (!this.trackedElements[selector][event]) {
  216. this.trackedElements[selector][event] = {};
  217. }
  218. this.trackedElements[selector][event][callback.name || 'anonymous'] = callback;
  219. }
  220. }
  221. /**
  222. * Observes DOM mutations and runs a callback function when
  223. * detecting one.
  224. *
  225. * @param {node} obj The DOM node you want to observe
  226. * @param {function} callback The callback function you want to call
  227. * @return {void}
  228. */
  229. function observeDOM(obj, callback) {
  230. var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
  231. /* istanbul ignore else */
  232. if (MutationObserver) {
  233. var obs = new MutationObserver(callback);
  234. obs.observe(obj, {
  235. childList: true,
  236. subtree: true
  237. });
  238. } else {
  239. obj.addEventListener('DOMNodeInserted', callback, false);
  240. obj.addEventListener('DOMNodeRemoved', callback, false);
  241. }
  242. }
  243. /**
  244. * Detects wether DOM nodes enter or leave the viewport
  245. *
  246. * @constructor
  247. * @param {object} options The configuration object
  248. */
  249. function OnScreen() {
  250. var _this = this;
  251. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
  252. tolerance: 0,
  253. debounce: 100,
  254. container: window
  255. };
  256. this.options = {};
  257. this.trackedElements = {};
  258. Object.defineProperties(this.options, {
  259. container: {
  260. configurable: false,
  261. enumerable: false,
  262. get: function get() {
  263. var container = void 0;
  264. if (typeof options.container === 'string') {
  265. container = document.querySelector(options.container);
  266. } else if (options.container instanceof HTMLElement) {
  267. container = options.container;
  268. }
  269. return container || window;
  270. },
  271. set: function set(value) {
  272. options.container = value;
  273. }
  274. },
  275. debounce: {
  276. get: function get() {
  277. return parseInt(options.debounce, 10) || 100;
  278. },
  279. set: function set(value) {
  280. options.debounce = value;
  281. }
  282. },
  283. tolerance: {
  284. get: function get() {
  285. return parseInt(options.tolerance, 10) || 0;
  286. },
  287. set: function set(value) {
  288. options.tolerance = value;
  289. }
  290. }
  291. });
  292. Object.defineProperty(this, '_scroll', {
  293. enumerable: false,
  294. configurable: false,
  295. writable: false,
  296. value: this._debouncedScroll.call(this)
  297. });
  298. observeDOM(document.querySelector('body'), function() {
  299. Object.keys(_this.trackedElements).forEach(function(element) {
  300. _this.on('enter', element);
  301. _this.on('leave', element);
  302. });
  303. });
  304. this.attach();
  305. }
  306. Object.defineProperties(OnScreen.prototype, {
  307. _debouncedScroll: {
  308. configurable: false,
  309. writable: false,
  310. enumerable: false,
  311. value: debouncedScroll
  312. },
  313. attach: {
  314. configurable: false,
  315. writable: false,
  316. enumerable: false,
  317. value: attach
  318. },
  319. destroy: {
  320. configurable: false,
  321. writable: false,
  322. enumerable: false,
  323. value: destroy
  324. },
  325. off: {
  326. configurable: false,
  327. writable: false,
  328. enumerable: false,
  329. value: off
  330. },
  331. on: {
  332. configurable: false,
  333. writable: false,
  334. enumerable: false,
  335. value: on
  336. }
  337. });
  338. OnScreen.check = inViewport;
  339. export default OnScreen;
  340. //# sourceMappingURL=on-screen.es6.js.map