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