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.

681 lines
20 KiB

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