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

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
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 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. 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 = (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 );