记录页面滚动

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

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

QingJ © 2025

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