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.

792 lines
24 KiB

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