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.

772 lines
24 KiB

10 years ago
10 years ago
10 years ago
  1. /*
  2. * # Semantic - Visibility
  3. * http://github.com/semantic-org/semantic-ui/
  4. *
  5. *
  6. * Copyright 2014 Contributor
  7. * Released under the MIT license
  8. * http://opensource.org/licenses/MIT
  9. *
  10. */
  11. ;(function ( $, window, document, undefined ) {
  12. $.fn.visibility = function(parameters) {
  13. var
  14. $allModules = $(this),
  15. moduleSelector = $allModules.selector || '',
  16. time = new Date().getTime(),
  17. performance = [],
  18. query = arguments[0],
  19. methodInvoked = (typeof query == 'string'),
  20. queryArguments = [].slice.call(arguments, 1),
  21. returnedValue
  22. ;
  23. $allModules
  24. .each(function() {
  25. var
  26. settings = $.extend(true, {}, $.fn.visibility.settings, parameters),
  27. className = settings.className,
  28. namespace = settings.namespace,
  29. error = settings.error,
  30. eventNamespace = '.' + namespace,
  31. moduleNamespace = 'module-' + namespace,
  32. $module = $(this),
  33. $window = $(window),
  34. $container = $module.offsetParent(),
  35. $context,
  36. selector = $module.selector || '',
  37. instance = $module.data(moduleNamespace),
  38. requestAnimationFrame = window.requestAnimationFrame
  39. || window.mozRequestAnimationFrame
  40. || window.webkitRequestAnimationFrame
  41. || window.msRequestAnimationFrame
  42. || function(callback) { setTimeout(callback, 0); },
  43. element = this,
  44. module
  45. ;
  46. module = {
  47. initialize: function() {
  48. module.verbose('Initializing visibility', settings);
  49. module.reset();
  50. module.save.position();
  51. module.bindEvents();
  52. module.instantiate();
  53. if(settings.type == 'image') {
  54. module.setup.image();
  55. }
  56. requestAnimationFrame(module.checkVisibility);
  57. },
  58. instantiate: function() {
  59. module.verbose('Storing instance of module', module);
  60. instance = module;
  61. $module
  62. .data(moduleNamespace, module)
  63. ;
  64. },
  65. destroy: function() {
  66. module.verbose('Destroying previous module');
  67. $window
  68. .off(eventNamespace)
  69. ;
  70. $module
  71. .off(eventNamespace)
  72. .removeData(moduleNamespace)
  73. ;
  74. },
  75. bindEvents: function() {
  76. module.verbose('Binding visibility events to scroll and resize');
  77. $window
  78. .on('resize', module.event.refresh)
  79. .on('scroll', module.event.scroll)
  80. ;
  81. },
  82. event: {
  83. refresh: function() {
  84. requestAnimationFrame(module.refresh);
  85. },
  86. scroll: function() {
  87. module.verbose('Scroll position changed');
  88. if(settings.throttle) {
  89. clearTimeout(module.timer);
  90. module.timer = setTimeout(module.checkVisibility, 200);
  91. }
  92. else {
  93. requestAnimationFrame(module.checkVisibility);
  94. }
  95. }
  96. },
  97. precache: function(images, callback) {
  98. if (!(images instanceof Array)) {
  99. images = [images];
  100. }
  101. var
  102. imagesLength = images.length,
  103. loadedCounter = 0,
  104. cache = [],
  105. cacheImage = document.createElement('img'),
  106. handleLoad = function() {
  107. loadedCounter++;
  108. if (loadedCounter >= images.length) {
  109. if ($.isFunction(callback)) {
  110. callback();
  111. }
  112. }
  113. }
  114. ;
  115. while (imagesLength--) {
  116. cacheImage = document.createElement('img');
  117. cacheImage.onload = handleLoad;
  118. cacheImage.onerror = handleLoad;
  119. cacheImage.src = images[imagesLength];
  120. cache.push(cacheImage);
  121. }
  122. },
  123. setup: {
  124. image: function() {
  125. var
  126. src = $module.data('src')
  127. ;
  128. if(src) {
  129. module.verbose('Lazy loading image', src);
  130. // show when top visible
  131. module.topVisible(function() {
  132. module.precache(src, function() {
  133. module.set.image(src);
  134. settings.onTopVisible = false;
  135. });
  136. });
  137. }
  138. }
  139. },
  140. set: {
  141. image: function(src) {
  142. var
  143. offScreen = (module.cache.screen.bottom + settings.offset < module.cache.element.top)
  144. ;
  145. $module
  146. .attr('src', src)
  147. ;
  148. if(offScreen) {
  149. $module.show();
  150. }
  151. else {
  152. if($.fn.transition !== undefined) {
  153. $module.transition(settings.transition, settings.duration);
  154. }
  155. else {
  156. $module.fadeIn(settings.duration);
  157. }
  158. }
  159. }
  160. },
  161. refresh: function() {
  162. module.debug('Refreshing constants (element width/height)');
  163. module.reset();
  164. module.save.position();
  165. module.checkVisibility();
  166. $.proxy(settings.onRefresh, element)();
  167. },
  168. reset: function() {
  169. module.verbose('Reseting all cached values');
  170. module.cache = {
  171. occurred: {},
  172. screen : {},
  173. element : {}
  174. };
  175. },
  176. checkVisibility: function() {
  177. module.verbose('Checking visibility of element', module.cache.element);
  178. module.save.scroll();
  179. module.save.direction();
  180. module.save.screenCalculations();
  181. module.save.elementCalculations();
  182. module.passed();
  183. module.passing();
  184. module.topVisible();
  185. module.bottomVisible();
  186. module.topPassed();
  187. module.bottomPassed();
  188. },
  189. passed: function(amount, newCallback) {
  190. var
  191. calculations = module.get.elementCalculations(),
  192. amountInPixels
  193. ;
  194. // assign callback
  195. if(amount !== undefined && newCallback !== undefined) {
  196. settings.onPassed[amount] = newCallback;
  197. }
  198. else if(amount !== undefined) {
  199. return (module.get.pixelsPassed(amount) > calculations.pixelsPassed);
  200. }
  201. else if(calculations.passing) {
  202. $.each(settings.onPassed, function(amount, callback) {
  203. if(calculations.bottomVisible || calculations.pixelsPassed > module.get.pixelsPassed(amount)) {
  204. module.execute(callback, amount);
  205. }
  206. else if(!settings.once) {
  207. module.remove.occurred(callback);
  208. }
  209. });
  210. }
  211. },
  212. passing: function(newCallback) {
  213. var
  214. calculations = module.get.elementCalculations(),
  215. callback = newCallback || settings.onPassing,
  216. callbackName = 'passing'
  217. ;
  218. if(newCallback) {
  219. module.debug('Adding callback for passing', newCallback);
  220. settings.onPassing = newCallback;
  221. }
  222. if(callback && calculations.passing) {
  223. module.execute(callback, callbackName);
  224. }
  225. else if(!settings.once) {
  226. module.remove.occurred(callbackName);
  227. }
  228. if(newCallback !== undefined) {
  229. return calculations.passing;
  230. }
  231. },
  232. topVisible: function(newCallback) {
  233. var
  234. calculations = module.get.elementCalculations(),
  235. callback = newCallback || settings.onTopVisible,
  236. callbackName = 'topVisible'
  237. ;
  238. if(newCallback) {
  239. module.debug('Adding callback for top visible', newCallback);
  240. settings.onTopVisible = newCallback;
  241. }
  242. if(callback && calculations.topVisible) {
  243. module.execute(callback, callbackName);
  244. }
  245. else if(!settings.once) {
  246. module.remove.occurred(callbackName);
  247. }
  248. if(newCallback === undefined) {
  249. return calculations.topVisible;
  250. }
  251. },
  252. bottomVisible: function(newCallback) {
  253. var
  254. calculations = module.get.elementCalculations(),
  255. callback = newCallback || settings.onBottomVisible,
  256. callbackName = 'bottomVisible'
  257. ;
  258. if(newCallback) {
  259. module.debug('Adding callback for bottom visible', newCallback);
  260. settings.onBottomVisible = newCallback;
  261. }
  262. if(callback && calculations.bottomVisible) {
  263. module.execute(callback, callbackName);
  264. }
  265. else if(!settings.once) {
  266. module.remove.occurred(callbackName);
  267. }
  268. if(newCallback === undefined) {
  269. return calculations.bottomVisible;
  270. }
  271. },
  272. topPassed: function(newCallback) {
  273. var
  274. calculations = module.get.elementCalculations(),
  275. callback = newCallback || settings.onTopPassed,
  276. callbackName = 'topPassed'
  277. ;
  278. if(newCallback) {
  279. module.debug('Adding callback for top passed', newCallback);
  280. settings.onTopPassed = newCallback;
  281. }
  282. if(callback && calculations.topPassed) {
  283. module.execute(callback, callbackName);
  284. }
  285. else if(!settings.once) {
  286. module.remove.occurred(callbackName);
  287. }
  288. if(newCallback === undefined) {
  289. return calculations.topPassed;
  290. }
  291. },
  292. bottomPassed: function(newCallback) {
  293. var
  294. calculations = module.get.elementCalculations(),
  295. callback = newCallback || settings.onBottomPassed,
  296. callbackName = 'bottomPassed'
  297. ;
  298. if(newCallback) {
  299. module.debug('Adding callback for bottom passed', newCallback);
  300. settings.bottomPassed = newCallback;
  301. }
  302. if(callback && calculations.bottomPassed) {
  303. module.execute(callback, callbackName);
  304. }
  305. else if(!settings.once) {
  306. module.remove.occurred(callbackName);
  307. }
  308. if(newCallback === undefined) {
  309. return calculations.bottomPassed;
  310. }
  311. },
  312. execute: function(callback, callbackName) {
  313. var
  314. calculations = module.get.elementCalculations(),
  315. screen = module.get.screenCalculations()
  316. ;
  317. if(settings.continuous) {
  318. module.debug('Callback called continuously on valid', callbackName, calculations);
  319. $.proxy(callback, element)(calculations, screen);
  320. }
  321. else if(!module.get.occurred(callbackName)) {
  322. module.debug('Callback conditions met', callbackName, calculations);
  323. $.proxy(callback, element)(calculations, screen);
  324. }
  325. module.save.occurred(callbackName);
  326. },
  327. remove: {
  328. occurred: function(callback) {
  329. if(callback) {
  330. module.cache.occurred[callback] = false;
  331. }
  332. else {
  333. module.cache.occurred = {};
  334. }
  335. }
  336. },
  337. save: {
  338. occurred: function(callback) {
  339. if(callback) {
  340. module.cache.occurred[callback] = true;
  341. }
  342. },
  343. scroll: function() {
  344. module.cache.scroll = $window.scrollTop() + settings.offset;
  345. },
  346. direction: function() {
  347. var
  348. scroll = module.get.scroll(),
  349. lastScroll = module.get.lastScroll(),
  350. direction
  351. ;
  352. if(scroll > lastScroll && lastScroll) {
  353. direction = 'down';
  354. }
  355. else if(scroll < lastScroll && lastScroll) {
  356. direction = 'up';
  357. }
  358. else {
  359. direction = 'static';
  360. }
  361. module.cache.direction = direction;
  362. return module.cache.direction;
  363. },
  364. elementPosition: function() {
  365. var
  366. screen = module.get.screenSize()
  367. ;
  368. module.verbose('Saving element position');
  369. $.extend(module.cache.element, {
  370. margin : {
  371. top : parseInt($module.css('margin-top'), 10),
  372. bottom : parseInt($module.css('margin-bottom'), 10)
  373. },
  374. fits : (element.height < screen.height),
  375. offset : $module.offset(),
  376. width : $module.outerWidth(),
  377. height : $module.outerHeight()
  378. });
  379. return module.cache.element;
  380. },
  381. elementCalculations: function() {
  382. var
  383. screen = module.get.screenCalculations(),
  384. element = module.get.elementPosition()
  385. ;
  386. // offset
  387. if(settings.includeMargin) {
  388. $.extend(module.cache.element, {
  389. top : element.offset.top - element.margin.top,
  390. bottom : element.offset.top + element.height + element.margin.bottom
  391. });
  392. }
  393. else {
  394. $.extend(module.cache.element, {
  395. top : element.offset.top,
  396. bottom : element.offset.top + element.height
  397. });
  398. }
  399. // visibility
  400. $.extend(module.cache.element, {
  401. topVisible : (screen.bottom > element.top),
  402. topPassed : (screen.top > element.top),
  403. bottomVisible : (screen.bottom > element.bottom),
  404. bottomPassed : (screen.top > element.bottom),
  405. pixelsPassed : 0,
  406. percentagePassed : 0
  407. });
  408. // meta calculations
  409. $.extend(module.cache.element, {
  410. visible : (module.cache.element.topVisible || module.cache.element.bottomVisible),
  411. passing : (module.cache.element.topPassed && !module.cache.element.bottomPassed),
  412. hidden : (!module.cache.element.topVisible && !module.cache.element.bottomVisible)
  413. });
  414. if(module.cache.element.passing) {
  415. module.cache.element.pixelsPassed = (screen.top - element.top);
  416. module.cache.element.percentagePassed = (screen.top - element.top) / element.height;
  417. }
  418. module.verbose('Updated element calculations', module.cache.element);
  419. },
  420. screenCalculations: function() {
  421. var
  422. scroll = $window.scrollTop()
  423. ;
  424. if(module.cache.scroll === undefined) {
  425. module.cache.scroll = $window.scrollTop();
  426. }
  427. module.save.direction();
  428. $.extend(module.cache.screen, {
  429. top : scroll - settings.offset,
  430. bottom : scroll - settings.offset + module.cache.screen.height
  431. });
  432. return module.cache.screen;
  433. },
  434. screenSize: function() {
  435. module.verbose('Saving window position');
  436. module.cache.screen = {
  437. height: $window.height()
  438. };
  439. },
  440. position: function() {
  441. module.save.screenSize();
  442. module.save.elementPosition();
  443. }
  444. },
  445. get: {
  446. pixelsPassed: function(amount) {
  447. var
  448. element = module.get.elementCalculations()
  449. ;
  450. if(amount.search('%') > -1) {
  451. return ( element.height * (parseInt(amount, 10) / 100) );
  452. }
  453. return parseInt(amount, 10);
  454. },
  455. occurred: function(callback) {
  456. return (module.cache.occurred !== undefined)
  457. ? module.cache.occurred[callback] || false
  458. : false
  459. ;
  460. },
  461. direction: function() {
  462. if(module.cache.direction === undefined) {
  463. module.save.direction();
  464. }
  465. return module.cache.direction;
  466. },
  467. elementPosition: function() {
  468. if(module.cache.element === undefined) {
  469. module.save.elementPosition();
  470. }
  471. return module.cache.element;
  472. },
  473. elementCalculations: function() {
  474. if(module.cache.element === undefined) {
  475. module.save.elementCalculations();
  476. }
  477. return module.cache.element;
  478. },
  479. screenCalculations: function() {
  480. if(module.cache.screen === undefined) {
  481. module.save.screenCalculations();
  482. }
  483. return module.cache.screen;
  484. },
  485. screenSize: function() {
  486. if(module.cache.screen === undefined) {
  487. module.save.screenSize();
  488. }
  489. return module.cache.screen;
  490. },
  491. scroll: function() {
  492. if(module.cache.scroll === undefined) {
  493. module.save.scroll();
  494. }
  495. return module.cache.scroll;
  496. },
  497. lastScroll: function() {
  498. if(module.cache.screen === undefined) {
  499. module.debug('First scroll event, no last scroll could be found');
  500. return false;
  501. }
  502. return module.cache.screen.top;
  503. }
  504. },
  505. setting: function(name, value) {
  506. if( $.isPlainObject(name) ) {
  507. $.extend(true, settings, name);
  508. }
  509. else if(value !== undefined) {
  510. settings[name] = value;
  511. }
  512. else {
  513. return settings[name];
  514. }
  515. },
  516. internal: function(name, value) {
  517. if( $.isPlainObject(name) ) {
  518. $.extend(true, module, name);
  519. }
  520. else if(value !== undefined) {
  521. module[name] = value;
  522. }
  523. else {
  524. return module[name];
  525. }
  526. },
  527. debug: function() {
  528. if(settings.debug) {
  529. if(settings.performance) {
  530. module.performance.log(arguments);
  531. }
  532. else {
  533. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  534. module.debug.apply(console, arguments);
  535. }
  536. }
  537. },
  538. verbose: function() {
  539. if(settings.verbose && settings.debug) {
  540. if(settings.performance) {
  541. module.performance.log(arguments);
  542. }
  543. else {
  544. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  545. module.verbose.apply(console, arguments);
  546. }
  547. }
  548. },
  549. error: function() {
  550. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  551. module.error.apply(console, arguments);
  552. },
  553. performance: {
  554. log: function(message) {
  555. var
  556. currentTime,
  557. executionTime,
  558. previousTime
  559. ;
  560. if(settings.performance) {
  561. currentTime = new Date().getTime();
  562. previousTime = time || currentTime;
  563. executionTime = currentTime - previousTime;
  564. time = currentTime;
  565. performance.push({
  566. 'Element' : element,
  567. 'Name' : message[0],
  568. 'Arguments' : [].slice.call(message, 1) || '',
  569. 'Execution Time' : executionTime
  570. });
  571. }
  572. clearTimeout(module.performance.timer);
  573. module.performance.timer = setTimeout(module.performance.display, 100);
  574. },
  575. display: function() {
  576. var
  577. title = settings.name + ':',
  578. totalTime = 0
  579. ;
  580. time = false;
  581. clearTimeout(module.performance.timer);
  582. $.each(performance, function(index, data) {
  583. totalTime += data['Execution Time'];
  584. });
  585. title += ' ' + totalTime + 'ms';
  586. if(moduleSelector) {
  587. title += ' \'' + moduleSelector + '\'';
  588. }
  589. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  590. console.groupCollapsed(title);
  591. if(console.table) {
  592. console.table(performance);
  593. }
  594. else {
  595. $.each(performance, function(index, data) {
  596. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  597. });
  598. }
  599. console.groupEnd();
  600. }
  601. performance = [];
  602. }
  603. },
  604. invoke: function(query, passedArguments, context) {
  605. var
  606. object = instance,
  607. maxDepth,
  608. found,
  609. response
  610. ;
  611. passedArguments = passedArguments || queryArguments;
  612. context = element || context;
  613. if(typeof query == 'string' && object !== undefined) {
  614. query = query.split(/[\. ]/);
  615. maxDepth = query.length - 1;
  616. $.each(query, function(depth, value) {
  617. var camelCaseValue = (depth != maxDepth)
  618. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  619. : query
  620. ;
  621. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  622. object = object[camelCaseValue];
  623. }
  624. else if( object[camelCaseValue] !== undefined ) {
  625. found = object[camelCaseValue];
  626. return false;
  627. }
  628. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  629. object = object[value];
  630. }
  631. else if( object[value] !== undefined ) {
  632. found = object[value];
  633. return false;
  634. }
  635. else {
  636. return false;
  637. }
  638. });
  639. }
  640. if ( $.isFunction( found ) ) {
  641. response = found.apply(context, passedArguments);
  642. }
  643. else if(found !== undefined) {
  644. response = found;
  645. }
  646. if($.isArray(returnedValue)) {
  647. returnedValue.push(response);
  648. }
  649. else if(returnedValue !== undefined) {
  650. returnedValue = [returnedValue, response];
  651. }
  652. else if(response !== undefined) {
  653. returnedValue = response;
  654. }
  655. return found;
  656. }
  657. };
  658. if(methodInvoked) {
  659. if(instance === undefined) {
  660. module.initialize();
  661. }
  662. module.invoke(query);
  663. }
  664. else {
  665. if(instance !== undefined) {
  666. module.destroy();
  667. }
  668. module.initialize();
  669. }
  670. })
  671. ;
  672. return (returnedValue !== undefined)
  673. ? returnedValue
  674. : this
  675. ;
  676. };
  677. $.fn.visibility.settings = {
  678. name : 'Visibility',
  679. namespace : 'visibility',
  680. debug : false,
  681. verbose : false,
  682. performance : true,
  683. offset : 0,
  684. includeMargin : false,
  685. // visibility check delay in ms
  686. throttle : false,
  687. // special visibility type
  688. type : false,
  689. transition : 'fade in',
  690. duration : 500,
  691. // array of callbacks
  692. onPassed : {},
  693. // standard callbacks
  694. onPassing : false,
  695. onTopVisible : false,
  696. onBottomVisible : false,
  697. onTopPassed : false,
  698. onBottomPassed : false,
  699. once : true,
  700. continuous : false,
  701. // utility callbacks
  702. onRefresh : function(){},
  703. onScroll : function(){},
  704. // not used currently waiting for (DOM Mutations API adoption)
  705. // https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#mutation-observers
  706. watch : true,
  707. watchedProperties : [
  708. 'offsetWidth',
  709. 'offsetHeight',
  710. 'top',
  711. 'left'
  712. ],
  713. error : {
  714. method : 'The method you called is not defined.'
  715. }
  716. };
  717. })( jQuery, window , document );