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.

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