In the previous articleUsing the getBoundingClientRect method to implement a simple sticky component introduced a simple implementation of the sticky component. After thinking about it for the past two days, I found that there are more implementations provided last time. The shortcomings are that the effects achieved on other websites are also somewhat different when unfixing. The unfixing method provided last time was not good. Based on the above, this article provides an improved version of the sticky component. The functions are more complete, I hope you are interested in reading.
1. Problems with old versions
There are multiple problems in the implementation of the previous sticky component:
First, in terms of the effect of sticky, before and after the sticky element is fixed, what will not change is the position relative to the left side of the browser and the overall width of the sticky element. What may change is the position relative to the top or bottom of the browser. and the height of the sticky element. In the implementation provided above, the latter two changing values are regarded as constant values. Why is the top value or bottom value always 0 when it is fixed? Of course, it can be other than 0, such as top: 20px, bottom: 15px. In some scenes, adding some such offsets will make the sticky effect look better, such as the affix component example used in the bootstrap official documentation (this component The function is similar to the sticky component implemented in this article):
It sets the position relative to the top of the browser to top: 20px when fixed. The same goes for the height of sticky elements. In order to display a better-looking effect when fixed, it is also a very common requirement to adjust the original Line-height or padding-top and other height-related attributes. For example, this page of Tmall Huabei, this Block content uses the sticky component:
Before fixed, the height of the sticky element is:
After fixed, the height of sticky element is:
Second, when canceling the fixation, taking the sticky element fixed at the top as an example, the implementation provided above is to directly cancel the position of the sticky element when the distance between the target element and the top of the browser is less than stickyHeight: fixed attribute, the sticky element is immediately restored to the normal document flow, and the effect is:
It disappears immediately when it reaches the critical point, but the effect of Tmall Huabei is not like this:
It does not disappear immediately when it reaches the critical point, but re-adjusts the top value of the sticky element so that it can scroll up along with the main content of the webpage in conjunction with the scroll bar:
From an experience point of view, it is obvious that the effect of Tmall Huabei is better. From a functional point of view, the implementation provided above has a fatal shortcoming: when the height of the sticky element is very large, it exceeds the browser's capabilities. When viewing the height of the area, there will be a bug that no matter how you scroll, you cannot browse all the contents of the sticky elements. If you are interested, you can try the code implemented last time on the sidebar of your blog. I tried and found this problem, so I wanted to improve the sticky component: (
Third, the last implementation still has several shortcomings:
1) documentElement.clientHeight is not cached, causing it to be re-acquired every time the critical point is judged:
2) The default value of the scroll callback interval is too large and should be set smaller. This time it is 5 and bootstrap uses 1. Only in this way can the effect be guaranteed to be smooth;
3) In some scenarios, the width of the sticky element may not need to be reset when resizing is required. An option should be added to control it;
4) When the sticky element is fixed and unfixed, a callback function should be provided so that other components can do things at key points when they depend on this component.
2. How to improve
The component options have been redefined:
var DEFAULTS = { target: '', //target元素的jq选择器 type: 'top', //固定的位置,top | bottom,默认为top,表示固定在顶部 wait: 5, //scroll事件回调的间隔 stickyOffset: 0, //固定时距离浏览器可视区顶部或底部的偏移,用来设置top跟bottom属性的值,默认为0 isFixedWidth: true, //sticky元素宽度是否固定,默认为true,如果是自适应的宽度,需设置为false getStickyWidth: undefined, //用来获取sticky元素宽度的回调,在不传该参数的情况下,stickyWidth将设置为sticky元素的offsetWidth unStickyDistance: undefined, //该参数决定sticky元素何时进入dynamicSticky状态 onSticky: undefined, ///sticky元素固定时的回调 onUnSticky: undefined ///sticky元素取消固定时的回调 };
The ones in bold are new or modified. The original height has been removed and replaced with unStickyDistance. When fixing, the position relative to the top or bottom of the browser is specified with stickyOffset, so that there is no need to write the top or bottom attribute value in the css of .sticky--in-top or .sticky--in-bottom. If isFixedWidth is false, a callback for refreshing the width of the sticky element during resize will be added:
!opts.isFixedWidth && $win.resize(throttle(function () { setStickyWidth(); $elem.hasClass(className) && $elem.css('width', stickyWidth); sticky(); }, opts.wait));
Compared with the last time, the trouble in this implementation is the logical processing when canceling the fixation. Last time, the sticky element only had two states, sticky or unsticky. This time it is different. The sticky state is divided into staticSticky and dynamicSticky. The former It represents the sticky state in which the top or bottom value remains unchanged. The latter represents the sticky state in which the top or bottom value changes. In fact, the latter corresponds to the range when the fixation is about to be cancelled. In order to solve this problem more clearly, the original judgment is The critical points and the code that performs different processing at different critical points are restructured into the following:
setSticky = function () { !$elem.hasClass(className) && $elem.addClass(className).css('width', stickyWidth) && (typeof opts.onSticky == 'function' && opts.onSticky($elem, $target)); return true; }, states = { staticSticky: function () { setSticky() && $elem.css(opts.type, opts.stickyOffset); }, dynamicSticky: function (rect) { setSticky() && $elem.css(opts.type, rules[opts.type].getDynamicOffset(rect)); }, unSticky: function () { $elem.hasClass(className) && $elem.removeClass(className).css('width', '').css(opts.type, '') && (typeof opts.onUnSticky == 'function' && opts.onUnSticky($elem, $target)); } }, rules = { top: { getState: function (rect) { if (rect.top < 0 && (rect.bottom - unStickyDistance) > 0) return 'staticSticky'; else if ((rect.bottom - unStickyDistance) <= 0 && rect.bottom > 0) return 'dynamicSticky'; else return 'unSticky'; }, getDynamicOffset: function (rect) { return -(unStickyDistance - rect.bottom); } }, bottom: { getState: function (rect) { if (rect.bottom > docClientHeight && (rect.top + unStickyDistance) < docClientHeight) return 'staticSticky'; else if ((rect.top + unStickyDistance) >= docClientHeight && rect.top < docClientHeight) return 'dynamicSticky'; else return 'unSticky'; }, getDynamicOffset: function (rect) { return -(unStickyDistance + rect.top - docClientHeight); } } } $win.scroll(throttle(sticky, opts.wait)); function sticky() { var rect = $target[0].getBoundingClientRect(), curState = rules[opts.type].getState(rect); states[curState](rect); }
There is a bit of the idea of the state model in it, but it is more concise. When I wrote this code, I really wanted to use the state machine I had learned about before. I thought it was definitely possible to write it using a state machine, but I just wanted to save one class library from being referenced. I’ll wait someday. Give it a try again when you want to practice state machines.
The overall implementation is as follows:
var Sticky = (function ($) { function throttle(func, wait) { var timer = null; return function () { var self = this, args = arguments; if (timer) clearTimeout(timer); timer = setTimeout(function () { return typeof func === 'function' && func.apply(self, args); }, wait); } } var DEFAULTS = { target: '', //target元素的jq选择器 type: 'top', //固定的位置,top | bottom,默认为top,表示固定在顶部 wait: 5, //scroll事件回调的间隔 stickyOffset: 0, //固定时距离浏览器可视区顶部或底部的偏移,用来设置top跟bottom属性的值,默认为0 isFixedWidth: true, //sticky元素宽度是否固定,默认为true,如果是自适应的宽度,需设置为false getStickyWidth: undefined, //用来获取sticky元素宽度的回调,在不传该参数的情况下,stickyWidth将设置为sticky元素的offsetWidth unStickyDistance: undefined, //该参数决定sticky元素何时进入dynamicSticky状态 onSticky: undefined, ///sticky元素固定时的回调 onUnSticky: undefined ///sticky元素取消固定时的回调 }; return function (elem, opts) { var $elem = $(elem); opts = $.extend({}, DEFAULTS, opts || {}, $elem.data() || {}); var $target = $(opts.target); if (!$elem.length || !$target.length) return; var stickyWidth, setStickyWidth = function () { stickyWidth = typeof opts.getStickyWidth === 'function' && opts.getStickyWidth($elem) || $elem[0].offsetWidth; }, docClientHeight = document.documentElement.clientHeight, unStickyDistance = opts.unStickyDistance || $elem[0].offsetHeight, setSticky = function () { !$elem.hasClass(className) && $elem.addClass(className).css('width', stickyWidth) && (typeof opts.onSticky == 'function' && opts.onSticky($elem, $target)); return true; }, states = { staticSticky: function () { setSticky() && $elem.css(opts.type, opts.stickyOffset); }, dynamicSticky: function (rect) { setSticky() && $elem.css(opts.type, rules[opts.type].getDynamicOffset(rect)); }, unSticky: function () { $elem.hasClass(className) && $elem.removeClass(className).css('width', '').css(opts.type, '') && (typeof opts.onUnSticky == 'function' && opts.onUnSticky($elem, $target)); } }, rules = { top: { getState: function (rect) { if (rect.top < 0 && (rect.bottom - unStickyDistance) > 0) return 'staticSticky'; else if ((rect.bottom - unStickyDistance) <= 0 && rect.bottom > 0) return 'dynamicSticky'; else return 'unSticky'; }, getDynamicOffset: function (rect) { return -(unStickyDistance - rect.bottom); } }, bottom: { getState: function (rect) { if (rect.bottom > docClientHeight && (rect.top + unStickyDistance) < docClientHeight) return 'staticSticky'; else if ((rect.top + unStickyDistance) >= docClientHeight && rect.top < docClientHeight) return 'dynamicSticky'; else return 'unSticky'; }, getDynamicOffset: function (rect) { return -(unStickyDistance + rect.top - docClientHeight); } } }, className = 'sticky--in-' + opts.type, $win = $(window); setStickyWidth(); $win.scroll(throttle(sticky, opts.wait)); !opts.isFixedWidth && $win.resize(throttle(function () { setStickyWidth(); $elem.hasClass(className) && $elem.css('width', stickyWidth); sticky(); }, opts.wait)); $win.resize(throttle(function () { docClientHeight = document.documentElement.clientHeight; }, opts.wait)); function sticky() { var rect = $target[0].getBoundingClientRect(), curState = rules[opts.type].getState(rect); states[curState](rect); } } })(jQuery);
What is difficult to understand may be the logic of the getState method. Some of the ideas in this part are explained in more detail in the previous blog.
3. Blog sidebar application instructions
First, you must paste this implementation into the blog settings footer html text field, and then add the following code to initialize:
var timer = setInterval(function(){ if($('#blogCalendar').length && $('#profile_block').length && $('#sidebar_search').length) { new Sticky('#sideBar', { target: '#main', onSticky: function($elem, $target){ $target.css('min-height',$elem.outerHeight()); $elem.css('left', '65px'); }, onUnSticky: function($elem, $target){ $target.css('min-height',''); $elem.css('left', ''); } }); } },100);
Timer is used because the content of the sidebar is loaded by ajax, and it is impossible to add callbacks during these ajax requests. You can only judge whether the sidebar is loaded through the content they return.
4. Summary
This weekend I thought about how to improve the sticky component. In addition, I spent most of the day writing this article. At least now I feel a little satisfied with the function and implementation of the sticky component. I finished writing last time. It felt weird, like it was lacking something, but it turned out to be because there were still so many things missing. At present, this component can only achieve the effect of fixing and unfixing. For actual work, the effect of this level may not be enough. The common functions on the Internet that support navigation scrolling or tab navigation while being fixed are also very common. Next This article will introduce the sticky component based on this article, how to implement navScrollSticky and tabSticky components, so stay tuned.
Thank you for reading :)
Additional explanation:
In IE and Firefox, when refreshing the page, if the page is scrolling before refreshing, the refresh operation will set the scroll position of the page to the refresh position, but the scroll event will not be triggered, so it must be called immediately after the component is initialized. A sticky function: