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.

771 lines
23 KiB

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