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.

768 lines
23 KiB

10 years ago
  1. /*
  2. * # Semantic - Sticky
  3. * http://github.com/semantic-org/semantic-ui/
  4. *
  5. *
  6. * Copyright 2014 Contributors
  7. * Released under the MIT license
  8. * http://opensource.org/licenses/MIT
  9. *
  10. */
  11. ;(function ( $, window, document, undefined ) {
  12. "use strict";
  13. $.fn.sticky = function(parameters) {
  14. var
  15. $allModules = $(this),
  16. moduleSelector = $allModules.selector || '',
  17. time = new Date().getTime(),
  18. performance = [],
  19. query = arguments[0],
  20. methodInvoked = (typeof query == 'string'),
  21. queryArguments = [].slice.call(arguments, 1),
  22. returnedValue
  23. ;
  24. $allModules
  25. .each(function() {
  26. var
  27. settings = $.extend(true, {}, $.fn.sticky.settings, parameters),
  28. className = settings.className,
  29. namespace = settings.namespace,
  30. error = settings.error,
  31. eventNamespace = '.' + namespace,
  32. moduleNamespace = 'module-' + namespace,
  33. $module = $(this),
  34. $window = $(window),
  35. $container = $module.offsetParent(),
  36. $scroll = $(settings.scrollContext),
  37. $context,
  38. selector = $module.selector || '',
  39. instance = $module.data(moduleNamespace),
  40. requestAnimationFrame = window.requestAnimationFrame
  41. || window.mozRequestAnimationFrame
  42. || window.webkitRequestAnimationFrame
  43. || window.msRequestAnimationFrame
  44. || function(callback) { setTimeout(callback, 0); },
  45. element = this,
  46. observer,
  47. module
  48. ;
  49. module = {
  50. initialize: function() {
  51. if(settings.context) {
  52. $context = $(settings.context);
  53. }
  54. else {
  55. $context = $container;
  56. }
  57. if($context.size() === 0) {
  58. module.error(error.invalidContext, settings.context, $module);
  59. return;
  60. }
  61. module.verbose('Initializing sticky', settings, $container);
  62. module.save.positions();
  63. // error conditions
  64. if( module.is.hidden() ) {
  65. module.error(error.visible, $module);
  66. }
  67. if(module.cache.element.height > module.cache.context.height) {
  68. module.reset();
  69. module.error(error.elementSize, $module);
  70. return;
  71. }
  72. $window
  73. .on('resize' + eventNamespace, module.event.resize)
  74. ;
  75. $scroll
  76. .on('scroll' + eventNamespace, module.event.scroll)
  77. ;
  78. module.observeChanges();
  79. module.instantiate();
  80. },
  81. instantiate: function() {
  82. module.verbose('Storing instance of module', module);
  83. instance = module;
  84. $module
  85. .data(moduleNamespace, module)
  86. ;
  87. },
  88. destroy: function() {
  89. module.verbose('Destroying previous module');
  90. module.reset();
  91. $window
  92. .off('resize' + eventNamespace, module.event.resize)
  93. ;
  94. $scroll
  95. .off('scroll' + eventNamespace, module.event.scroll)
  96. ;
  97. $module
  98. .removeData(moduleNamespace)
  99. ;
  100. },
  101. observeChanges: function() {
  102. var
  103. context = $context[0]
  104. ;
  105. if(MutationObserver !== undefined) {
  106. observer = new MutationObserver(function(mutations) {
  107. clearTimeout(module.timer);
  108. module.timer = setTimeout(function() {
  109. module.verbose('DOM tree modified, updating sticky menu');
  110. module.refresh();
  111. }, 200);
  112. });
  113. observer.observe(element, {
  114. childList : true,
  115. subtree : true
  116. });
  117. observer.observe(context, {
  118. childList : true,
  119. subtree : true
  120. });
  121. module.debug('Setting up mutation observer', observer);
  122. }
  123. },
  124. event: {
  125. resize: function() {
  126. requestAnimationFrame(function() {
  127. module.refresh();
  128. module.stick();
  129. });
  130. },
  131. scroll: function() {
  132. requestAnimationFrame(function() {
  133. module.stick();
  134. $.proxy(settings.onScroll, element)();
  135. });
  136. }
  137. },
  138. refresh: function(hardRefresh) {
  139. module.reset();
  140. if(hardRefresh) {
  141. $container = $module.offsetParent();
  142. }
  143. module.save.positions();
  144. module.stick();
  145. $.proxy(settings.onReposition, element)();
  146. },
  147. supports: {
  148. sticky: function() {
  149. var
  150. $element = $('<div/>'),
  151. element = $element.get()
  152. ;
  153. $element
  154. .addClass(className.supported)
  155. ;
  156. return($element.css('position').match('sticky'));
  157. }
  158. },
  159. save: {
  160. scroll: function(scroll) {
  161. module.lastScroll = scroll;
  162. },
  163. positions: function() {
  164. var
  165. window = {
  166. height: $window.height()
  167. },
  168. element = {
  169. margin: {
  170. top : parseInt($module.css('margin-top'), 10),
  171. bottom : parseInt($module.css('margin-bottom'), 10),
  172. },
  173. offset : $module.offset(),
  174. width : $module.outerWidth(),
  175. height : $module.outerHeight()
  176. },
  177. context = {
  178. offset: $context.offset(),
  179. height: $context.outerHeight()
  180. }
  181. ;
  182. module.cache = {
  183. fits : ( element.height < window.height ),
  184. window: {
  185. height: window.height
  186. },
  187. element: {
  188. margin : element.margin,
  189. top : element.offset.top - element.margin.top,
  190. left : element.offset.left,
  191. width : element.width,
  192. height : element.height,
  193. bottom : element.offset.top + element.height
  194. },
  195. context: {
  196. top : context.offset.top,
  197. height : context.height,
  198. bottom : context.offset.top + context.height
  199. }
  200. };
  201. module.set.containerSize();
  202. module.set.size();
  203. module.stick();
  204. module.debug('Caching element positions', module.cache);
  205. }
  206. },
  207. get: {
  208. direction: function(scroll) {
  209. var
  210. direction = 'down'
  211. ;
  212. scroll = scroll || $scroll.scrollTop();
  213. if(module.lastScroll !== undefined) {
  214. if(module.lastScroll < scroll) {
  215. direction = 'down';
  216. }
  217. else if(module.lastScroll > scroll) {
  218. direction = 'up';
  219. }
  220. }
  221. return direction;
  222. },
  223. scrollChange: function(scroll) {
  224. scroll = scroll || $scroll.scrollTop();
  225. return (module.lastScroll)
  226. ? (scroll - module.lastScroll)
  227. : 0
  228. ;
  229. },
  230. currentElementScroll: function() {
  231. return ( module.is.top() )
  232. ? Math.abs(parseInt($module.css('top'), 10)) || 0
  233. : Math.abs(parseInt($module.css('bottom'), 10)) || 0
  234. ;
  235. },
  236. elementScroll: function(scroll) {
  237. scroll = scroll || $scroll.scrollTop();
  238. var
  239. element = module.cache.element,
  240. window = module.cache.window,
  241. delta = module.get.scrollChange(scroll),
  242. maxScroll = (element.height - window.height + settings.offset),
  243. currentScroll = module.get.currentElementScroll(),
  244. possibleScroll = (currentScroll + delta),
  245. elementScroll
  246. ;
  247. if(module.cache.fits || possibleScroll < 0) {
  248. elementScroll = 0;
  249. }
  250. else if (possibleScroll > maxScroll ) {
  251. elementScroll = maxScroll;
  252. }
  253. else {
  254. elementScroll = possibleScroll;
  255. }
  256. return elementScroll;
  257. }
  258. },
  259. remove: {
  260. offset: function() {
  261. $module.css('margin-top', '');
  262. }
  263. },
  264. set: {
  265. offset: function() {
  266. module.verbose('Setting offset on element', settings.offset);
  267. $module.css('margin-top', settings.offset);
  268. },
  269. containerSize: function() {
  270. var
  271. tagName = $container.get(0).tagName
  272. ;
  273. if(tagName === 'HTML' || tagName == 'body') {
  274. // this can trigger for too many reasons
  275. //module.error(error.container, tagName, $module);
  276. $container = $module.offsetParent();
  277. }
  278. else {
  279. module.debug('Settings container size', module.cache.context.height);
  280. $container.height(module.cache.context.height);
  281. }
  282. },
  283. scroll: function(scroll) {
  284. module.debug('Setting scroll on element', scroll);
  285. if( module.is.top() ) {
  286. $module
  287. .css('bottom', '')
  288. .css('top', -scroll)
  289. ;
  290. }
  291. if( module.is.bottom() ) {
  292. $module
  293. .css('top', '')
  294. .css('bottom', scroll)
  295. ;
  296. }
  297. },
  298. size: function() {
  299. if(module.cache.element.height !== 0 && module.cache.element.width !== 0) {
  300. $module
  301. .css({
  302. width : module.cache.element.width,
  303. height : module.cache.element.height
  304. })
  305. ;
  306. }
  307. }
  308. },
  309. is: {
  310. top: function() {
  311. return $module.hasClass(className.top);
  312. },
  313. bottom: function() {
  314. return $module.hasClass(className.bottom);
  315. },
  316. initialPosition: function() {
  317. return (!module.is.fixed() && !module.is.bound());
  318. },
  319. hidden: function() {
  320. return (!$module.is(':visible'));
  321. },
  322. bound: function() {
  323. return $module.hasClass(className.bound);
  324. },
  325. fixed: function() {
  326. return $module.hasClass(className.fixed);
  327. }
  328. },
  329. stick: function() {
  330. var
  331. cache = module.cache,
  332. fits = cache.fits,
  333. element = cache.element,
  334. window = cache.window,
  335. context = cache.context,
  336. scroll = {
  337. top : $scroll.scrollTop() + settings.offset,
  338. bottom : $scroll.scrollTop() + settings.offset + window.height
  339. },
  340. direction = module.get.direction(scroll.top),
  341. elementScroll = module.get.elementScroll(scroll.top),
  342. // shorthand
  343. doesntFit = !fits,
  344. elementVisible = (element.height !== 0)
  345. ;
  346. // save current scroll for next run
  347. module.save.scroll(scroll.top);
  348. if(elementVisible) {
  349. if( module.is.initialPosition() ) {
  350. if(scroll.top >= element.top) {
  351. module.debug('Element passed, fixing element to page');
  352. module.fixTop();
  353. }
  354. }
  355. else if( module.is.fixed() ) {
  356. // currently fixed top
  357. if( module.is.top() ) {
  358. if( scroll.top < element.top ) {
  359. module.debug('Fixed element reached top of container');
  360. module.setInitialPosition();
  361. }
  362. else if( (element.height + scroll.top - elementScroll) > context.bottom ) {
  363. module.debug('Fixed element reached bottom of container');
  364. module.bindBottom();
  365. }
  366. // scroll element if larger than screen
  367. else if(doesntFit) {
  368. module.set.scroll(elementScroll);
  369. }
  370. }
  371. // currently fixed bottom
  372. else if(module.is.bottom() ) {
  373. // top edge
  374. if( (scroll.bottom - element.height) < element.top) {
  375. module.debug('Bottom fixed rail has reached top of container');
  376. module.setInitialPosition();
  377. }
  378. // bottom edge
  379. else if(scroll.bottom > context.bottom) {
  380. module.debug('Bottom fixed rail has reached bottom of container');
  381. module.bindBottom();
  382. }
  383. // scroll element if larger than screen
  384. else if(doesntFit) {
  385. module.set.scroll(elementScroll);
  386. }
  387. }
  388. }
  389. else if( module.is.bottom() ) {
  390. // fix to bottom of screen on way back up
  391. if( module.is.bottom() ) {
  392. if(settings.pushing) {
  393. if(module.is.bound() && scroll.bottom < context.bottom ) {
  394. module.debug('Fixing bottom attached element to bottom of browser.');
  395. module.fixBottom();
  396. }
  397. }
  398. else {
  399. if(module.is.bound() && (scroll.top < context.bottom - element.height) ) {
  400. module.debug('Fixing bottom attached element to top of browser.');
  401. module.fixTop();
  402. }
  403. }
  404. }
  405. }
  406. }
  407. },
  408. bindTop: function() {
  409. module.debug('Binding element to top of parent container');
  410. module.remove.offset();
  411. $module
  412. .css('left' , '')
  413. .css('top' , '')
  414. .css('bottom' , '')
  415. .removeClass(className.fixed)
  416. .removeClass(className.bottom)
  417. .addClass(className.bound)
  418. .addClass(className.top)
  419. ;
  420. $.proxy(settings.onTop, element)();
  421. $.proxy(settings.onUnstick, element)();
  422. },
  423. bindBottom: function() {
  424. module.debug('Binding element to bottom of parent container');
  425. module.remove.offset();
  426. $module
  427. .css('left' , '')
  428. .css('top' , '')
  429. .css('bottom' , '')
  430. .removeClass(className.fixed)
  431. .removeClass(className.top)
  432. .addClass(className.bound)
  433. .addClass(className.bottom)
  434. ;
  435. $.proxy(settings.onBottom, element)();
  436. $.proxy(settings.onUnstick, element)();
  437. },
  438. setInitialPosition: function() {
  439. module.unfix();
  440. module.unbind();
  441. },
  442. fixTop: function() {
  443. module.debug('Fixing element to top of page');
  444. module.set.offset();
  445. $module
  446. .css('left', module.cache.element.left)
  447. .removeClass(className.bound)
  448. .removeClass(className.bottom)
  449. .addClass(className.fixed)
  450. .addClass(className.top)
  451. ;
  452. $.proxy(settings.onStick, element)();
  453. },
  454. fixBottom: function() {
  455. module.debug('Sticking element to bottom of page');
  456. module.set.offset();
  457. $module
  458. .css('left', module.cache.element.left)
  459. .removeClass(className.bound)
  460. .removeClass(className.top)
  461. .addClass(className.fixed)
  462. .addClass(className.bottom)
  463. ;
  464. $.proxy(settings.onStick, element)();
  465. },
  466. unbind: function() {
  467. module.debug('Removing absolute position on element');
  468. module.remove.offset();
  469. $module
  470. .removeClass(className.bound)
  471. .removeClass(className.top)
  472. .removeClass(className.bottom)
  473. ;
  474. },
  475. unfix: function() {
  476. module.debug('Removing fixed position on element');
  477. module.remove.offset();
  478. $module
  479. .removeClass(className.fixed)
  480. .removeClass(className.top)
  481. .removeClass(className.bottom)
  482. ;
  483. $.proxy(settings.onUnstick, this)();
  484. },
  485. reset: function() {
  486. module.debug('Reseting elements position');
  487. module.unbind();
  488. module.unfix();
  489. module.resetCSS();
  490. },
  491. resetCSS: function() {
  492. $module
  493. .css({
  494. top : '',
  495. bottom : '',
  496. width : '',
  497. height : ''
  498. })
  499. ;
  500. $container
  501. .css({
  502. height: ''
  503. })
  504. ;
  505. },
  506. setting: function(name, value) {
  507. if( $.isPlainObject(name) ) {
  508. $.extend(true, settings, name);
  509. }
  510. else if(value !== undefined) {
  511. settings[name] = value;
  512. }
  513. else {
  514. return settings[name];
  515. }
  516. },
  517. internal: function(name, value) {
  518. if( $.isPlainObject(name) ) {
  519. $.extend(true, module, name);
  520. }
  521. else if(value !== undefined) {
  522. module[name] = value;
  523. }
  524. else {
  525. return module[name];
  526. }
  527. },
  528. debug: function() {
  529. if(settings.debug) {
  530. if(settings.performance) {
  531. module.performance.log(arguments);
  532. }
  533. else {
  534. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  535. module.debug.apply(console, arguments);
  536. }
  537. }
  538. },
  539. verbose: function() {
  540. if(settings.verbose && settings.debug) {
  541. if(settings.performance) {
  542. module.performance.log(arguments);
  543. }
  544. else {
  545. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  546. module.verbose.apply(console, arguments);
  547. }
  548. }
  549. },
  550. error: function() {
  551. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  552. module.error.apply(console, arguments);
  553. },
  554. performance: {
  555. log: function(message) {
  556. var
  557. currentTime,
  558. executionTime,
  559. previousTime
  560. ;
  561. if(settings.performance) {
  562. currentTime = new Date().getTime();
  563. previousTime = time || currentTime;
  564. executionTime = currentTime - previousTime;
  565. time = currentTime;
  566. performance.push({
  567. 'Name' : message[0],
  568. 'Arguments' : [].slice.call(message, 1) || '',
  569. 'Element' : element,
  570. 'Execution Time' : executionTime
  571. });
  572. }
  573. clearTimeout(module.performance.timer);
  574. module.performance.timer = setTimeout(module.performance.display, 0);
  575. },
  576. display: function() {
  577. var
  578. title = settings.name + ':',
  579. totalTime = 0
  580. ;
  581. time = false;
  582. clearTimeout(module.performance.timer);
  583. $.each(performance, function(index, data) {
  584. totalTime += data['Execution Time'];
  585. });
  586. title += ' ' + totalTime + 'ms';
  587. if(moduleSelector) {
  588. title += ' \'' + moduleSelector + '\'';
  589. }
  590. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  591. console.groupCollapsed(title);
  592. if(console.table) {
  593. console.table(performance);
  594. }
  595. else {
  596. $.each(performance, function(index, data) {
  597. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  598. });
  599. }
  600. console.groupEnd();
  601. }
  602. performance = [];
  603. }
  604. },
  605. invoke: function(query, passedArguments, context) {
  606. var
  607. object = instance,
  608. maxDepth,
  609. found,
  610. response
  611. ;
  612. passedArguments = passedArguments || queryArguments;
  613. context = element || context;
  614. if(typeof query == 'string' && object !== undefined) {
  615. query = query.split(/[\. ]/);
  616. maxDepth = query.length - 1;
  617. $.each(query, function(depth, value) {
  618. var camelCaseValue = (depth != maxDepth)
  619. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  620. : query
  621. ;
  622. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  623. object = object[camelCaseValue];
  624. }
  625. else if( object[camelCaseValue] !== undefined ) {
  626. found = object[camelCaseValue];
  627. return false;
  628. }
  629. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  630. object = object[value];
  631. }
  632. else if( object[value] !== undefined ) {
  633. found = object[value];
  634. return false;
  635. }
  636. else {
  637. return false;
  638. }
  639. });
  640. }
  641. if ( $.isFunction( found ) ) {
  642. response = found.apply(context, passedArguments);
  643. }
  644. else if(found !== undefined) {
  645. response = found;
  646. }
  647. if($.isArray(returnedValue)) {
  648. returnedValue.push(response);
  649. }
  650. else if(returnedValue !== undefined) {
  651. returnedValue = [returnedValue, response];
  652. }
  653. else if(response !== undefined) {
  654. returnedValue = response;
  655. }
  656. return found;
  657. }
  658. };
  659. if(methodInvoked) {
  660. if(instance === undefined) {
  661. module.initialize();
  662. }
  663. module.invoke(query);
  664. }
  665. else {
  666. if(instance !== undefined) {
  667. module.destroy();
  668. }
  669. module.initialize();
  670. }
  671. })
  672. ;
  673. return (returnedValue !== undefined)
  674. ? returnedValue
  675. : this
  676. ;
  677. };
  678. $.fn.sticky.settings = {
  679. name : 'Sticky',
  680. namespace : 'sticky',
  681. debug : false,
  682. verbose : false,
  683. performance : false,
  684. pushing : false,
  685. context : false,
  686. scrollContext : window,
  687. offset : 0,
  688. onReposition : function(){},
  689. onScroll : function(){},
  690. onStick : function(){},
  691. onUnstick : function(){},
  692. onTop : function(){},
  693. onBottom : function(){},
  694. error : {
  695. container : 'Sticky element must be inside a relative container',
  696. visible : 'Element is hidden, you must call refresh after element becomes visible',
  697. method : 'The method you called is not defined.',
  698. invalidContext : 'Context specified does not exist',
  699. elementSize : 'Sticky element is larger than its container, cannot create sticky.'
  700. },
  701. className : {
  702. bound : 'bound',
  703. fixed : 'fixed',
  704. supported : 'native',
  705. top : 'top',
  706. bottom : 'bottom'
  707. }
  708. };
  709. })( jQuery, window , document );