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.

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