记录页面滚动

记录页面滚动容器和位置,下次页面加载完成时恢复,脚本菜单可以控制网站禁用与启用

// ==UserScript==
// @name         记录页面滚动
// @version      4
// @description  记录页面滚动容器和位置,下次页面加载完成时恢复,脚本菜单可以控制网站禁用与启用
// @author       Lemon399
// @match        *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at       document-end
// @namespace https://greasyfork.org/users/452911
// ==/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};

const blackKey = "recordScrollKey";
const savedBlack = JSON.parse(GM_getValue(blackKey, "[]"));
config.black = savedBlack;

if (savedBlack.indexOf(location.hostname) < 0) {
  GM_registerMenuCommand("在此域名禁用", () => {
    savedBlack.push(location.hostname);
    GM_setValue(blackKey, JSON.stringify(savedBlack));
    location.reload();
  })
} else {
  GM_registerMenuCommand("在此域名启用", () => {
    GM_setValue(blackKey, JSON.stringify(savedBlack.filter((domain) => domain !== location.hostname)));
    location.reload();
  })
}
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();
            }
        });
    });
});

})();