记录页面滚动

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

  1. // ==UserScript==
  2. // @name 记录页面滚动
  3. // @version 4
  4. // @description 记录页面滚动容器和位置,下次页面加载完成时恢复,脚本菜单可以控制网站禁用与启用
  5. // @author Lemon399
  6. // @match *://*/*
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @grant GM_registerMenuCommand
  10. // @run-at document-end
  11. // @namespace https://gf.qytechs.cn/users/452911
  12. // ==/UserScript==
  13. (function(){
  14. const id = decodeURIComponent('3753');
  15.  
  16. function runOnce(fn, key) {
  17. const uniqId = 'BEXT_UNIQ_ID_' + id + (key ? key : '');
  18. if (window[uniqId]) {
  19. return;
  20. }
  21. window[uniqId] = true;
  22. fn && fn();
  23. }
  24.  
  25. function runNeed(
  26. condition,
  27. fn,
  28. option = {
  29. count: 20,
  30. delay: 200,
  31. failFn: () => null,
  32. },
  33. ...args
  34. ) {
  35. if (typeof condition != 'function' || typeof fn != 'function') return;
  36. if (
  37. !option ||
  38. typeof option.count != 'number' ||
  39. typeof option.delay != 'number' ||
  40. typeof option.failFn != 'function'
  41. ) {
  42. option = {
  43. count: 20,
  44. delay: 200,
  45. failFn: () => null,
  46. };
  47. }
  48. let sleep = () => {
  49. return new Promise((resolve) => setTimeout(resolve, option.delay));
  50. },
  51. ok = false;
  52. new Promise(async (resolve, reject) => {
  53. for (let c = 0; !ok && c < option.count; c++) {
  54. await sleep();
  55. ok = condition.call(this, c + 1);
  56. }
  57. if (ok) {
  58. resolve();
  59. } else {
  60. reject();
  61. }
  62. }).then(fn.bind(this, ...args), option.failFn);
  63. }
  64.  
  65. function runAt(start, fn, ...args) {
  66. if (typeof fn !== 'function') return;
  67. switch (start) {
  68. case 'document-end':
  69. if (
  70. document.readyState === 'interactive' ||
  71. document.readyState === 'complete'
  72. ) {
  73. fn.call(this, ...args);
  74. } else {
  75. document.addEventListener('DOMContentLoaded', fn.bind(this, ...args));
  76. }
  77. break;
  78. case 'document-idle':
  79. if (document.readyState === 'complete') {
  80. fn.call(this, ...args);
  81. } else {
  82. window.addEventListener('load', fn.bind(this, ...args));
  83. }
  84. break;
  85. default:
  86. if (document.readyState === 'complete') {
  87. setTimeout(fn, start, ...args);
  88. } else {
  89. window.addEventListener('load', () => {
  90. setTimeout(fn, start, ...args);
  91. });
  92. }
  93. }
  94. }
  95.  
  96. function runMatch(opt = {}) {
  97. const { white = [], black = [], full = true } = opt;
  98. let addr = full ? location.href : location.hostname,
  99. matcher = (url) => {
  100. if (url.startsWith('//') && url.endsWith('//')) {
  101. try {
  102. let expr = new RegExp(url.slice(2).slice(0, -2), 'gu');
  103. return expr.test(addr);
  104. } catch (e) {
  105. console.error(e);
  106. return addr.indexOf(url) >= 0;
  107. }
  108. }
  109. return addr.indexOf(url) >= 0;
  110. },
  111. ok = true,
  112. pick = addr;
  113. return new Promise((resolve, reject) => {
  114. black.forEach((r) => {
  115. if (matcher(r)) {
  116. ok = false;
  117. pick = r;
  118. }
  119. });
  120. if (white.length > 0) {
  121. ok = false;
  122. white.forEach((r) => {
  123. if (matcher(r)) {
  124. ok = true;
  125. pick = r;
  126. }
  127. });
  128. }
  129. if (ok) {
  130. resolve(pick);
  131. } else reject(pick);
  132. });
  133. }
  134.  
  135. function addElement({
  136. tag,
  137. attrs = {},
  138. to = document.body || document.documentElement,
  139. }) {
  140. const el = document.createElement(tag);
  141. Object.assign(el, attrs);
  142. to.appendChild(el);
  143. return el;
  144. }
  145.  
  146. function addStyle(css) {
  147. return addElement({
  148. tag: 'style',
  149. attrs: {
  150. textContent: css,
  151. },
  152. to: document.head,
  153. });
  154. }
  155.  
  156. var config = {"toast":0.1,"out":1};
  157.  
  158. const blackKey = "recordScrollKey";
  159. const savedBlack = JSON.parse(GM_getValue(blackKey, "[]"));
  160. config.black = savedBlack;
  161.  
  162. if (savedBlack.indexOf(location.hostname) < 0) {
  163. GM_registerMenuCommand("在此域名禁用", () => {
  164. savedBlack.push(location.hostname);
  165. GM_setValue(blackKey, JSON.stringify(savedBlack));
  166. location.reload();
  167. })
  168. } else {
  169. GM_registerMenuCommand("在此域名启用", () => {
  170. GM_setValue(blackKey, JSON.stringify(savedBlack.filter((domain) => domain !== location.hostname)));
  171. location.reload();
  172. })
  173. }
  174. function toast(text, time = 3, callback, transition = 0.2) {
  175. let isObj = (o) =>
  176. typeof o == 'object' &&
  177. typeof o.toString == 'function' &&
  178. o.toString() === '[object Object]',
  179. timeout,
  180. toastTransCount = 0;
  181. if (typeof text != 'string') text = String(text);
  182. if (typeof time != 'number' || time <= 0) time = 3;
  183. if (typeof transition != 'number' || transition < 0) transition = 0.2;
  184. if (callback && !isObj(callback)) callback = undefined;
  185. if (callback) {
  186. if (callback.text && typeof callback.text != 'string')
  187. callback.text = String(callback.text);
  188. if (
  189. callback.color &&
  190. (typeof callback.color != 'string' || callback.color === '')
  191. )
  192. delete callback.color;
  193. if (callback.onclick && typeof callback.onclick != 'function')
  194. callback.onclick = () => null;
  195. if (callback.onclose && typeof callback.onclose != 'function')
  196. delete callback.onclose;
  197. }
  198.  
  199. let toastStyle = addStyle(`
  200. #bextToast {
  201. all: initial;
  202. display: flex;
  203. position: fixed;
  204. left: 0;
  205. right: 0;
  206. bottom: 10vh;
  207. width: max-content;
  208. max-width: 80vw;
  209. max-height: 80vh;
  210. margin: 0 auto;
  211. border-radius: 20px;
  212. padding: .5em 1em;
  213. font-size: 16px;
  214. background-color: rgba(0,0,0,0.5);
  215. color: white;
  216. z-index: 1000002;
  217. opacity: 0%;
  218. transition: opacity ${transition}s;
  219. }
  220. #bextToast > * {
  221. display: -webkit-box;
  222. height: max-content;
  223. margin: auto .25em;
  224. width: max-content;
  225. max-width: calc(40vw - .5em);
  226. max-height: 80vh;
  227. overflow: hidden;
  228. -webkit-line-clamp: 22;
  229. -webkit-box-orient: vertical;
  230. text-overflow: ellipsis;
  231. overflow-wrap: anywhere;
  232. }
  233. #bextToastBtn {
  234. color: ${callback && callback.color ? callback.color : 'turquoise'}
  235. }
  236. #bextToast.bextToastShow {
  237. opacity: 1;
  238. }
  239. `),
  240. toastDiv = addElement({
  241. tag: 'div',
  242. attrs: {
  243. id: 'bextToast',
  244. },
  245. }),
  246. toastShow = () => {
  247. toastDiv.classList.toggle('bextToastShow');
  248. toastTransCount++;
  249. if (toastTransCount >= 2) {
  250. setTimeout(function () {
  251. toastDiv.remove();
  252. toastStyle.remove();
  253. if (callback && callback.onclose) callback.onclose.call(this);
  254. }, transition * 1000 + 1);
  255. }
  256. };
  257. addElement({
  258. tag: 'div',
  259. attrs: {
  260. id: 'bextToastText',
  261. innerText: text,
  262. },
  263. to: toastDiv,
  264. });
  265. if (callback && callback.text) {
  266. addElement({
  267. tag: 'div',
  268. attrs: {
  269. id: 'bextToastBtn',
  270. innerText: callback.text,
  271. onclick:
  272. callback && callback.onclick
  273. ? () => {
  274. callback.onclick.call(this);
  275. clearTimeout(timeout);
  276. toastShow();
  277. }
  278. : null,
  279. },
  280. to: toastDiv,
  281. });
  282. }
  283. setTimeout(toastShow, 1);
  284. timeout = setTimeout(toastShow, (time + transition * 2) * 1000);
  285. }
  286.  
  287.  
  288. var now = Date.now || function() {
  289. return new Date().getTime();
  290. };
  291.  
  292.  
  293.  
  294.  
  295.  
  296.  
  297. function throttle(func, wait, options) {
  298. var timeout, context, args, result;
  299. var previous = 0;
  300. if (!options) options = {};
  301.  
  302. var later = function() {
  303. previous = options.leading === false ? 0 : now();
  304. timeout = null;
  305. result = func.apply(context, args);
  306. if (!timeout) context = args = null;
  307. };
  308.  
  309. var throttled = function() {
  310. var _now = now();
  311. if (!previous && options.leading === false) previous = _now;
  312. var remaining = wait - (_now - previous);
  313. context = this;
  314. args = arguments;
  315. if (remaining <= 0 || remaining > wait) {
  316. if (timeout) {
  317. clearTimeout(timeout);
  318. timeout = null;
  319. }
  320. previous = _now;
  321. result = func.apply(context, args);
  322. if (!timeout) context = args = null;
  323. } else if (!timeout && options.trailing !== false) {
  324. timeout = setTimeout(later, remaining);
  325. }
  326. return result;
  327. };
  328.  
  329. throttled.cancel = function() {
  330. clearTimeout(timeout);
  331. previous = 0;
  332. timeout = context = args = null;
  333. };
  334.  
  335. return throttled;
  336. }
  337.  
  338. runOnce(() => {
  339. if (!config.hasOwnProperty('black')) config.black = [];
  340. if (!config.hasOwnProperty('white')) config.white = [];
  341. runMatch({
  342. black: config.black,
  343. white: config.white,
  344. full: true
  345. }).then(() => {
  346. (() => {
  347. function isDocument(d) {
  348. return d && d.nodeType === 9;
  349. }
  350. function getDocument(node) {
  351. if (isDocument(node)) {
  352. return node;
  353. } else if (isDocument(node.ownerDocument)) {
  354. return node.ownerDocument;
  355.  
  356. } else if (isDocument(node.document)) {
  357. return node.document;
  358.  
  359. } else if (node.parentNode) {
  360. return getDocument(node.parentNode);
  361. } else if (node.commonAncestorContainer) {
  362. return getDocument(node.commonAncestorContainer);
  363. } else if (node.startContainer) {
  364. return getDocument(node.startContainer);
  365. } else if (node.anchorNode) {
  366. return getDocument(node.anchorNode);
  367. }
  368. }
  369. class DOMException {
  370. constructor(message, name) {
  371. this.message = message;
  372. this.name = name;
  373. this.stack = (new Error()).stack;
  374. }
  375. }
  376. DOMException.prototype = new Error();
  377. DOMException.prototype.toString = function () {
  378. return `${this.name}: ${this.message}`
  379. };
  380. const FIRST_ORDERED_NODE_TYPE = 9;
  381. const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
  382. window.sXPath = {};
  383. window.sXPath.fromNode = (node, root = null) => {
  384. if (node === undefined) {
  385. throw new Error('missing required parameter "node"')
  386. }
  387. root = root || getDocument(node);
  388. let path = '/';
  389. while (node !== root) {
  390. if (!node) {
  391. let message = 'The supplied node is not contained by the root node.';
  392. let name = 'InvalidNodeTypeError';
  393. throw new DOMException(message, name)
  394. }
  395. path = `/${nodeName(node)}[${nodePosition(node)}]${path}`;
  396. node = node.parentNode;
  397. }
  398. return path.replace(/\/$/, '')
  399. };
  400. window.sXPath.toNode = (path, root, resolver = null) => {
  401. if (path === undefined) {
  402. throw new Error('missing required parameter "path"')
  403. }
  404. if (root === undefined) {
  405. throw new Error('missing required parameter "root"')
  406. }
  407. let document = getDocument(root);
  408. if (root !== document) path = path.replace(/^\//, './');
  409. let documentElement = document.documentElement;
  410. if (resolver === null && documentElement.lookupNamespaceURI) {
  411. let defaultNS = documentElement.lookupNamespaceURI(null) || HTML_NAMESPACE;
  412. resolver = (prefix) => {
  413. let ns = { '_default_': defaultNS };
  414. return ns[prefix] || documentElement.lookupNamespaceURI(prefix)
  415. };
  416. }
  417. return resolve(path, root, resolver)
  418. };
  419. function nodeName(node) {
  420. switch (node.nodeName) {
  421. case '#text': return 'text()'
  422. case '#comment': return 'comment()'
  423. case '#cdata-section': return 'cdata-section()'
  424. default: return node.nodeName.toLowerCase()
  425. }
  426. }
  427. function nodePosition(node) {
  428. let name = node.nodeName;
  429. let position = 1;
  430. while ((node = node.previousSibling)) {
  431. if (node.nodeName === name) position += 1;
  432. }
  433. return position
  434. }
  435. function resolve(path, root, resolver) {
  436. try {
  437. let nspath = path.replace(/\/(?!\.)([^\/:\(]+)(?=\/|$)/g, '/_default_:$1');
  438. return platformResolve(nspath, root, resolver)
  439. } catch (err) {
  440. return fallbackResolve(path, root)
  441. }
  442. }
  443. function fallbackResolve(path, root) {
  444. let steps = path.split("/");
  445. let node = root;
  446. while (node) {
  447. let step = steps.shift();
  448. if (step === undefined) break
  449. if (step === '.') continue
  450. let [name, position] = step.split(/[\[\]]/);
  451. name = name.replace('_default_:', '');
  452. position = position ? parseInt(position) : 1;
  453. node = findChild(node, name, position);
  454. }
  455. return node
  456. }
  457. function platformResolve(path, root, resolver) {
  458. let document = getDocument(root);
  459. let r = document.evaluate(path, root, resolver, FIRST_ORDERED_NODE_TYPE, null);
  460. return r.singleNodeValue
  461. }
  462. function findChild(node, name, position) {
  463. for (node = node.firstChild; node; node = node.nextSibling) {
  464. if (nodeName(node) === name && --position === 0) break
  465. }
  466. return node
  467. }
  468.  
  469. let urlChangeFn = null;
  470. history.pushState = (f => function pushState() {
  471. var ret = f.apply(this, arguments);
  472. window.dispatchEvent(new Event('pushstate'));
  473. window.dispatchEvent(new Event('urlchange'));
  474. return ret;
  475. })(history.pushState);
  476. history.replaceState = (f => function replaceState() {
  477. var ret = f.apply(this, arguments);
  478. window.dispatchEvent(new Event('replacestate'));
  479. window.dispatchEvent(new Event('urlchange'));
  480. return ret;
  481. })(history.replaceState);
  482. window.addEventListener('popstate', () => {
  483. window.dispatchEvent(new Event('urlchange'));
  484. });
  485. Object.defineProperty(window, 'onurlchange', {
  486. get() { return urlChangeFn; },
  487. set(fn) {
  488. if (typeof fn === 'function') {
  489. urlChangeFn = fn;
  490. window.addEventListener('urlchange', urlChangeFn);
  491. } else {
  492. window.removeEventListener('urlchange', urlChangeFn);
  493. urlChangeFn = null;
  494. }
  495. },
  496. });
  497. })();
  498. runAt('document-end', () => {
  499. const stor = window.localStorage,
  500. boxkey = 'lemonScrollBox';
  501. let boxobj = null, box = null, boxel = null;
  502. function getScrollBox(e) {
  503. boxel = e.target;
  504. let pageid = location.href;
  505. if (boxel.scrollTop === undefined) boxel = document.documentElement;
  506. try {
  507. box = window.sXPath.fromNode(boxel, document.documentElement);
  508. } catch (e) {
  509. box = '.';
  510. }
  511. if (!boxobj) boxobj = {};
  512. boxobj[pageid] =
  513. {
  514. box: box,
  515. pos: boxel.scrollTop,
  516. class: boxel.className,
  517. id: boxel.id
  518. };
  519. stor.setItem(
  520. boxkey,
  521. JSON.stringify(boxobj)
  522. );
  523. }
  524. function startNewRecord() {
  525. toast('开始记录滚动', config.toast);
  526. document.addEventListener('scroll', throttle(getScrollBox, 300), true);
  527. }
  528. function scanPage() {
  529. boxobj = JSON.parse(stor.getItem(boxkey));
  530. let pageid = location.href;
  531. if (boxobj[pageid]) {
  532. runNeed(
  533. () => {
  534. boxel = (boxobj[pageid].box === '') ?
  535. document.documentElement : window.sXPath.toNode(
  536. boxobj[pageid].box,
  537. document.documentElement
  538. );
  539. if (boxel &&
  540. boxel.id === boxobj[pageid].id &&
  541. boxel.className === boxobj[pageid].class &&
  542. boxel.scrollHeight > window.innerHeight) {
  543. return true;
  544. } else return false;
  545. },
  546. () => {
  547. setTimeout(() => {
  548. boxel.scrollTop = boxobj[pageid].pos;
  549. }, config.out);
  550. }
  551. );
  552. document.addEventListener('scroll', throttle(getScrollBox, 300), true);
  553. } else startNewRecord();
  554. }
  555. if (stor.hasOwnProperty(boxkey)) {
  556. window.onurlchange = scanPage;
  557. window.onhashchange = scanPage;
  558. scanPage();
  559. } else {
  560. startNewRecord();
  561. }
  562. });
  563. });
  564. });
  565.  
  566. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址