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.

653 lines
20 KiB

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