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.

811 lines
25 KiB

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