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.

779 lines
24 KiB

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