SE Preview on hover

Shows preview of the linked questions/answers on hover

当前为 2017-02-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name SE Preview on hover
  3. // @description Shows preview of the linked questions/answers on hover
  4. // @version 0.2.6
  5. // @author wOxxOm
  6. // @namespace wOxxOm.scripts
  7. // @license MIT License
  8. // @match *://*.stackoverflow.com/*
  9. // @match *://*.superuser.com/*
  10. // @match *://*.serverfault.com/*
  11. // @match *://*.askubuntu.com/*
  12. // @match *://*.stackapps.com/*
  13. // @match *://*.mathoverflow.net/*
  14. // @match *://*.stackexchange.com/*
  15. // @require https://gf.qytechs.cn/scripts/12228/code/setMutationHandler.js
  16. // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js
  17. // @grant GM_addStyle
  18. // @grant GM_xmlhttpRequest
  19. // @connect stackoverflow.com
  20. // @connect superuser.com
  21. // @connect serverfault.com
  22. // @connect askubuntu.com
  23. // @connect stackapps.com
  24. // @connect mathoverflow.net
  25. // @connect stackexchange.com
  26. // @connect cdn.sstatic.net
  27. // @run-at document-end
  28. // @noframes
  29. // ==/UserScript==
  30.  
  31. /* jshint lastsemic:true, multistr:true, laxbreak:true, -W030, -W041, -W084 */
  32.  
  33. const PREVIEW_DELAY = 200;
  34. const CACHE_DURATION = 1 * 60 * 1000; // 1 minute for the recently active posts, scales up logarithmically
  35. const COLORS = {
  36. question: {
  37. backRGB: '80, 133, 195',
  38. fore: '#265184',
  39. },
  40. answer: {
  41. backRGB: '112, 195, 80',
  42. fore: '#3f7722',
  43. foreInv: 'white',
  44. },
  45. deleted: {
  46. backRGB: '181, 103, 103',
  47. fore: 'rgb(181, 103, 103)',
  48. foreInv: 'white',
  49. },
  50. closed: {
  51. backRGB: '255, 206, 93',
  52. fore: 'rgb(204, 143, 0)',
  53. foreInv: 'white',
  54. },
  55. };
  56.  
  57. let xhr;
  58. let preview = {
  59. frame: null,
  60. link: null,
  61. hover: {x:0, y:0},
  62. timer: 0,
  63. cacheCSS: {},
  64. stylesOverride: '',
  65. };
  66.  
  67. const rxPreviewable = getURLregexForMatchedSites();
  68. const thisPageUrls = getPageBaseUrls(location.href);
  69.  
  70. initStyles();
  71. initPolyfills();
  72. setMutationHandler('a', onLinkAdded, {processExisting: true});
  73. setTimeout(cleanupCache, 10000);
  74.  
  75. /**************************************************************/
  76.  
  77. function onLinkAdded(links) {
  78. for (let i = 0, link; (link = links[i++]); ) {
  79. if (isLinkPreviewable(link)) {
  80. link.removeAttribute('title');
  81. $on('mouseover', link, onLinkHovered);
  82. }
  83. }
  84. }
  85.  
  86. function onLinkHovered(e) {
  87. if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey)
  88. return;
  89. preview.link = this;
  90. $on('mousemove', this, onLinkMouseMove);
  91. $on('mouseout', this, abortPreview);
  92. $on('mousedown', this, abortPreview);
  93. restartPreviewTimer(this);
  94. }
  95.  
  96. function onLinkMouseMove(e) {
  97. let stoppedMoving = Math.abs(preview.hover.x - e.clientX) < 2 &&
  98. Math.abs(preview.hover.y - e.clientY) < 2;
  99. if (!stoppedMoving)
  100. return;
  101. preview.hover.x = e.clientX;
  102. preview.hover.y = e.clientY;
  103. restartPreviewTimer(this);
  104. }
  105.  
  106. function restartPreviewTimer(link) {
  107. clearTimeout(preview.timer);
  108. preview.timer = setTimeout(() => {
  109. preview.timer = 0;
  110. $off('mousemove', link, onLinkMouseMove);
  111. if (link.matches(':hover'))
  112. downloadPreview(link.href);
  113. }, PREVIEW_DELAY);
  114. }
  115.  
  116. function abortPreview(e) {
  117. releaseLinkListeners(this);
  118. preview.timer = setTimeout(link => {
  119. if (link == preview.link && preview.frame && !preview.frame.matches(':hover')) {
  120. releaseLinkListeners(link);
  121. preview.frame.contentWindow.postMessage('SEpreview-hidden', '*');
  122. fadeOut(preview.frame);
  123. }
  124. }, PREVIEW_DELAY * 3, this);
  125. if (xhr)
  126. xhr.abort();
  127. }
  128.  
  129. function releaseLinkListeners(link) {
  130. $off('mousemove', link, onLinkMouseMove);
  131. $off('mouseout', link, abortPreview);
  132. $off('mousedown', link, abortPreview);
  133. clearTimeout(preview.timer);
  134. }
  135.  
  136. function fadeOut(element, transition) {
  137. if (transition) {
  138. element.style.transition = typeof transition == 'number' ? `opacity ${transition}s ease-in-out` : transition;
  139. return setTimeout(fadeOut, 0, element);
  140. }
  141. element.style.opacity = 0;
  142. $on('transitionend', element, function remove() {
  143. $off('transitionend', element, remove);
  144. if (+element.style.opacity === 0)
  145. element.style.display = 'none';
  146. });
  147. }
  148.  
  149. function downloadPreview(url) {
  150. let cached = readCache(url);
  151. if (cached)
  152. showPreview(cached);
  153. else {
  154. xhr = GM_xmlhttpRequest({
  155. method: 'GET',
  156. url: httpsUrl(url),
  157. onload: r => {
  158. let html = r.responseText;
  159. let lastActivity = showPreview({finalUrl: r.finalUrl, html});
  160. let inactiveDays = Math.max(0, (Date.now() - lastActivity) / (24 * 3600 * 1000));
  161. let cacheDuration = CACHE_DURATION * Math.pow(Math.log(inactiveDays + 1) + 1, 2);
  162. writeCache({url, finalUrl: r.finalUrl, html, cacheDuration});
  163. },
  164. });
  165. }
  166. }
  167.  
  168. function showPreview({finalUrl, html, doc}) {
  169. doc = doc || new DOMParser().parseFromString(html, 'text/html');
  170. if (!doc || !doc.head) {
  171. error('no HEAD in the document received for', finalUrl);
  172. return;
  173. }
  174.  
  175. if (!$('base', doc))
  176. doc.head.insertAdjacentHTML('afterbegin', `<base href="${finalUrl}">`);
  177.  
  178. const answerIdMatch = finalUrl.match(/questions\/\d+\/[^\/]+\/(\d+)/);
  179. const isQuestion = !answerIdMatch;
  180. const postId = answerIdMatch ? '#answer-' + answerIdMatch[1] : '#question';
  181. const post = $(postId + ' .post-text', doc);
  182. if (!post)
  183. return error('No parsable post found', doc);
  184. const isDeleted = post.closest('.deleted-answer');
  185. const title = $('meta[property="og:title"]', doc).content;
  186. const status = isQuestion && !$('.question-status', post) && $('.question-status', doc);
  187. const isClosed = $('.question-originals-of-duplicate, .close-as-off-topic-status-list, .close-status-suffix', doc);
  188. const comments = $(`${postId} .comments`, doc);
  189. const commentsHidden = +$('tbody', comments).dataset.remainingCommentsCount;
  190. const commentsShowLink = commentsHidden && $(`${postId} .js-show-link.comments-link`, doc);
  191. const finalUrlOfQuestion = getCacheableUrl(finalUrl);
  192.  
  193. const lastActivity = +doc.body.getAttribute('SEpreview-lastActivity')
  194. || tryCatch(() => new Date($('.lastactivity-link', doc).title).getTime())
  195. || Date.now();
  196. if (lastActivity)
  197. doc.body.setAttribute('SEpreview-lastActivity', lastActivity);
  198.  
  199. markPreviewableLinks(doc);
  200. $$remove('script', doc);
  201.  
  202. if (!preview.frame) {
  203. preview.frame = document.createElement('iframe');
  204. preview.frame.id = 'SEpreview';
  205. document.body.appendChild(preview.frame);
  206. }
  207.  
  208. let pvDoc, pvWin;
  209. preview.frame.setAttribute('SEpreview-type',
  210. isDeleted ? 'deleted' : isQuestion ? (isClosed ? 'closed' : 'question') : 'answer');
  211. onFrameReady(preview.frame).then(
  212. () => {
  213. pvDoc = preview.frame.contentDocument;
  214. pvWin = preview.frame.contentWindow;
  215. initPolyfills(pvWin);
  216. })
  217. .then(addStyles)
  218. .then(render)
  219. .then(show);
  220. return lastActivity;
  221.  
  222. function markPreviewableLinks(container) {
  223. for (let link of $$('a:not(.SEpreviewable)', container)) {
  224. if (rxPreviewable.test(link.href)) {
  225. link.removeAttribute('title');
  226. link.classList.add('SEpreviewable');
  227. }
  228. }
  229. }
  230.  
  231. function addStyles() {
  232. const SEpreviewStyles = $replaceOrCreate({
  233. id: 'SEpreviewStyles',
  234. tag: 'style', parent: pvDoc.head, className: 'SEpreview-reuse',
  235. innerHTML: preview.stylesOverride,
  236. });
  237.  
  238. $replaceOrCreate($$('style, link[rel="stylesheet"]', doc).map(e =>
  239. e.localName == 'style' ? {
  240. id: 'SEpreview' + e.innerHTML.replace(/\W+/g, '').length,
  241. tag: 'style', before: SEpreviewStyles, className: 'SEpreview-reuse',
  242. innerHTML: e.innerHTML,
  243. } : {
  244. id: e.href.replace(/\W+/g, ''),
  245. tag: 'link', before: SEpreviewStyles, className: 'SEpreview-reuse',
  246. href: e.href, rel: 'stylesheet',
  247. })
  248. );
  249.  
  250. return onStyleSheetsReady($$('link[rel="stylesheet"]', pvDoc));
  251. }
  252.  
  253. function render() {
  254. pvDoc.body.setAttribute('SEpreview-type', preview.frame.getAttribute('SEpreview-type'));
  255.  
  256. $replaceOrCreate([{
  257. // title
  258. id: 'SEpreview-title', tag: 'a',
  259. parent: pvDoc.body, className: 'SEpreviewable',
  260. href: finalUrlOfQuestion,
  261. textContent: title,
  262. }, {
  263. // vote count, date, views#
  264. id: 'SEpreview-meta',
  265. parent: pvDoc.body,
  266. innerHTML: [
  267. $text('.vote-count-post', post.closest('table')).replace(/(-?)(\d+)/,
  268. (s, sign, v) => s == '0' ? '' : `<b>${s}</b> vote${+v > 1 ? 's' : ''}, `),
  269. isQuestion
  270. ? $$('#qinfo tr', doc)
  271. .map(row => $$('.label-key', row).map($text).join(' '))
  272. .join(', ').replace(/^((.+?) (.+?), .+?), .+? \3$/, '$1')
  273. : [...$$('.user-action-time', post.closest('.answer'))]
  274. .reverse().map($text).join(', ')
  275. ].join('')
  276. }, {
  277. // content wrapper
  278. id: 'SEpreview-body',
  279. parent: pvDoc.body,
  280. className: isDeleted ? 'deleted-answer' : '',
  281. children: [status, post.parentElement, comments, commentsShowLink],
  282. }]);
  283.  
  284. renderCode();
  285.  
  286. // render bottom shelf
  287. const answers = $$('.answer', doc);
  288. if (answers.length > (isQuestion ? 0 : 1)) {
  289. $replaceOrCreate({
  290. id: 'SEpreview-answers',
  291. parent: pvDoc.body,
  292. innerHTML: answers.map(renderShelfAnswer).join(' '),
  293. });
  294. } else
  295. $$remove('#SEpreview-answers', pvDoc);
  296.  
  297. // cleanup leftovers from previously displayed post and foreign elements not injected by us
  298. $$('style, link, body script, html > *:not(head):not(body)', pvDoc).forEach(e => {
  299. if (e.classList.contains('SEpreview-reuse'))
  300. e.classList.remove('SEpreview-reuse');
  301. else
  302. e.remove();
  303. });
  304. }
  305.  
  306. function renderCode() {
  307. const codeBlocks = $$('pre code', pvDoc);
  308. if (codeBlocks.length) {
  309. codeBlocks.forEach(e => e.parentElement.classList.add('prettyprint'));
  310. if (!pvWin.StackExchange) {
  311. pvWin.StackExchange = {};
  312. let script = $scriptIn(pvDoc.head);
  313. script.text = 'StackExchange = {}';
  314. script = $scriptIn(pvDoc.head);
  315. script.src = 'https://cdn.sstatic.net/Js/prettify-full.en.js';
  316. script.setAttribute('onload', 'prettyPrint()');
  317. } else
  318. $scriptIn(pvDoc.body).text = 'prettyPrint()';
  319. }
  320. }
  321.  
  322. function renderShelfAnswer(e) {
  323. const shortUrl = $('.short-link', e).href.replace(/(\d+)\/\d+/, '$1');
  324. const extraClasses = (e.matches(postId) ? ' SEpreviewed' : '') +
  325. (e.matches('.deleted-answer') ? ' deleted-answer' : '');
  326. const author = $('.post-signature:last-child', e);
  327. const title = $text('.user-details a', author) + ' (rep ' +
  328. $text('.reputation-score', author) + ')\n' +
  329. $text('.user-action-time', author);
  330. const gravatar = $('img, .anonymous-gravatar, .community-wiki', author);
  331. const accepted = !!$('.vote-accepted-on', e);
  332. return (
  333. `<a href="${shortUrl}" title="${title}" class="SEpreviewable${extraClasses}">` +
  334. $text('.vote-count-post', e) + ' ' +
  335. (!accepted ? '' : '<span class="vote-accepted-on"></span>') +
  336. (!gravatar ? '' : gravatar.src ? `<img src="${gravatar.src}">` : gravatar.outerHTML) +
  337. '</a>');
  338. }
  339.  
  340. function show() {
  341. pvDoc.onmouseover = retainMainScrollPos;
  342. pvDoc.onclick = interceptLinks;
  343. pvWin.onmessage = e => {
  344. if (e.data == 'SEpreview-hidden') {
  345. pvWin.onmessage = null;
  346. pvDoc.onmouseover = null;
  347. pvDoc.onclick = null;
  348. }
  349. };
  350.  
  351. $('#SEpreview-body', pvDoc).scrollTop = 0;
  352. preview.frame.style.opacity = 1;
  353. preview.frame.style.display = '';
  354. }
  355.  
  356. function retainMainScrollPos(e) {
  357. let scrollPos = {x:scrollX, y:scrollY};
  358. $on('scroll', preventScroll);
  359. $on('mouseover', releaseScrollLock);
  360.  
  361. function preventScroll(e) {
  362. scrollTo(scrollPos.x, scrollPos.y);
  363. }
  364.  
  365. function releaseScrollLock(e) {
  366. $off('mouseout', releaseScrollLock);
  367. $off('scroll', preventScroll);
  368. }
  369. }
  370.  
  371. function interceptLinks(e) {
  372. const link = e.target.closest('a');
  373. if (!link)
  374. return;
  375. if (link.matches('.js-show-link.comments-link')) {
  376. fadeOut(link, 0.5);
  377. loadComments();
  378. }
  379. else if (e.button || e.ctrlKey || e.altKey || e.shiftKey || e.metaKey || !link.matches('.SEpreviewable'))
  380. return (link.target = '_blank');
  381. else if (link.matches('#SEpreview-answers a, a#SEpreview-title'))
  382. showPreview({
  383. finalUrl: finalUrlOfQuestion + (link.id == 'SEpreview-title' ? '' : '/' + link.pathname.match(/\/(\d+)/)[1]),
  384. doc
  385. });
  386. else
  387. downloadPreview(link.getAttribute('SEpreview-fullUrl') || link.href);
  388. e.preventDefault();
  389. }
  390.  
  391. function loadComments() {
  392. GM_xmlhttpRequest({
  393. method: 'GET',
  394. url: new URL(finalUrl).origin + '/posts/' + comments.id.match(/\d+/)[0] + '/comments',
  395. onload: r => {
  396. let tbody = $(`#${comments.id} tbody`, pvDoc);
  397. let oldIds = new Set([...tbody.rows].map(e => e.id));
  398. tbody.innerHTML = r.responseText;
  399. tbody.closest('.comments').style.display = 'block';
  400. for (let tr of tbody.rows)
  401. if (!oldIds.has(tr.id))
  402. tr.classList.add('new-comment-highlight');
  403. markPreviewableLinks(tbody);
  404. },
  405. });
  406. }
  407. }
  408.  
  409. function getCacheableUrl(url) {
  410. // strips querys and hashes and anything after the main part https://site/questions/####/title/
  411. return url
  412. .replace(/(\/q(?:uestions)?\/\d+\/[^\/]+).*/, '$1')
  413. .replace(/(\/a(?:nswers)?\/\d+).*/, '$1')
  414. .replace(/[?#].*$/, '');
  415. }
  416.  
  417. function readCache(url) {
  418. keyUrl = getCacheableUrl(url);
  419. const meta = (localStorage[keyUrl] || '').split('\t');
  420. const expired = +meta[0] < Date.now();
  421. const finalUrl = meta[1] || url;
  422. const keyFinalUrl = meta[1] ? getCacheableUrl(finalUrl) : keyUrl;
  423. return !expired && {
  424. finalUrl,
  425. html: LZString.decompressFromUTF16(localStorage[keyFinalUrl + '\thtml']),
  426. };
  427. }
  428.  
  429. function writeCache({url, finalUrl, html, cacheDuration = CACHE_DURATION, cleanupRetry}) {
  430. // keyUrl=expires
  431. // redirected keyUrl=expires+finalUrl, and an additional entry keyFinalUrl=expires is created
  432. // keyFinalUrl\thtml=html
  433. cacheDuration = Math.max(CACHE_DURATION, Math.min(0xDEADBEEF, Math.floor(cacheDuration)));
  434. finalUrl = finalUrl.replace(/[?#].*/, '');
  435. const keyUrl = getCacheableUrl(url);
  436. const keyFinalUrl = getCacheableUrl(finalUrl);
  437. const expires = Date.now() + cacheDuration;
  438. if (!tryCatch(() => localStorage[keyFinalUrl + '\thtml'] = LZString.compressToUTF16(html))) {
  439. if (cleanupRetry)
  440. return error('localStorage write error');
  441. cleanupCache({aggressive: true});
  442. setIimeout(writeCache, 0, {url, finalUrl, html, cacheDuration, cleanupRetry: true});
  443. }
  444. localStorage[keyFinalUrl] = expires;
  445. if (keyUrl != keyFinalUrl)
  446. localStorage[keyUrl] = expires + '\t' + finalUrl;
  447. setTimeout(() => {
  448. [keyUrl, keyFinalUrl, keyFinalUrl + '\thtml'].forEach(e => localStorage.removeItem(e));
  449. }, cacheDuration + 1000);
  450. }
  451.  
  452. function cleanupCache({aggressive = false} = {}) {
  453. Object.keys(localStorage).forEach(k => {
  454. if (k.match(/^https?:\/\/[^\t]+$/)) {
  455. let meta = (localStorage[k] || '').split('\t');
  456. if (+meta[0] > Date.now() && !aggressive)
  457. return;
  458. if (meta[1])
  459. localStorage.removeItem(meta[1]);
  460. localStorage.removeItem(`${meta[1] || k}\thtml`);
  461. localStorage.removeItem(k);
  462. }
  463. });
  464. }
  465.  
  466. function onFrameReady(frame) {
  467. if (frame.contentDocument.readyState == 'complete')
  468. return Promise.resolve();
  469. else
  470. return new Promise(resolve => {
  471. $on('load', frame, function onLoad() {
  472. $off('load', frame, onLoad);
  473. resolve();
  474. });
  475. });
  476. }
  477.  
  478. function onStyleSheetsReady(linkElements) {
  479. return new Promise(function retry(resolve) {
  480. if (linkElements.every(e => e.sheet && e.sheet.href == e.href))
  481. resolve();
  482. else
  483. setTimeout(retry, 0, resolve);
  484. });
  485. }
  486.  
  487. function getURLregexForMatchedSites() {
  488. return new RegExp('https?://(\\w*\\.)*(' + GM_info.script.matches.map(m =>
  489. m.match(/^.*?\/\/\W*(\w.*?)\//)[1].replace(/\./g, '\\.')
  490. ).join('|') + ')/(questions|q|a)/\\d+');
  491. }
  492.  
  493. function isLinkPreviewable(link) {
  494. const inPreview = link.ownerDocument != document;
  495. if (!rxPreviewable.test(link.href) || link.matches('.short-link'))
  496. return false;
  497. const pageUrls = inPreview ? getPageBaseUrls(preview.link.href) : thisPageUrls;
  498. const url = httpsUrl(link.href);
  499. return !url.startsWith(pageUrls.base) &&
  500. !url.startsWith(pageUrls.short);
  501. }
  502.  
  503. function getPageBaseUrls(url) {
  504. const base = httpsUrl((url.match(rxPreviewable) || [])[0]);
  505. return base ? {
  506. base,
  507. short: base.replace('/questions/', '/q/'),
  508. } : {};
  509. }
  510.  
  511. function httpsUrl(url) {
  512. return (url || '').replace(/^http:/, 'https:');
  513. }
  514.  
  515. function $(selector, node = document) {
  516. return node.querySelector(selector);
  517. }
  518.  
  519. function $$(selector, node = document) {
  520. return node.querySelectorAll(selector);
  521. }
  522.  
  523. function $text(selector, node = document) {
  524. const e = typeof selector == 'string' ? node.querySelector(selector) : selector;
  525. return e ? e.textContent.trim() : '';
  526. }
  527.  
  528. function $$remove(selector, node = document) {
  529. node.querySelectorAll(selector).forEach(e => e.remove());
  530. }
  531.  
  532. function $appendChildren(newParent, elements) {
  533. const doc = newParent.ownerDocument;
  534. for (let e of elements)
  535. if (e)
  536. newParent.appendChild(e.ownerDocument == doc ? e : doc.importNode(e, true));
  537. }
  538.  
  539. function $replaceOrCreate(options) {
  540. if (options.length && typeof options[0] == 'object')
  541. return [].map.call(options, $replaceOrCreate);
  542. const doc = (options.parent || options.before).ownerDocument;
  543. const el = doc.getElementById(options.id) || doc.createElement(options.tag || 'div');
  544. for (let key of Object.keys(options)) {
  545. switch (key) {
  546. case 'tag':
  547. case 'parent':
  548. case 'before':
  549. break;
  550. case 'children':
  551. if (el.children.length)
  552. el.innerHTML = '';
  553. $appendChildren(el, options[key]);
  554. break;
  555. default:
  556. const value = options[key];
  557. if (key in el && el[key] != value)
  558. el[key] = value;
  559. }
  560. }
  561. if (!el.parentElement)
  562. (options.parent || options.before.parentElement).insertBefore(el, options.before);
  563. return el;
  564. }
  565.  
  566. function $scriptIn(element) {
  567. return element.appendChild(element.ownerDocument.createElement('script'));
  568. }
  569.  
  570. function $on(eventName, ...args) {
  571. // eventName, selector, node, callback, options
  572. // eventName, selector, callback, options
  573. // eventName, node, callback, options
  574. // eventName, callback, options
  575. const selector = typeof args[0] == 'string' ? args[0] : null;
  576. const node = args[0] instanceof Node ? args[0] : args[1] instanceof Node ? args[1] : document;
  577. const callback = args[typeof args[0] == 'function' ? 0 : typeof args[1] == 'function' ? 1 : 2];
  578. const options = args[args.length - 1] != callback ? args[args.length - 1] : undefined;
  579. const method = this == 'removeEventListener' ? this : 'addEventListener';
  580. (selector ? node.querySelector(selector) : node)[method](eventName, callback, options);
  581. }
  582.  
  583. function $off(eventName, ...args) {
  584. $on.apply('removeEventListener', arguments);
  585. }
  586.  
  587. function log(...args) {
  588. console.log(GM_info.script.name, ...args);
  589. }
  590.  
  591. function error(...args) {
  592. console.error(GM_info.script.name, ...args);
  593. }
  594.  
  595. function tryCatch(fn) {
  596. try { return fn() }
  597. catch(e) {}
  598. }
  599.  
  600. function initPolyfills(context = window) {
  601. for (let method of ['forEach', 'filter', 'map', 'every', context.Symbol.iterator])
  602. if (!context.NodeList.prototype[method])
  603. context.NodeList.prototype[method] = context.Array.prototype[method];
  604. }
  605.  
  606. function initStyles() {
  607. GM_addStyle(`
  608. #SEpreview {
  609. all: unset;
  610. box-sizing: content-box;
  611. width: 720px; /* 660px + 30px + 30px */
  612. height: 33%;
  613. min-height: 200px;
  614. position: fixed;
  615. opacity: 0;
  616. transition: opacity .5s cubic-bezier(.88,.02,.92,.66);
  617. right: 0;
  618. bottom: 0;
  619. padding: 0;
  620. margin: 0;
  621. background: white;
  622. box-shadow: 0 0 100px rgba(0,0,0,0.5);
  623. z-index: 999999;
  624. border-width: 8px;
  625. border-style: solid;
  626. }
  627. `
  628. + Object.keys(COLORS).map(s => `
  629. #SEpreview[SEpreview-type="${s}"] {
  630. border-color: rgb(${COLORS[s].backRGB});
  631. }
  632. `).join('')
  633. );
  634.  
  635. preview.stylesOverride = `
  636. body, html {
  637. min-width: unset!important;
  638. box-shadow: none!important;
  639. padding: 0!important;
  640. margin: 0!important;
  641. }
  642. html, body {
  643. background: unset!important;;
  644. }
  645. body {
  646. display: flex;
  647. flex-direction: column;
  648. height: 100vh;
  649. }
  650. #SEpreview-body a.SEpreviewable {
  651. text-decoration: underline !important;
  652. }
  653. #SEpreview-title {
  654. all: unset;
  655. display: block;
  656. padding: 20px 30px;
  657. font-weight: bold;
  658. font-size: 18px;
  659. line-height: 1.2;
  660. cursor: pointer;
  661. }
  662. #SEpreview-title:hover {
  663. text-decoration: underline;
  664. }
  665. #SEpreview-meta {
  666. position: absolute;
  667. top: .5ex;
  668. left: 30px;
  669. opacity: 0.5;
  670. }
  671. #SEpreview-title:hover + #SEpreview-meta {
  672. opacity: 1.0;
  673. }
  674.  
  675. #SEpreview-body {
  676. padding: 30px!important;
  677. overflow: auto;
  678. flex-grow: 2;
  679. }
  680. #SEpreview-body .post-menu {
  681. display: none!important;
  682. }
  683. #SEpreview-body > .question-status {
  684. margin: -30px -30px 30px;
  685. padding-left: 30px;
  686. }
  687. #SEpreview-body .question-originals-of-duplicate {
  688. margin: -30px -30px 30px;
  689. padding: 15px 30px;
  690. }
  691. #SEpreview-body > .question-status h2 {
  692. font-weight: normal;
  693. }
  694.  
  695. #SEpreview-answers {
  696. all: unset;
  697. display: block;
  698. padding: 10px 10px 10px 30px;
  699. font-weight: bold;
  700. line-height: 1.0;
  701. border-top: 4px solid rgba(${COLORS.answer.backRGB}, 0.37);
  702. background-color: rgba(${COLORS.answer.backRGB}, 0.37);
  703. color: ${COLORS.answer.fore};
  704. word-break: break-word;
  705. }
  706. #SEpreview-answers:before {
  707. content: "Answers:";
  708. margin-right: 1ex;
  709. font-size: 20px;
  710. line-height: 48px;
  711. }
  712. #SEpreview-answers a {
  713. color: ${COLORS.answer.fore};
  714. text-decoration: none;
  715. font-size: 11px;
  716. font-family: monospace;
  717. width: 32px;
  718. display: inline-block;
  719. vertical-align: top;
  720. margin: 0 1ex 1ex 0;
  721. }
  722. #SEpreview-answers img {
  723. width: 32px;
  724. height: 32px;
  725. }
  726. #SEpreview-answers .vote-accepted-on {
  727. position: absolute;
  728. margin: -12px 0 0 6px;
  729. filter: drop-shadow(1px 2px 1px rgba(0,0,0,1));
  730. }
  731. #SEpreview-answers a.deleted-answer {
  732. color: ${COLORS.deleted.fore};
  733. background: transparent;
  734. opacity: 0.25;
  735. }
  736. #SEpreview-answers a.deleted-answer:hover {
  737. opacity: 1.0;
  738. }
  739. #SEpreview-answers a:hover:not(.SEpreviewed) {
  740. text-decoration: underline;
  741. }
  742. #SEpreview-answers a.SEpreviewed {
  743. background-color: ${COLORS.answer.fore};
  744. color: ${COLORS.answer.foreInv};
  745. position: relative;
  746. }
  747. #SEpreview-answers a.SEpreviewed:after {
  748. display: block;
  749. content: " ";
  750. position: absolute;
  751. left: -4px;
  752. top: -4px;
  753. right: -4px;
  754. bottom: -4px;
  755. border: 4px solid ${COLORS.answer.fore};
  756. }
  757.  
  758. .delete-tag,
  759. .comment-actions td:last-child {
  760. display: none;
  761. }
  762. .comments {
  763. border-top: none;
  764. }
  765. .comments tr:last-child td {
  766. border-bottom: none;
  767. }
  768. .comments .new-comment-highlight {
  769. -webkit-animation: highlight 9s cubic-bezier(0,.8,.37,.88);
  770. -moz-animation: highlight 9s cubic-bezier(0,.8,.37,.88);
  771. animation: highlight 9s cubic-bezier(0,.8,.37,.88);
  772. }
  773.  
  774. @-webkit-keyframes highlight {
  775. from {background-color: #ffcf78}
  776. to {background-color: none}
  777. }
  778. `
  779. + Object.keys(COLORS).map(s => `
  780. body[SEpreview-type="${s}"] #SEpreview-title {
  781. background-color: rgba(${COLORS[s].backRGB}, 0.37);
  782. color: ${COLORS[s].fore};
  783. }
  784. body[SEpreview-type="${s}"] #SEpreview-body::-webkit-scrollbar {
  785. background-color: rgba(${COLORS[s].backRGB}, 0.1); }
  786. body[SEpreview-type="${s}"] #SEpreview-body::-webkit-scrollbar-thumb {
  787. background-color: rgba(${COLORS[s].backRGB}, 0.2); }
  788. body[SEpreview-type="${s}"] #SEpreview-body::-webkit-scrollbar-thumb:hover {
  789. background-color: rgba(${COLORS[s].backRGB}, 0.3); }
  790. body[SEpreview-type="${s}"] #SEpreview-body::-webkit-scrollbar-thumb:active {
  791. background-color: rgba(${COLORS[s].backRGB}, 0.75); }
  792. `).join('')
  793. + ['deleted', 'closed'].map(s => `
  794. body[SEpreview-type="${s}"] #SEpreview-answers {
  795. border-top-color: rgba(${COLORS[s].backRGB}, 0.37);
  796. background-color: rgba(${COLORS[s].backRGB}, 0.37);
  797. color: ${COLORS[s].fore};
  798. }
  799. body[SEpreview-type="${s}"] #SEpreview-answers a.SEpreviewed {
  800. background-color: ${COLORS[s].fore};
  801. color: ${COLORS[s].foreInv};
  802. }
  803. body[SEpreview-type="${s}"] #SEpreview-answers a.SEpreviewed:after {
  804. border-color: ${COLORS[s].fore};
  805. }
  806. `).join('');
  807. }

QingJ © 2025

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