├── docs ├── index.html ├── css │ └── index.css └── js │ └── scrollToTop.js ├── README.md └── scrollToTop.js /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 回到顶部 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 20000px; 3 | background-color: #eee; 4 | margin: 0; 5 | } 6 | 7 | #toTop { 8 | position: fixed; 9 | right: 20px; 10 | bottom: 20px; 11 | margin-bottom: 20px; 12 | cursor: pointer; 13 | width: 0; 14 | height: 0; 15 | border-top: 30px solid transparent; 16 | border-bottom: 60px solid #3498db; 17 | border-left: 30px solid transparent; 18 | border-right: 30px solid transparent; 19 | } 20 | 21 | #toTop.backing { 22 | border-bottom: 60px solid #e74c3c; 23 | } 24 | 25 | .rule { 26 | height: 100px; 27 | width: 10%; 28 | line-height: 100px; 29 | text-align: center; 30 | color: #fff; 31 | background-color: #1abc9c; 32 | } 33 | 34 | .rule-list { 35 | list-style: none; 36 | margin: 0; 37 | padding: 0; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scrollTop 2 | 3 | ## 介绍 4 | 5 | 原生 JavaScript 实现的回到顶部库,适应 PC 和移动,兼容到 IE7+。 6 | 7 | 效果演示:[https://mqyqingfeng.github.io/ScrollToTop/](https://mqyqingfeng.github.io/ScrollToTop/) 8 | 9 | ## 兼容性 10 | 11 | 移动和 PC ,其中 PC 端支持 IE7+ 12 | 13 | ## 依赖 14 | 15 | 原生 JavaScript 实现,无依赖。 16 | 17 | ## 下载 18 | 19 | ```js 20 | git clone git@github.com:mqyqingfeng/ScrollToTop.git 21 | ``` 22 | 23 | ## 使用 24 | 25 | ```html 26 | 27 | ``` 28 | 29 | 或者 30 | 31 | ```js 32 | import ScrollToTop from 'path/ScrollToTop.js' 33 | ``` 34 | 35 | ## 示例 36 | 37 | ```html 38 | 39 | ``` 40 | 41 | ```css 42 | #toTop { 43 | position: fixed; right: 20px; bottom: 20px; margin-bottom: 20px; cursor: pointer; width: 0; height: 0; border-top: 30px solid transparent; border-bottom: 60px solid #3498db; border-left: 30px solid transparent; border-right: 30px solid transparent; 44 | } 45 | ``` 46 | 47 | ```js 48 | var toTop = new ScrollToTop("#top"); 49 | ``` 50 | 51 | 你也可以传入参数: 52 | 53 | ```js 54 | var toTop = new ScrollToTop("#top", { 55 | // 滚动条向下滑动多少时,出现回到顶部按钮 56 | showWhen: 100, 57 | // 回到顶部的速度。 58 | speed: 100, 59 | // 元素淡入和淡出的速度 60 | fadeSpeed: 10 61 | }); 62 | ``` 63 | 64 | 注意:当要兼容 IE7 的时候,不能传入选择符字符串,直接传入元素: 65 | 66 | ```js 67 | var topElement = document.getElementById("toTop"); 68 | var toTop = new ScrollToTop(topElement) 69 | ``` 70 | 71 | ## API 72 | 73 | ### 初始化 74 | 75 | ```js 76 | var toTop = new ScrollToTop("selector", options); 77 | ``` 78 | 79 | ### options 80 | 81 | **1.showWhen** 82 | 83 | 默认值为 100,表示滚动条向下滑动 100px 时,出现回到顶部按钮 84 | 85 | **2.speed** 86 | 87 | 回到顶部的速度。默认值为 100,数值越大,速度越快。 88 | 89 | 100 表示浏览器每次重绘,scrollTop 就减去 100px。 90 | 91 | **3.fadeSpeed** 92 | 93 | 元素淡入和淡出的速度。默认值为 10,数值越大,速度越快。 94 | 95 | 10 表示浏览器每次重绘,元素透明度以 10% 递增或者递减。 96 | 97 | ### backing 98 | 99 | 当回到顶部时,按钮元素会添加一个名为 backing 的类,当按钮元素隐藏时,该类会被删除。 100 | 101 | 可以使用此类名实现正在回到顶部时的效果。 102 | -------------------------------------------------------------------------------- /scrollToTop.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var root = (typeof self == 'object' && self.self == self && self) || 3 | (typeof global == 'object' && global.global == global && global) || 4 | this || {}; 5 | 6 | // requestAnimationFrame 兼容到 IE6 7 | var lastTime = 0; 8 | var vendors = ['webkit', 'moz']; 9 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 10 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 11 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || // Webkit中此取消方法的名字变了 12 | window[vendors[x] + 'CancelRequestAnimationFrame']; 13 | } 14 | 15 | if (!window.requestAnimationFrame) { 16 | window.requestAnimationFrame = function(callback, element) { 17 | var currTime = new Date().getTime(); 18 | var timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); 19 | var id = window.setTimeout(function() { 20 | callback(currTime + timeToCall); 21 | }, timeToCall); 22 | lastTime = currTime + timeToCall; 23 | return id; 24 | }; 25 | } 26 | if (!window.cancelAnimationFrame) { 27 | window.cancelAnimationFrame = function(id) { 28 | clearTimeout(id); 29 | }; 30 | } 31 | 32 | // bind 函数在 IE7-8 不能使用 33 | Function.prototype.bind = Function.prototype.bind || function (context) { 34 | if (typeof this !== "function") { 35 | throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); 36 | } 37 | 38 | var self = this; 39 | var args = Array.prototype.slice.call(arguments, 1); 40 | 41 | var fNOP = function () {}; 42 | 43 | var fBound = function () { 44 | var bindArgs = Array.prototype.slice.call(arguments); 45 | self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); 46 | } 47 | 48 | fNOP.prototype = this.prototype; 49 | fBound.prototype = new fNOP(); 50 | return fBound; 51 | } 52 | 53 | 54 | var util = { 55 | extend: function(target) { 56 | for (var i = 1, len = arguments.length; i < len; i++) { 57 | for (var prop in arguments[i]) { 58 | if (arguments[i].hasOwnProperty(prop)) { 59 | target[prop] = arguments[i][prop] 60 | } 61 | } 62 | } 63 | 64 | return target 65 | }, 66 | getStyle: function(element, prop) { 67 | return element.currentStyle ? element.currentStyle[prop] : document.defaultView.getComputedStyle(element)[prop] 68 | }, 69 | getScrollOffsets: function() { 70 | var w = window; 71 | if (w.pageXOffset != null) return { x: w.pageXOffset, y: w.pageYOffset }; 72 | var d = w.document; 73 | if (document.compatMode == "CSS1Compat") { 74 | return { 75 | x: d.documentElement.scrollLeft, 76 | y: d.documentElement.scrollTop 77 | } 78 | } 79 | return { x: d.body.scrollLeft, y: d.body.scrollTop } 80 | }, 81 | setOpacity: function(ele, opacity) { 82 | if (ele.style.opacity != undefined) { 83 | ele.style.opacity = opacity / 100; 84 | } else { 85 | // 兼容低版本 IE 浏览器 86 | ele.style.filter = "alpha(opacity=" + opacity + ")"; 87 | } 88 | }, 89 | fadeIn: function(element, speed) { 90 | var opacity = 0; 91 | util.setOpacity(element, 0) 92 | var timer; 93 | 94 | function step() { 95 | util.setOpacity(element, opacity += speed) 96 | if (opacity < 100) { 97 | timer = requestAnimationFrame(step); 98 | } else { 99 | cancelAnimationFrame(timer) 100 | } 101 | } 102 | requestAnimationFrame(step) 103 | }, 104 | fadeOut: function(element, speed) { 105 | var opacity = 100; 106 | util.setOpacity(element, 100) 107 | var timer; 108 | 109 | function step() { 110 | util.setOpacity(element, opacity -= speed) 111 | if (opacity > 0) { 112 | timer = requestAnimationFrame(step); 113 | } else { 114 | cancelAnimationFrame(timer) 115 | } 116 | } 117 | requestAnimationFrame(step) 118 | }, 119 | addEvent: function(element, type, fn) { 120 | if (document.addEventListener) { 121 | element.addEventListener(type, fn, false); 122 | return fn; 123 | } else if (document.attachEvent) { 124 | var bound = function() { 125 | return fn.apply(element, arguments) 126 | } 127 | element.attachEvent('on' + type, bound); 128 | return bound; 129 | } 130 | }, 131 | indexOf: function(array, item) { 132 | var result = -1; 133 | for (var i = 0, len = array.length; i < len; i++) { 134 | if (array[i] === item) { 135 | result = i; 136 | break; 137 | } 138 | } 139 | return result; 140 | }, 141 | addClass: function(element, className) { 142 | var classNames = element.className.split(/\s+/); 143 | if (util.indexOf(classNames, className) == -1) { 144 | classNames.push(className); 145 | } 146 | element.className = classNames.join(' ') 147 | }, 148 | removeClass: function(element, className) { 149 | var classNames = element.className.split(/\s+/); 150 | var index = util.indexOf(classNames, className) 151 | if (index !== -1) { 152 | classNames.splice(index, 1); 153 | } 154 | element.className = classNames.join(' ') 155 | }, 156 | supportTouch: function() { 157 | return 'ontouchstart' in window || 158 | window.DocumentTouch && document instanceof window.DocumentTouch; 159 | 160 | }, 161 | getTime: function() { 162 | return new Date().getTime(); 163 | } 164 | } 165 | 166 | function ScrollToTop(element, options) { 167 | this.element = typeof element === 'string' ? document.querySelector(element) : element; 168 | 169 | this.options = util.extend({}, this.constructor.defaultOptions, options) 170 | 171 | this.init(); 172 | } 173 | 174 | ScrollToTop.VERSION = '1.0.0'; 175 | 176 | ScrollToTop.defaultOptions = { 177 | // 默认值为 100,表示滚动条向下滑动 100px 时,出现回到顶部按钮 178 | showWhen: 100, 179 | // 回到顶部的速度。默认值为 100,数值越大,速度越快。 100 表示浏览器每次重绘,scrollTop 就减去 100px。 180 | speed: 100, 181 | // 元素淡入和淡出的速度。默认值为 10,数值越大,速度越快。 10 表示浏览器每次重绘,元素透明度以 10% 递增或者递减。 182 | fadeSpeed: 10 183 | } 184 | 185 | var proto = ScrollToTop.prototype; 186 | 187 | proto.init = function() { 188 | this.hideElement(); 189 | this.bindScrollEvent(); 190 | this.bindToTopEvent(); 191 | } 192 | 193 | proto.hideElement = function() { 194 | util.setOpacity(this.element, 0); 195 | this.status = "hide"; 196 | } 197 | 198 | proto.bindScrollEvent = function() { 199 | var self = this; 200 | util.addEvent(window, "scroll", function() { 201 | if (util.getScrollOffsets().y > self.options.showWhen) { 202 | if (self.status == 'hide') { 203 | util.fadeIn(self.element, self.options.fadeSpeed) 204 | self.status = 'show'; 205 | } 206 | } else { 207 | if (self.status == 'show') { 208 | util.fadeOut(self.element, self.options.fadeSpeed); 209 | self.status = 'hide'; 210 | util.removeClass(self.element, 'backing') 211 | } 212 | } 213 | }) 214 | 215 | } 216 | 217 | proto.handleBack = function() { 218 | var timer, self = this; 219 | util.addClass(self.element, 'backing'); 220 | cancelAnimationFrame(timer); 221 | timer = requestAnimationFrame(function fn() { 222 | var oTop = document.body.scrollTop || document.documentElement.scrollTop; 223 | if (oTop > 0) { 224 | document.body.scrollTop = document.documentElement.scrollTop = oTop - self.options.speed; 225 | timer = requestAnimationFrame(fn); 226 | } else { 227 | cancelAnimationFrame(timer); 228 | } 229 | }) 230 | } 231 | 232 | proto.bindToTopEvent = function() { 233 | var self = this; 234 | 235 | util.addEvent(self.element, "click", self.handleBack.bind(self)) 236 | 237 | if (util.supportTouch()) { 238 | util.addEvent(self.element, "touchstart", function(e) { 239 | self._startX = e.touches[0].pageX; 240 | self._startY = e.touches[0].pageY; 241 | self._startTime = util.getTime(); 242 | }) 243 | 244 | util.addEvent(self.element, "touchmove", function(e) { 245 | self._moveX = e.touches[0].pageX; 246 | self._moveY = e.touches[0].pageY; 247 | }) 248 | 249 | util.addEvent(self.element, "touchend", function(e) { 250 | 251 | var endTime = util.getTime(); 252 | if (self._moveX !== null && Math.abs(self._moveX - self.startX) > 10 || 253 | self._moveY !== null && Math.abs(self._moveY - self.startY) > 10) { 254 | 255 | } else { 256 | // 手指移动的距离小于 10 像素并且手指和屏幕的接触时间要小于 500 毫秒 257 | if (endTime - self._startTime < 500) { 258 | self.handleBack() 259 | } 260 | } 261 | }) 262 | 263 | } 264 | 265 | } 266 | 267 | if (typeof exports != 'undefined' && !exports.nodeType) { 268 | if (typeof module != 'undefined' && !module.nodeType && module.exports) { 269 | exports = module.exports = ScrollToTop; 270 | } 271 | exports.ScrollToTop = ScrollToTop; 272 | } else { 273 | root.ScrollToTop = ScrollToTop; 274 | } 275 | }()); -------------------------------------------------------------------------------- /docs/js/scrollToTop.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var root = (typeof self == 'object' && self.self == self && self) || 3 | (typeof global == 'object' && global.global == global && global) || 4 | this || {}; 5 | 6 | // requestAnimationFrame 兼容到 IE6 7 | var lastTime = 0; 8 | var vendors = ['webkit', 'moz']; 9 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 10 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 11 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || // Webkit中此取消方法的名字变了 12 | window[vendors[x] + 'CancelRequestAnimationFrame']; 13 | } 14 | 15 | if (!window.requestAnimationFrame) { 16 | window.requestAnimationFrame = function(callback, element) { 17 | var currTime = new Date().getTime(); 18 | var timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); 19 | var id = window.setTimeout(function() { 20 | callback(currTime + timeToCall); 21 | }, timeToCall); 22 | lastTime = currTime + timeToCall; 23 | return id; 24 | }; 25 | } 26 | if (!window.cancelAnimationFrame) { 27 | window.cancelAnimationFrame = function(id) { 28 | clearTimeout(id); 29 | }; 30 | } 31 | 32 | // bind 函数在 IE7-8 不能使用 33 | Function.prototype.bind = Function.prototype.bind || function (context) { 34 | if (typeof this !== "function") { 35 | throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); 36 | } 37 | 38 | var self = this; 39 | var args = Array.prototype.slice.call(arguments, 1); 40 | 41 | var fNOP = function () {}; 42 | 43 | var fBound = function () { 44 | var bindArgs = Array.prototype.slice.call(arguments); 45 | self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); 46 | } 47 | 48 | fNOP.prototype = this.prototype; 49 | fBound.prototype = new fNOP(); 50 | return fBound; 51 | } 52 | 53 | 54 | var util = { 55 | extend: function(target) { 56 | for (var i = 1, len = arguments.length; i < len; i++) { 57 | for (var prop in arguments[i]) { 58 | if (arguments[i].hasOwnProperty(prop)) { 59 | target[prop] = arguments[i][prop] 60 | } 61 | } 62 | } 63 | 64 | return target 65 | }, 66 | getStyle: function(element, prop) { 67 | return element.currentStyle ? element.currentStyle[prop] : document.defaultView.getComputedStyle(element)[prop] 68 | }, 69 | getScrollOffsets: function() { 70 | var w = window; 71 | if (w.pageXOffset != null) return { x: w.pageXOffset, y: w.pageYOffset }; 72 | var d = w.document; 73 | if (document.compatMode == "CSS1Compat") { 74 | return { 75 | x: d.documentElement.scrollLeft, 76 | y: d.documentElement.scrollTop 77 | } 78 | } 79 | return { x: d.body.scrollLeft, y: d.body.scrollTop } 80 | }, 81 | setOpacity: function(ele, opacity) { 82 | if (ele.style.opacity != undefined) { 83 | ele.style.opacity = opacity / 100; 84 | } else { 85 | // 兼容低版本 IE 浏览器 86 | ele.style.filter = "alpha(opacity=" + opacity + ")"; 87 | } 88 | }, 89 | fadeIn: function(element, speed) { 90 | var opacity = 0; 91 | util.setOpacity(element, 0) 92 | var timer; 93 | 94 | function step() { 95 | util.setOpacity(element, opacity += speed) 96 | if (opacity < 100) { 97 | timer = requestAnimationFrame(step); 98 | } else { 99 | cancelAnimationFrame(timer) 100 | } 101 | } 102 | requestAnimationFrame(step) 103 | }, 104 | fadeOut: function(element, speed) { 105 | var opacity = 100; 106 | util.setOpacity(element, 100) 107 | var timer; 108 | 109 | function step() { 110 | util.setOpacity(element, opacity -= speed) 111 | if (opacity > 0) { 112 | timer = requestAnimationFrame(step); 113 | } else { 114 | cancelAnimationFrame(timer) 115 | } 116 | } 117 | requestAnimationFrame(step) 118 | }, 119 | addEvent: function(element, type, fn) { 120 | if (document.addEventListener) { 121 | element.addEventListener(type, fn, false); 122 | return fn; 123 | } else if (document.attachEvent) { 124 | var bound = function() { 125 | return fn.apply(element, arguments) 126 | } 127 | element.attachEvent('on' + type, bound); 128 | return bound; 129 | } 130 | }, 131 | indexOf: function(array, item) { 132 | var result = -1; 133 | for (var i = 0, len = array.length; i < len; i++) { 134 | if (array[i] === item) { 135 | result = i; 136 | break; 137 | } 138 | } 139 | return result; 140 | }, 141 | addClass: function(element, className) { 142 | var classNames = element.className.split(/\s+/); 143 | if (util.indexOf(classNames, className) == -1) { 144 | classNames.push(className); 145 | } 146 | element.className = classNames.join(' ') 147 | }, 148 | removeClass: function(element, className) { 149 | var classNames = element.className.split(/\s+/); 150 | var index = util.indexOf(classNames, className) 151 | if (index !== -1) { 152 | classNames.splice(index, 1); 153 | } 154 | element.className = classNames.join(' ') 155 | }, 156 | supportTouch: function() { 157 | return 'ontouchstart' in window || 158 | window.DocumentTouch && document instanceof window.DocumentTouch; 159 | 160 | }, 161 | getTime: function() { 162 | return new Date().getTime(); 163 | } 164 | } 165 | 166 | function ScrollToTop(element, options) { 167 | this.element = typeof element === 'string' ? document.querySelector(element) : element; 168 | 169 | this.options = util.extend({}, this.constructor.defaultOptions, options) 170 | 171 | this.init(); 172 | } 173 | 174 | ScrollToTop.VERSION = '1.0.0'; 175 | 176 | ScrollToTop.defaultOptions = { 177 | // 默认值为 100,表示滚动条向下滑动 100px 时,出现回到顶部按钮 178 | showWhen: 100, 179 | // 回到顶部的速度。默认值为 100,数值越大,速度越快。 100 表示浏览器每次重绘,scrollTop 就减去 100px。 180 | speed: 100, 181 | // 元素淡入和淡出的速度。默认值为 10,数值越大,速度越快。 10 表示浏览器每次重绘,元素透明度以 10% 递增或者递减。 182 | fadeSpeed: 10 183 | } 184 | 185 | var proto = ScrollToTop.prototype; 186 | 187 | proto.init = function() { 188 | this.hideElement(); 189 | this.bindScrollEvent(); 190 | this.bindToTopEvent(); 191 | } 192 | 193 | proto.hideElement = function() { 194 | util.setOpacity(this.element, 0); 195 | this.status = "hide"; 196 | } 197 | 198 | proto.bindScrollEvent = function() { 199 | var self = this; 200 | util.addEvent(window, "scroll", function() { 201 | if (util.getScrollOffsets().y > self.options.showWhen) { 202 | if (self.status == 'hide') { 203 | util.fadeIn(self.element, self.options.fadeSpeed) 204 | self.status = 'show'; 205 | } 206 | } else { 207 | if (self.status == 'show') { 208 | util.fadeOut(self.element, self.options.fadeSpeed); 209 | self.status = 'hide'; 210 | util.removeClass(self.element, 'backing') 211 | } 212 | } 213 | }) 214 | 215 | } 216 | 217 | proto.handleBack = function() { 218 | var timer, self = this; 219 | util.addClass(self.element, 'backing'); 220 | cancelAnimationFrame(timer); 221 | timer = requestAnimationFrame(function fn() { 222 | var oTop = document.body.scrollTop || document.documentElement.scrollTop; 223 | if (oTop > 0) { 224 | document.body.scrollTop = document.documentElement.scrollTop = oTop - self.options.speed; 225 | timer = requestAnimationFrame(fn); 226 | } else { 227 | cancelAnimationFrame(timer); 228 | } 229 | }) 230 | } 231 | 232 | proto.bindToTopEvent = function() { 233 | var self = this; 234 | 235 | util.addEvent(self.element, "click", self.handleBack.bind(self)) 236 | 237 | if (util.supportTouch()) { 238 | util.addEvent(self.element, "touchstart", function(e) { 239 | self._startX = e.touches[0].pageX; 240 | self._startY = e.touches[0].pageY; 241 | self._startTime = util.getTime(); 242 | }) 243 | 244 | util.addEvent(self.element, "touchmove", function(e) { 245 | self._moveX = e.touches[0].pageX; 246 | self._moveY = e.touches[0].pageY; 247 | }) 248 | 249 | util.addEvent(self.element, "touchend", function(e) { 250 | 251 | var endTime = util.getTime(); 252 | if (self._moveX !== null && Math.abs(self._moveX - self.startX) > 10 || 253 | self._moveY !== null && Math.abs(self._moveY - self.startY) > 10) { 254 | 255 | } else { 256 | // 手指移动的距离小于 10 像素并且手指和屏幕的接触时间要小于 500 毫秒 257 | if (endTime - self._startTime < 500) { 258 | self.handleBack() 259 | } 260 | } 261 | }) 262 | 263 | } 264 | 265 | } 266 | 267 | if (typeof exports != 'undefined' && !exports.nodeType) { 268 | if (typeof module != 'undefined' && !module.nodeType && module.exports) { 269 | exports = module.exports = ScrollToTop; 270 | } 271 | exports.ScrollToTop = ScrollToTop; 272 | } else { 273 | root.ScrollToTop = ScrollToTop; 274 | } 275 | }()); --------------------------------------------------------------------------------