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.

621 lines
19 KiB

10 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. setTimeout(module.checkVisibility, settings.loadWait);
  54. },
  55. instantiate: function() {
  56. module.verbose('Storing instance of module', module);
  57. instance = module;
  58. $module
  59. .data(moduleNamespace, module)
  60. ;
  61. },
  62. destroy: function() {
  63. module.verbose('Destroying previous module');
  64. $window
  65. .off(eventNamespace)
  66. ;
  67. $module
  68. .off(eventNamespace)
  69. .removeData(moduleNamespace)
  70. ;
  71. },
  72. bindEvents: function() {
  73. $window
  74. .on('resize', module.event.refresh)
  75. .on('scroll', module.event.scroll)
  76. ;
  77. },
  78. event: {
  79. refresh: function() {
  80. requestAnimationFrame(module.refresh);
  81. },
  82. scroll: function() {
  83. requestAnimationFrame(module.checkVisibility);
  84. }
  85. },
  86. refresh: function() {
  87. module.save.position();
  88. module.checkVisibility();
  89. $.proxy(settings.onRefresh, element)();
  90. },
  91. reset: function() {
  92. module.cache = {
  93. screen : {},
  94. element : {}
  95. };
  96. },
  97. checkVisibility: function() {
  98. module.verbose('Updating visibility of element', module.cache.element);
  99. module.save.scroll();
  100. module.save.direction();
  101. module.save.screenCalculations();
  102. module.save.elementCalculations();
  103. module.passed();
  104. module.passing();
  105. module.topVisible();
  106. module.bottomVisible();
  107. module.topPassed();
  108. module.bottomPassed();
  109. },
  110. passing: function(newCallback) {
  111. var
  112. calculations = module.get.elementCalculations(),
  113. screen = module.get.screenCalculations(),
  114. callback = newCallback || settings.onPassing
  115. ;
  116. if(newCallback) {
  117. module.debug('Adding callback for passing', newCallback);
  118. settings.onPassing = newCallback;
  119. }
  120. if(callback && calculations.passing) {
  121. $.proxy(callback, element)(calculations, screen);
  122. }
  123. else {
  124. return calculations.passing;
  125. }
  126. },
  127. passed: function(amount, newCallback) {
  128. var
  129. calculations = module.get.elementCalculations(),
  130. amountInPixels
  131. ;
  132. // assign callback
  133. if(amount !== undefined && newCallback !== undefined) {
  134. settings.onPassed[amount] = newCallback;
  135. }
  136. else if(amount !== undefined) {
  137. return (module.get.pixelsPassed(amount) > calculations.pixelsPassed);
  138. }
  139. else if(calculations.passing) {
  140. $.each(settings.onPassed, function(amount, callback) {
  141. if(calculations.bottomVisible || calculations.pixelsPassed > module.get.pixelsPassed(amount)) {
  142. callback();
  143. }
  144. });
  145. }
  146. },
  147. topVisible: function(newCallback) {
  148. var
  149. calculations = module.get.elementCalculations(),
  150. screen = module.get.screenCalculations(),
  151. callback = newCallback || settings.onTopVisible
  152. ;
  153. if(newCallback) {
  154. module.debug('Adding callback for top visible', newCallback);
  155. settings.onTopVisible = newCallback;
  156. }
  157. if(callback && calculations.topVisible) {
  158. $.proxy(callback, element)(calculations, screen);
  159. }
  160. else {
  161. return calculations.topVisible;
  162. }
  163. },
  164. bottomVisible: function(newCallback) {
  165. var
  166. calculations = module.get.elementCalculations(),
  167. screen = module.get.screenCalculations(),
  168. callback = newCallback || settings.onBottomVisible
  169. ;
  170. if(newCallback) {
  171. module.debug('Adding callback for bottom visible', newCallback);
  172. settings.onBottomVisible = newCallback;
  173. }
  174. if(callback && calculations.bottomVisible) {
  175. $.proxy(callback, element)(calculations, screen);
  176. }
  177. else {
  178. return calculations.bottomVisible;
  179. }
  180. },
  181. topPassed: function(newCallback) {
  182. var
  183. calculations = module.get.elementCalculations(),
  184. screen = module.get.screenCalculations(),
  185. callback = newCallback || settings.onTopPassed
  186. ;
  187. if(newCallback) {
  188. module.debug('Adding callback for top passed', newCallback);
  189. settings.onTopPassed = newCallback;
  190. }
  191. if(callback && calculations.topPassed) {
  192. $.proxy(callback, element)(calculations, screen);
  193. }
  194. else {
  195. return calculations.topPassed;
  196. }
  197. },
  198. bottomPassed: function(newCallback) {
  199. var
  200. calculations = module.get.elementCalculations(),
  201. screen = module.get.screenCalculations(),
  202. callback = newCallback || settings.onBottomPassed
  203. ;
  204. if(newCallback) {
  205. module.debug('Adding callback for bottom passed', newCallback);
  206. settings.bottomPassed = newCallback;
  207. }
  208. if(callback && calculations.bottomPassed) {
  209. $.proxy(callback, element)(calculations, screen);
  210. }
  211. else {
  212. return calculations.bottomPassed;
  213. }
  214. },
  215. save: {
  216. scroll: function() {
  217. module.cache.scroll = $window.scrollTop() + settings.offset;
  218. },
  219. direction: function() {
  220. var
  221. scroll = module.get.scroll(),
  222. lastScroll = module.get.lastScroll(),
  223. direction
  224. ;
  225. if(scroll > lastScroll && lastScroll) {
  226. direction = 'down';
  227. }
  228. else if(scroll < lastScroll && lastScroll) {
  229. direction = 'up';
  230. }
  231. else {
  232. direction = 'static';
  233. }
  234. module.cache.direction = direction;
  235. return module.cache.direction;
  236. },
  237. elementPosition: function() {
  238. var
  239. screen = module.get.screenSize()
  240. ;
  241. module.verbose('Saving element position');
  242. $.extend(module.cache.element, {
  243. margin : {
  244. top : parseInt($module.css('margin-top'), 10),
  245. bottom : parseInt($module.css('margin-bottom'), 10)
  246. },
  247. fits : (element.height < screen.height),
  248. offset : $module.offset(),
  249. width : $module.outerWidth(),
  250. height : $module.outerHeight()
  251. });
  252. return module.cache.element;
  253. },
  254. elementCalculations: function() {
  255. var
  256. screen = module.get.screenCalculations(),
  257. element = module.get.elementPosition()
  258. ;
  259. // offset
  260. if(settings.includeMargin) {
  261. $.extend(module.cache.element, {
  262. top : element.offset.top - element.margin.top,
  263. bottom : element.offset.top + element.height + element.margin.bottom
  264. });
  265. }
  266. else {
  267. $.extend(module.cache.element, {
  268. top : element.offset.top,
  269. bottom : element.offset.top + element.height
  270. });
  271. }
  272. // visibility
  273. $.extend(module.cache.element, {
  274. topVisible : (screen.bottom > element.top),
  275. topPassed : (screen.top > element.top),
  276. bottomVisible : (screen.bottom > element.bottom),
  277. bottomPassed : (screen.top > element.bottom),
  278. pixelsPassed : 0,
  279. percentagePassed : 0
  280. });
  281. // meta calculations
  282. $.extend(module.cache.element, {
  283. visible : (module.cache.element.topVisible || module.cache.element.bottomVisible),
  284. passing : (module.cache.element.topPassed && !module.cache.element.bottomPassed),
  285. hidden : (!module.cache.element.topVisible && !module.cache.element.bottomVisible)
  286. });
  287. if(module.cache.element.passing) {
  288. module.cache.element.pixelsPassed = (screen.top - element.top);
  289. module.cache.element.percentagePassed = (screen.top - element.top) / element.height;
  290. }
  291. module.verbose('Updated element calculations', module.cache.element);
  292. },
  293. screenCalculations: function() {
  294. var
  295. scroll = $window.scrollTop()
  296. ;
  297. if(module.cache.scroll === undefined) {
  298. module.cache.scroll = $window.scrollTop();
  299. }
  300. module.save.direction();
  301. $.extend(module.cache.screen, {
  302. top : scroll + settings.offset,
  303. bottom : scroll + settings.offset + module.cache.screen.height
  304. });
  305. return module.cache.screen;
  306. },
  307. screenSize: function() {
  308. module.verbose('Saving window position');
  309. module.cache.screen = {
  310. height: $window.height()
  311. };
  312. },
  313. position: function() {
  314. module.save.screenSize();
  315. module.save.elementPosition();
  316. }
  317. },
  318. get: {
  319. pixelsPassed: function(amount) {
  320. var
  321. element = module.get.elementCalculations()
  322. ;
  323. if(amount.search('%') > -1) {
  324. return ( element.height * (parseInt(amount, 10) / 100) );
  325. }
  326. return parseInt(amount, 10);
  327. },
  328. direction: function() {
  329. if(module.cache.direction === undefined) {
  330. module.save.direction();
  331. }
  332. return module.cache.direction;
  333. },
  334. elementPosition: function() {
  335. if(module.cache.element === undefined) {
  336. module.save.elementPosition();
  337. }
  338. return module.cache.element;
  339. },
  340. elementCalculations: function() {
  341. if(module.cache.element === undefined) {
  342. module.save.elementCalculations();
  343. }
  344. return module.cache.element;
  345. },
  346. screenCalculations: function() {
  347. if(module.cache.screen === undefined) {
  348. module.save.screenCalculations();
  349. }
  350. return module.cache.screen;
  351. },
  352. screenSize: function() {
  353. if(module.cache.screen === undefined) {
  354. module.save.screenSize();
  355. }
  356. return module.cache.screen;
  357. },
  358. scroll: function() {
  359. if(module.cache.scroll === undefined) {
  360. module.save.scroll();
  361. }
  362. return module.cache.scroll;
  363. },
  364. lastScroll: function() {
  365. if(module.cache.screen === undefined) {
  366. module.debug('First scroll event, no last scroll could be found');
  367. return false;
  368. }
  369. return module.cache.screen.top;
  370. }
  371. },
  372. setting: function(name, value) {
  373. if( $.isPlainObject(name) ) {
  374. $.extend(true, settings, name);
  375. }
  376. else if(value !== undefined) {
  377. settings[name] = value;
  378. }
  379. else {
  380. return settings[name];
  381. }
  382. },
  383. internal: function(name, value) {
  384. if( $.isPlainObject(name) ) {
  385. $.extend(true, module, name);
  386. }
  387. else if(value !== undefined) {
  388. module[name] = value;
  389. }
  390. else {
  391. return module[name];
  392. }
  393. },
  394. debug: function() {
  395. if(settings.debug) {
  396. if(settings.performance) {
  397. module.performance.log(arguments);
  398. }
  399. else {
  400. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  401. module.debug.apply(console, arguments);
  402. }
  403. }
  404. },
  405. verbose: function() {
  406. if(settings.verbose && settings.debug) {
  407. if(settings.performance) {
  408. module.performance.log(arguments);
  409. }
  410. else {
  411. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  412. module.verbose.apply(console, arguments);
  413. }
  414. }
  415. },
  416. error: function() {
  417. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  418. module.error.apply(console, arguments);
  419. },
  420. performance: {
  421. log: function(message) {
  422. var
  423. currentTime,
  424. executionTime,
  425. previousTime
  426. ;
  427. if(settings.performance) {
  428. currentTime = new Date().getTime();
  429. previousTime = time || currentTime;
  430. executionTime = currentTime - previousTime;
  431. time = currentTime;
  432. performance.push({
  433. 'Element' : element,
  434. 'Name' : message[0],
  435. 'Arguments' : [].slice.call(message, 1) || '',
  436. 'Execution Time' : executionTime
  437. });
  438. }
  439. clearTimeout(module.performance.timer);
  440. module.performance.timer = setTimeout(module.performance.display, 100);
  441. },
  442. display: function() {
  443. var
  444. title = settings.name + ':',
  445. totalTime = 0
  446. ;
  447. time = false;
  448. clearTimeout(module.performance.timer);
  449. $.each(performance, function(index, data) {
  450. totalTime += data['Execution Time'];
  451. });
  452. title += ' ' + totalTime + 'ms';
  453. if(moduleSelector) {
  454. title += ' \'' + moduleSelector + '\'';
  455. }
  456. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  457. console.groupCollapsed(title);
  458. if(console.table) {
  459. console.table(performance);
  460. }
  461. else {
  462. $.each(performance, function(index, data) {
  463. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  464. });
  465. }
  466. console.groupEnd();
  467. }
  468. performance = [];
  469. }
  470. },
  471. invoke: function(query, passedArguments, context) {
  472. var
  473. object = instance,
  474. maxDepth,
  475. found,
  476. response
  477. ;
  478. passedArguments = passedArguments || queryArguments;
  479. context = element || context;
  480. if(typeof query == 'string' && object !== undefined) {
  481. query = query.split(/[\. ]/);
  482. maxDepth = query.length - 1;
  483. $.each(query, function(depth, value) {
  484. var camelCaseValue = (depth != maxDepth)
  485. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  486. : query
  487. ;
  488. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  489. object = object[camelCaseValue];
  490. }
  491. else if( object[camelCaseValue] !== undefined ) {
  492. found = object[camelCaseValue];
  493. return false;
  494. }
  495. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  496. object = object[value];
  497. }
  498. else if( object[value] !== undefined ) {
  499. found = object[value];
  500. return false;
  501. }
  502. else {
  503. return false;
  504. }
  505. });
  506. }
  507. if ( $.isFunction( found ) ) {
  508. response = found.apply(context, passedArguments);
  509. }
  510. else if(found !== undefined) {
  511. response = found;
  512. }
  513. if($.isArray(returnedValue)) {
  514. returnedValue.push(response);
  515. }
  516. else if(returnedValue !== undefined) {
  517. returnedValue = [returnedValue, response];
  518. }
  519. else if(response !== undefined) {
  520. returnedValue = response;
  521. }
  522. return found;
  523. }
  524. };
  525. if(methodInvoked) {
  526. if(instance === undefined) {
  527. module.initialize();
  528. }
  529. module.invoke(query);
  530. }
  531. else {
  532. if(instance !== undefined) {
  533. module.destroy();
  534. }
  535. module.initialize();
  536. }
  537. })
  538. ;
  539. return (returnedValue !== undefined)
  540. ? returnedValue
  541. : this
  542. ;
  543. };
  544. $.fn.visibility.settings = {
  545. name : 'Visibility',
  546. namespace : 'visibility',
  547. debug : false,
  548. verbose : false,
  549. performance : true,
  550. loadWait : 1000,
  551. watch : true,
  552. offset : 0,
  553. includeMargin : false,
  554. // array of callbacks
  555. onPassed : {},
  556. // standard callbacks
  557. onPassing : false,
  558. onTopVisible : false,
  559. onBottomVisible : false,
  560. onTopPassed : false,
  561. onBottomPassed : false,
  562. // utility callbacks
  563. onRefresh : function(){},
  564. onScroll : function(){},
  565. watchedProperties : [
  566. 'offsetWidth',
  567. 'offsetHeight',
  568. 'top',
  569. 'left'
  570. ],
  571. error : {
  572. method : 'The method you called is not defined.'
  573. }
  574. };
  575. })( jQuery, window , document );