arrive.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /*globals jQuery,Window,HTMLElement,HTMLDocument,HTMLCollection,NodeList,MutationObserver */
  2. /*exported Arrive*/
  3. /*jshint latedef:false */
  4. /*
  5. * arrive.js
  6. * v2.4.1
  7. * https://github.com/uzairfarooq/arrive
  8. * MIT licensed
  9. *
  10. * Copyright (c) 2014-2017 Uzair Farooq
  11. */
  12. var Arrive = (function(window, $, undefined) {
  13. "use strict";
  14. if(!window.MutationObserver || typeof HTMLElement === 'undefined'){
  15. return; //for unsupported browsers
  16. }
  17. var arriveUniqueId = 0;
  18. var utils = (function() {
  19. var matches = HTMLElement.prototype.matches || HTMLElement.prototype.webkitMatchesSelector || HTMLElement.prototype.mozMatchesSelector
  20. || HTMLElement.prototype.msMatchesSelector;
  21. return {
  22. matchesSelector: function(elem, selector) {
  23. return elem instanceof HTMLElement && matches.call(elem, selector);
  24. },
  25. // to enable function overloading - By John Resig (MIT Licensed)
  26. addMethod: function (object, name, fn) {
  27. var old = object[ name ];
  28. object[ name ] = function(){
  29. if ( fn.length == arguments.length ) {
  30. return fn.apply( this, arguments );
  31. }
  32. else if ( typeof old == 'function' ) {
  33. return old.apply( this, arguments );
  34. }
  35. };
  36. },
  37. callCallbacks: function(callbacksToBeCalled, registrationData) {
  38. if (registrationData && registrationData.options.onceOnly && registrationData.firedElems.length == 1) {
  39. // as onlyOnce param is true, make sure we fire the event for only one item
  40. callbacksToBeCalled = [callbacksToBeCalled[0]];
  41. }
  42. for (var i = 0, cb; (cb = callbacksToBeCalled[i]); i++) {
  43. if (cb && cb.callback) {
  44. cb.callback.call(cb.elem, cb.elem);
  45. }
  46. }
  47. if (registrationData && registrationData.options.onceOnly && registrationData.firedElems.length == 1) {
  48. // unbind event after first callback as onceOnly is true.
  49. registrationData.me.unbindEventWithSelectorAndCallback.call(
  50. registrationData.target, registrationData.selector, registrationData.callback);
  51. }
  52. },
  53. // traverse through all descendants of a node to check if event should be fired for any descendant
  54. checkChildNodesRecursively: function(nodes, registrationData, matchFunc, callbacksToBeCalled) {
  55. // check each new node if it matches the selector
  56. for (var i=0, node; (node = nodes[i]); i++) {
  57. if (matchFunc(node, registrationData, callbacksToBeCalled)) {
  58. callbacksToBeCalled.push({ callback: registrationData.callback, elem: node });
  59. }
  60. if (node.childNodes.length > 0) {
  61. utils.checkChildNodesRecursively(node.childNodes, registrationData, matchFunc, callbacksToBeCalled);
  62. }
  63. }
  64. },
  65. mergeArrays: function(firstArr, secondArr){
  66. // Overwrites default options with user-defined options.
  67. var options = {},
  68. attrName;
  69. for (attrName in firstArr) {
  70. if (firstArr.hasOwnProperty(attrName)) {
  71. options[attrName] = firstArr[attrName];
  72. }
  73. }
  74. for (attrName in secondArr) {
  75. if (secondArr.hasOwnProperty(attrName)) {
  76. options[attrName] = secondArr[attrName];
  77. }
  78. }
  79. return options;
  80. },
  81. toElementsArray: function (elements) {
  82. // check if object is an array (or array like object)
  83. // Note: window object has .length property but it's not array of elements so don't consider it an array
  84. if (typeof elements !== "undefined" && (typeof elements.length !== "number" || elements === window)) {
  85. elements = [elements];
  86. }
  87. return elements;
  88. }
  89. };
  90. })();
  91. // Class to maintain state of all registered events of a single type
  92. var EventsBucket = (function() {
  93. var EventsBucket = function() {
  94. // holds all the events
  95. this._eventsBucket = [];
  96. // function to be called while adding an event, the function should do the event initialization/registration
  97. this._beforeAdding = null;
  98. // function to be called while removing an event, the function should do the event destruction
  99. this._beforeRemoving = null;
  100. };
  101. EventsBucket.prototype.addEvent = function(target, selector, options, callback) {
  102. var newEvent = {
  103. target: target,
  104. selector: selector,
  105. options: options,
  106. callback: callback,
  107. firedElems: []
  108. };
  109. if (this._beforeAdding) {
  110. this._beforeAdding(newEvent);
  111. }
  112. this._eventsBucket.push(newEvent);
  113. return newEvent;
  114. };
  115. EventsBucket.prototype.removeEvent = function(compareFunction) {
  116. for (var i=this._eventsBucket.length - 1, registeredEvent; (registeredEvent = this._eventsBucket[i]); i--) {
  117. if (compareFunction(registeredEvent)) {
  118. if (this._beforeRemoving) {
  119. this._beforeRemoving(registeredEvent);
  120. }
  121. // mark callback as null so that even if an event mutation was already triggered it does not call callback
  122. var removedEvents = this._eventsBucket.splice(i, 1);
  123. if (removedEvents && removedEvents.length) {
  124. removedEvents[0].callback = null;
  125. }
  126. }
  127. }
  128. };
  129. EventsBucket.prototype.beforeAdding = function(beforeAdding) {
  130. this._beforeAdding = beforeAdding;
  131. };
  132. EventsBucket.prototype.beforeRemoving = function(beforeRemoving) {
  133. this._beforeRemoving = beforeRemoving;
  134. };
  135. return EventsBucket;
  136. })();
  137. /**
  138. * @constructor
  139. * General class for binding/unbinding arrive and leave events
  140. */
  141. var MutationEvents = function(getObserverConfig, onMutation) {
  142. var eventsBucket = new EventsBucket(),
  143. me = this;
  144. var defaultOptions = {
  145. fireOnAttributesModification: false
  146. };
  147. // actual event registration before adding it to bucket
  148. eventsBucket.beforeAdding(function(registrationData) {
  149. var
  150. target = registrationData.target,
  151. observer;
  152. // mutation observer does not work on window or document
  153. if (target === window.document || target === window) {
  154. target = document.getElementsByTagName("html")[0];
  155. }
  156. // Create an observer instance
  157. observer = new MutationObserver(function(e) {
  158. onMutation.call(this, e, registrationData);
  159. });
  160. var config = getObserverConfig(registrationData.options);
  161. observer.observe(target, config);
  162. registrationData.observer = observer;
  163. registrationData.me = me;
  164. });
  165. // cleanup/unregister before removing an event
  166. eventsBucket.beforeRemoving(function (eventData) {
  167. eventData.observer.disconnect();
  168. });
  169. this.bindEvent = function(selector, options, callback) {
  170. options = utils.mergeArrays(defaultOptions, options);
  171. var elements = utils.toElementsArray(this);
  172. for (var i = 0; i < elements.length; i++) {
  173. eventsBucket.addEvent(elements[i], selector, options, callback);
  174. }
  175. };
  176. this.unbindEvent = function() {
  177. var elements = utils.toElementsArray(this);
  178. eventsBucket.removeEvent(function(eventObj) {
  179. for (var i = 0; i < elements.length; i++) {
  180. if (this === undefined || eventObj.target === elements[i]) {
  181. return true;
  182. }
  183. }
  184. return false;
  185. });
  186. };
  187. this.unbindEventWithSelectorOrCallback = function(selector) {
  188. var elements = utils.toElementsArray(this),
  189. callback = selector,
  190. compareFunction;
  191. if (typeof selector === "function") {
  192. compareFunction = function(eventObj) {
  193. for (var i = 0; i < elements.length; i++) {
  194. if ((this === undefined || eventObj.target === elements[i]) && eventObj.callback === callback) {
  195. return true;
  196. }
  197. }
  198. return false;
  199. };
  200. }
  201. else {
  202. compareFunction = function(eventObj) {
  203. for (var i = 0; i < elements.length; i++) {
  204. if ((this === undefined || eventObj.target === elements[i]) && eventObj.selector === selector) {
  205. return true;
  206. }
  207. }
  208. return false;
  209. };
  210. }
  211. eventsBucket.removeEvent(compareFunction);
  212. };
  213. this.unbindEventWithSelectorAndCallback = function(selector, callback) {
  214. var elements = utils.toElementsArray(this);
  215. eventsBucket.removeEvent(function(eventObj) {
  216. for (var i = 0; i < elements.length; i++) {
  217. if ((this === undefined || eventObj.target === elements[i]) && eventObj.selector === selector && eventObj.callback === callback) {
  218. return true;
  219. }
  220. }
  221. return false;
  222. });
  223. };
  224. return this;
  225. };
  226. /**
  227. * @constructor
  228. * Processes 'arrive' events
  229. */
  230. var ArriveEvents = function() {
  231. // Default options for 'arrive' event
  232. var arriveDefaultOptions = {
  233. fireOnAttributesModification: false,
  234. onceOnly: false,
  235. existing: false
  236. };
  237. function getArriveObserverConfig(options) {
  238. var config = {
  239. attributes: false,
  240. childList: true,
  241. subtree: true
  242. };
  243. if (options.fireOnAttributesModification) {
  244. config.attributes = true;
  245. }
  246. return config;
  247. }
  248. function onArriveMutation(mutations, registrationData) {
  249. mutations.forEach(function( mutation ) {
  250. var newNodes = mutation.addedNodes,
  251. targetNode = mutation.target,
  252. callbacksToBeCalled = [],
  253. node;
  254. // If new nodes are added
  255. if( newNodes !== null && newNodes.length > 0 ) {
  256. utils.checkChildNodesRecursively(newNodes, registrationData, nodeMatchFunc, callbacksToBeCalled);
  257. }
  258. else if (mutation.type === "attributes") {
  259. if (nodeMatchFunc(targetNode, registrationData, callbacksToBeCalled)) {
  260. callbacksToBeCalled.push({ callback: registrationData.callback, elem: targetNode });
  261. }
  262. }
  263. utils.callCallbacks(callbacksToBeCalled, registrationData);
  264. });
  265. }
  266. function nodeMatchFunc(node, registrationData, callbacksToBeCalled) {
  267. // check a single node to see if it matches the selector
  268. if (utils.matchesSelector(node, registrationData.selector)) {
  269. if(node._id === undefined) {
  270. node._id = arriveUniqueId++;
  271. }
  272. // make sure the arrive event is not already fired for the element
  273. if (registrationData.firedElems.indexOf(node._id) == -1) {
  274. registrationData.firedElems.push(node._id);
  275. return true;
  276. }
  277. }
  278. return false;
  279. }
  280. arriveEvents = new MutationEvents(getArriveObserverConfig, onArriveMutation);
  281. var mutationBindEvent = arriveEvents.bindEvent;
  282. // override bindEvent function
  283. arriveEvents.bindEvent = function(selector, options, callback) {
  284. if (typeof callback === "undefined") {
  285. callback = options;
  286. options = arriveDefaultOptions;
  287. } else {
  288. options = utils.mergeArrays(arriveDefaultOptions, options);
  289. }
  290. var elements = utils.toElementsArray(this);
  291. if (options.existing) {
  292. var existing = [];
  293. for (var i = 0; i < elements.length; i++) {
  294. var nodes = elements[i].querySelectorAll(selector);
  295. for (var j = 0; j < nodes.length; j++) {
  296. existing.push({ callback: callback, elem: nodes[j] });
  297. }
  298. }
  299. // no need to bind event if the callback has to be fired only once and we have already found the element
  300. if (options.onceOnly && existing.length) {
  301. return callback.call(existing[0].elem, existing[0].elem);
  302. }
  303. setTimeout(utils.callCallbacks, 1, existing);
  304. }
  305. mutationBindEvent.call(this, selector, options, callback);
  306. };
  307. return arriveEvents;
  308. };
  309. /**
  310. * @constructor
  311. * Processes 'leave' events
  312. */
  313. var LeaveEvents = function() {
  314. // Default options for 'leave' event
  315. var leaveDefaultOptions = {};
  316. function getLeaveObserverConfig() {
  317. var config = {
  318. childList: true,
  319. subtree: true
  320. };
  321. return config;
  322. }
  323. function onLeaveMutation(mutations, registrationData) {
  324. mutations.forEach(function( mutation ) {
  325. var removedNodes = mutation.removedNodes,
  326. callbacksToBeCalled = [];
  327. if( removedNodes !== null && removedNodes.length > 0 ) {
  328. utils.checkChildNodesRecursively(removedNodes, registrationData, nodeMatchFunc, callbacksToBeCalled);
  329. }
  330. utils.callCallbacks(callbacksToBeCalled, registrationData);
  331. });
  332. }
  333. function nodeMatchFunc(node, registrationData) {
  334. return utils.matchesSelector(node, registrationData.selector);
  335. }
  336. leaveEvents = new MutationEvents(getLeaveObserverConfig, onLeaveMutation);
  337. var mutationBindEvent = leaveEvents.bindEvent;
  338. // override bindEvent function
  339. leaveEvents.bindEvent = function(selector, options, callback) {
  340. if (typeof callback === "undefined") {
  341. callback = options;
  342. options = leaveDefaultOptions;
  343. } else {
  344. options = utils.mergeArrays(leaveDefaultOptions, options);
  345. }
  346. mutationBindEvent.call(this, selector, options, callback);
  347. };
  348. return leaveEvents;
  349. };
  350. var arriveEvents = new ArriveEvents(),
  351. leaveEvents = new LeaveEvents();
  352. function exposeUnbindApi(eventObj, exposeTo, funcName) {
  353. // expose unbind function with function overriding
  354. utils.addMethod(exposeTo, funcName, eventObj.unbindEvent);
  355. utils.addMethod(exposeTo, funcName, eventObj.unbindEventWithSelectorOrCallback);
  356. utils.addMethod(exposeTo, funcName, eventObj.unbindEventWithSelectorAndCallback);
  357. }
  358. /*** expose APIs ***/
  359. function exposeApi(exposeTo) {
  360. exposeTo.arrive = arriveEvents.bindEvent;
  361. exposeUnbindApi(arriveEvents, exposeTo, "unbindArrive");
  362. exposeTo.leave = leaveEvents.bindEvent;
  363. exposeUnbindApi(leaveEvents, exposeTo, "unbindLeave");
  364. }
  365. if ($) {
  366. exposeApi($.fn);
  367. }
  368. exposeApi(HTMLElement.prototype);
  369. exposeApi(NodeList.prototype);
  370. exposeApi(HTMLCollection.prototype);
  371. exposeApi(HTMLDocument.prototype);
  372. exposeApi(Window.prototype);
  373. var Arrive = {};
  374. // expose functions to unbind all arrive/leave events
  375. exposeUnbindApi(arriveEvents, Arrive, "unbindAllArrive");
  376. exposeUnbindApi(leaveEvents, Arrive, "unbindAllLeave");
  377. return Arrive;
  378. })(window, typeof jQuery === 'undefined' ? null : jQuery, undefined);