sticky.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. // jQuery HC-Sticky
  2. // =============
  3. // Version: 1.2.43
  4. // Copyright: Some Web Media
  5. // Author: Some Web Guy
  6. // Author URL: http://twitter.com/some_web_guy
  7. // Website: http://someweblog.com/
  8. // Plugin URL: https://github.com/somewebmedia/hc-sticky
  9. // License: Released under the MIT License www.opensource.org/licenses/mit-license.php
  10. // Description: Cross-browser jQuery plugin that makes any element attached to the page and always visible while you scroll.
  11. (function($, window, undefined) {
  12. "use strict";
  13. // console.log shortcut
  14. var log = function(t){console.log(t)};
  15. var $window = $(window),
  16. document = window.document,
  17. $document = $(document);
  18. // detect IE version
  19. var ie = (function(){var undef, v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while (div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->', all[0]){}; return v > 4 ? v : undef})();
  20. /*----------------------------------------------------
  21. Global functions
  22. ----------------------------------------------------*/
  23. // check for scroll direction and speed
  24. var getScroll = function() {
  25. var pageXOffset = window.pageXOffset !== undefined ? window.pageXOffset : (document.compatMode == "CSS1Compat" ? window.document.documentElement.scrollLeft : window.document.body.scrollLeft),
  26. pageYOffset = window.pageYOffset !== undefined ? window.pageYOffset : (document.compatMode == "CSS1Compat" ? window.document.documentElement.scrollTop : window.document.body.scrollTop);
  27. if (typeof getScroll.x == 'undefined') {
  28. getScroll.x = pageXOffset;
  29. getScroll.y = pageYOffset;
  30. }
  31. if (typeof getScroll.distanceX == 'undefined') {
  32. getScroll.distanceX = pageXOffset;
  33. getScroll.distanceY = pageYOffset;
  34. } else {
  35. getScroll.distanceX = pageXOffset - getScroll.x;
  36. getScroll.distanceY = pageYOffset - getScroll.y;
  37. }
  38. var diffX = getScroll.x - pageXOffset,
  39. diffY = getScroll.y - pageYOffset;
  40. getScroll.direction = diffX < 0 ? 'right' :
  41. diffX > 0 ? 'left' :
  42. diffY <= 0 ? 'down' :
  43. diffY > 0 ? 'up' : 'first';
  44. getScroll.x = pageXOffset;
  45. getScroll.y = pageYOffset;
  46. };
  47. $window.on('scroll', getScroll);
  48. // little original style plugin
  49. $.fn.style = function(style) {
  50. if (!style) return null;
  51. var $this = $(this),
  52. value;
  53. // clone element
  54. var $clone = $this.clone().css('display','none');
  55. // randomize the name of cloned radio buttons, otherwise selections get screwed
  56. $clone.find('input:radio').attr('name','copy-' + Math.floor((Math.random()*100)+1));
  57. // insert clone to DOM
  58. $this.after($clone);
  59. var getStyle = function(el, style){
  60. var val;
  61. if (el.currentStyle) {
  62. // replace dashes with capitalized letter, e.g. padding-left to paddingLeft
  63. val = el.currentStyle[style.replace(/-\w/g, function(s){return s.toUpperCase().replace('-','')})];
  64. } else if (window.getComputedStyle) {
  65. val = document.defaultView.getComputedStyle(el,null).getPropertyValue(style);
  66. }
  67. // check for margin:auto
  68. val = (/margin/g.test(style)) ? ((parseInt(val) === $this[0].offsetLeft) ? val : 'auto') : val;
  69. return val;
  70. };
  71. if (typeof style == 'string') {
  72. value = getStyle($clone[0], style);
  73. } else {
  74. value = {};
  75. $.each(style, function(i, s){
  76. value[s] = getStyle($clone[0], s);
  77. });
  78. }
  79. // destroy clone
  80. $clone.remove();
  81. return value || null;
  82. };
  83. /*----------------------------------------------------
  84. jQuery plugin
  85. ----------------------------------------------------*/
  86. $.fn.extend({
  87. hcSticky: function(options) {
  88. // check if selected element exist in DOM, user doesn't have to worry about that
  89. if (this.length == 0) return this;
  90. this.pluginOptions('hcSticky', {
  91. top: 0,
  92. bottom: 0,
  93. bottomEnd: 0,
  94. innerTop: 0,
  95. innerSticker: null,
  96. className: 'sticky',
  97. wrapperClassName: 'wrapper-sticky',
  98. stickTo: null,
  99. responsive: true,
  100. followScroll: true,
  101. offResolutions: null,
  102. onStart: $.noop,
  103. onStop: $.noop,
  104. on: true,
  105. fn: null // used only by the plugin
  106. }, options || {}, {
  107. reinit: function(){
  108. // just call itself again
  109. $(this).hcSticky();
  110. },
  111. stop: function(){
  112. $(this).pluginOptions('hcSticky', {on: false}).each(function(){
  113. var $this = $(this),
  114. options = $this.pluginOptions('hcSticky'),
  115. $wrapper = $this.parent('.' + options.wrapperClassName);
  116. // set current position
  117. var top = $this.offset().top - $wrapper.offset().top;
  118. $this.css({
  119. position: 'absolute',
  120. top: top,
  121. bottom: 'auto',
  122. left: 'auto',
  123. right: 'auto'
  124. }).removeClass(options.className);
  125. });
  126. },
  127. off: function(){
  128. $(this).pluginOptions('hcSticky', {on: false}).each(function(){
  129. var $this = $(this),
  130. options = $this.pluginOptions('hcSticky'),
  131. $wrapper = $this.parent('.' + options.wrapperClassName);
  132. // clear position
  133. $this.css({
  134. position: 'relative',
  135. top: 'auto',
  136. bottom: 'auto',
  137. left: 'auto',
  138. right: 'auto'
  139. }).removeClass(options.className);
  140. $wrapper.css('height', 'auto');
  141. });
  142. },
  143. on: function(){
  144. $(this).each(function(){
  145. $(this).pluginOptions('hcSticky', {
  146. on: true,
  147. remember: {
  148. offsetTop: $window.scrollTop()
  149. }
  150. }).hcSticky();
  151. });
  152. },
  153. destroy: function(){
  154. var $this = $(this),
  155. options = $this.pluginOptions('hcSticky'),
  156. $wrapper = $this.parent('.' + options.wrapperClassName);
  157. // reset position to original
  158. $this.removeData('hcStickyInit').css({
  159. position: $wrapper.css('position'),
  160. top: $wrapper.css('top'),
  161. bottom: $wrapper.css('bottom'),
  162. left: $wrapper.css('left'),
  163. right: $wrapper.css('right')
  164. }).removeClass(options.className);
  165. // remove events
  166. $window.off('resize', options.fn.resize).off('scroll', options.fn.scroll);
  167. // destroy wrapper
  168. $this.unwrap();
  169. }
  170. });
  171. // on/off settings
  172. if (options && typeof options.on != 'undefined') {
  173. if (options.on) {
  174. this.hcSticky('on');
  175. } else {
  176. this.hcSticky('off');
  177. }
  178. }
  179. // stop on commands
  180. if (typeof options == 'string') return this;
  181. // do our thing
  182. return this.each(function(){
  183. var $this = $(this),
  184. options = $this.pluginOptions('hcSticky');
  185. var $wrapper = (function(){ // wrapper exists
  186. var $this_wrapper = $this.parent('.' + options.wrapperClassName);
  187. if ($this_wrapper.length > 0) {
  188. $this_wrapper.css({
  189. 'height': $this.outerHeight(true),
  190. 'width': (function(){
  191. // check if wrapper already has width in %
  192. var width = $this_wrapper.style('width');
  193. if (width.indexOf('%') >= 0 || width == 'auto') {
  194. if ($this.css('box-sizing') == 'border-box' || $this.css('-moz-box-sizing') == 'border-box') {
  195. $this.css('width', $this_wrapper.width());
  196. } else {
  197. $this.css('width', $this_wrapper.width() - parseInt($this.css('padding-left') - parseInt($this.css('padding-right'))));
  198. }
  199. return width;
  200. } else {
  201. return $this.outerWidth(true);
  202. }
  203. })()
  204. });
  205. return $this_wrapper;
  206. } else {
  207. return false;
  208. }
  209. })() || (function(){ // wrapper doesn't exist
  210. var this_css = $this.style(['width', 'margin-left', 'left', 'right', 'top', 'bottom', 'float', 'display']);
  211. var display = $this.css('display');
  212. var $this_wrapper = $('<div>', {
  213. 'class': options.wrapperClassName
  214. }).css({
  215. 'display': display,
  216. 'height': $this.outerHeight(true),
  217. 'width': (function(){
  218. if (this_css['width'].indexOf('%') >= 0 || (this_css['width'] == 'auto' && display != 'inline-block' && display != 'inline')) { // check if element has width in %
  219. $this.css('width', parseFloat($this.css('width')));
  220. return this_css['width'];
  221. } else if (this_css['width'] == 'auto' && (display == 'inline-block' || display == 'inline')) {
  222. return $this.width();
  223. } else {
  224. // check if margin is set to 'auto'
  225. return (this_css['margin-left'] == 'auto') ? $this.outerWidth() : $this.outerWidth(true);
  226. }
  227. })(),
  228. 'margin': (this_css['margin-left']) ? 'auto' : null,
  229. 'position': (function(){
  230. var position = $this.css('position');
  231. return position == 'static' ? 'relative' : position;
  232. })(),
  233. 'float': this_css['float'] || null,
  234. 'left': this_css['left'],
  235. 'right': this_css['right'],
  236. 'top': this_css['top'],
  237. 'bottom': this_css['bottom'],
  238. 'vertical-align': 'top'
  239. });
  240. $this.wrap($this_wrapper);
  241. // ie7 inline-block fix
  242. if (ie === 7) {
  243. if ($('head').find('style#hcsticky-iefix').length === 0) {
  244. $('<style id="hcsticky-iefix">.' + options.wrapperClassName + ' {zoom: 1;}</style>').appendTo('head');
  245. }
  246. }
  247. // return appended element
  248. return $this.parent();
  249. })();
  250. // check if we should go further
  251. if ($this.data('hcStickyInit')) return;
  252. // leave our mark
  253. $this.data('hcStickyInit', true);
  254. // check if referring element is document
  255. var stickTo_document = options.stickTo && (options.stickTo == 'document' || (options.stickTo.nodeType && options.stickTo.nodeType == 9) || (typeof options.stickTo == 'object' && options.stickTo instanceof (typeof HTMLDocument != 'undefined' ? HTMLDocument : Document))) ? true : false;
  256. // select container ;)
  257. var $container = options.stickTo
  258. ? stickTo_document
  259. ? $document
  260. : typeof options.stickTo == 'string'
  261. ? $(options.stickTo)
  262. : options.stickTo
  263. : $wrapper.parent();
  264. // clear sticky styles
  265. $this.css({
  266. top: 'auto',
  267. bottom: 'auto',
  268. left: 'auto',
  269. right: 'auto'
  270. });
  271. // attach event on entire page load, maybe some images inside element has been loading, so chek height again
  272. $window.load(function(){
  273. if ($this.outerHeight(true) > $container.height()) {
  274. $wrapper.css('height', $this.outerHeight(true));
  275. $this.hcSticky('reinit');
  276. }
  277. });
  278. // functions for attachiung and detaching sticky
  279. var _setFixed = function(args) {
  280. // check if already floating
  281. if ($this.hasClass(options.className)) return;
  282. // apply styles
  283. args = args || {};
  284. $this.css({
  285. position: 'fixed',
  286. top: args.top || 0,
  287. left: args.left || $wrapper.offset().left
  288. }).addClass(options.className);
  289. // start event
  290. options.onStart.apply($this[0]);
  291. // add class to wrpaeer
  292. $wrapper.addClass('sticky-active');
  293. },
  294. _reset = function(args) {
  295. args = args || {};
  296. args.position = args.position || 'absolute';
  297. args.top = args.top || 0;
  298. args.left = args.left || 0;
  299. // check if we should apply css
  300. if ($this.css('position') != 'fixed' && parseInt($this.css('top')) == args.top) return;
  301. // apply styles
  302. $this.css({
  303. position: args.position,
  304. top: args.top,
  305. left: args.left
  306. }).removeClass(options.className);
  307. // stop event
  308. options.onStop.apply($this[0]);
  309. // remove class from wrpaeer
  310. $wrapper.removeClass('sticky-active');
  311. };
  312. // sticky scroll function
  313. var onScroll = function(init) {
  314. // check if we need to run sticky
  315. if (!options.on || $this.outerHeight(true) >= $container.height()) return;
  316. var top_spacing = (options.innerSticker) ? $(options.innerSticker).position().top : ((options.innerTop) ? options.innerTop : 0),
  317. wrapper_inner_top = $wrapper.offset().top,
  318. bottom_limit = $container.height() - options.bottomEnd + (stickTo_document ? 0 : wrapper_inner_top),
  319. top_limit = $wrapper.offset().top - options.top + top_spacing,
  320. this_height = $this.outerHeight(true) + options.bottom,
  321. window_height = $window.height(),
  322. offset_top = $window.scrollTop(),
  323. this_document_top = $this.offset().top,
  324. this_window_top = this_document_top - offset_top,
  325. bottom_distance;
  326. // if sticky has been restarted with on/off wait for it to reach top or bottom
  327. if (typeof options.remember != 'undefined' && options.remember) {
  328. var position_top = this_document_top - options.top - top_spacing;
  329. if (this_height - top_spacing > window_height && options.followScroll) { // element bigger than window with follow scroll on
  330. if (position_top < offset_top && offset_top + window_height <= position_top + $this.height()) {
  331. // element is in the middle of the screen, let our primary calculations do the work
  332. options.remember = false;
  333. }
  334. } else { // element smaller than window or follow scroll turned off
  335. if (options.remember.offsetTop > position_top) {
  336. // slide up
  337. if (offset_top <= position_top) {
  338. _setFixed({
  339. top: options.top - top_spacing
  340. });
  341. options.remember = false;
  342. }
  343. } else {
  344. // slide down
  345. if (offset_top >= position_top) {
  346. _setFixed({
  347. top: options.top - top_spacing
  348. });
  349. options.remember = false;
  350. }
  351. }
  352. }
  353. return;
  354. }
  355. if (offset_top > top_limit) {
  356. // http://geek-and-poke.com/geekandpoke/2012/7/27/simply-explained.html
  357. if (bottom_limit + options.bottom - (options.followScroll && window_height < this_height ? 0 : options.top) <= offset_top + this_height - top_spacing - ((this_height - top_spacing > window_height - (top_limit - top_spacing) && options.followScroll) ? (((bottom_distance = this_height - window_height - top_spacing) > 0) ? bottom_distance : 0) : 0)) {
  358. // bottom reached end
  359. _reset({
  360. top: bottom_limit - this_height + options.bottom - wrapper_inner_top
  361. });
  362. } else if (this_height - top_spacing > window_height && options.followScroll) {
  363. if (this_window_top + this_height <= window_height) { // element bigger than window with follow scroll on
  364. if (getScroll.direction == 'down') {
  365. // scroll down
  366. _setFixed({
  367. top: window_height - this_height
  368. });
  369. } else {
  370. // scroll up
  371. if (this_window_top < 0 && $this.css('position') == 'fixed') {
  372. _reset({
  373. top: this_document_top - (top_limit + options.top - top_spacing) - getScroll.distanceY
  374. });
  375. }
  376. }
  377. } else { // element smaller than window or follow scroll turned off
  378. if (getScroll.direction == 'up' && this_document_top >= offset_top + options.top - top_spacing) {
  379. // scroll up
  380. _setFixed({
  381. top: options.top - top_spacing
  382. });
  383. } else if (getScroll.direction == 'down' && this_document_top + this_height > window_height && $this.css('position') == 'fixed') {
  384. // scroll down
  385. _reset({
  386. top: this_document_top - (top_limit + options.top - top_spacing) - getScroll.distanceY
  387. });
  388. }
  389. }
  390. } else {
  391. // starting (top) fixed position
  392. _setFixed({
  393. top: options.top - top_spacing
  394. });
  395. }
  396. } else {
  397. // reset
  398. _reset();
  399. }
  400. };
  401. // store resize data in case responsive is on
  402. var resize_timeout = false,
  403. $resize_clone = false;
  404. var onResize = function() {
  405. // check if sticky is attached to scroll event
  406. attachScroll();
  407. // check for off resolutions
  408. checkResolutions();
  409. // check if we need to run sticky
  410. if (!options.on) return;
  411. var setLeft = function(){
  412. // set new left position
  413. if ($this.css('position') == 'fixed') {
  414. $this.css('left', $wrapper.offset().left);
  415. } else {
  416. $this.css('left', 0);
  417. }
  418. };
  419. // check for width change (css media queries)
  420. if (options.responsive) {
  421. // clone element and make it invisible
  422. if (!$resize_clone) {
  423. $resize_clone = $this.clone().attr('style', '').css({
  424. visibility: 'hidden',
  425. height: 0,
  426. overflow: 'hidden',
  427. paddingTop: 0,
  428. paddingBottom: 0,
  429. marginTop: 0,
  430. marginBottom: 0
  431. });
  432. $wrapper.after($resize_clone);
  433. }
  434. var wrapper_width = $wrapper.style('width');
  435. var resize_clone_width = $resize_clone.style('width');
  436. if (resize_clone_width == 'auto' && wrapper_width != 'auto') {
  437. resize_clone_width = parseInt($this.css('width'));
  438. }
  439. // recalculate wrpaeer width
  440. if (resize_clone_width != wrapper_width) {
  441. $wrapper.width(resize_clone_width);
  442. }
  443. // clear previous timeout
  444. if (resize_timeout) {
  445. clearTimeout(resize_timeout);
  446. }
  447. // timedout destroing of cloned elements so we don't clone it again and again while resizing the window
  448. resize_timeout = setTimeout(function() {
  449. // clear timeout id
  450. resize_timeout = false;
  451. // destroy cloned element
  452. $resize_clone.remove();
  453. $resize_clone = false;
  454. }, 250);
  455. }
  456. // set new left position
  457. setLeft();
  458. // recalculate inner element width (maybe original width was in %)
  459. if ($this.outerWidth(true) != $wrapper.width()) {
  460. var this_w = ($this.css('box-sizing') == 'border-box' || $this.css('-moz-box-sizing') == 'border-box')
  461. ? $wrapper.width()
  462. : $wrapper.width() - parseInt($this.css('padding-left')) - parseInt($this.css('padding-right'));
  463. // subtract margins
  464. this_w = this_w - parseInt($this.css('margin-left')) - parseInt($this.css('margin-right'));
  465. // set new width
  466. $this.css('width', this_w);
  467. }
  468. };
  469. // remember scroll and resize callbacks so we can attach and detach them
  470. $this.pluginOptions('hcSticky', {fn: {
  471. scroll: onScroll,
  472. resize: onResize
  473. }});
  474. // check for off resolutions
  475. var checkResolutions = function(){
  476. if (options.offResolutions) {
  477. // convert to array
  478. if (!$.isArray(options.offResolutions)) {
  479. options.offResolutions = [options.offResolutions];
  480. }
  481. var isOn = true;
  482. $.each(options.offResolutions, function(i, rez){
  483. if (rez < 0) {
  484. // below
  485. if ($window.width() < rez * -1) {
  486. isOn = false;
  487. $this.hcSticky('off');
  488. }
  489. } else {
  490. // abowe
  491. if ($window.width() > rez) {
  492. isOn = false;
  493. $this.hcSticky('off');
  494. }
  495. }
  496. });
  497. // turn on again
  498. if (isOn && !options.on) {
  499. $this.hcSticky('on');
  500. }
  501. }
  502. };
  503. checkResolutions();
  504. // attach resize function to event
  505. $window.on('resize', onResize);
  506. // attaching scroll function to event
  507. var attachScroll = function(){
  508. // check if element height is bigger than the content
  509. if ($this.outerHeight(true) < $container.height()) {
  510. var isAttached = false;
  511. if ($._data(window, 'events').scroll != undefined) {
  512. $.each($._data(window, 'events').scroll, function(i, f){
  513. if (f.handler == options.fn.scroll) {
  514. isAttached = true;
  515. }
  516. });
  517. }
  518. if (!isAttached) {
  519. // run it once to disable glitching
  520. options.fn.scroll(true);
  521. // attach function to scroll event only once
  522. $window.on('scroll', options.fn.scroll);
  523. }
  524. }
  525. };
  526. attachScroll();
  527. });
  528. }
  529. });
  530. })(jQuery, this);
  531. // jQuery HC-PluginOptions
  532. // =============
  533. // Version: 1.0
  534. // Copyright: Some Web Media
  535. // Author: Some Web Guy
  536. // Author URL: http://twitter.com/some_web_guy
  537. // Website: http://someweblog.com/
  538. // License: Released under the MIT License www.opensource.org/licenses/mit-license.php
  539. (function($, undefined) {
  540. "use strict";
  541. $.fn.extend({
  542. pluginOptions: function(pluginName, defaultOptions, userOptions, commands) {
  543. // create object to store data
  544. if (!this.data(pluginName)) this.data(pluginName, {});
  545. // return options
  546. if (pluginName && typeof defaultOptions == 'undefined') return this.data(pluginName).options;
  547. // update
  548. userOptions = userOptions || (defaultOptions || {});
  549. if (typeof userOptions == 'object' || userOptions === undefined) {
  550. // options
  551. return this.each(function(){
  552. var $this = $(this);
  553. if (!$this.data(pluginName).options) {
  554. // init our options and attach to element
  555. $this.data(pluginName, {options: $.extend(defaultOptions, userOptions || {})});
  556. // attach commands if any
  557. if (commands) {
  558. $this.data(pluginName).commands = commands;
  559. }
  560. } else {
  561. // update existing options
  562. $this.data(pluginName, $.extend($this.data(pluginName), {options: $.extend($this.data(pluginName).options, userOptions || {})}));
  563. }
  564. });
  565. } else if (typeof userOptions == 'string') {
  566. return this.each(function(){
  567. $(this).data(pluginName).commands[userOptions].call(this);
  568. });
  569. } else {
  570. return this;
  571. }
  572. }
  573. });
  574. })(jQuery);