No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

on-screen.es6.js 11KB

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