├── .gitignore ├── LICENSE.md ├── README.md ├── bower.json ├── example-center.html ├── example-header.html ├── example-right.html ├── jquery.sticky.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.psd 2 | *~ 3 | node_modules/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2014-2016 Anthony Garand 2 | http://garand.me 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sticky 2 | 3 | Sticky is a jQuery plugin that gives you the ability to make any element on your page always stay visible. 4 | 5 | ## Sticky in brief 6 | 7 | This is how it works: 8 | 9 | - When the target element is about to be hidden, the plugin will add the class `className` to it (and to a wrapper added as its parent), set it to `position: fixed` and calculate its new `top`, based on the element's height, the page height and the `topSpacing` and `bottomSpacing` options. 10 | - That's it. 11 | In some cases you might need to set a fixed width to your element when it is "sticked". 12 | But by default (`widthFromWrapper == true`) sticky updates elements's width to the wrapper's width. 13 | Check the `example-*.html` files for some examples. 14 | 15 | ## Usage 16 | 17 | - Include jQuery & Sticky. 18 | - Call Sticky. 19 | 20 | ```html 21 | 22 | 23 | 28 | ``` 29 | 30 | - Edit your css to position the elements (check the examples in `example-*.html`). 31 | 32 | - To unstick an object 33 | 34 | ```html 35 | 38 | ``` 39 | 40 | ## Options 41 | 42 | - `topSpacing`: (default: `0`) Pixels between the page top and the element's top. 43 | - `bottomSpacing`: (default: `0`) Pixels between the page bottom and the element's bottom. 44 | - `className`: (default: `'is-sticky'`) CSS class added to the element's wrapper when "sticked". 45 | - `wrapperClassName`: (default: `'sticky-wrapper'`) CSS class added to the wrapper. 46 | - `center`: (default: `false`) Boolean determining whether the sticky element should be horizontally centered in the page. 47 | - `getWidthFrom`: (default: `''`) Selector of element referenced to set fixed width of "sticky" element. 48 | - `widthFromWrapper`: (default: `true`) Boolean determining whether width of the "sticky" element should be updated to match the wrapper's width. Wrapper is a placeholder for "sticky" element while it is fixed (out of static elements flow), and its width depends on the context and CSS rules. Works only as long `getWidthForm` isn't set. 49 | - `responsiveWidth`: (default: `false`) Boolean determining whether widths will be recalculated on window resize (using getWidthfrom). 50 | - `zIndex`: (default: `inherit`) controls z-index of the sticked element. 51 | 52 | ## Methods 53 | 54 | - `sticky(options)`: Initializer. `options` is optional. 55 | - `sticky('update')`: Recalculates the element's position. 56 | 57 | ## Events 58 | 59 | - `sticky-start`: When the element becomes sticky. 60 | - `sticky-end`: When the element returns to its original location 61 | - `sticky-update`: When the element is sticked but position must be updated for constraints reasons 62 | - `sticky-bottom-reached`: When the element reached the bottom space limit 63 | - `sticky-bottom-unreached`: When the element unreached the bottom space limit 64 | 65 | To subscribe to events use jquery: 66 | 67 | ```html 68 | 75 | ``` 76 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-sticky", 3 | "description": "Sticky is a jQuery plugin that gives you the ability to make any element on your page always stay visible.", 4 | "categories": [ "UI", "DOM" ], 5 | "author": { 6 | "name": "Anthony Garand" 7 | }, 8 | "contributors": [ 9 | { 10 | "name": "German M. Bravo" 11 | }, 12 | { 13 | "name": "Ruud Kamphuis" 14 | }, 15 | { 16 | "name": "Leonardo C. Daronco" 17 | } 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/garand/sticky.git" 22 | }, 23 | "bugs": { 24 | "web": "https://github.com/garand/sticky/issues" 25 | }, 26 | "licenses": [ 27 | { 28 | "type": "MIT", 29 | "url": "http://jquery.com/blob/master/MIT-LICENSE.txt" 30 | }, 31 | { 32 | "type": "GPL", 33 | "url": "http://jquery.com/blob/master/GPL-LICENSE.txt" 34 | } 35 | ], 36 | "github": "https://github.com/garand/sticky", 37 | 38 | "main": "jquery.sticky.js", 39 | "dependencies": { 40 | "jquery": null 41 | }, 42 | "ignore": [] 43 | } 44 | -------------------------------------------------------------------------------- /example-center.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sticky Plugin 5 | 6 | 7 | 12 | 34 | 35 | 36 |

