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.

820 lines
25 KiB

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