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

9 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 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
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
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. $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 );