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.

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