// ==UserScript== // @name 记录页面滚动 // @namespace 记录页面滚动 // @version 2 // @description 记录页面滚动容器和位置,下次页面加载完成时恢复 // @author 酷安@耗子Sky // @match *://*/* // @downloadURL none // ==/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(); } }); }); }); })();