visibility.js 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313
  1. /*!
  2. * # Fomantic-UI - Visibility
  3. * http://github.com/fomantic/Fomantic-UI/
  4. *
  5. *
  6. * Released under the MIT license
  7. * http://opensource.org/licenses/MIT
  8. *
  9. */
  10. ;(function ($, window, document, undefined) {
  11. 'use strict';
  12. $.isFunction = $.isFunction || function(obj) {
  13. return typeof obj === "function" && typeof obj.nodeType !== "number";
  14. };
  15. window = (typeof window != 'undefined' && window.Math == Math)
  16. ? window
  17. : (typeof self != 'undefined' && self.Math == Math)
  18. ? self
  19. : Function('return this')()
  20. ;
  21. $.fn.visibility = function(parameters) {
  22. var
  23. $allModules = $(this),
  24. moduleSelector = $allModules.selector || '',
  25. time = new Date().getTime(),
  26. performance = [],
  27. query = arguments[0],
  28. methodInvoked = (typeof query == 'string'),
  29. queryArguments = [].slice.call(arguments, 1),
  30. returnedValue,
  31. moduleCount = $allModules.length,
  32. loadedCount = 0
  33. ;
  34. $allModules
  35. .each(function() {
  36. var
  37. settings = ( $.isPlainObject(parameters) )
  38. ? $.extend(true, {}, $.fn.visibility.settings, parameters)
  39. : $.extend({}, $.fn.visibility.settings),
  40. className = settings.className,
  41. namespace = settings.namespace,
  42. error = settings.error,
  43. metadata = settings.metadata,
  44. eventNamespace = '.' + namespace,
  45. moduleNamespace = 'module-' + namespace,
  46. $window = $(window),
  47. $module = $(this),
  48. $context = $(settings.context),
  49. $placeholder,
  50. instance = $module.data(moduleNamespace),
  51. requestAnimationFrame = window.requestAnimationFrame
  52. || window.mozRequestAnimationFrame
  53. || window.webkitRequestAnimationFrame
  54. || window.msRequestAnimationFrame
  55. || function(callback) { setTimeout(callback, 0); },
  56. element = this,
  57. disabled = false,
  58. contextObserver,
  59. observer,
  60. module
  61. ;
  62. module = {
  63. initialize: function() {
  64. module.debug('Initializing', settings);
  65. module.setup.cache();
  66. if( module.should.trackChanges() ) {
  67. if(settings.type == 'image') {
  68. module.setup.image();
  69. }
  70. if(settings.type == 'fixed') {
  71. module.setup.fixed();
  72. }
  73. if(settings.observeChanges) {
  74. module.observeChanges();
  75. }
  76. module.bind.events();
  77. }
  78. module.save.position();
  79. if( !module.is.visible() ) {
  80. module.error(error.visible, $module);
  81. }
  82. if(settings.initialCheck) {
  83. module.checkVisibility();
  84. }
  85. module.instantiate();
  86. },
  87. instantiate: function() {
  88. module.debug('Storing instance', module);
  89. $module
  90. .data(moduleNamespace, module)
  91. ;
  92. instance = module;
  93. },
  94. destroy: function() {
  95. module.verbose('Destroying previous module');
  96. if(observer) {
  97. observer.disconnect();
  98. }
  99. if(contextObserver) {
  100. contextObserver.disconnect();
  101. }
  102. $window
  103. .off('load' + eventNamespace, module.event.load)
  104. .off('resize' + eventNamespace, module.event.resize)
  105. ;
  106. $context
  107. .off('scroll' + eventNamespace, module.event.scroll)
  108. .off('scrollchange' + eventNamespace, module.event.scrollchange)
  109. ;
  110. if(settings.type == 'fixed') {
  111. module.resetFixed();
  112. module.remove.placeholder();
  113. }
  114. $module
  115. .off(eventNamespace)
  116. .removeData(moduleNamespace)
  117. ;
  118. },
  119. observeChanges: function() {
  120. if('MutationObserver' in window) {
  121. contextObserver = new MutationObserver(module.event.contextChanged);
  122. observer = new MutationObserver(module.event.changed);
  123. contextObserver.observe(document, {
  124. childList : true,
  125. subtree : true
  126. });
  127. observer.observe(element, {
  128. childList : true,
  129. subtree : true
  130. });
  131. module.debug('Setting up mutation observer', observer);
  132. }
  133. },
  134. bind: {
  135. events: function() {
  136. module.verbose('Binding visibility events to scroll and resize');
  137. if(settings.refreshOnLoad) {
  138. $window
  139. .on('load' + eventNamespace, module.event.load)
  140. ;
  141. }
  142. $window
  143. .on('resize' + eventNamespace, module.event.resize)
  144. ;
  145. // pub/sub pattern
  146. $context
  147. .off('scroll' + eventNamespace)
  148. .on('scroll' + eventNamespace, module.event.scroll)
  149. .on('scrollchange' + eventNamespace, module.event.scrollchange)
  150. ;
  151. }
  152. },
  153. event: {
  154. changed: function(mutations) {
  155. module.verbose('DOM tree modified, updating visibility calculations');
  156. module.timer = setTimeout(function() {
  157. module.verbose('DOM tree modified, updating sticky menu');
  158. module.refresh();
  159. }, 100);
  160. },
  161. contextChanged: function(mutations) {
  162. [].forEach.call(mutations, function(mutation) {
  163. if(mutation.removedNodes) {
  164. [].forEach.call(mutation.removedNodes, function(node) {
  165. if(node == element || $(node).find(element).length > 0) {
  166. module.debug('Element removed from DOM, tearing down events');
  167. module.destroy();
  168. }
  169. });
  170. }
  171. });
  172. },
  173. resize: function() {
  174. module.debug('Window resized');
  175. if(settings.refreshOnResize) {
  176. requestAnimationFrame(module.refresh);
  177. }
  178. },
  179. load: function() {
  180. module.debug('Page finished loading');
  181. requestAnimationFrame(module.refresh);
  182. },
  183. // publishes scrollchange event on one scroll
  184. scroll: function() {
  185. if(settings.throttle) {
  186. clearTimeout(module.timer);
  187. module.timer = setTimeout(function() {
  188. $context.triggerHandler('scrollchange' + eventNamespace, [ $context.scrollTop() ]);
  189. }, settings.throttle);
  190. }
  191. else {
  192. requestAnimationFrame(function() {
  193. $context.triggerHandler('scrollchange' + eventNamespace, [ $context.scrollTop() ]);
  194. });
  195. }
  196. },
  197. // subscribes to scrollchange
  198. scrollchange: function(event, scrollPosition) {
  199. module.checkVisibility(scrollPosition);
  200. },
  201. },
  202. precache: function(images, callback) {
  203. if (!(images instanceof Array)) {
  204. images = [images];
  205. }
  206. var
  207. imagesLength = images.length,
  208. loadedCounter = 0,
  209. cache = [],
  210. cacheImage = document.createElement('img'),
  211. handleLoad = function() {
  212. loadedCounter++;
  213. if (loadedCounter >= images.length) {
  214. if ($.isFunction(callback)) {
  215. callback();
  216. }
  217. }
  218. }
  219. ;
  220. while (imagesLength--) {
  221. cacheImage = document.createElement('img');
  222. cacheImage.onload = handleLoad;
  223. cacheImage.onerror = handleLoad;
  224. cacheImage.src = images[imagesLength];
  225. cache.push(cacheImage);
  226. }
  227. },
  228. enableCallbacks: function() {
  229. module.debug('Allowing callbacks to occur');
  230. disabled = false;
  231. },
  232. disableCallbacks: function() {
  233. module.debug('Disabling all callbacks temporarily');
  234. disabled = true;
  235. },
  236. should: {
  237. trackChanges: function() {
  238. if(methodInvoked) {
  239. module.debug('One time query, no need to bind events');
  240. return false;
  241. }
  242. module.debug('Callbacks being attached');
  243. return true;
  244. }
  245. },
  246. setup: {
  247. cache: function() {
  248. module.cache = {
  249. occurred : {},
  250. screen : {},
  251. element : {},
  252. };
  253. },
  254. image: function() {
  255. var
  256. src = $module.data(metadata.src)
  257. ;
  258. if(src) {
  259. module.verbose('Lazy loading image', src);
  260. settings.once = true;
  261. settings.observeChanges = false;
  262. // show when top visible
  263. settings.onOnScreen = function() {
  264. module.debug('Image on screen', element);
  265. module.precache(src, function() {
  266. module.set.image(src, function() {
  267. loadedCount++;
  268. if(loadedCount == moduleCount) {
  269. settings.onAllLoaded.call(this);
  270. }
  271. settings.onLoad.call(this);
  272. });
  273. });
  274. };
  275. }
  276. },
  277. fixed: function() {
  278. module.debug('Setting up fixed');
  279. settings.once = false;
  280. settings.observeChanges = false;
  281. settings.initialCheck = true;
  282. settings.refreshOnLoad = true;
  283. if(!parameters.transition) {
  284. settings.transition = false;
  285. }
  286. module.create.placeholder();
  287. module.debug('Added placeholder', $placeholder);
  288. settings.onTopPassed = function() {
  289. module.debug('Element passed, adding fixed position', $module);
  290. module.show.placeholder();
  291. module.set.fixed();
  292. if(settings.transition) {
  293. if($.fn.transition !== undefined) {
  294. $module.transition(settings.transition, settings.duration);
  295. }
  296. }
  297. };
  298. settings.onTopPassedReverse = function() {
  299. module.debug('Element returned to position, removing fixed', $module);
  300. module.hide.placeholder();
  301. module.remove.fixed();
  302. };
  303. }
  304. },
  305. create: {
  306. placeholder: function() {
  307. module.verbose('Creating fixed position placeholder');
  308. $placeholder = $module
  309. .clone(false)
  310. .css('display', 'none')
  311. .addClass(className.placeholder)
  312. .insertAfter($module)
  313. ;
  314. }
  315. },
  316. show: {
  317. placeholder: function() {
  318. module.verbose('Showing placeholder');
  319. $placeholder
  320. .css('display', 'block')
  321. .css('visibility', 'hidden')
  322. ;
  323. }
  324. },
  325. hide: {
  326. placeholder: function() {
  327. module.verbose('Hiding placeholder');
  328. $placeholder
  329. .css('display', 'none')
  330. .css('visibility', '')
  331. ;
  332. }
  333. },
  334. set: {
  335. fixed: function() {
  336. module.verbose('Setting element to fixed position');
  337. $module
  338. .addClass(className.fixed)
  339. .css({
  340. position : 'fixed',
  341. top : settings.offset + 'px',
  342. left : 'auto',
  343. zIndex : settings.zIndex
  344. })
  345. ;
  346. settings.onFixed.call(element);
  347. },
  348. image: function(src, callback) {
  349. $module
  350. .attr('src', src)
  351. ;
  352. if(settings.transition) {
  353. if( $.fn.transition !== undefined) {
  354. if($module.hasClass(className.visible)) {
  355. module.debug('Transition already occurred on this image, skipping animation');
  356. return;
  357. }
  358. $module.transition(settings.transition, settings.duration, callback);
  359. }
  360. else {
  361. $module.fadeIn(settings.duration, callback);
  362. }
  363. }
  364. else {
  365. $module.show();
  366. }
  367. }
  368. },
  369. is: {
  370. onScreen: function() {
  371. var
  372. calculations = module.get.elementCalculations()
  373. ;
  374. return calculations.onScreen;
  375. },
  376. offScreen: function() {
  377. var
  378. calculations = module.get.elementCalculations()
  379. ;
  380. return calculations.offScreen;
  381. },
  382. visible: function() {
  383. if(module.cache && module.cache.element) {
  384. return !(module.cache.element.width === 0 && module.cache.element.offset.top === 0);
  385. }
  386. return false;
  387. },
  388. verticallyScrollableContext: function() {
  389. var
  390. overflowY = ($context.get(0) !== window)
  391. ? $context.css('overflow-y')
  392. : false
  393. ;
  394. return (overflowY == 'auto' || overflowY == 'scroll');
  395. },
  396. horizontallyScrollableContext: function() {
  397. var
  398. overflowX = ($context.get(0) !== window)
  399. ? $context.css('overflow-x')
  400. : false
  401. ;
  402. return (overflowX == 'auto' || overflowX == 'scroll');
  403. }
  404. },
  405. refresh: function() {
  406. module.debug('Refreshing constants (width/height)');
  407. if(settings.type == 'fixed') {
  408. module.resetFixed();
  409. }
  410. module.reset();
  411. module.save.position();
  412. if(settings.checkOnRefresh) {
  413. module.checkVisibility();
  414. }
  415. settings.onRefresh.call(element);
  416. },
  417. resetFixed: function () {
  418. module.remove.fixed();
  419. module.remove.occurred();
  420. },
  421. reset: function() {
  422. module.verbose('Resetting all cached values');
  423. if( $.isPlainObject(module.cache) ) {
  424. module.cache.screen = {};
  425. module.cache.element = {};
  426. }
  427. },
  428. checkVisibility: function(scroll) {
  429. module.verbose('Checking visibility of element', module.cache.element);
  430. if( !disabled && module.is.visible() ) {
  431. // save scroll position
  432. module.save.scroll(scroll);
  433. // update calculations derived from scroll
  434. module.save.calculations();
  435. // percentage
  436. module.passed();
  437. // reverse (must be first)
  438. module.passingReverse();
  439. module.topVisibleReverse();
  440. module.bottomVisibleReverse();
  441. module.topPassedReverse();
  442. module.bottomPassedReverse();
  443. // one time
  444. module.onScreen();
  445. module.offScreen();
  446. module.passing();
  447. module.topVisible();
  448. module.bottomVisible();
  449. module.topPassed();
  450. module.bottomPassed();
  451. // on update callback
  452. if(settings.onUpdate) {
  453. settings.onUpdate.call(element, module.get.elementCalculations());
  454. }
  455. }
  456. },
  457. passed: function(amount, newCallback) {
  458. var
  459. calculations = module.get.elementCalculations()
  460. ;
  461. // assign callback
  462. if(amount && newCallback) {
  463. settings.onPassed[amount] = newCallback;
  464. }
  465. else if(amount !== undefined) {
  466. return (module.get.pixelsPassed(amount) > calculations.pixelsPassed);
  467. }
  468. else if(calculations.passing) {
  469. $.each(settings.onPassed, function(amount, callback) {
  470. if(calculations.bottomVisible || calculations.pixelsPassed > module.get.pixelsPassed(amount)) {
  471. module.execute(callback, amount);
  472. }
  473. else if(!settings.once) {
  474. module.remove.occurred(callback);
  475. }
  476. });
  477. }
  478. },
  479. onScreen: function(newCallback) {
  480. var
  481. calculations = module.get.elementCalculations(),
  482. callback = newCallback || settings.onOnScreen,
  483. callbackName = 'onScreen'
  484. ;
  485. if(newCallback) {
  486. module.debug('Adding callback for onScreen', newCallback);
  487. settings.onOnScreen = newCallback;
  488. }
  489. if(calculations.onScreen) {
  490. module.execute(callback, callbackName);
  491. }
  492. else if(!settings.once) {
  493. module.remove.occurred(callbackName);
  494. }
  495. if(newCallback !== undefined) {
  496. return calculations.onOnScreen;
  497. }
  498. },
  499. offScreen: function(newCallback) {
  500. var
  501. calculations = module.get.elementCalculations(),
  502. callback = newCallback || settings.onOffScreen,
  503. callbackName = 'offScreen'
  504. ;
  505. if(newCallback) {
  506. module.debug('Adding callback for offScreen', newCallback);
  507. settings.onOffScreen = newCallback;
  508. }
  509. if(calculations.offScreen) {
  510. module.execute(callback, callbackName);
  511. }
  512. else if(!settings.once) {
  513. module.remove.occurred(callbackName);
  514. }
  515. if(newCallback !== undefined) {
  516. return calculations.onOffScreen;
  517. }
  518. },
  519. passing: function(newCallback) {
  520. var
  521. calculations = module.get.elementCalculations(),
  522. callback = newCallback || settings.onPassing,
  523. callbackName = 'passing'
  524. ;
  525. if(newCallback) {
  526. module.debug('Adding callback for passing', newCallback);
  527. settings.onPassing = newCallback;
  528. }
  529. if(calculations.passing) {
  530. module.execute(callback, callbackName);
  531. }
  532. else if(!settings.once) {
  533. module.remove.occurred(callbackName);
  534. }
  535. if(newCallback !== undefined) {
  536. return calculations.passing;
  537. }
  538. },
  539. topVisible: function(newCallback) {
  540. var
  541. calculations = module.get.elementCalculations(),
  542. callback = newCallback || settings.onTopVisible,
  543. callbackName = 'topVisible'
  544. ;
  545. if(newCallback) {
  546. module.debug('Adding callback for top visible', newCallback);
  547. settings.onTopVisible = newCallback;
  548. }
  549. if(calculations.topVisible) {
  550. module.execute(callback, callbackName);
  551. }
  552. else if(!settings.once) {
  553. module.remove.occurred(callbackName);
  554. }
  555. if(newCallback === undefined) {
  556. return calculations.topVisible;
  557. }
  558. },
  559. bottomVisible: function(newCallback) {
  560. var
  561. calculations = module.get.elementCalculations(),
  562. callback = newCallback || settings.onBottomVisible,
  563. callbackName = 'bottomVisible'
  564. ;
  565. if(newCallback) {
  566. module.debug('Adding callback for bottom visible', newCallback);
  567. settings.onBottomVisible = newCallback;
  568. }
  569. if(calculations.bottomVisible) {
  570. module.execute(callback, callbackName);
  571. }
  572. else if(!settings.once) {
  573. module.remove.occurred(callbackName);
  574. }
  575. if(newCallback === undefined) {
  576. return calculations.bottomVisible;
  577. }
  578. },
  579. topPassed: function(newCallback) {
  580. var
  581. calculations = module.get.elementCalculations(),
  582. callback = newCallback || settings.onTopPassed,
  583. callbackName = 'topPassed'
  584. ;
  585. if(newCallback) {
  586. module.debug('Adding callback for top passed', newCallback);
  587. settings.onTopPassed = newCallback;
  588. }
  589. if(calculations.topPassed) {
  590. module.execute(callback, callbackName);
  591. }
  592. else if(!settings.once) {
  593. module.remove.occurred(callbackName);
  594. }
  595. if(newCallback === undefined) {
  596. return calculations.topPassed;
  597. }
  598. },
  599. bottomPassed: function(newCallback) {
  600. var
  601. calculations = module.get.elementCalculations(),
  602. callback = newCallback || settings.onBottomPassed,
  603. callbackName = 'bottomPassed'
  604. ;
  605. if(newCallback) {
  606. module.debug('Adding callback for bottom passed', newCallback);
  607. settings.onBottomPassed = newCallback;
  608. }
  609. if(calculations.bottomPassed) {
  610. module.execute(callback, callbackName);
  611. }
  612. else if(!settings.once) {
  613. module.remove.occurred(callbackName);
  614. }
  615. if(newCallback === undefined) {
  616. return calculations.bottomPassed;
  617. }
  618. },
  619. passingReverse: function(newCallback) {
  620. var
  621. calculations = module.get.elementCalculations(),
  622. callback = newCallback || settings.onPassingReverse,
  623. callbackName = 'passingReverse'
  624. ;
  625. if(newCallback) {
  626. module.debug('Adding callback for passing reverse', newCallback);
  627. settings.onPassingReverse = newCallback;
  628. }
  629. if(!calculations.passing) {
  630. if(module.get.occurred('passing')) {
  631. module.execute(callback, callbackName);
  632. }
  633. }
  634. else if(!settings.once) {
  635. module.remove.occurred(callbackName);
  636. }
  637. if(newCallback !== undefined) {
  638. return !calculations.passing;
  639. }
  640. },
  641. topVisibleReverse: function(newCallback) {
  642. var
  643. calculations = module.get.elementCalculations(),
  644. callback = newCallback || settings.onTopVisibleReverse,
  645. callbackName = 'topVisibleReverse'
  646. ;
  647. if(newCallback) {
  648. module.debug('Adding callback for top visible reverse', newCallback);
  649. settings.onTopVisibleReverse = newCallback;
  650. }
  651. if(!calculations.topVisible) {
  652. if(module.get.occurred('topVisible')) {
  653. module.execute(callback, callbackName);
  654. }
  655. }
  656. else if(!settings.once) {
  657. module.remove.occurred(callbackName);
  658. }
  659. if(newCallback === undefined) {
  660. return !calculations.topVisible;
  661. }
  662. },
  663. bottomVisibleReverse: function(newCallback) {
  664. var
  665. calculations = module.get.elementCalculations(),
  666. callback = newCallback || settings.onBottomVisibleReverse,
  667. callbackName = 'bottomVisibleReverse'
  668. ;
  669. if(newCallback) {
  670. module.debug('Adding callback for bottom visible reverse', newCallback);
  671. settings.onBottomVisibleReverse = newCallback;
  672. }
  673. if(!calculations.bottomVisible) {
  674. if(module.get.occurred('bottomVisible')) {
  675. module.execute(callback, callbackName);
  676. }
  677. }
  678. else if(!settings.once) {
  679. module.remove.occurred(callbackName);
  680. }
  681. if(newCallback === undefined) {
  682. return !calculations.bottomVisible;
  683. }
  684. },
  685. topPassedReverse: function(newCallback) {
  686. var
  687. calculations = module.get.elementCalculations(),
  688. callback = newCallback || settings.onTopPassedReverse,
  689. callbackName = 'topPassedReverse'
  690. ;
  691. if(newCallback) {
  692. module.debug('Adding callback for top passed reverse', newCallback);
  693. settings.onTopPassedReverse = newCallback;
  694. }
  695. if(!calculations.topPassed) {
  696. if(module.get.occurred('topPassed')) {
  697. module.execute(callback, callbackName);
  698. }
  699. }
  700. else if(!settings.once) {
  701. module.remove.occurred(callbackName);
  702. }
  703. if(newCallback === undefined) {
  704. return !calculations.onTopPassed;
  705. }
  706. },
  707. bottomPassedReverse: function(newCallback) {
  708. var
  709. calculations = module.get.elementCalculations(),
  710. callback = newCallback || settings.onBottomPassedReverse,
  711. callbackName = 'bottomPassedReverse'
  712. ;
  713. if(newCallback) {
  714. module.debug('Adding callback for bottom passed reverse', newCallback);
  715. settings.onBottomPassedReverse = newCallback;
  716. }
  717. if(!calculations.bottomPassed) {
  718. if(module.get.occurred('bottomPassed')) {
  719. module.execute(callback, callbackName);
  720. }
  721. }
  722. else if(!settings.once) {
  723. module.remove.occurred(callbackName);
  724. }
  725. if(newCallback === undefined) {
  726. return !calculations.bottomPassed;
  727. }
  728. },
  729. execute: function(callback, callbackName) {
  730. var
  731. calculations = module.get.elementCalculations(),
  732. screen = module.get.screenCalculations()
  733. ;
  734. callback = callback || false;
  735. if(callback) {
  736. if(settings.continuous) {
  737. module.debug('Callback being called continuously', callbackName, calculations);
  738. callback.call(element, calculations, screen);
  739. }
  740. else if(!module.get.occurred(callbackName)) {
  741. module.debug('Conditions met', callbackName, calculations);
  742. callback.call(element, calculations, screen);
  743. }
  744. }
  745. module.save.occurred(callbackName);
  746. },
  747. remove: {
  748. fixed: function() {
  749. module.debug('Removing fixed position');
  750. $module
  751. .removeClass(className.fixed)
  752. .css({
  753. position : '',
  754. top : '',
  755. left : '',
  756. zIndex : ''
  757. })
  758. ;
  759. settings.onUnfixed.call(element);
  760. },
  761. placeholder: function() {
  762. module.debug('Removing placeholder content');
  763. if($placeholder) {
  764. $placeholder.remove();
  765. }
  766. },
  767. occurred: function(callback) {
  768. if(callback) {
  769. var
  770. occurred = module.cache.occurred
  771. ;
  772. if(occurred[callback] !== undefined && occurred[callback] === true) {
  773. module.debug('Callback can now be called again', callback);
  774. module.cache.occurred[callback] = false;
  775. }
  776. }
  777. else {
  778. module.cache.occurred = {};
  779. }
  780. }
  781. },
  782. save: {
  783. calculations: function() {
  784. module.verbose('Saving all calculations necessary to determine positioning');
  785. module.save.direction();
  786. module.save.screenCalculations();
  787. module.save.elementCalculations();
  788. },
  789. occurred: function(callback) {
  790. if(callback) {
  791. if(module.cache.occurred[callback] === undefined || (module.cache.occurred[callback] !== true)) {
  792. module.verbose('Saving callback occurred', callback);
  793. module.cache.occurred[callback] = true;
  794. }
  795. }
  796. },
  797. scroll: function(scrollPosition) {
  798. scrollPosition = scrollPosition + settings.offset || $context.scrollTop() + settings.offset;
  799. module.cache.scroll = scrollPosition;
  800. },
  801. direction: function() {
  802. var
  803. scroll = module.get.scroll(),
  804. lastScroll = module.get.lastScroll(),
  805. direction
  806. ;
  807. if(scroll > lastScroll && lastScroll) {
  808. direction = 'down';
  809. }
  810. else if(scroll < lastScroll && lastScroll) {
  811. direction = 'up';
  812. }
  813. else {
  814. direction = 'static';
  815. }
  816. module.cache.direction = direction;
  817. return module.cache.direction;
  818. },
  819. elementPosition: function() {
  820. var
  821. element = module.cache.element,
  822. screen = module.get.screenSize()
  823. ;
  824. module.verbose('Saving element position');
  825. // (quicker than $.extend)
  826. element.fits = (element.height < screen.height);
  827. element.offset = $module.offset();
  828. element.width = $module.outerWidth();
  829. element.height = $module.outerHeight();
  830. // compensate for scroll in context
  831. if(module.is.verticallyScrollableContext()) {
  832. element.offset.top += $context.scrollTop() - $context.offset().top;
  833. }
  834. if(module.is.horizontallyScrollableContext()) {
  835. element.offset.left += $context.scrollLeft - $context.offset().left;
  836. }
  837. // store
  838. module.cache.element = element;
  839. return element;
  840. },
  841. elementCalculations: function() {
  842. var
  843. screen = module.get.screenCalculations(),
  844. element = module.get.elementPosition()
  845. ;
  846. // offset
  847. if(settings.includeMargin) {
  848. element.margin = {};
  849. element.margin.top = parseInt($module.css('margin-top'), 10);
  850. element.margin.bottom = parseInt($module.css('margin-bottom'), 10);
  851. element.top = element.offset.top - element.margin.top;
  852. element.bottom = element.offset.top + element.height + element.margin.bottom;
  853. }
  854. else {
  855. element.top = element.offset.top;
  856. element.bottom = element.offset.top + element.height;
  857. }
  858. // visibility
  859. element.topPassed = (screen.top >= element.top);
  860. element.bottomPassed = (screen.top >= element.bottom);
  861. element.topVisible = (screen.bottom >= element.top) && !element.topPassed;
  862. element.bottomVisible = (screen.bottom >= element.bottom) && !element.bottomPassed;
  863. element.pixelsPassed = 0;
  864. element.percentagePassed = 0;
  865. // meta calculations
  866. element.onScreen = ((element.topVisible || element.passing) && !element.bottomPassed);
  867. element.passing = (element.topPassed && !element.bottomPassed);
  868. element.offScreen = (!element.onScreen);
  869. // passing calculations
  870. if(element.passing) {
  871. element.pixelsPassed = (screen.top - element.top);
  872. element.percentagePassed = (screen.top - element.top) / element.height;
  873. }
  874. module.cache.element = element;
  875. module.verbose('Updated element calculations', element);
  876. return element;
  877. },
  878. screenCalculations: function() {
  879. var
  880. scroll = module.get.scroll()
  881. ;
  882. module.save.direction();
  883. module.cache.screen.top = scroll;
  884. module.cache.screen.bottom = scroll + module.cache.screen.height;
  885. return module.cache.screen;
  886. },
  887. screenSize: function() {
  888. module.verbose('Saving window position');
  889. module.cache.screen = {
  890. height: $context.height()
  891. };
  892. },
  893. position: function() {
  894. module.save.screenSize();
  895. module.save.elementPosition();
  896. }
  897. },
  898. get: {
  899. pixelsPassed: function(amount) {
  900. var
  901. element = module.get.elementCalculations()
  902. ;
  903. if(amount.search('%') > -1) {
  904. return ( element.height * (parseInt(amount, 10) / 100) );
  905. }
  906. return parseInt(amount, 10);
  907. },
  908. occurred: function(callback) {
  909. return (module.cache.occurred !== undefined)
  910. ? module.cache.occurred[callback] || false
  911. : false
  912. ;
  913. },
  914. direction: function() {
  915. if(module.cache.direction === undefined) {
  916. module.save.direction();
  917. }
  918. return module.cache.direction;
  919. },
  920. elementPosition: function() {
  921. if(module.cache.element === undefined) {
  922. module.save.elementPosition();
  923. }
  924. return module.cache.element;
  925. },
  926. elementCalculations: function() {
  927. if(module.cache.element === undefined) {
  928. module.save.elementCalculations();
  929. }
  930. return module.cache.element;
  931. },
  932. screenCalculations: function() {
  933. if(module.cache.screen === undefined) {
  934. module.save.screenCalculations();
  935. }
  936. return module.cache.screen;
  937. },
  938. screenSize: function() {
  939. if(module.cache.screen === undefined) {
  940. module.save.screenSize();
  941. }
  942. return module.cache.screen;
  943. },
  944. scroll: function() {
  945. if(module.cache.scroll === undefined) {
  946. module.save.scroll();
  947. }
  948. return module.cache.scroll;
  949. },
  950. lastScroll: function() {
  951. if(module.cache.screen === undefined) {
  952. module.debug('First scroll event, no last scroll could be found');
  953. return false;
  954. }
  955. return module.cache.screen.top;
  956. }
  957. },
  958. setting: function(name, value) {
  959. if( $.isPlainObject(name) ) {
  960. $.extend(true, settings, name);
  961. }
  962. else if(value !== undefined) {
  963. settings[name] = value;
  964. }
  965. else {
  966. return settings[name];
  967. }
  968. },
  969. internal: function(name, value) {
  970. if( $.isPlainObject(name) ) {
  971. $.extend(true, module, name);
  972. }
  973. else if(value !== undefined) {
  974. module[name] = value;
  975. }
  976. else {
  977. return module[name];
  978. }
  979. },
  980. debug: function() {
  981. if(!settings.silent && settings.debug) {
  982. if(settings.performance) {
  983. module.performance.log(arguments);
  984. }
  985. else {
  986. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  987. module.debug.apply(console, arguments);
  988. }
  989. }
  990. },
  991. verbose: function() {
  992. if(!settings.silent && settings.verbose && settings.debug) {
  993. if(settings.performance) {
  994. module.performance.log(arguments);
  995. }
  996. else {
  997. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  998. module.verbose.apply(console, arguments);
  999. }
  1000. }
  1001. },
  1002. error: function() {
  1003. if(!settings.silent) {
  1004. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  1005. module.error.apply(console, arguments);
  1006. }
  1007. },
  1008. performance: {
  1009. log: function(message) {
  1010. var
  1011. currentTime,
  1012. executionTime,
  1013. previousTime
  1014. ;
  1015. if(settings.performance) {
  1016. currentTime = new Date().getTime();
  1017. previousTime = time || currentTime;
  1018. executionTime = currentTime - previousTime;
  1019. time = currentTime;
  1020. performance.push({
  1021. 'Name' : message[0],
  1022. 'Arguments' : [].slice.call(message, 1) || '',
  1023. 'Element' : element,
  1024. 'Execution Time' : executionTime
  1025. });
  1026. }
  1027. clearTimeout(module.performance.timer);
  1028. module.performance.timer = setTimeout(module.performance.display, 500);
  1029. },
  1030. display: function() {
  1031. var
  1032. title = settings.name + ':',
  1033. totalTime = 0
  1034. ;
  1035. time = false;
  1036. clearTimeout(module.performance.timer);
  1037. $.each(performance, function(index, data) {
  1038. totalTime += data['Execution Time'];
  1039. });
  1040. title += ' ' + totalTime + 'ms';
  1041. if(moduleSelector) {
  1042. title += ' \'' + moduleSelector + '\'';
  1043. }
  1044. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  1045. console.groupCollapsed(title);
  1046. if(console.table) {
  1047. console.table(performance);
  1048. }
  1049. else {
  1050. $.each(performance, function(index, data) {
  1051. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  1052. });
  1053. }
  1054. console.groupEnd();
  1055. }
  1056. performance = [];
  1057. }
  1058. },
  1059. invoke: function(query, passedArguments, context) {
  1060. var
  1061. object = instance,
  1062. maxDepth,
  1063. found,
  1064. response
  1065. ;
  1066. passedArguments = passedArguments || queryArguments;
  1067. context = element || context;
  1068. if(typeof query == 'string' && object !== undefined) {
  1069. query = query.split(/[\. ]/);
  1070. maxDepth = query.length - 1;
  1071. $.each(query, function(depth, value) {
  1072. var camelCaseValue = (depth != maxDepth)
  1073. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  1074. : query
  1075. ;
  1076. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  1077. object = object[camelCaseValue];
  1078. }
  1079. else if( object[camelCaseValue] !== undefined ) {
  1080. found = object[camelCaseValue];
  1081. return false;
  1082. }
  1083. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  1084. object = object[value];
  1085. }
  1086. else if( object[value] !== undefined ) {
  1087. found = object[value];
  1088. return false;
  1089. }
  1090. else {
  1091. module.error(error.method, query);
  1092. return false;
  1093. }
  1094. });
  1095. }
  1096. if ( $.isFunction( found ) ) {
  1097. response = found.apply(context, passedArguments);
  1098. }
  1099. else if(found !== undefined) {
  1100. response = found;
  1101. }
  1102. if(Array.isArray(returnedValue)) {
  1103. returnedValue.push(response);
  1104. }
  1105. else if(returnedValue !== undefined) {
  1106. returnedValue = [returnedValue, response];
  1107. }
  1108. else if(response !== undefined) {
  1109. returnedValue = response;
  1110. }
  1111. return found;
  1112. }
  1113. };
  1114. if(methodInvoked) {
  1115. if(instance === undefined) {
  1116. module.initialize();
  1117. }
  1118. instance.save.scroll();
  1119. instance.save.calculations();
  1120. module.invoke(query);
  1121. }
  1122. else {
  1123. if(instance !== undefined) {
  1124. instance.invoke('destroy');
  1125. }
  1126. module.initialize();
  1127. }
  1128. })
  1129. ;
  1130. return (returnedValue !== undefined)
  1131. ? returnedValue
  1132. : this
  1133. ;
  1134. };
  1135. $.fn.visibility.settings = {
  1136. name : 'Visibility',
  1137. namespace : 'visibility',
  1138. debug : false,
  1139. verbose : false,
  1140. performance : true,
  1141. // whether to use mutation observers to follow changes
  1142. observeChanges : true,
  1143. // check position immediately on init
  1144. initialCheck : true,
  1145. // whether to refresh calculations after all page images load
  1146. refreshOnLoad : true,
  1147. // whether to refresh calculations after page resize event
  1148. refreshOnResize : true,
  1149. // should call callbacks on refresh event (resize, etc)
  1150. checkOnRefresh : true,
  1151. // callback should only occur one time
  1152. once : true,
  1153. // callback should fire continuously whe evaluates to true
  1154. continuous : false,
  1155. // offset to use with scroll top
  1156. offset : 0,
  1157. // whether to include margin in elements position
  1158. includeMargin : false,
  1159. // scroll context for visibility checks
  1160. context : window,
  1161. // visibility check delay in ms (defaults to animationFrame)
  1162. throttle : false,
  1163. // special visibility type (image, fixed)
  1164. type : false,
  1165. // z-index to use with visibility 'fixed'
  1166. zIndex : '10',
  1167. // image only animation settings
  1168. transition : 'fade in',
  1169. duration : 1000,
  1170. // array of callbacks for percentage
  1171. onPassed : {},
  1172. // standard callbacks
  1173. onOnScreen : false,
  1174. onOffScreen : false,
  1175. onPassing : false,
  1176. onTopVisible : false,
  1177. onBottomVisible : false,
  1178. onTopPassed : false,
  1179. onBottomPassed : false,
  1180. // reverse callbacks
  1181. onPassingReverse : false,
  1182. onTopVisibleReverse : false,
  1183. onBottomVisibleReverse : false,
  1184. onTopPassedReverse : false,
  1185. onBottomPassedReverse : false,
  1186. // special callbacks for image
  1187. onLoad : function() {},
  1188. onAllLoaded : function() {},
  1189. // special callbacks for fixed position
  1190. onFixed : function() {},
  1191. onUnfixed : function() {},
  1192. // utility callbacks
  1193. onUpdate : false, // disabled by default for performance
  1194. onRefresh : function(){},
  1195. metadata : {
  1196. src: 'src'
  1197. },
  1198. className: {
  1199. fixed : 'fixed',
  1200. placeholder : 'constraint',
  1201. visible : 'visible'
  1202. },
  1203. error : {
  1204. method : 'The method you called is not defined.',
  1205. visible : 'Element is hidden, you must call refresh after element becomes visible'
  1206. }
  1207. };
  1208. })( jQuery, window, document );