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.

767 lines
23 KiB

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