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.

812 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
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
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
  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. currentScroll = module.get.currentElementScroll(),
  269. possibleScroll = (currentScroll + 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. module.debug('Settings container size', module.cache.context.height);
  305. if( Math.abs($container.height() - module.cache.context.height) > 5) {
  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 = module.get.elementScroll(scroll.top),
  375. // shorthand
  376. doesntFit = !fits,
  377. elementVisible = (element.height !== 0)
  378. ;
  379. // save current scroll for next run
  380. module.save.lastScroll(scroll.top);
  381. if(elementVisible) {
  382. if( module.is.initialPosition() ) {
  383. if(scroll.top >= context.bottom) {
  384. module.debug('Element bottom of container');
  385. module.bindBottom();
  386. }
  387. else if(scroll.top >= element.top) {
  388. module.debug('Element passed, fixing element to page');
  389. module.fixTop();
  390. }
  391. }
  392. else if( module.is.fixed() ) {
  393. // currently fixed top
  394. if( module.is.top() ) {
  395. if( scroll.top < element.top ) {
  396. module.debug('Fixed element reached top of container');
  397. module.setInitialPosition();
  398. }
  399. else if( (element.height + scroll.top - elementScroll) > context.bottom ) {
  400. module.debug('Fixed element reached bottom of container');
  401. module.bindBottom();
  402. }
  403. // scroll element if larger than screen
  404. else if(doesntFit) {
  405. module.set.scroll(elementScroll);
  406. }
  407. }
  408. // currently fixed bottom
  409. else if(module.is.bottom() ) {
  410. // top edge
  411. if( (scroll.bottom - element.height) < element.top) {
  412. module.debug('Bottom fixed rail has reached top of container');
  413. module.setInitialPosition();
  414. }
  415. // bottom edge
  416. else if(scroll.bottom > context.bottom) {
  417. module.debug('Bottom fixed rail has reached bottom of container');
  418. module.bindBottom();
  419. }
  420. // scroll element if larger than screen
  421. else if(doesntFit) {
  422. module.set.scroll(elementScroll);
  423. }
  424. }
  425. }
  426. else if( module.is.bottom() ) {
  427. if(settings.pushing) {
  428. if(module.is.bound() && scroll.bottom < context.bottom ) {
  429. module.debug('Fixing bottom attached element to bottom of browser.');
  430. module.fixBottom();
  431. }
  432. }
  433. else {
  434. if(module.is.bound() && (scroll.top < context.bottom - element.height) ) {
  435. module.debug('Fixing bottom attached element to top of browser.');
  436. module.fixTop();
  437. }
  438. }
  439. }
  440. }
  441. },
  442. bindTop: function() {
  443. module.debug('Binding element to top of parent container');
  444. module.remove.offset();
  445. $module
  446. .css('left' , '')
  447. .css('top' , '')
  448. .css('margin-bottom' , '')
  449. .removeClass(className.fixed)
  450. .removeClass(className.bottom)
  451. .addClass(className.bound)
  452. .addClass(className.top)
  453. ;
  454. settings.onTop.call(element);
  455. settings.onUnstick.call(element);
  456. },
  457. bindBottom: function() {
  458. module.debug('Binding element to bottom of parent container');
  459. module.remove.offset();
  460. $module
  461. .css('left' , '')
  462. .css('top' , '')
  463. .css('margin-bottom' , module.cache.context.bottomPadding)
  464. .removeClass(className.fixed)
  465. .removeClass(className.top)
  466. .addClass(className.bound)
  467. .addClass(className.bottom)
  468. ;
  469. settings.onBottom.call(element);
  470. settings.onUnstick.call(element);
  471. },
  472. setInitialPosition: function() {
  473. module.unfix();
  474. module.unbind();
  475. },
  476. fixTop: function() {
  477. module.debug('Fixing element to top of page');
  478. module.set.offset();
  479. $module
  480. .css('left', module.cache.element.left)
  481. .css('bottom' , '')
  482. .removeClass(className.bound)
  483. .removeClass(className.bottom)
  484. .addClass(className.fixed)
  485. .addClass(className.top)
  486. ;
  487. settings.onStick.call(element);
  488. },
  489. fixBottom: function() {
  490. module.debug('Sticking element to bottom of page');
  491. module.set.offset();
  492. $module
  493. .css('left', module.cache.element.left)
  494. .css('bottom' , '')
  495. .removeClass(className.bound)
  496. .removeClass(className.top)
  497. .addClass(className.fixed)
  498. .addClass(className.bottom)
  499. ;
  500. settings.onStick.call(element);
  501. },
  502. unbind: function() {
  503. module.debug('Removing absolute position on element');
  504. module.remove.offset();
  505. $module
  506. .removeClass(className.bound)
  507. .removeClass(className.top)
  508. .removeClass(className.bottom)
  509. ;
  510. },
  511. unfix: function() {
  512. module.debug('Removing fixed position on element');
  513. module.remove.offset();
  514. $module
  515. .removeClass(className.fixed)
  516. .removeClass(className.top)
  517. .removeClass(className.bottom)
  518. ;
  519. settings.onUnstick.call(element);
  520. },
  521. reset: function() {
  522. module.debug('Reseting elements position');
  523. module.unbind();
  524. module.unfix();
  525. module.resetCSS();
  526. module.remove.offset();
  527. },
  528. resetCSS: function() {
  529. $module
  530. .css({
  531. top : '',
  532. bottom : '',
  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 );