HeiGoBackTop.js

可能是最漂亮的返回顶部插件。可以用来返回页面顶部,或者跳转底部,也可以用来自动化滑动页面。已经开源于github。

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/426407/1549492/HeiGoBackTopjs.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

/*
 * HeiGoBackTop.js V1.0.5
 * @author hei-jack
 * @link https://github.com/hei-jack/HeiGoBackTop/
 * Probably the most beautiful back to top widget
 * 可能是最漂亮的返回顶部小插件
 * first: GMT2021-07-22
 * update: GMT2025-03-08
 *
 */
; (function (global) {
  "use strict";

  var instance;
  //构造函数
  function HeiGoBackTop(mode, el, speed, distance, smooth) {
    if (instance) {
      return instance;
    }
    instance = this;
    this.mode = mode === undefined ? 0 : mode;
    this.el = el === undefined ? '#__go-back-top' : el;
    this.realScrollEle = null;
    this.iframe = null;
    this.speed = speed === undefined ? 500 : speed;
    this.distance = distance === undefined ? 100 : distance;
    this.smooth = smooth === undefined ? true : smooth;
    this.show_height = 400;
    this.flag = false;
    this.show_flag = false;
    this.first_scroll_flag = false;
    this.find_scroll_ele_flag = false;
    this.width = '150px';
    this.height = '40px';
    this.bottom = '5%';
    this.right = '5%';
    this.text = '返回顶部';
    this.text_color = '#fff';
    this.radius = '40px';
    this.themes = 0;
    this.color = 'linear-gradient(to right,#6966ff,#37e2d3,#63e8dd,#ccff66)';
    this.shadow = '0 4px 15px 0 rgba(41, 163, 163,0.75)';
    this.version = 'V1.0.5';
    this.home = 'https://github.com/hei-jack/HeiGoBackTop/';
    //页面加载结束才进行初始化 没有必要执行实时载入
    this.onLoad(this);
  };

  //拓展方法
  HeiGoBackTop.prototype = {
    constructor: HeiGoBackTop,
    //钩子回调
    hook: function (func) {
      return func === undefined ? false : this.callBack(func.bind(this));
    },
    //beforeCreate 初始化之前执行
    onBeforeCreate: function (func) {
      this.beforeCreate = func;
    },
    //初始化结束事件
    onAfterCreate: function (func) {
      this.afterCreate = func;
    },
    onShow: function (func) {
      this.show = func;
    },
    onHide: function (func) {
      this.hide = func;
    },
    onClick: function (func) {
      this.click = func;
    },
    //回调
    callBack: function (func) {
      return func();
    },
    //初始化方法
    init: function () {
      this.showVersion();
      var notHtmlOrBody = !this.isRealScrollHtmlOrBody();
      // 先执行一次 获取html,body的可滚动距离
      if (notHtmlOrBody) {
        setTimeout(function () {
          // 延迟3秒执行 可能为渐进时网站 需要读取api再渲染
          // 不是html或body为实际滚动元素 则寻找实际滚动元素
          this.realScrollEle = this.findMainScrollingElement();
        }.bind(this), 3000);
      }
      this.hook(this.beforeCreate);
      this.unset('onBeforeCreate', null);
      this.unset('beforeCreate');
      //检查参数合法性
      if (!this.checkArgs()) throw (new Error('arguments is error!'));
      this.unset('checkNum', null);
      this.unset('checkEl', null);
      this.unset('checkArgs', null);
      //初始化按钮
      this.createBtn();
      this.unset('createBtn', null);
      this.unset('unsetUseless', null);
      //绑定自定义鼠标滑动事件到全局滑动事件
      this.bindOn(window, 'scroll', this.throttle(this.checkBtn.bind(this), 100), notHtmlOrBody);
      this.bindOn(window, 'scroll', this.scroll, notHtmlOrBody);
      this.controller();
      this.unset('controller', null);
      this.hook(this.afterCreate);
      this.unset('onAfterCreate', null);
      this.unset('afterCreate');
      this.unset('bindOn', null);
      this.unset('addEventListener', null);
      this.unset('isBrowser', null);
      this.unset('onLoad', null);
    },
    //检查参数
    checkArgs: function () {
      if (this.checkNum(this.mode)) return false;
      if (this.mode > 3 || this.mode < 0) return false;
      if (this.checkNum(this.speed) || this.speed <= 0) return false;
      if (this.checkNum(this.distance) || this.distance <= 0) return false;
      if (this.checkEl(this.el)) return false;
      return true
    },
    //检查参数类型是否为数字
    checkNum: function (arg) {
      return typeof (arg) !== 'number';
    },
    //检查参数类型是否为不为0 且包含#号的字符串
    checkEl: function (arg) {
      return typeof (arg) !== 'string' || arg.length === 0 || arg.indexOf('#') === -1;
    },
    //创建按钮
    createBtn: function () {
      //如果不是初始元素id 说明用户自定义挂载元素
      if (this.el !== '#__go-back-top') {
        this.el = this.el.replace('#', '');
        if (document.getElementById(this.el)) {
          this.btn = document.getElementById(this.el);
          this.unsetUseless();
          return false
        }
        //挂载元素id没有发现 直接抛出错误
        throw (new Error('element is error!'));
      }

      var iframeW = parseInt(this.width.replace("px", "")) + 20;
      var iframeH = parseInt(this.height.replace("px", "")) + 40;

      var style_text = '#__go-back-top{display:none;position:fixed;width:' + iframeW + 'px;height:' + iframeH + 'px;bottom:' + this.bottom + ';right:' + this.right + ';z-index:2147483647;border:0;outline:0;background: transparent;}';
      var style_el = document.createElement('style');
      var style_node = document.createTextNode(style_text);
      style_el.appendChild(style_node);
      document.body.appendChild(style_el);

      var iframe = document.createElement('iframe');
      iframe.setAttribute('id', this.el.replace('#', ''));
      iframe.src = 'about:blank';
      iframe.frameborder = "0";
      iframe.allowtransparency = "true";
      document.body.appendChild(iframe);
      this.iframe = iframe;
      var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;

      var style_text_btn = 'html,body{padding:0;margin:0;background:transparent;}#__go-back-top{width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;align-content:center;background:transparent;}.__go-back-btn{width:' + this.width + ';height:' + this.height + ';background:' + this.color + ';background-size: 300% 100%;cursor: pointer;border:none;border-radius:' + this.radius + ';box-shadow:' + this.shadow + ';color:' + this.text_color + ';outline:none;letter-spacing:2px;font-weight:600;font-family: "YouYuan","Microsoft YaHei","SimHei","SimSun","Arial",sans-serif;transition: all .4s ease-in-out;moz-transition: all .4s ease-in-out;-o-transition: all .4s ease-in-out;-webkit-transition: all .4s ease-in-out;}.__go-back-btn:hover{background-position: 100% 0%;moz-transition: all .4s ease-in-out;-o-transition: all .4s ease-in-out;-webkit-transition: all .4s ease-in-out;transition: all .4s ease-in-out;}.__go-back-btn:focus{border:none;outline:none;}.go-back-top-themes1{background-image:linear-gradient(to right,#25aae1,#40e495,#30dd8a,#2bb673);box-shadow:0 4px 15px 0 rgba(49,196,190,0.75);}.go-back-top-themes2{background-image:linear-gradient(to right,#f5ce62,#e43603,#fa7199,#e85a19);box-shadow:0 4px 15px 0 rgba(229,66,10,0.75);}.go-back-top-themes3{background-image:linear-gradient(to right,#667eea,#764ba2,#6B8DD6,#8E37D7);box-shadow:0 4px 15px 0 rgba(116,79,168,0.75);}.go-back-top-themes4{background-image:linear-gradient(to right,#fc6076,#ff9a44,#ef9d43,#e75516);box-shadow:0 4px 15px 0 rgba(252,104,110,0.75);}.go-back-top-themes5{background-image:linear-gradient(to right,#0ba360,#3cba92,#30dd8a,#2bb673);box-shadow:0 4px 15px 0 rgba(23,168,108,0.75);}.go-back-top-themes6{background-image:linear-gradient(to right,#009245,#FCEE21,#00A8C5,#D9E021);box-shadow:0 4px 15px 0 rgba(83,176,57,0.75);}.go-back-top-themes7{background-image:linear-gradient(to right,#6253e1,#852D91,#A3A1FF,#F24645);box-shadow:0 4px 15px 0 rgba(126,52,161,0.75);}.go-back-top-themes8{background-image:linear-gradient(to right,#29323c,#485563,#2b5876,#4e4376);box-shadow:0 4px 15px 0 rgba(45,54,65,0.75);}.go-back-top-themes9{background-image:linear-gradient(to right,#25aae1,#4481eb,#04befe,#3f86ed);box-shadow:0 4px 15px 0 rgba(65,132,234,0.75);}.go-back-top-themes10{background-image:linear-gradient(to right,#ed6ea0,#ec8c69,#f7186a,#FBB03B);box-shadow:0 4px 15px 0 rgba(236,116,149,0.75);}.go-back-top-themes11{background-image:linear-gradient(to right,#eb3941,#f15e64,#e14e53,#e2373f);box-shadow:0 5px 15px rgba(242,97,103,.4);}';
      var style_el_btn = iframeDocument.createElement('style');
      var style_node_btn = iframeDocument.createTextNode(style_text_btn);
      style_el_btn.appendChild(style_node_btn);
      iframeDocument.body.appendChild(style_el_btn);

      var div = document.createElement('div');
      div.setAttribute('id', this.el.replace('#', ''));
      var btn = document.createElement('button');
      //设置主题  为0是默认主题 只有默认主题允许更改样式
      this.themes === 0 ? btn.setAttribute('class', this.el.replace('#', '').replace('top', 'btn')) : btn.setAttribute('class', this.el.replace('#', '').replace('top', 'btn') + ' go-back-top-themes' + this.themes);
      var btn_name = document.createTextNode(this.text);
      btn.appendChild(btn_name);
      div.appendChild(btn);
      iframeDocument.body.appendChild(div);
      this.btn = btn;
      //创建结束 销毁已经没用的属性
      this.unsetUseless();
    },
    showBtn: function () {
      if (!this.show_flag) {
        if (this.iframe) {
          this.iframe.style.cssText = 'display:block';
        }
        this.btn.style.cssText = 'display:block';
        this.hook(this.show);
        this.show_flag = true;
      }
    },
    hideBtn: function () {
      if (this.show_flag) {
        if (this.iframe) {
          this.iframe.style.cssText = 'display:none';
        }
        this.btn.style.cssText = 'display:none';
        this.hook(this.hide);
        this.show_flag = false;
      }
    },
    //载入方法 在页面加载结束后开始执行初始化工作
    onLoad: function (self) {
      if (!this.isBrowser()) throw (new Error('The current environment is not the browser!'));
      if (this.isIE() && this.getIEVersion() < 9) throw (new Error('HeiGoBackTop does not support ie 9 the following browsers!'));
      this.bindOn(window, 'load', self.init);
    },
    //当滑动事件发生时
    onScroll: function (func) {
      this.scroll = func;
    },
    //绑定元素事件
    bindOn: function (el, event, func, useCapture) {
      if (func === undefined) return false;
      this.addEventListener(el, event, func.bind(this), useCapture)
    },
    //获取页面滚动的距离
    getScrollTop: function () {
      if (this.realScrollEle) {
        return this.realScrollEle.scrollTop;
      }
      return document.documentElement.scrollTop || document.body.scrollTop;
    },
    //检查按钮何时显示和隐藏
    checkBtn: function (event) {
      if (!this.isRealScrollHtmlOrBody()) {
        if (!this.first_scroll_flag) {
          // 在用户第一次滚动的时候再来获取一边真实滚动元素
          this.realScrollEle = this.findMainScrollingElement();
        }
        if (!this.realScrollEle.isConnected) {
          // 真实滚动旧元素已经被移除 则重新寻找
          this.realScrollEle = this.findMainScrollingElement();
        }
        // 如果用户滚动的元素和旧的真实滚动元素不一致
        if (event.target !== this.realScrollEle) {
          /*
          // 判断谁的区域更大
          var targetRect = event.target.getBoundingClientRect();
          var rect = this.realScrollEle.getBoundingClientRect();
          // 宽度优先法则 理论上主要滚动区域宽度都会占主体比较多的区域 而侧边栏或者其他区域虽然也可以滚动 但是区域会更小
          if (targetRect.width >= rect.width) {
            //直接记录
            this.realScrollEle = event.target;
          }
          */

          // 滚动优先法则 用户滚动的是哪个区域 则点击按钮返回哪个区域的顶部
          this.realScrollEle = event.target;
        }
      }
      this.first_scroll_flag = true;
      this.getScrollTop() > this.show_height ? this.showBtn() : this.hideBtn();
    },
    //绑定元素事件兼容处理函数
    addEventListener: function (el, type, fn, useCapture) {
      if (el.addEventListener) {
        el.addEventListener(type, fn, useCapture ? true : false);
      } else if (el.attachEvent) {
        el.attachEvent('on' + type, fn);
      } else {
        return false;
      }
    },
    //展示版本信息
    showVersion: function () {
      this.isIE() ? console.log("HeiGoBackTop " + this.version + ' ' + this.home) : console.log("\n\n %c HeiGoBackTop " + this.version + " %c " + this.home + " \n\n", "color: #fff; background: linear-gradient(90deg, #8080ff, #ff99ff); padding:5px 1px;", "background: linear-gradient(90deg,#ffccff,#80ffd4); padding:5px 0px;")
    },
    //模式分发控制器
    controller: function () {
      switch (this.mode) {
        case 0:
          this.bindOn(this.btn, 'click', this.goBackTop);
          break;
        case 1:
          this.bindOn(this.btn, 'click', this.goDown);
          break;
        case 2:
          this.bindOn(this.btn, 'click', this.goBackTopSlow);
          break;
        case 3:
          this.bindOn(this.btn, 'click', this.goDownSlow)
      }
    },
    //返回顶部
    goBackTop: function () {
      // 如果开启平滑模式
      if (this.smooth) {
        this.scrollToSmooth(0);
      } else {
        this.setScrollTop(0);
      }
      //滑动结束钩子
      this.hook(this.scrollOver);
    },
    //慢滑到顶部
    goBackTopSlow: function () {
      this.scrollSpeed(this.getScrollTop(), 0);
    },
    //跳转底部
    goDown: function () {
      // 如果开启平滑模式
      if (this.smooth) {
        this.scrollToSmooth(this.getScrollHeight());
      } else {
        this.setScrollTop(this.getScrollHeight());
      }
      this.hook(this.scrollOver)
    },
    //慢滑到底部
    goDownSlow: function () {
      this.scrollSpeed(this.getScrollTop(), this.getScrollHeight());
    },
    /*
     * 滑动方法
     * @param number start 开始滑动位置
     * @param number end 结束滑动位置
     * 
     */
    scrollSpeed: function (start, end) {
      //防止重复点击导致定时器多次调用
      if (this.flag) return false;
      this.flag = true;
      var timer = null;
      if (end === 0) {
        //如果是滑动到顶部
        timer = setInterval(function () {
          var scroll_top = this.getScrollTop();
          if (scroll_top >= this.distance) {
            start -= this.distance;
            this.setScrollTop(start);
          } else {
            if (scroll_top === 0) {
              this.flag = false;
              clearInterval(timer);
              this.hook(this.scrollOver);
            } else {
              this.setScrollTop(0);
            }
          }
        }.bind(this), this.speed);
      } else {
        //如果是滑动到底部
        timer = setInterval(function () {
          //getLast方法获取滚动条距离底部还剩多少距离 如果大于0 说明未到达底部 只管继续滑动即可
          //向下取整 修复个别浏览器还剩余0.3左右的问题
          if (Math.floor(this.getLast()) > 0) {
            start += this.distance;
            this.setScrollTop(start);
          } else {
            this.flag = false;
            this.setScrollTop(end);
            clearInterval(timer);
            this.hook(this.scrollOver);
          }
        }.bind(this), this.speed);
      }
    },
    // 平滑的滚动
    scrollToSmooth: function (target) {
      //  IE和safari不支持 options.behavior:"smooth"
      // 目标位置
      var targetPosition = target;
      // 开始滚动位置
      var startPosition = this.getScrollTop();

      try {
        if (this.realScrollEle) {
          this.realScrollEle.scrollTo({
            top: targetPosition,
            behavior: 'smooth'
          });
          return;
        }

        //如果出现异常 说明不支持behavior: 'smooth'
        global.scrollTo({
          top: targetPosition,
          behavior: 'smooth'
        });
      } catch (e) {
        var distance = targetPosition - startPosition;
        var duration = 500; // 滚动持续时间,单位为毫秒
        var startTime = null;

        function animation(currentTime) {
          if (startTime === null) startTime = currentTime;
          var timeElapsed = currentTime - startTime;
          var run = ease(timeElapsed, startPosition, distance, duration);
          this.realScrollEle ? realScrollEle.scrollTop(0, run) : global.scrollTo(0, run);
          if (timeElapsed < duration) global.requestAnimationFrame(animation);
        }

        function ease(t, b, c, d) {
          t /= d / 2;
          if (t < 1) return c / 2 * t * t + b;
          t--;
          return -c / 2 * (t * (t - 2) - 1) + b;
        }

        global.requestAnimationFrame(animation);
      }

    },

    //设置当前滚动所在高度
    setScrollTop: function (height) {
      if (this.realScrollEle) {
        return this.realScrollEle.scrollTop = height;
      }
      //处理兼容性问题
      document.documentElement.scrollTop ? document.documentElement.scrollTop = height : document.body.scrollTop = height;
    },
    //获取滚动条高度 即可滚动的高度
    getScrollHeight: function () {
      if (this.realScrollEle) {
        return this.realScrollEle.scrollHeight;
      }
      //兼容标准模式 strict mode 和 混杂模式 quirks mode
      return document.compatMode === 'CSS1Compat' ? document.documentElement.scrollHeight : document.body.scrollHeight;
    },
    /*
     * 销毁属性和方法
     * @param string key 属性或方法名称
     * @param set 可选参数 传入除undefined任意值则说明是方法 不传表示属性
     */
    unset: function (key, set) {
      if (set === undefined) {
        delete this[key];
      } else {
        //ie不支持delete方法
        this.isIE() && this.getIEVersion() <= 10 ? this[key] = undefined : this.__proto__[key] = undefined;
      }
    },
    unsetUseless: function () {
      this.unset('el');
      this.unset('width');
      this.unset('height');
      this.unset('top');
      this.unset('right');
      this.unset('text');
      this.unset('text_color');
      this.unset('radius');
      this.unset('themes');
      this.unset('color');
      this.unset('shadow')
    },
    //获取当前滚动条所在位置到页面底部还剩多少距离
    getLast: function () {
      var margin_bot = 0;
      if (this.realScrollEle) {
        margin_bot = this.realScrollEle.scrollHeight - this.realScrollEle.scrollTop - this.realScrollEle.clientHeight;
        return margin_bot;
      }
      if (document.compatMode === "CSS1Compat") {
        margin_bot = document.documentElement.scrollHeight - (document.documentElement.scrollTop + document.body.scrollTop) - document.documentElement.clientHeight;
      } else {
        margin_bot = document.body.scrollHeight - document.body.scrollTop - document.body.clientHeight;
      }
      return margin_bot;
    },
    //滑动结束执行事件
    onScrollOver: function (func) {
      this.scrollOver = func;
    },
    //判断当前浏览器是否为safari
    isSafari: function () {
      return (/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent));
    },
    //获取当前浏览器是否为ie
    isIE: function () {
      if (!!window.ActiveXObject || "ActiveXObject" in window) return true;
      return false;
    },
    //获取当前ie版本号
    getIEVersion: function () {
      var ua = navigator.userAgent;
      //如果不是ie浏览器 设置初始值0
      var ver = 0;
      if (ua) {
        if (ua.match(/MSIE\s+([\d]+)\./i)) {
          //其他ie版本
          ver = RegExp.$1;
        } else if (ua.match(/Trident.*rv\s*:\s*([\d]+)\./i)) {
          //ie11
          ver = RegExp.$1;
        }
      }
      return parseInt(ver);
    },
    //获取当前运行环境是否为浏览器
    isBrowser: function () {
      return typeof (window) === "undefined" ? false : true;
    },
    // 寻找实际滚动的是哪个元素 部分网站 实际滚动的区域是其他元素 例如vue之类可能滚动的是app元素 很多ai聊天网站滚动的下面布局元素
    findMainScrollingElement: function () {
      if (this.find_scroll_ele_flag) {
        return;
      }
      this.find_scroll_ele_flag = true;
      var bestCandidate = null;
      var maxArea = 0;
      var queue = [document.body]; // 从 body 开始层级遍历
      while (queue.length > 0) {
        var node = queue.shift();
        // 检查当前节点是否可滚动
        if (node.scrollHeight > node.clientHeight && window.getComputedStyle(node).overflowY !== 'hidden') {
          var rect = node.getBoundingClientRect();
          var area = rect.width * rect.height;
          // 选择可视区域最大的可滚动元素
          if (area > maxArea) {
            maxArea = area;
            bestCandidate = node;
          }
        }
        // 将子节点加入队列
        var children = node.children;
        for (let i = 0; i < children.length; i++) {
          queue.push(children[i]);
        }
      }
      this.find_scroll_ele_flag = false;
      // 如果没有找到可滚动元素,则返回 documentElement
      return bestCandidate || document.documentElement;
    },
    // 判断真实滚动元素是否为html或body
    isRealScrollHtmlOrBody: function () {
      return document.documentElement.clientHeight < document.documentElement.scrollHeight || document.body.clientHeight < document.body.scrollHeight
    },
    // 节流函数
    throttle: function (func, wait) {
      var timer = null;
      var startTime = Date.now();
      return function () {
        var curTime = Date.now();
        var remaining = wait - (curTime - startTime);
        var context = this;
        var args = arguments;
        clearTimeout(timer);
        if (remaining <= 0) {
          func.apply(context, args);
          startTime = Date.now();
        } else {
          timer = setTimeout(function () {
            func.apply(context, args);
          }, remaining);  // 如果小于wait 保证在差值时间后执行
        }
      }
    }
  };
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = HeiGoBackTop;
  } else if (typeof define === 'function') {
    define(function () {
      return HeiGoBackTop;
    })
  } else {
    // 避免重复挂载
    if (!global.HeiGoBackTop) {
      global.HeiGoBackTop = HeiGoBackTop;
    }
  }
})(this);