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.

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