SE Preview on hover

Shows preview of the linked questions/answers on hover

目前为 2017-02-16 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name SE Preview on hover
  3. // @description Shows preview of the linked questions/answers on hover
  4. // @version 0.1.9
  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://greasyfork.org/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 = 100;
  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. link.addEventListener('mouseover', 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. preview.link.addEventListener('mousemove', onLinkMouseMove);
  86. preview.link.addEventListener('mouseout', abortPreview);
  87. preview.link.addEventListener('mousedown', 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. link.removeEventListener('mousemove', 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. link.removeEventListener('mousemove', onLinkMouseMove);
  126. link.removeEventListener('mouseout', abortPreview);
  127. link.removeEventListener('mousedown', 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. element.addEventListener('transitionend', function remove() {
  138. element.removeEventListener('transitionend', 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 (!$(doc, 'base'))
  171. doc.head.insertAdjacentHTML('afterbegin', `<base href="${finalUrl}">`);
  172.  
  173. const answerIdMatch = finalUrl.match(/questions\/.+?\/(\d+)/);
  174. const isQuestion = !answerIdMatch;
  175. const postId = answerIdMatch ? '#answer-' + answerIdMatch[1] : '#question';
  176. const post = $(doc, postId + ' .post-text');
  177. if (!post)
  178. return error('No parsable post found', doc);
  179. const isDeleted = post.closest('.deleted-answer');
  180. const title = $(doc, 'meta[property="og:title"]').content;
  181. const status = isQuestion && !$(post, '.question-status') && $(doc, '.question-status');
  182. const comments = $(doc, `${postId} .comments`);
  183. const commentsHidden = +$(comments, 'tbody').dataset.remainingCommentsCount;
  184. const commentsShowLink = commentsHidden && $(doc, `${postId} .js-show-link.comments-link`);
  185.  
  186. const lastActivity = +doc.body.getAttribute('SEpreview-lastActivity')
  187. || tryCatch(() => new Date($(doc, '.lastactivity-link').title).getTime())
  188. || Date.now();
  189. if (lastActivity)
  190. doc.body.setAttribute('SEpreview-lastActivity', lastActivity);
  191.  
  192. $$remove(doc, 'script');
  193.  
  194. // underline previewable links
  195. for (let link of $$(doc, 'a:not(.SEpreviewable)')) {
  196. if (rxPreviewable.test(link.href)) {
  197. link.removeAttribute('title');
  198. link.classList.add('SEpreviewable');
  199. }
  200. }
  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. preview.frame.setAttribute('SEpreviewType', isDeleted ? 'deleted' : isQuestion ? 'question' : 'answer');
  209. onFrameReady(preview.frame, addStyles);
  210. return lastActivity;
  211.  
  212. function addStyles() {
  213. const pvDoc = preview.frame.contentDocument;
  214. const SEpreviewStyles = $replaceOrCreate({
  215. id: 'SEpreviewStyles',
  216. tag: 'style', parent: pvDoc.head, className: 'SEpreviewReuse',
  217. innerHTML: preview.stylesOverride,
  218. });
  219.  
  220. $replaceOrCreate($$(doc, 'style, link[rel="stylesheet"]').map(e =>
  221. e.localName == 'style' ? {
  222. id: 'SEpreview' + e.innerHTML.replace(/\W+/g, '').length,
  223. tag: 'style', before: SEpreviewStyles, className: 'SEpreviewReuse',
  224. innerHTML: e.innerHTML,
  225. } : {
  226. id: e.href.replace(/\W+/g, ''),
  227. tag: 'link', before: SEpreviewStyles, className: 'SEpreviewReuse',
  228. href: e.href, rel: 'stylesheet',
  229. })
  230. );
  231.  
  232. onStyleSheetsReady([...$$(pvDoc, 'link[rel="stylesheet"]')], render);
  233. }
  234.  
  235. function render() {
  236. const finalUrlOfQuestion = getCacheableUrl(finalUrl);
  237. const pvDoc = preview.frame.contentDocument;
  238. pvDoc.body.setAttribute('SEpreviewType', preview.frame.getAttribute('SEpreviewType'));
  239.  
  240. $replaceOrCreate([{
  241. id: 'SEpreviewTitle',
  242. tag: 'a', parent: pvDoc.body, className: 'SEpreviewable',
  243. href: isQuestion ? finalUrl : finalUrlOfQuestion,
  244. textContent: title,
  245. }, {
  246. id: 'SEpreviewBody',
  247. tag: 'div', parent: pvDoc.body, className: isDeleted ? 'deleted-answer' : '',
  248. children: [post.parentElement, comments, commentsShowLink, status],
  249. }]);
  250.  
  251. const codeBlocks = $$(pvDoc, 'pre code');
  252. if (codeBlocks.length) {
  253. codeBlocks.forEach(e => e.parentElement.classList.add('prettyprint'));
  254. if (!preview.frame.contentWindow.StackExchange) {
  255. preview.frame.contentWindow.StackExchange = {};
  256. let script = $scriptIn(pvDoc.head);
  257. script.text = 'StackExchange = {}';
  258. script = $scriptIn(pvDoc.head);
  259. script.src = 'https://cdn.sstatic.net/Js/prettify-full.en.js';
  260. script.setAttribute('onload', 'prettyPrint()');
  261. } else
  262. $scriptIn(pvDoc.body).text = 'prettyPrint()';
  263. }
  264.  
  265. const answers = $$(doc, '.answer');
  266. if (answers.length > (isQuestion ? 0 : 1)) {
  267. $replaceOrCreate({
  268. id: 'SEpreviewAnswers',
  269. tag: 'div', parent: pvDoc.body,
  270. innerHTML: 'Answers:&nbsp;' + answers.map((e, index) => {
  271. const shortUrl = $(e, '.short-link').href.replace(/(\d+)\/\d+/, '$1');
  272. const extraClasses = (e.matches(postId) ? ' SEpreviewed' : '') +
  273. (e.matches('.deleted-answer') ? ' deleted-answer' : '');
  274. const author = $(e, '.post-signature:last-child');
  275. return `<a href="${shortUrl}"
  276. SEpreviewFullUrl="${finalUrlOfQuestion + '/' + shortUrl.match(/\/(\d+)/)[1]}"
  277. title="${$text(author, '.user-details a') +
  278. ' (rep '+$text(author, '.reputation-score') + ')\n' +
  279. $text(author, '.user-action-time') +
  280. $text(author, '.vote-count-post').replace(/-?\d+/, s =>
  281. s == '0' ? '' : '\n' + s + ' vote' + (+s > 1 ? 's' : ''))}"
  282. class="SEpreviewable${extraClasses}"
  283. >${index + 1}</a>`;
  284. }).join(''),
  285. });
  286. } else
  287. $$remove(pvDoc, '#SEpreviewAnswers');
  288.  
  289. [...$$(pvDoc.head, 'style, link'), ...$$(pvDoc.body, 'script')].forEach(e => {
  290. if (e.classList.contains('SEpreviewReuse'))
  291. e.classList.remove('SEpreviewReuse');
  292. else
  293. e.remove();
  294. });
  295.  
  296. pvDoc.onmouseover = retainMainScrollPos;
  297. pvDoc.onclick = interceptLinks;
  298. preview.frame.contentWindow.onmessage = e => {
  299. if (e.data == 'SEpreviewHidden') {
  300. preview.frame.contentWindow.onmessage = null;
  301. pvDoc.onmouseover = null;
  302. pvDoc.onclick = null;
  303. }
  304. };
  305.  
  306. $(pvDoc, '#SEpreviewBody').scrollTop = 0;
  307. preview.frame.style.opacity = 1;
  308. preview.frame.style.display = '';
  309. }
  310.  
  311. function interceptLinks(e) {
  312. const link = e.target.closest('a');
  313. if (!link)
  314. return;
  315. if (link.matches('.js-show-link.comments-link')) {
  316. fadeOut(link, 0.5);
  317. downloadComments();
  318. }
  319. else if (e.button || e.ctrlKey || e.altKey || e.shiftKey || e.metaKey || !link.matches('.SEpreviewable'))
  320. return (link.target = '_blank');
  321. else if (link.matches('#SEpreviewAnswers a, a#SEpreviewTitle'))
  322. showPreview({
  323. finalUrl: link.getAttribute('SEpreviewFullUrl') || link.href,
  324. doc
  325. });
  326. else
  327. downloadPreview(link.getAttribute('SEpreviewFullUrl') || link.href);
  328. e.preventDefault();
  329. }
  330.  
  331. function downloadComments() {
  332. GM_xmlhttpRequest({
  333. method: 'GET',
  334. url: new URL(finalUrl).origin + '/posts/' + comments.id.match(/\d+/)[0] + '/comments',
  335. onload: r => showComments(r.responseText),
  336. });
  337. }
  338.  
  339. function showComments(html) {
  340. let tbody = $(preview.frame.contentDocument, `#${comments.id} tbody`);
  341. let oldIds = new Set([...tbody.rows].map(e => e.id));
  342. tbody.innerHTML = html;
  343. for (let tr of tbody.rows)
  344. if (!oldIds.has(tr.id))
  345. tr.classList.add('new-comment-highlight');
  346. }
  347. }
  348.  
  349. function retainMainScrollPos(e) {
  350. let scrollPos = {x:scrollX, y:scrollY};
  351. document.addEventListener('scroll', preventScroll);
  352. document.addEventListener('mouseover', releaseScrollLock);
  353.  
  354. function preventScroll(e) {
  355. scrollTo(scrollPos.x, scrollPos.y);
  356. }
  357.  
  358. function releaseScrollLock(e) {
  359. document.removeEventListener('mouseout', releaseScrollLock);
  360. document.removeEventListener('scroll', preventScroll);
  361. }
  362. }
  363.  
  364. function getCacheableUrl(url) {
  365. // strips querys and hashes and anything after the main part https://site/questions/####/title/
  366. return url
  367. .replace(/(\/q(?:uestions)?\/\d+\/[^\/]+).*/, '$1')
  368. .replace(/(\/a(?:nswers)?\/\d+).*/, '$1')
  369. .replace(/[?#].*$/, '');
  370. }
  371.  
  372. function readCache(url) {
  373. keyUrl = getCacheableUrl(url);
  374. const meta = (localStorage[keyUrl] || '').split('\t');
  375. const expired = +meta[0] < Date.now();
  376. const finalUrl = meta[1] || url;
  377. const keyFinalUrl = meta[1] ? getCacheableUrl(finalUrl) : keyUrl;
  378. return !expired && {
  379. finalUrl,
  380. html: LZString.decompressFromUTF16(localStorage[keyFinalUrl + '\thtml']),
  381. };
  382. }
  383.  
  384. function writeCache({url, finalUrl, html, cacheDuration = CACHE_DURATION, cleanupRetry}) {
  385. // keyUrl=expires
  386. // redirected keyUrl=expires+finalUrl, and an additional entry keyFinalUrl=expires is created
  387. // keyFinalUrl\thtml=html
  388. cacheDuration = Math.max(CACHE_DURATION, Math.min(0xDEADBEEF, Math.floor(cacheDuration)));
  389. finalUrl = finalUrl.replace(/[?#].*/, '');
  390. const keyUrl = getCacheableUrl(url);
  391. const keyFinalUrl = getCacheableUrl(finalUrl);
  392. const expires = Date.now() + cacheDuration;
  393. if (!tryCatch(() => localStorage[keyFinalUrl + '\thtml'] = LZString.compressToUTF16(html))) {
  394. if (cleanupRetry)
  395. return error('localStorage write error');
  396. cleanupCache({aggressive: true});
  397. setIimeout(writeCache, 0, {url, finalUrl, html, cacheDuration, cleanupRetry: true});
  398. }
  399. localStorage[keyFinalUrl] = expires;
  400. if (keyUrl != keyFinalUrl)
  401. localStorage[keyUrl] = expires + '\t' + finalUrl;
  402. setTimeout(() => {
  403. [keyUrl, keyFinalUrl, keyFinalUrl + '\thtml'].forEach(e => localStorage.removeItem(e));
  404. }, cacheDuration + 1000);
  405. }
  406.  
  407. function cleanupCache({aggressive = false} = {}) {
  408. Object.keys(localStorage).forEach(k => {
  409. if (k.match(/^https?:\/\/[^\t]+$/)) {
  410. let meta = (localStorage[k] || '').split('\t');
  411. if (+meta[0] > Date.now() && !aggressive)
  412. return;
  413. if (meta[1])
  414. localStorage.removeItem(meta[1]);
  415. localStorage.removeItem(`${meta[1] || k}\thtml`);
  416. localStorage.removeItem(k);
  417. }
  418. });
  419. }
  420.  
  421. function onFrameReady(frame, callback, ...args) {
  422. if (frame.contentDocument.readyState == 'complete')
  423. return callback.call(frame, ...args);
  424. else
  425. frame.addEventListener('load', function onLoad() {
  426. frame.removeEventListener('load', onLoad);
  427. callback.call(frame, ...args);
  428. });
  429. }
  430.  
  431. function onStyleSheetsReady(linkElements, callback, ...args) {
  432. if (linkElements.every(e => e.sheet && e.sheet.href == e.href))
  433. return callback(...args);
  434. else
  435. setTimeout(onStyleSheetsReady, 0, linkElements, callback, ...args);
  436. }
  437.  
  438. function getURLregexForMatchedSites() {
  439. return new RegExp('https?://(\\w*\\.)*(' + GM_info.script.matches.map(m =>
  440. m.match(/^.*?\/\/\W*(\w.*?)\//)[1].replace(/\./g, '\\.')
  441. ).join('|') + ')/(questions|q|a)/\\d+');
  442. }
  443.  
  444. function isLinkPreviewable(link) {
  445. const inPreview = link.ownerDocument != document;
  446. if (!rxPreviewable.test(link.href) || link.matches('.short-link'))
  447. return false;
  448. const pageUrls = inPreview ? getPageBaseUrls(preview.link.href) : thisPageUrls;
  449. const url = httpsUrl(link.href);
  450. return !url.startsWith(pageUrls.base) &&
  451. !url.startsWith(pageUrls.short);
  452. }
  453.  
  454. function getPageBaseUrls(url) {
  455. const base = httpsUrl((url.match(rxPreviewable) || [])[0]);
  456. return base ? {
  457. base,
  458. short: base.replace('/questions/', '/q/'),
  459. } : {};
  460. }
  461.  
  462. function httpsUrl(url) {
  463. return (url || '').replace(/^http:/, 'https:');
  464. }
  465.  
  466. function $(node__optional, selector) {
  467. return (node__optional || document).querySelector(selector || node__optional);
  468. }
  469.  
  470. function $$(node__optional, selector) {
  471. return (node__optional || document).querySelectorAll(selector || node__optional);
  472. }
  473.  
  474. function $text(node__optional, selector) {
  475. const e = $(node__optional, selector);
  476. return e ? e.textContent.trim() : '';
  477. }
  478.  
  479. function $$remove(node__optional, selector) {
  480. (node__optional || document).querySelectorAll(selector || node__optional)
  481. .forEach(e => e.remove());
  482. }
  483.  
  484. function $appendTo(newParent, elements) {
  485. const doc = newParent.ownerDocument;
  486. for (let e of elements)
  487. if (e)
  488. newParent.appendChild(e.ownerDocument == doc ? e : doc.importNode(e, true));
  489. }
  490.  
  491. function $replaceOrCreate(options) {
  492. if (options.length && typeof options[0] == 'object')
  493. return [].map.call(options, $replaceOrCreate);
  494. const doc = (options.parent || options.before).ownerDocument;
  495. const el = doc.getElementById(options.id) || doc.createElement(options.tag);
  496. for (let key of Object.keys(options)) {
  497. switch (key) {
  498. case 'tag':
  499. case 'parent':
  500. case 'before':
  501. break;
  502. case 'children':
  503. if (el.children.length)
  504. el.innerHTML = '';
  505. $appendTo(el, options[key]);
  506. break;
  507. default:
  508. const value = options[key];
  509. if (key in el && el[key] != value)
  510. el[key] = value;
  511. }
  512. }
  513. if (!el.parentElement)
  514. (options.parent || options.before.parentElement).insertBefore(el, options.before);
  515. return el;
  516. }
  517.  
  518. function $scriptIn(element) {
  519. return element.appendChild(element.ownerDocument.createElement('script'));
  520. }
  521.  
  522. function log(...args) {
  523. console.log(GM_info.script.name, ...args);
  524. }
  525.  
  526. function error(...args) {
  527. console.error(GM_info.script.name, ...args);
  528. }
  529.  
  530. function tryCatch(fn) {
  531. try { return fn() }
  532. catch(e) {}
  533. }
  534.  
  535. function initPolyfills() {
  536. for (let method of ['forEach', 'filter', 'map', Symbol.iterator])
  537. if (!NodeList.prototype[method])
  538. NodeList.prototype[method] = Array.prototype[method];
  539. }
  540.  
  541. function initStyles() {
  542. GM_addStyle(`
  543. #SEpreview {
  544. all: unset;
  545. box-sizing: content-box;
  546. width: 720px; /* 660px + 30px + 30px */
  547. height: 33%;
  548. min-height: 200px;
  549. position: fixed;
  550. opacity: 0;
  551. transition: opacity .5s cubic-bezier(.88,.02,.92,.66);
  552. right: 0;
  553. bottom: 0;
  554. padding: 0;
  555. margin: 0;
  556. background: white;
  557. box-shadow: 0 0 100px rgba(0,0,0,0.5);
  558. z-index: 999999;
  559. border: 8px solid rgb(${COLORS.question.backRGB});
  560. }
  561. #SEpreview[SEpreviewType="answer"] {
  562. border-color: rgb(${COLORS.answer.backRGB});
  563. }
  564. #SEpreview[SEpreviewType="deleted"] {
  565. border-color: rgba(${COLORS.deleted.backRGB}, 0.65);
  566. }
  567. `);
  568.  
  569. preview.stylesOverride = `
  570. body, html {
  571. min-width: unset!important;
  572. box-shadow: none!important;
  573. padding: 0!important;
  574. margin: 0!important;
  575. }
  576. html, body {
  577. background: unset!important;;
  578. }
  579. body {
  580. display: flex;
  581. flex-direction: column;
  582. height: 100vh;
  583. }
  584. a.SEpreviewable {
  585. text-decoration: underline !important;
  586. }
  587. #SEpreviewTitle {
  588. all: unset;
  589. display: block;
  590. padding: 20px 30px;
  591. font-weight: bold;
  592. font-size: 20px;
  593. line-height: 1.3;
  594. background-color: rgba(${COLORS.question.backRGB}, 0.37);
  595. color: ${COLORS.question.fore};
  596. cursor: pointer;
  597. }
  598. #SEpreviewTitle:hover {
  599. text-decoration: underline;
  600. }
  601. #SEpreviewBody {
  602. padding: 30px!important;
  603. overflow: auto;
  604. flex-grow: 2;
  605. }
  606. #SEpreviewBody .post-menu {
  607. display: none!important;
  608. }
  609. #SEpreviewBody > .question-status {
  610. margin: -10px -30px -30px;
  611. padding-left: 30px;
  612. }
  613. #SEpreviewBody > .question-status h2 {
  614. font-weight: normal;
  615. }
  616.  
  617. #SEpreviewBody::-webkit-scrollbar {
  618. background-color: rgba(${COLORS.question.backRGB}, 0.1);
  619. }
  620. #SEpreviewBody::-webkit-scrollbar-thumb {
  621. background-color: rgba(${COLORS.question.backRGB}, 0.2);
  622. }
  623. #SEpreviewBody::-webkit-scrollbar-thumb:hover {
  624. background-color: rgba(${COLORS.question.backRGB}, 0.3);
  625. }
  626. #SEpreviewBody::-webkit-scrollbar-thumb:active {
  627. background-color: rgba(${COLORS.question.backRGB}, 0.75);
  628. }
  629. /* answer */
  630. body[SEpreviewType="answer"] #SEpreviewTitle {
  631. background-color: rgba(${COLORS.answer.backRGB}, 0.37);
  632. color: ${COLORS.answer.fore};
  633. }
  634. body[SEpreviewType="answer"] #SEpreviewBody::-webkit-scrollbar {
  635. background-color: rgba(${COLORS.answer.backRGB}, 0.1);
  636. }
  637. body[SEpreviewType="answer"] #SEpreviewBody::-webkit-scrollbar-thumb {
  638. background-color: rgba(${COLORS.answer.backRGB}, 0.2);
  639. }
  640. body[SEpreviewType="answer"] #SEpreviewBody::-webkit-scrollbar-thumb:hover {
  641. background-color: rgba(${COLORS.answer.backRGB}, 0.3);
  642. }
  643. body[SEpreviewType="answer"] #SEpreviewBody::-webkit-scrollbar-thumb:active {
  644. background-color: rgba(${COLORS.answer.backRGB}, 0.75);
  645. }
  646. /* deleted */
  647. body[SEpreviewType="deleted"] #SEpreviewTitle {
  648. background-color: rgba(${COLORS.deleted.backRGB}, 0.37);
  649. color: ${COLORS.deleted.fore};
  650. }
  651. body[SEpreviewType="deleted"] #SEpreviewBody::-webkit-scrollbar {
  652. background-color: rgba(${COLORS.deleted.backRGB}, 0.1);
  653. }
  654. body[SEpreviewType="deleted"] #SEpreviewBody::-webkit-scrollbar-thumb {
  655. background-color: rgba(${COLORS.deleted.backRGB}, 0.2);
  656. }
  657. body[SEpreviewType="deleted"] #SEpreviewBody::-webkit-scrollbar-thumb:hover {
  658. background-color: rgba(${COLORS.deleted.backRGB}, 0.3);
  659. }
  660. body[SEpreviewType="deleted"] #SEpreviewBody::-webkit-scrollbar-thumb:active {
  661. background-color: rgba(${COLORS.deleted.backRGB}, 0.75);
  662. }
  663. /********/
  664. #SEpreviewAnswers {
  665. all: unset;
  666. display: block;
  667. padding: 10px 30px;
  668. font-weight: bold;
  669. font-size: 20px;
  670. line-height: 1.3;
  671. border-top: 4px solid rgba(${COLORS.answer.backRGB}, 0.37);
  672. background-color: rgba(${COLORS.answer.backRGB}, 0.37);
  673. color: ${COLORS.answer.fore};
  674. word-break: break-word;
  675. }
  676. #SEpreviewAnswers a {
  677. color: ${COLORS.answer.fore};
  678. padding: .25ex .75ex;
  679. text-decoration: none;
  680. }
  681. #SEpreviewAnswers a.deleted-answer {
  682. color: ${COLORS.deleted.fore};
  683. background: transparent;
  684. }
  685. #SEpreviewAnswers a:hover:not(.SEpreviewed) {
  686. text-decoration: underline;
  687. }
  688. #SEpreviewAnswers a.SEpreviewed {
  689. background-color: ${COLORS.answer.fore};
  690. color: ${COLORS.answer.foreInv};
  691. }
  692. /* deleted */
  693. body[SEpreviewType="deleted"] #SEpreviewAnswers {
  694. border-top-color: rgba(${COLORS.deleted.backRGB}, 0.37);
  695. background-color: rgba(${COLORS.deleted.backRGB}, 0.37);
  696. color: ${COLORS.deleted.fore};
  697. }
  698. body[SEpreviewType="deleted"] #SEpreviewAnswers a.SEpreviewed {
  699. background-color: ${COLORS.deleted.fore};
  700. color: ${COLORS.deleted.foreInv};
  701. }
  702. /********/
  703. .comments .new-comment-highlight {
  704. -webkit-animation: highlight 9s cubic-bezier(0,.8,.37,.88);
  705. -moz-animation: highlight 9s cubic-bezier(0,.8,.37,.88);
  706. animation: highlight 9s cubic-bezier(0,.8,.37,.88);
  707. }
  708.  
  709. @-webkit-keyframes highlight {
  710. from {background-color: #ffcf78}
  711. to {background-color: none}
  712. }
  713. `;
  714. }