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.

690 lines
21 KiB

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