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.

766 lines
24 KiB

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