Greasy Fork

Greasy Fork is available in English.

记录页面滚动

记录页面滚动容器和位置,下次页面加载完成时恢复

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         记录页面滚动
// @namespace 记录页面滚动
// @version      2
// @description  记录页面滚动容器和位置,下次页面加载完成时恢复
// @author       酷安@耗子Sky
// @match        *://*/*
// ==/UserScript==

(function(){
const id = decodeURIComponent('3753');

function runOnce(fn, key) {
  const uniqId = 'BEXT_UNIQ_ID_' + id + (key ? key : '');
  if (window[uniqId]) {
    return;
  }
  window[uniqId] = true;
  fn && fn();
}

function runNeed(
  condition,
  fn,
  option = {
    count: 20,
    delay: 200,
    failFn: () => null,
  },
  ...args
) {
  if (typeof condition != 'function' || typeof fn != 'function') return;
  if (
    !option ||
    typeof option.count != 'number' ||
    typeof option.delay != 'number' ||
    typeof option.failFn != 'function'
  ) {
    option = {
      count: 20,
      delay: 200,
      failFn: () => null,
    };
  }
  let sleep = () => {
      return new Promise((resolve) => setTimeout(resolve, option.delay));
    },
    ok = false;
  new Promise(async (resolve, reject) => {
    for (let c = 0; !ok && c < option.count; c++) {
      await sleep();
      ok = condition.call(this, c + 1);
    }
    if (ok) {
      resolve();
    } else {
      reject();
    }
  }).then(fn.bind(this, ...args), option.failFn);
}

function runAt(start, fn, ...args) {
  if (typeof fn !== 'function') return;
  switch (start) {
    case 'document-end':
      if (
        document.readyState === 'interactive' ||
        document.readyState === 'complete'
      ) {
        fn.call(this, ...args);
      } else {
        document.addEventListener('DOMContentLoaded', fn.bind(this, ...args));
      }
      break;
    case 'document-idle':
      if (document.readyState === 'complete') {
        fn.call(this, ...args);
      } else {
        window.addEventListener('load', fn.bind(this, ...args));
      }
      break;
    default:
      if (document.readyState === 'complete') {
        setTimeout(fn, start, ...args);
      } else {
        window.addEventListener('load', () => {
          setTimeout(fn, start, ...args);
        });
      }
  }
}

function runMatch(opt = {}) {
  const { white = [], black = [], full = true } = opt;
  let addr = full ? location.href : location.hostname,
    matcher = (url) => {
      if (url.startsWith('//') && url.endsWith('//')) {
        try {
          let expr = new RegExp(url.slice(2).slice(0, -2), 'gu');
          return expr.test(addr);
        } catch (e) {
          console.error(e);
          return addr.indexOf(url) >= 0;
        }
      }
      return addr.indexOf(url) >= 0;
    },
    ok = true,
    pick = addr;
  return new Promise((resolve, reject) => {
    black.forEach((r) => {
      if (matcher(r)) {
        ok = false;
        pick = r;
      }
    });
    if (white.length > 0) {
      ok = false;
      white.forEach((r) => {
        if (matcher(r)) {
          ok = true;
          pick = r;
        }
      });
    }
    if (ok) {
      resolve(pick);
    } else reject(pick);
  });
}

function addElement({
  tag,
  attrs = {},
  to = document.body || document.documentElement,
}) {
  const el = document.createElement(tag);
  Object.assign(el, attrs);
  to.appendChild(el);
  return el;
}

function addStyle(css) {
  return addElement({
    tag: 'style',
    attrs: {
      textContent: css,
    },
    to: document.head,
  });
}

var config = {"toast":0.1,"out":1};

function toast(text, time = 3, callback, transition = 0.2) {
  let isObj = (o) =>
      typeof o == 'object' &&
      typeof o.toString == 'function' &&
      o.toString() === '[object Object]',
    timeout,
    toastTransCount = 0;
  if (typeof text != 'string') text = String(text);
  if (typeof time != 'number' || time <= 0) time = 3;
  if (typeof transition != 'number' || transition < 0) transition = 0.2;
  if (callback && !isObj(callback)) callback = undefined;
  if (callback) {
    if (callback.text && typeof callback.text != 'string')
      callback.text = String(callback.text);
    if (
      callback.color &&
      (typeof callback.color != 'string' || callback.color === '')
    )
      delete callback.color;
    if (callback.onclick && typeof callback.onclick != 'function')
      callback.onclick = () => null;
    if (callback.onclose && typeof callback.onclose != 'function')
      delete callback.onclose;
  }

  let toastStyle = addStyle(`
  #bextToast {
    all: initial;
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 10vh;
    width: max-content;
    max-width: 80vw;
    max-height: 80vh;
    margin: 0 auto;
    border-radius: 20px;
    padding: .5em 1em;
    font-size: 16px;
    background-color: rgba(0,0,0,0.5);
    color: white;
    z-index: 1000002;
    opacity: 0%;
    transition: opacity ${transition}s;
  }
  #bextToast > * {
    display: -webkit-box;
    height: max-content;
    margin: auto .25em;
    width: max-content;
    max-width: calc(40vw - .5em);
    max-height: 80vh;
    overflow: hidden;
    -webkit-line-clamp: 22;
    -webkit-box-orient: vertical;
    text-overflow: ellipsis;
    overflow-wrap: anywhere;
  }
  #bextToastBtn {
    color: ${callback && callback.color ? callback.color : 'turquoise'}
  }
  #bextToast.bextToastShow {
    opacity: 1;
  }
    `),
    toastDiv = addElement({
      tag: 'div',
      attrs: {
        id: 'bextToast',
      },
    }),
    toastShow = () => {
      toastDiv.classList.toggle('bextToastShow');
      toastTransCount++;
      if (toastTransCount >= 2) {
        setTimeout(function () {
          toastDiv.remove();
          toastStyle.remove();
          if (callback && callback.onclose) callback.onclose.call(this);
        }, transition * 1000 + 1);
      }
    };
  addElement({
    tag: 'div',
    attrs: {
      id: 'bextToastText',
      innerText: text,
    },
    to: toastDiv,
  });
  if (callback && callback.text) {
    addElement({
      tag: 'div',
      attrs: {
        id: 'bextToastBtn',
        innerText: callback.text,
        onclick:
          callback && callback.onclick
            ? () => {
                callback.onclick.call(this);
                clearTimeout(timeout);
                toastShow();
              }
            : null,
      },
      to: toastDiv,
    });
  }
  setTimeout(toastShow, 1);
  timeout = setTimeout(toastShow, (time + transition * 2) * 1000);
}


var now = Date.now || function() {
  return new Date().getTime();
};






function throttle(func, wait, options) {
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};

  var later = function() {
    previous = options.leading === false ? 0 : now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    var _now = now();
    if (!previous && options.leading === false) previous = _now;
    var remaining = wait - (_now - previous);
    context = this;
    args = arguments;
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = _now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
}

runOnce(() => {
    if (!config.hasOwnProperty('black')) config.black = [];
    if (!config.hasOwnProperty('white')) config.white = [];
    runMatch({
        black: config.black,
        white: config.white,
        full: true
    }).then(() => {
        (() => {
            function isDocument(d) {
                return d && d.nodeType === 9;
            }
            function getDocument(node) {
                if (isDocument(node)) {
                    return node;
                } else if (isDocument(node.ownerDocument)) {
                    return node.ownerDocument;

                } else if (isDocument(node.document)) {
                    return node.document;

                } else if (node.parentNode) {
                    return getDocument(node.parentNode);
                } else if (node.commonAncestorContainer) {
                    return getDocument(node.commonAncestorContainer);
                } else if (node.startContainer) {
                    return getDocument(node.startContainer);
                } else if (node.anchorNode) {
                    return getDocument(node.anchorNode);
                }
            }
            class DOMException {
                constructor(message, name) {
                    this.message = message;
                    this.name = name;
                    this.stack = (new Error()).stack;
                }
            }
            DOMException.prototype = new Error();
            DOMException.prototype.toString = function () {
                return `${this.name}: ${this.message}`
            };
            const FIRST_ORDERED_NODE_TYPE = 9;
            const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
            window.sXPath = {};
            window.sXPath.fromNode = (node, root = null) => {
                if (node === undefined) {
                    throw new Error('missing required parameter "node"')
                }
                root = root || getDocument(node);
                let path = '/';
                while (node !== root) {
                    if (!node) {
                        let message = 'The supplied node is not contained by the root node.';
                        let name = 'InvalidNodeTypeError';
                        throw new DOMException(message, name)
                    }
                    path = `/${nodeName(node)}[${nodePosition(node)}]${path}`;
                    node = node.parentNode;
                }
                return path.replace(/\/$/, '')
            };
            window.sXPath.toNode = (path, root, resolver = null) => {
                if (path === undefined) {
                    throw new Error('missing required parameter "path"')
                }
                if (root === undefined) {
                    throw new Error('missing required parameter "root"')
                }
                let document = getDocument(root);
                if (root !== document) path = path.replace(/^\//, './');
                let documentElement = document.documentElement;
                if (resolver === null && documentElement.lookupNamespaceURI) {
                    let defaultNS = documentElement.lookupNamespaceURI(null) || HTML_NAMESPACE;
                    resolver = (prefix) => {
                        let ns = { '_default_': defaultNS };
                        return ns[prefix] || documentElement.lookupNamespaceURI(prefix)
                    };
                }
                return resolve(path, root, resolver)
            };
            function nodeName(node) {
                switch (node.nodeName) {
                    case '#text': return 'text()'
                    case '#comment': return 'comment()'
                    case '#cdata-section': return 'cdata-section()'
                    default: return node.nodeName.toLowerCase()
                }
            }
            function nodePosition(node) {
                let name = node.nodeName;
                let position = 1;
                while ((node = node.previousSibling)) {
                    if (node.nodeName === name) position += 1;
                }
                return position
            }
            function resolve(path, root, resolver) {
                try {
                    let nspath = path.replace(/\/(?!\.)([^\/:\(]+)(?=\/|$)/g, '/_default_:$1');
                    return platformResolve(nspath, root, resolver)
                } catch (err) {
                    return fallbackResolve(path, root)
                }
            }
            function fallbackResolve(path, root) {
                let steps = path.split("/");
                let node = root;
                while (node) {
                    let step = steps.shift();
                    if (step === undefined) break
                    if (step === '.') continue
                    let [name, position] = step.split(/[\[\]]/);
                    name = name.replace('_default_:', '');
                    position = position ? parseInt(position) : 1;
                    node = findChild(node, name, position);
                }
                return node
            }
            function platformResolve(path, root, resolver) {
                let document = getDocument(root);
                let r = document.evaluate(path, root, resolver, FIRST_ORDERED_NODE_TYPE, null);
                return r.singleNodeValue
            }
            function findChild(node, name, position) {
                for (node = node.firstChild; node; node = node.nextSibling) {
                    if (nodeName(node) === name && --position === 0) break
                }
                return node
            }

            let urlChangeFn = null;
            history.pushState = (f => function pushState() {
                var ret = f.apply(this, arguments);
                window.dispatchEvent(new Event('pushstate'));
                window.dispatchEvent(new Event('urlchange'));
                return ret;
            })(history.pushState);
            history.replaceState = (f => function replaceState() {
                var ret = f.apply(this, arguments);
                window.dispatchEvent(new Event('replacestate'));
                window.dispatchEvent(new Event('urlchange'));
                return ret;
            })(history.replaceState);
            window.addEventListener('popstate', () => {
                window.dispatchEvent(new Event('urlchange'));
            });
            Object.defineProperty(window, 'onurlchange', {
                get() { return urlChangeFn; },
                set(fn) {
                    if (typeof fn === 'function') {
                        urlChangeFn = fn;
                        window.addEventListener('urlchange', urlChangeFn);
                    } else {
                        window.removeEventListener('urlchange', urlChangeFn);
                        urlChangeFn = null;
                    }
                },
            });
        })();
        runAt('document-end', () => {
            const stor = window.localStorage,
                boxkey = 'lemonScrollBox';
            let boxobj = null, box = null, boxel = null;
            function getScrollBox(e) {
                boxel = e.target;
                let pageid = location.href;
                if (boxel.scrollTop === undefined) boxel = document.documentElement;
                try {
                    box = window.sXPath.fromNode(boxel, document.documentElement);
                } catch (e) {
                    box = '.';
                }
                if (!boxobj) boxobj = {};
                boxobj[pageid] =
                {
                    box: box,
                    pos: boxel.scrollTop,
                    class: boxel.className,
                    id: boxel.id
                };
                stor.setItem(
                    boxkey,
                    JSON.stringify(boxobj)
                );
            }
            function startNewRecord() {
                // toast('开始记录滚动', config.toast);
                document.addEventListener('scroll', throttle(getScrollBox, 300), true);
            }
            function scanPage() {
                boxobj = JSON.parse(stor.getItem(boxkey));
                let pageid = location.href;
                if (boxobj[pageid]) {
                    runNeed(
                        () => {
                            boxel = (boxobj[pageid].box === '') ?
                                document.documentElement : window.sXPath.toNode(
                                    boxobj[pageid].box,
                                    document.documentElement
                                );
                            if (boxel &&
                                boxel.id === boxobj[pageid].id &&
                                boxel.className === boxobj[pageid].class &&
                                boxel.scrollHeight > window.innerHeight) {
                                return true;
                            } else return false;
                        },
                        () => {
                            setTimeout(() => {
                                boxel.scrollTop = boxobj[pageid].pos;
                            }, config.out);
                        }
                    );
                    document.addEventListener('scroll', throttle(getScrollBox, 300), true);
                } else startNewRecord();
            }
            if (stor.hasOwnProperty(boxkey)) {
                window.onurlchange = scanPage;
                window.onhashchange = scanPage;
                scanPage();
            } else {
                startNewRecord();
            }
        });
    });
});

})();