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.

817 lines
25 KiB

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
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
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
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
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
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 x.x - 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.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. 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.is.top() ) {
  320. $module
  321. .css('bottom', '')
  322. .css('top', -scroll)
  323. ;
  324. }
  325. if( module.is.bottom() ) {
  326. $module
  327. .css('top', '')
  328. .css('bottom', scroll)
  329. ;
  330. }
  331. },
  332. size: function() {
  333. if(module.cache.element.height !== 0 && module.cache.element.width !== 0) {
  334. $module
  335. .css({
  336. width : module.cache.element.width,
  337. height : module.cache.element.height
  338. })
  339. ;
  340. }
  341. }
  342. },
  343. is: {
  344. top: function() {
  345. return $module.hasClass(className.top);
  346. },
  347. bottom: function() {
  348. return $module.hasClass(className.bottom);
  349. },
  350. initialPosition: function() {
  351. return (!module.is.fixed() && !module.is.bound());
  352. },
  353. hidden: function() {
  354. return (!$module.is(':visible'));
  355. },
  356. bound: function() {
  357. return $module.hasClass(className.bound);
  358. },
  359. fixed: function() {
  360. return $module.hasClass(className.fixed);
  361. }
  362. },
  363. stick: function(scroll) {
  364. var
  365. cachedPosition = scroll || $scroll.scrollTop(),
  366. cache = module.cache,
  367. fits = cache.fits,
  368. element = cache.element,
  369. window = cache.window,
  370. context = cache.context,
  371. offset = (module.is.bottom() && settings.pushing)
  372. ? settings.bottomOffset
  373. : settings.offset,
  374. scroll = {
  375. top : cachedPosition + offset,
  376. bottom : cachedPosition + offset + window.height
  377. },
  378. direction = module.get.direction(scroll.top),
  379. elementScroll = (fits)
  380. ? 0
  381. : module.get.elementScroll(scroll.top),
  382. // shorthand
  383. doesntFit = !fits,
  384. elementVisible = (element.height !== 0)
  385. ;
  386. // save current scroll for next run
  387. module.save.lastScroll(scroll.top);
  388. module.save.elementScroll(elementScroll);
  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. },
  450. bindTop: function() {
  451. module.debug('Binding element to top of parent container');
  452. module.remove.offset();
  453. $module
  454. .css('left' , '')
  455. .css('top' , '')
  456. .css('margin-bottom' , '')
  457. .removeClass(className.fixed)
  458. .removeClass(className.bottom)
  459. .addClass(className.bound)
  460. .addClass(className.top)
  461. ;
  462. settings.onTop.call(element);
  463. settings.onUnstick.call(element);
  464. },
  465. bindBottom: function() {
  466. module.debug('Binding element to bottom of parent container');
  467. module.remove.offset();
  468. $module
  469. .css('left' , '')
  470. .css('top' , '')
  471. .css('margin-bottom' , module.cache.context.bottomPadding)
  472. .removeClass(className.fixed)
  473. .removeClass(className.top)
  474. .addClass(className.bound)
  475. .addClass(className.bottom)
  476. ;
  477. settings.onBottom.call(element);
  478. settings.onUnstick.call(element);
  479. },
  480. setInitialPosition: function() {
  481. module.unfix();
  482. module.unbind();
  483. },
  484. fixTop: function() {
  485. module.debug('Fixing element to top of page');
  486. module.set.offset();
  487. $module
  488. .css('left', module.cache.element.left)
  489. .css('bottom' , '')
  490. .removeClass(className.bound)
  491. .removeClass(className.bottom)
  492. .addClass(className.fixed)
  493. .addClass(className.top)
  494. ;
  495. settings.onStick.call(element);
  496. },
  497. fixBottom: function() {
  498. module.debug('Sticking element to bottom of page');
  499. module.set.offset();
  500. $module
  501. .css('left', module.cache.element.left)
  502. .css('bottom' , '')
  503. .removeClass(className.bound)
  504. .removeClass(className.top)
  505. .addClass(className.fixed)
  506. .addClass(className.bottom)
  507. ;
  508. settings.onStick.call(element);
  509. },
  510. unbind: function() {
  511. module.debug('Removing absolute position on element');
  512. module.remove.offset();
  513. $module
  514. .removeClass(className.bound)
  515. .removeClass(className.top)
  516. .removeClass(className.bottom)
  517. ;
  518. },
  519. unfix: function() {
  520. module.debug('Removing fixed position on element');
  521. module.remove.offset();
  522. $module
  523. .removeClass(className.fixed)
  524. .removeClass(className.top)
  525. .removeClass(className.bottom)
  526. ;
  527. settings.onUnstick.call(element);
  528. },
  529. reset: function() {
  530. module.debug('Reseting elements position');
  531. module.unbind();
  532. module.unfix();
  533. module.resetCSS();
  534. module.remove.offset();
  535. },
  536. resetCSS: function() {
  537. $module
  538. .css({
  539. width : '',
  540. height : ''
  541. })
  542. ;
  543. $container
  544. .css({
  545. height: ''
  546. })
  547. ;
  548. },
  549. setting: function(name, value) {
  550. if( $.isPlainObject(name) ) {
  551. $.extend(true, settings, name);
  552. }
  553. else if(value !== undefined) {
  554. settings[name] = value;
  555. }
  556. else {
  557. return settings[name];
  558. }
  559. },
  560. internal: function(name, value) {
  561. if( $.isPlainObject(name) ) {
  562. $.extend(true, module, name);
  563. }
  564. else if(value !== undefined) {
  565. module[name] = value;
  566. }
  567. else {
  568. return module[name];
  569. }
  570. },
  571. debug: function() {
  572. if(settings.debug) {
  573. if(settings.performance) {
  574. module.performance.log(arguments);
  575. }
  576. else {
  577. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  578. module.debug.apply(console, arguments);
  579. }
  580. }
  581. },
  582. verbose: function() {
  583. if(settings.verbose && settings.debug) {
  584. if(settings.performance) {
  585. module.performance.log(arguments);
  586. }
  587. else {
  588. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  589. module.verbose.apply(console, arguments);
  590. }
  591. }
  592. },
  593. error: function() {
  594. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  595. module.error.apply(console, arguments);
  596. },
  597. performance: {
  598. log: function(message) {
  599. var
  600. currentTime,
  601. executionTime,
  602. previousTime
  603. ;
  604. if(settings.performance) {
  605. currentTime = new Date().getTime();
  606. previousTime = time || currentTime;
  607. executionTime = currentTime - previousTime;
  608. time = currentTime;
  609. performance.push({
  610. 'Name' : message[0],
  611. 'Arguments' : [].slice.call(message, 1) || '',
  612. 'Element' : element,
  613. 'Execution Time' : executionTime
  614. });
  615. }
  616. clearTimeout(module.performance.timer);
  617. module.performance.timer = setTimeout(module.performance.display, 0);
  618. },
  619. display: function() {
  620. var
  621. title = settings.name + ':',
  622. totalTime = 0
  623. ;
  624. time = false;
  625. clearTimeout(module.performance.timer);
  626. $.each(performance, function(index, data) {
  627. totalTime += data['Execution Time'];
  628. });
  629. title += ' ' + totalTime + 'ms';
  630. if(moduleSelector) {
  631. title += ' \'' + moduleSelector + '\'';
  632. }
  633. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  634. console.groupCollapsed(title);
  635. if(console.table) {
  636. console.table(performance);
  637. }
  638. else {
  639. $.each(performance, function(index, data) {
  640. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  641. });
  642. }
  643. console.groupEnd();
  644. }
  645. performance = [];
  646. }
  647. },
  648. invoke: function(query, passedArguments, context) {
  649. var
  650. object = instance,
  651. maxDepth,
  652. found,
  653. response
  654. ;
  655. passedArguments = passedArguments || queryArguments;
  656. context = element || context;
  657. if(typeof query == 'string' && object !== undefined) {
  658. query = query.split(/[\. ]/);
  659. maxDepth = query.length - 1;
  660. $.each(query, function(depth, value) {
  661. var camelCaseValue = (depth != maxDepth)
  662. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  663. : query
  664. ;
  665. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  666. object = object[camelCaseValue];
  667. }
  668. else if( object[camelCaseValue] !== undefined ) {
  669. found = object[camelCaseValue];
  670. return false;
  671. }
  672. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  673. object = object[value];
  674. }
  675. else if( object[value] !== undefined ) {
  676. found = object[value];
  677. return false;
  678. }
  679. else {
  680. return false;
  681. }
  682. });
  683. }
  684. if ( $.isFunction( found ) ) {
  685. response = found.apply(context, passedArguments);
  686. }
  687. else if(found !== undefined) {
  688. response = found;
  689. }
  690. if($.isArray(returnedValue)) {
  691. returnedValue.push(response);
  692. }
  693. else if(returnedValue !== undefined) {
  694. returnedValue = [returnedValue, response];
  695. }
  696. else if(response !== undefined) {
  697. returnedValue = response;
  698. }
  699. return found;
  700. }
  701. };
  702. if(methodInvoked) {
  703. if(instance === undefined) {
  704. module.initialize();
  705. }
  706. module.invoke(query);
  707. }
  708. else {
  709. if(instance !== undefined) {
  710. instance.invoke('destroy');
  711. }
  712. module.initialize();
  713. }
  714. })
  715. ;
  716. return (returnedValue !== undefined)
  717. ? returnedValue
  718. : this
  719. ;
  720. };
  721. $.fn.sticky.settings = {
  722. name : 'Sticky',
  723. namespace : 'sticky',
  724. debug : false,
  725. verbose : true,
  726. performance : true,
  727. pushing : false,
  728. context : false,
  729. scrollContext : window,
  730. offset : 0,
  731. bottomOffset : 0,
  732. observeChanges : false,
  733. onReposition : function(){},
  734. onScroll : function(){},
  735. onStick : function(){},
  736. onUnstick : function(){},
  737. onTop : function(){},
  738. onBottom : function(){},
  739. error : {
  740. container : 'Sticky element must be inside a relative container',
  741. visible : 'Element is hidden, you must call refresh after element becomes visible',
  742. method : 'The method you called is not defined.',
  743. invalidContext : 'Context specified does not exist',
  744. elementSize : 'Sticky element is larger than its container, cannot create sticky.'
  745. },
  746. className : {
  747. bound : 'bound',
  748. fixed : 'fixed',
  749. supported : 'native',
  750. top : 'top',
  751. bottom : 'bottom'
  752. }
  753. };
  754. })( jQuery, window , document );