arrive.js 2.5.2

A lightweight JS library for watching DOM element creation and removal using Mutation Observers.

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/519079/1506007/arrivejs%20252.js

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