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

QingJ © 2025

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