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.

776 lines
24 KiB

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