This is test this is text this is text at the top.

37 |
38 |

This is the sticky thingy that is really cool.

39 |
40 |

This is test this is text this is text at the bottom.

41 | 42 | 43 | -------------------------------------------------------------------------------- /example-header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sticky Plugin 5 | 6 | 7 | 12 | 33 | 34 | 35 |

This is test this is text this is text at the top.

36 | 39 |

This is test this is text this is text at the bottom.

40 | 41 | 42 | -------------------------------------------------------------------------------- /example-right.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sticky Plugin 5 | 6 | 7 | 12 | 39 | 40 | 41 |
42 |

This is test this is text this is text at the top.

43 |
44 |

This is the sticky thingy that is really cool.

45 |
46 |

This is test this is text this is text at the bottom.

47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /jquery.sticky.js: -------------------------------------------------------------------------------- 1 | // Sticky Plugin v1.0.4 for jQuery 2 | // ============= 3 | // Author: Anthony Garand 4 | // Improvements by German M. Bravo (Kronuz) and Ruud Kamphuis (ruudk) 5 | // Improvements by Leonardo C. Daronco (daronco) 6 | // Created: 02/14/2011 7 | // Date: 07/20/2015 8 | // Website: http://stickyjs.com/ 9 | // Description: Makes an element on the page stick on the screen as you scroll 10 | // It will only set the 'top' and 'position' of your element, you 11 | // might need to adjust the width in some cases. 12 | 13 | (function (factory) { 14 | if (typeof define === 'function' && define.amd) { 15 | // AMD. Register as an anonymous module. 16 | define(['jquery'], factory); 17 | } else if (typeof module === 'object' && module.exports) { 18 | // Node/CommonJS 19 | module.exports = factory(require('jquery')); 20 | } else { 21 | // Browser globals 22 | factory(jQuery); 23 | } 24 | }(function ($) { 25 | var slice = Array.prototype.slice; // save ref to original slice() 26 | var splice = Array.prototype.splice; // save ref to original slice() 27 | 28 | var defaults = { 29 | topSpacing: 0, 30 | bottomSpacing: 0, 31 | className: 'is-sticky', 32 | wrapperClassName: 'sticky-wrapper', 33 | center: false, 34 | getWidthFrom: '', 35 | widthFromWrapper: true, // works only when .getWidthFrom is empty 36 | responsiveWidth: false, 37 | zIndex: 'inherit' 38 | }, 39 | $window = $(window), 40 | $document = $(document), 41 | sticked = [], 42 | windowHeight = $window.height(), 43 | scroller = function() { 44 | var scrollTop = $window.scrollTop(), 45 | documentHeight = $document.height(), 46 | dwh = documentHeight - windowHeight, 47 | extra = (scrollTop > dwh) ? dwh - scrollTop : 0; 48 | 49 | for (var i = 0, l = sticked.length; i < l; i++) { 50 | var s = sticked[i], 51 | elementTop = s.stickyWrapper.offset().top, 52 | etse = elementTop - s.topSpacing - extra; 53 | 54 | //update height in case of dynamic content 55 | s.stickyWrapper.css('height', s.stickyElement.outerHeight()); 56 | 57 | if (scrollTop <= etse) { 58 | if (s.currentTop !== null) { 59 | s.stickyElement 60 | .css({ 61 | 'width': '', 62 | 'position': '', 63 | 'top': '', 64 | 'z-index': '' 65 | }); 66 | s.stickyElement.parent().removeClass(s.className); 67 | s.stickyElement.trigger('sticky-end', [s]); 68 | s.currentTop = null; 69 | } 70 | } 71 | else { 72 | var newTop = documentHeight - s.stickyElement.outerHeight() 73 | - s.topSpacing - s.bottomSpacing - scrollTop - extra; 74 | if (newTop < 0) { 75 | newTop = newTop + s.topSpacing; 76 | } else { 77 | newTop = s.topSpacing; 78 | } 79 | if (s.currentTop !== newTop) { 80 | var newWidth; 81 | if (s.getWidthFrom) { 82 | padding = s.stickyElement.innerWidth() - s.stickyElement.width(); 83 | newWidth = $(s.getWidthFrom).width() - padding || null; 84 | } else if (s.widthFromWrapper) { 85 | newWidth = s.stickyWrapper.width(); 86 | } 87 | if (newWidth == null) { 88 | newWidth = s.stickyElement.width(); 89 | } 90 | s.stickyElement 91 | .css('width', newWidth) 92 | .css('position', 'fixed') 93 | .css('top', newTop) 94 | .css('z-index', s.zIndex); 95 | 96 | s.stickyElement.parent().addClass(s.className); 97 | 98 | if (s.currentTop === null) { 99 | s.stickyElement.trigger('sticky-start', [s]); 100 | } else { 101 | // sticky is started but it have to be repositioned 102 | s.stickyElement.trigger('sticky-update', [s]); 103 | } 104 | 105 | if (s.currentTop === s.topSpacing && s.currentTop > newTop || s.currentTop === null && newTop < s.topSpacing) { 106 | // just reached bottom || just started to stick but bottom is already reached 107 | s.stickyElement.trigger('sticky-bottom-reached', [s]); 108 | } else if(s.currentTop !== null && newTop === s.topSpacing && s.currentTop < newTop) { 109 | // sticky is started && sticked at topSpacing && overflowing from top just finished 110 | s.stickyElement.trigger('sticky-bottom-unreached', [s]); 111 | } 112 | 113 | s.currentTop = newTop; 114 | } 115 | 116 | // Check if sticky has reached end of container and stop sticking 117 | var stickyWrapperContainer = s.stickyWrapper.parent(); 118 | var unstick = (s.stickyElement.offset().top + s.stickyElement.outerHeight() >= stickyWrapperContainer.offset().top + stickyWrapperContainer.outerHeight()) && (s.stickyElement.offset().top <= s.topSpacing); 119 | 120 | if( unstick ) { 121 | s.stickyElement 122 | .css('position', 'absolute') 123 | .css('top', '') 124 | .css('bottom', 0) 125 | .css('z-index', ''); 126 | } else { 127 | s.stickyElement 128 | .css('position', 'fixed') 129 | .css('top', newTop) 130 | .css('bottom', '') 131 | .css('z-index', s.zIndex); 132 | } 133 | } 134 | } 135 | }, 136 | resizer = function() { 137 | windowHeight = $window.height(); 138 | 139 | for (var i = 0, l = sticked.length; i < l; i++) { 140 | var s = sticked[i]; 141 | var newWidth = null; 142 | if (s.getWidthFrom) { 143 | if (s.responsiveWidth) { 144 | newWidth = $(s.getWidthFrom).width(); 145 | } 146 | } else if(s.widthFromWrapper) { 147 | newWidth = s.stickyWrapper.width(); 148 | } 149 | if (newWidth != null) { 150 | s.stickyElement.css('width', newWidth); 151 | } 152 | } 153 | }, 154 | methods = { 155 | init: function(options) { 156 | return this.each(function() { 157 | var o = $.extend({}, defaults, options); 158 | var stickyElement = $(this); 159 | 160 | var stickyId = stickyElement.attr('id'); 161 | var wrapperId = stickyId ? stickyId + '-' + defaults.wrapperClassName : defaults.wrapperClassName; 162 | var wrapper = $('
') 163 | .attr('id', wrapperId) 164 | .addClass(o.wrapperClassName); 165 | 166 | stickyElement.wrapAll(function() { 167 | if ($(this).parent("#" + wrapperId).length == 0) { 168 | return wrapper; 169 | } 170 | }); 171 | 172 | var stickyWrapper = stickyElement.parent(); 173 | 174 | if (o.center) { 175 | stickyWrapper.css({width:stickyElement.outerWidth(),marginLeft:"auto",marginRight:"auto"}); 176 | } 177 | 178 | if (stickyElement.css("float") === "right") { 179 | stickyElement.css({"float":"none"}).parent().css({"float":"right"}); 180 | } 181 | 182 | o.stickyElement = stickyElement; 183 | o.stickyWrapper = stickyWrapper; 184 | o.currentTop = null; 185 | 186 | sticked.push(o); 187 | 188 | methods.setWrapperHeight(this); 189 | methods.setupChangeListeners(this); 190 | }); 191 | }, 192 | 193 | setWrapperHeight: function(stickyElement) { 194 | var element = $(stickyElement); 195 | var stickyWrapper = element.parent(); 196 | if (stickyWrapper) { 197 | stickyWrapper.css('height', element.outerHeight()); 198 | } 199 | }, 200 | 201 | setupChangeListeners: function(stickyElement) { 202 | if (window.MutationObserver) { 203 | var mutationObserver = new window.MutationObserver(function(mutations) { 204 | if (mutations[0].addedNodes.length || mutations[0].removedNodes.length) { 205 | methods.setWrapperHeight(stickyElement); 206 | } 207 | }); 208 | mutationObserver.observe(stickyElement, {subtree: true, childList: true}); 209 | } else { 210 | if (window.addEventListener) { 211 | stickyElement.addEventListener('DOMNodeInserted', function() { 212 | methods.setWrapperHeight(stickyElement); 213 | }, false); 214 | stickyElement.addEventListener('DOMNodeRemoved', function() { 215 | methods.setWrapperHeight(stickyElement); 216 | }, false); 217 | } else if (window.attachEvent) { 218 | stickyElement.attachEvent('onDOMNodeInserted', function() { 219 | methods.setWrapperHeight(stickyElement); 220 | }); 221 | stickyElement.attachEvent('onDOMNodeRemoved', function() { 222 | methods.setWrapperHeight(stickyElement); 223 | }); 224 | } 225 | } 226 | }, 227 | update: scroller, 228 | unstick: function(options) { 229 | return this.each(function() { 230 | var that = this; 231 | var unstickyElement = $(that); 232 | 233 | var removeIdx = -1; 234 | var i = sticked.length; 235 | while (i-- > 0) { 236 | if (sticked[i].stickyElement.get(0) === that) { 237 | splice.call(sticked,i,1); 238 | removeIdx = i; 239 | } 240 | } 241 | if(removeIdx !== -1) { 242 | unstickyElement.unwrap(); 243 | unstickyElement 244 | .css({ 245 | 'width': '', 246 | 'position': '', 247 | 'top': '', 248 | 'float': '', 249 | 'z-index': '' 250 | }) 251 | ; 252 | } 253 | }); 254 | } 255 | }; 256 | 257 | // should be more efficient than using $window.scroll(scroller) and $window.resize(resizer): 258 | if (window.addEventListener) { 259 | window.addEventListener('scroll', scroller, false); 260 | window.addEventListener('resize', resizer, false); 261 | } else if (window.attachEvent) { 262 | window.attachEvent('onscroll', scroller); 263 | window.attachEvent('onresize', resizer); 264 | } 265 | 266 | $.fn.sticky = function(method) { 267 | if (methods[method]) { 268 | return methods[method].apply(this, slice.call(arguments, 1)); 269 | } else if (typeof method === 'object' || !method ) { 270 | return methods.init.apply( this, arguments ); 271 | } else { 272 | $.error('Method ' + method + ' does not exist on jQuery.sticky'); 273 | } 274 | }; 275 | 276 | $.fn.unstick = function(method) { 277 | if (methods[method]) { 278 | return methods[method].apply(this, slice.call(arguments, 1)); 279 | } else if (typeof method === 'object' || !method ) { 280 | return methods.unstick.apply( this, arguments ); 281 | } else { 282 | $.error('Method ' + method + ' does not exist on jQuery.sticky'); 283 | } 284 | }; 285 | $(function() { 286 | setTimeout(scroller, 0); 287 | }); 288 | })); 289 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-sticky", 3 | "version": "1.0.4", 4 | "description": "Sticky is a jQuery plugin that gives you the ability to make any element on your page always stay visible.", 5 | "main": "jquery.sticky.js", 6 | "files": [ 7 | "jquery.sticky.js" 8 | ], 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": "garand/sticky", 13 | "keywords": [ 14 | "dom", 15 | "jquery-plugin", 16 | "ui" 17 | ], 18 | "author": "Anthony Garand", 19 | "license": "(MIT OR GPL-3.0)", 20 | "bugs": { 21 | "url": "https://github.com/garand/sticky/issues" 22 | }, 23 | "homepage": "https://github.com/garand/sticky#readme", 24 | "peerDependencies": { 25 | "jquery": "*" 26 | } 27 | } 28 | --------------------------------------------------------------------------------