您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows preview of the linked questions/answers on hover
当前为
// ==UserScript== // @name SE Preview on hover // @description Shows preview of the linked questions/answers on hover // @version 0.5.4 // @author wOxxOm // @namespace wOxxOm.scripts // @license MIT License // @match *://*.stackoverflow.com/* // @match *://*.superuser.com/* // @match *://*.serverfault.com/* // @match *://*.askubuntu.com/* // @match *://*.stackapps.com/* // @match *://*.mathoverflow.net/* // @match *://*.stackexchange.com/* // @include /https?:\/\/www\.?google(\.com?)?(\.\w\w)?\/(webhp|q|.*?[?#]q=|search).*/ // @match *://*.bing.com/* // @match *://*.yahoo.com/* // @match *://*.yahoo.co.jp/* // @match *://*.yahoo.cn/* // @include /https?:\/\/(\w+\.)*yahoo.(com|\w\w(\.\w\w)?)\/.*/ // @require https://greatest.deepsurf.us/scripts/12228/code/setMutationHandler.js // @require https://greatest.deepsurf.us/scripts/27531/code/LZString-2xspeedup.js // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @connect stackoverflow.com // @connect superuser.com // @connect serverfault.com // @connect askubuntu.com // @connect stackapps.com // @connect mathoverflow.net // @connect stackexchange.com // @connect cdn.sstatic.net // @run-at document-end // @noframes // ==/UserScript== /* jshint lastsemic:true, multistr:true, laxbreak:true, -W030, -W041, -W084 */ const PREVIEW_DELAY = 200; const BUSY_CURSOR_DELAY = 1000; const CACHE_DURATION = 1 * 60 * 1000; // 1 minute for the recently active posts, scales up logarithmically const MIN_HEIGHT = 400; // px const COLORS = { question: { backRGB: '80, 133, 195', fore: '#265184', }, answer: { backRGB: '112, 195, 80', fore: '#3f7722', foreInv: 'white', }, deleted: { backRGB: '181, 103, 103', fore: 'rgb(181, 103, 103)', foreInv: 'white', }, closed: { backRGB: '255, 206, 93', fore: 'rgb(194, 136, 0)', foreInv: 'white', }, }; let xhr; const xhrNoSSL = new Set(); const preview = { frame: null, link: null, hover: {x:0, y:0}, timer: 0, timerCursor: 0, stylesOverride: '', }; const lockScroll = {}; const {full: rxPreviewable, siteOnly: rxPreviewableSite} = getURLregexForMatchedSites(); const thisPageUrls = getPageBaseUrls(location.href); initStyles(); initPolyfills(); setMutationHandler('a', onLinkAdded, {processExisting: true}); setTimeout(cleanupCache, 10000); /**************************************************************/ function onLinkAdded(links) { for (let i = 0, link; (link = links[i++]); ) { if (isLinkPreviewable(link)) { link.removeAttribute('title'); $on('mouseover', link, onLinkHovered); } } } function onLinkHovered(e) { if (hasKeyModifiers(e)) return; preview.link = this; $on('mousemove', this, onLinkMouseMove); $on('mouseout', this, abortPreview); $on('mousedown', this, abortPreview); restartPreviewTimer(this); } function onLinkMouseMove(e) { let stoppedMoving = Math.abs(preview.hover.x - e.clientX) < 2 && Math.abs(preview.hover.y - e.clientY) < 2; if (!stoppedMoving) return; preview.hover.x = e.clientX; preview.hover.y = e.clientY; restartPreviewTimer(this); } function restartPreviewTimer(link) { clearTimeout(preview.timer); preview.timer = setTimeout(() => { preview.timer = 0; if (!link.matches(':hover')) return releaseLinkListeners(link); $off('mousemove', link, onLinkMouseMove); downloadPreview(link); }, PREVIEW_DELAY); } function abortPreview(e) { releaseLinkListeners(this); preview.timer = setTimeout(link => { if (link == preview.link && preview.frame && !preview.frame.matches(':hover')) preview.frame.contentWindow.postMessage('SEpreview-hidden', '*'); }, PREVIEW_DELAY * 3, this); if (xhr) xhr.abort(); if (this.style.cursor == 'wait') this.style.cursor = ''; } function releaseLinkListeners(link = preview.link) { $off('mousemove', link, onLinkMouseMove); $off('mouseout', link, abortPreview); $off('mousedown', link, abortPreview); stopTimers(); } function stopTimers(names) { for (let k in preview) { if (k.startsWith('timer') && preview[k]) { clearTimeout(preview[k]); preview[k] = 0; } } } function fadeOut(element, transition) { return new Promise(resolve => { if (transition) { element.style.transition = typeof transition == 'number' ? `opacity ${transition}s ease-in-out` : transition; setTimeout(doFadeOut); } else doFadeOut(); function doFadeOut() { element.style.opacity = '0'; $on('transitionend', element, done); $on('visibilitychange', done); function done(e) { $off('transitionend', element, done); $off('visibilitychange', done); if (element.style.opacity == '0') element.style.display = 'none'; resolve(); } } }); } function fadeIn(element) { element.style.opacity = '0'; element.style.display = 'block'; setTimeout(() => element.style.opacity = '1'); } function downloadPreview(link) { const cached = readCache(link.href); if (cached) return showPreview(cached); preview.timerCursor = setTimeout(() => { preview.timerCursor = 0; link.style.cursor = 'wait'; }, BUSY_CURSOR_DELAY); doXHR({url: httpsUrl(link.href)}).then(r => { const html = r.responseText; const finalUrl = r.finalUrl; if (link.matches(':hover') || preview.frame && preview.frame.matches(':hover')) return { html, finalUrl, lastActivity: showPreview({finalUrl, html}), }; }).then(({html, finalUrl, lastActivity} = {}) => { if (preview.timerCursor) clearTimeout(preview.timerCursor), preview.timerCursor = 0; if (link.style.cursor == 'wait') link.style.cursor = ''; if (lastActivity) { const inactiveDays = Math.max(0, (Date.now() - lastActivity) / (24 * 3600 * 1000)); const cacheDuration = CACHE_DURATION * Math.pow(Math.log(inactiveDays + 1) + 1, 2); setTimeout(writeCache, 1000, {url: link.href, finalUrl, html, cacheDuration}); } }); } function initPreview() { preview.frame = document.createElement('iframe'); preview.frame.id = 'SEpreview'; document.body.appendChild(preview.frame); makeResizable(); lockScroll.attach = e => { if (lockScroll.pos) return; lockScroll.pos = {x: scrollX, y: scrollY}; $on('scroll', document, lockScroll.run); $on('mouseover', document, lockScroll.detach); }; lockScroll.run = e => scrollTo(lockScroll.pos.x, lockScroll.pos.y); lockScroll.detach = e => { if (!lockScroll.pos) return; lockScroll.pos = null; $off('mouseover', document, lockScroll.detach); $off('scroll', document, lockScroll.run); }; const killer = mutations => mutations.forEach(m => [...m.addedNodes].forEach(n => n.remove())); const killerMO = { head: new MutationObserver(killer), documentElement: new MutationObserver(killer), }; preview.killInvaders = { start: () => Object.keys(killerMO).forEach(k => killerMO[k].observe(preview.frame.contentDocument[k], {childList: true})), stop: () => Object.keys(killerMO).forEach(k => killerMO[k].disconnect()), }; } function showPreview({finalUrl, html, doc}) { doc = doc || new DOMParser().parseFromString(html, 'text/html'); if (!doc || !doc.head) return error('no HEAD in the document received for', finalUrl); if (!$('base', doc)) doc.head.insertAdjacentHTML('afterbegin', `<base href="${finalUrl}">`); const answerIdMatch = finalUrl.match(/questions\/\d+\/[^\/]+\/(\d+)/); const isQuestion = !answerIdMatch; const postId = answerIdMatch ? '#answer-' + answerIdMatch[1] : '#question'; const post = $(postId + ' .post-text', doc); if (!post) return error('No parsable post found', doc); const isDeleted = !!post.closest('.deleted-answer'); const title = $('meta[property="og:title"]', doc).content; const status = isQuestion && !$('.question-status', post) ? $('.question-status', doc) : null; const isClosed = $('.question-originals-of-duplicate, .close-as-off-topic-status-list, .close-status-suffix', doc); const comments = $(`${postId} .comments`, doc); const commentsHidden = +$('tbody', comments).dataset.remainingCommentsCount; const commentsShowLink = commentsHidden && $(`${postId} .js-show-link.comments-link`, doc); const finalUrlOfQuestion = getCacheableUrl(finalUrl); const lastActivity = tryCatch(() => new Date($('.lastactivity-link', doc).title).getTime()) || Date.now(); const answers = $$('.answer', doc); const hasAnswers = answers.length > (isQuestion ? 0 : 1); markPreviewableLinks(doc); $$remove('script', doc); if (!preview.frame) initPreview(); let pvDoc, pvWin; preview.frame.style.display = ''; preview.frame.setAttribute('SEpreview-type', isDeleted ? 'deleted' : isQuestion ? (isClosed ? 'closed' : 'question') : 'answer'); preview.frame.classList.toggle('SEpreview-hasAnswers', hasAnswers); return onFrameReady(preview.frame).then( () => { pvDoc = preview.frame.contentDocument; pvWin = preview.frame.contentWindow; initPolyfills(pvWin); preview.killInvaders.stop(); }) .then(addStyles) .then(render) .then(show) .then(() => lastActivity); function markPreviewableLinks(container) { for (let link of $$('a:not(.SEpreviewable)', container)) { if (rxPreviewable.test(link.href)) { link.removeAttribute('title'); link.classList.add('SEpreviewable'); } } } function markHoverableUsers(container) { for (let link of $$('a[href*="/users/"]', container)) { if (rxPreviewableSite.test(link.href) && link.pathname.match(/^\/users\/\d+/)) { link.onmouseover = loadUserCard; link.classList.add('SEpreview-userLink'); } } } function addStyles() { const SEpreviewStyles = $replaceOrCreate({ id: 'SEpreviewStyles', tag: 'style', parent: pvDoc.head, className: 'SEpreview-reuse', innerHTML: preview.stylesOverride, }); $replaceOrCreate($$('style', doc).map(e => ({ id: 'SEpreview' + e.innerHTML.replace(/\W+/g, '').length, tag: 'style', before: SEpreviewStyles, className: 'SEpreview-reuse', innerHTML: e.innerHTML, }))); return onStyleSheetsReady({ doc: pvDoc, urls: $$('link[rel="stylesheet"]', doc).map(e => e.href), onBeforeRequest: preview.frame.style.opacity != '1' ? null : () => { preview.frame.style.transition = 'border-color .5s ease-in-out'; $on('transitionend', preview.frame, () => preview.frame.style.transition = '', {once: true}); }, }).then(els => { els.forEach(e => e.className = 'SEpreview-reuse'); }); } function render() { pvDoc.body.setAttribute('SEpreview-type', preview.frame.getAttribute('SEpreview-type')); $replaceOrCreate([{ // base id: 'SEpreview-base', tag: 'base', parent: pvDoc.head, href: $('base', doc).href, }, { // title id: 'SEpreview-title', tag: 'a', parent: pvDoc.body, className: 'SEpreviewable', href: finalUrlOfQuestion, textContent: title, }, { // close button id: 'SEpreview-close', parent: pvDoc.body, title: 'Or press Esc key while the preview is focused (also when just shown)', }, { // vote count, date, views# id: 'SEpreview-meta', parent: pvDoc.body, innerHTML: [ $text('.vote-count-post', post.closest('table')).replace(/(-?)(\d+)/, (s, sign, v) => s == '0' ? '' : `<b>${s}</b> vote${+v > 1 ? 's' : ''}, `), isQuestion ? $$('#qinfo tr', doc) .map(row => $$('.label-key', row).map($text).join(' ')) .join(', ').replace(/^((.+?) (.+?), .+?), .+? \3$/, '$1') : [...$$('.user-action-time', post.closest('.answer'))] .reverse().map($text).join(', ') ].join('') }, { // content wrapper id: 'SEpreview-body', parent: pvDoc.body, className: isDeleted ? 'deleted-answer' : '', children: [status, post.parentElement, comments, commentsShowLink], }]); // delinkify/remove non-functional items in post-menu $$remove('.short-link, .flag-post-link', pvDoc); $$('.post-menu a:not(.edit-post)', pvDoc).forEach(a => { if (a.children.length) a.outerHTML = `<span>${a.innerHTML}</span>`; else a.remove(); }); // add a timeline link if (isQuestion) $('.post-menu', pvDoc).insertAdjacentHTML('beforeend', '<span class="lsep">|</span>' + `<a href="/posts/${new URL(finalUrl).pathname.match(/\d+/)[0]}/timeline">timeline</a>`); // prettify code blocks const codeBlocks = $$('pre code', pvDoc); if (codeBlocks.length) { codeBlocks.forEach(e => e.parentElement.classList.add('prettyprint')); if (!pvWin.StackExchange) { pvWin.StackExchange = {}; let script = $scriptIn(pvDoc.head); script.text = 'StackExchange = {}'; script = $scriptIn(pvDoc.head); script.src = 'https://cdn.sstatic.net/Js/prettify-full.en.js'; script.setAttribute('onload', 'prettyPrint()'); } else $scriptIn(pvDoc.body).text = 'prettyPrint()'; } // render bottom shelf if (hasAnswers) { $replaceOrCreate({ id: 'SEpreview-answers', parent: pvDoc.body, innerHTML: answers.map(renderShelfAnswer).join(' '), }); } else $$remove('#SEpreview-answers', pvDoc); // cleanup leftovers from previously displayed post and foreign elements not injected by us $$('style, link, body script, html > *:not(head):not(body), .post-menu .lsep + .lsep', pvDoc).forEach(e => { if (e.classList.contains('SEpreview-reuse')) e.classList.remove('SEpreview-reuse'); else e.remove(); }); } function renderShelfAnswer(e) { const shortUrl = $('.short-link', e).href.replace(/(\d+)\/\d+/, '$1'); const extraClasses = (e.matches(postId) ? ' SEpreviewed' : '') + (e.matches('.deleted-answer') ? ' deleted-answer' : '') + ($('.vote-accepted-on', e) ? ' SEpreview-accepted' : ''); const author = $('.post-signature:last-child', e); const title = $text('.user-details a', author) + ' (rep ' + $text('.reputation-score', author) + ')\n' + $text('.user-action-time', author); const gravatar = $('img, .anonymous-gravatar, .community-wiki', author); return ( `<a href="${shortUrl}" title="${title}" class="SEpreviewable${extraClasses}">` + $text('.vote-count-post', e).replace(/^0$/, ' ') + ' ' + (!gravatar ? '' : gravatar.src ? `<img src="${gravatar.src}">` : gravatar.outerHTML) + '</a>'); } function show() { pvDoc.onmouseover = lockScroll.attach; pvDoc.onclick = onClick; pvDoc.onkeydown = e => { if (!hasKeyModifiers(e) && e.keyCode == 27) hide() }; pvWin.onmessage = e => { if (e.data == 'SEpreview-hidden') hide({fade: true}) }; markHoverableUsers(pvDoc); preview.killInvaders.start(); $('#SEpreview-body', pvDoc).scrollTop = 0; preview.frame.style.opacity = '1'; preview.frame.focus(); } function hide({fade = false} = {}) { releaseLinkListeners(); releasePreviewListeners(); const cleanup = () => preview.frame.style.opacity == '0' && $removeChildren(pvDoc.body); if (fade) fadeOut(preview.frame).then(cleanup); else { preview.frame.style.opacity = '0'; preview.frame.style.display = 'none'; cleanup(); } } function releasePreviewListeners(e) { pvWin.onmessage = null; pvDoc.onmouseover = null; pvDoc.onclick = null; pvDoc.onkeydown = null; } function onClick(e) { if (e.target.id == 'SEpreview-close') return hide(); const link = e.target.closest('a'); if (!link) return; if (link.matches('.js-show-link.comments-link')) { fadeOut(link, 0.5); loadComments(); return e.preventDefault(); } if (e.button || hasKeyModifiers(e) || !link.matches('.SEpreviewable')) return (link.target = '_blank'); e.preventDefault(); if (link.id == 'SEpreview-title') showPreview({doc, finalUrl: finalUrlOfQuestion}); else if (link.matches('#SEpreview-answers a')) showPreview({doc, finalUrl: finalUrlOfQuestion + '/' + link.pathname.match(/\/(\d+)/)[1]}); else downloadPreview(link); } function loadComments() { const url = new URL(finalUrl).origin + '/posts/' + comments.id.match(/\d+/)[0] + '/comments'; doXHR({url}).then(r => { const tbody = $(`#${comments.id} tbody`, pvDoc); const oldIds = new Set([...tbody.rows].map(e => e.id)); tbody.innerHTML = r.responseText; tbody.closest('.comments').style.display = 'block'; for (let tr of tbody.rows) if (!oldIds.has(tr.id)) tr.classList.add('new-comment-highlight'); markPreviewableLinks(tbody); markHoverableUsers(tbody); }); } function loadUserCard(e, ready) { if (ready !== true) return setTimeout(loadUserCard, PREVIEW_DELAY * 2, e, true); const link = e.target.closest('a'); if (!link.matches(':hover')) return; let timer; let userCard = link.nextElementSibling; if (userCard && userCard.matches('.SEpreview-userCard')) return fadeInUserCard(); const url = link.origin + '/users/user-info/' + link.pathname.match(/\d+/)[0]; Promise.resolve( readCache(url) || doXHR({url}).then(r => { writeCache({url, html: r.responseText, cacheDuration: CACHE_DURATION * 100}); return {html: r.responseText}; }) ).then(renderUserCard); function renderUserCard({html}) { const linkBounds = link.getBoundingClientRect(); const wrapperBounds = $('#SEpreview-body', pvDoc).getBoundingClientRect(); userCard = $replaceOrCreate({id: 'user-menu-tmp', className: 'SEpreview-userCard', innerHTML: html, after: link}); userCard.style.left = Math.min(linkBounds.left - 20, pvWin.innerWidth - 350) + 'px'; if (linkBounds.bottom + 100 > wrapperBounds.bottom) userCard.style.marginTop = '-5rem'; userCard.onmouseout = e => { if (e.target != userCard || userCard.contains(e.relatedTarget)) if (e.relatedTarget) // null if mouse is outside the preview return; fadeOut(userCard); clearTimeout(timer); timer = 0; }; fadeInUserCard(); } function fadeInUserCard() { if (userCard.id != 'user-menu') { $$('#user-menu', pvDoc).forEach(e => e.id = e.style.display = '' ); userCard.id = 'user-menu'; } userCard.style.opacity = '0'; userCard.style.display = 'block'; timer = setTimeout(() => timer && (userCard.style.opacity = '1')); } } } function getCacheableUrl(url) { // strips queries and hashes and anything after the main part https://site/questions/####/title/ return url .replace(/(\/q(?:uestions)?\/\d+\/[^\/]+).*/, '$1') .replace(/(\/a(?:nswers)?\/\d+).*/, '$1') .replace(/[?#].*$/, ''); } function readCache(url) { const keyUrl = getCacheableUrl(url); const meta = (localStorage[keyUrl] || '').split('\t'); const expired = +meta[0] < Date.now(); const finalUrl = meta[1] || url; const keyFinalUrl = meta[1] ? getCacheableUrl(finalUrl) : keyUrl; return !expired && { finalUrl, html: LZString.decompressFromUTF16(localStorage[keyFinalUrl + '\thtml']), }; } function writeCache({url, finalUrl, html, cacheDuration = CACHE_DURATION, cleanupRetry}) { // keyUrl=expires // redirected keyUrl=expires+finalUrl, and an additional entry keyFinalUrl=expires is created // keyFinalUrl\thtml=html cacheDuration = Math.max(CACHE_DURATION, Math.min(0xDEADBEEF, Math.floor(cacheDuration))); finalUrl = (finalUrl || url).replace(/[?#].*/, ''); const keyUrl = getCacheableUrl(url); const keyFinalUrl = getCacheableUrl(finalUrl); const expires = Date.now() + cacheDuration; const lz = LZString.compressToUTF16(html); if (!tryCatch(() => localStorage[keyFinalUrl + '\thtml'] = lz)) { if (cleanupRetry) return error('localStorage write error'); cleanupCache({aggressive: true}); setIimeout(writeCache, 0, {url, finalUrl, html, cacheDuration, cleanupRetry: true}); } localStorage[keyFinalUrl] = expires; if (keyUrl != keyFinalUrl) localStorage[keyUrl] = expires + '\t' + finalUrl; setTimeout(() => { [keyUrl, keyFinalUrl, keyFinalUrl + '\thtml'].forEach(e => localStorage.removeItem(e)); }, cacheDuration + 1000); } function cleanupCache({aggressive = false} = {}) { Object.keys(localStorage).forEach(k => { if (k.match(/^https?:\/\/[^\t]+$/)) { let meta = (localStorage[k] || '').split('\t'); if (+meta[0] > Date.now() && !aggressive) return; if (meta[1]) localStorage.removeItem(meta[1]); localStorage.removeItem(`${meta[1] || k}\thtml`); localStorage.removeItem(k); } }); } function onFrameReady(frame) { if (frame.contentDocument.readyState == 'complete') return Promise.resolve(); else return new Promise(resolve => { $on('load', frame, function onLoad() { $off('load', frame, onLoad); resolve(); }); }); } function onStyleSheetsReady({urls, doc = document, onBeforeRequest = null}) { return Promise.all( urls.map(url => $(`link[href="${url}"]`, doc) || new Promise(resolve => { if (typeof onBeforeRequest == 'function') onBeforeRequest(url); doXHR({url}).then(() => { const sheetElement = $replaceOrCreate({tag: 'link', href: url, rel: 'stylesheet', parent: doc.head}); const timeout = setTimeout(doResolve, 100); sheetElement.onload = doResolve; function doResolve() { sheetElement.onload = null; clearTimeout(timeout); resolve(sheetElement); } }); })) ); } function getURLregexForMatchedSites() { const sites = 'https?://(\\w*\\.)*(' + GM_info.script.matches.map( m => m.match(/^.*?\/\/\W*(\w.*?)\//)[1].replace(/\./g, '\\.')).join('|') + ')/'; return { full: new RegExp(sites + '(questions|q|a|posts\/comments)/\\d+'), siteOnly: new RegExp(sites), }; } function isLinkPreviewable(link) { if (!rxPreviewable.test(link.href) || link.matches('.short-link')) return false; const inPreview = preview.frame && link.ownerDocument == preview.frame.contentDocument; const pageUrls = inPreview ? getPageBaseUrls(preview.link.href) : thisPageUrls; const url = httpsUrl(link.href); return url.indexOf(pageUrls.base) && url.indexOf(pageUrls.short); } function getPageBaseUrls(url) { const base = httpsUrl((url.match(rxPreviewable) || [])[0]); return base ? { base, short: base.replace('/questions/', '/q/'), } : {}; } function httpsUrl(url) { return (url || '').replace(/^http:/, 'https:'); } function doXHR(options) { options = Object.assign({method: 'GET'}, options); const useHttpUrl = () => options.url = options.url.replace(/^https/, 'http'); const hostname = new URL(options.url).hostname; if (xhrNoSSL.has(hostname)) useHttpUrl(); else if (options.url.startsWith('https')) { options.onerror = e => { useHttpUrl(); xhrNoSSL.add(hostname); xhr = GM_xmlhttpRequest(options); }; } if (options.onload) return (xhr = GM_xmlhttpRequest(options)); else return new Promise(resolve => { xhr = GM_xmlhttpRequest(Object.assign(options, {onload: resolve})); }); } function makeResizable() { let heightOnClick; const pvDoc = preview.frame.contentDocument; const topBorderHeight = (preview.frame.offsetHeight - preview.frame.clientHeight) / 2; setHeight(GM_getValue('height', innerHeight / 3) |0); // mouseover in the main page is fired only on the border of the iframe $on('mouseover', preview.frame, onOverAttach); $on('message', preview.frame.contentWindow, e => { if (e.data != 'SEpreview-hidden') return; if (heightOnClick) { releaseResizeListeners(); setHeight(heightOnClick); } if (preview.frame.style.cursor) onOutDetach(); }); function setCursorStyle(e) { return (preview.frame.style.cursor = e.offsetY <= 0 ? 's-resize' : ''); } function onOverAttach(e) { setCursorStyle(e); $on('mouseout', preview.frame, onOutDetach); $on('mousemove', preview.frame, setCursorStyle); $on('mousedown', onDownStartResize); } function onOutDetach(e) { if (!e || !e.relatedTarget || !pvDoc.contains(e.relatedTarget)) { $off('mouseout', preview.frame, onOutDetach); $off('mousemove', preview.frame, setCursorStyle); $off('mousedown', onDownStartResize); preview.frame.style.cursor = ''; } } function onDownStartResize(e) { if (!preview.frame.style.cursor) return; heightOnClick = preview.frame.clientHeight; $off('mouseover', preview.frame, onOverAttach); $off('mousemove', preview.frame, setCursorStyle); $off('mouseout', preview.frame, onOutDetach); document.documentElement.style.cursor = 's-resize'; document.body.style.cssText += ';pointer-events: none!important'; $on('mousemove', onMoveResize); $on('mouseup', onUpConfirm); } function onMoveResize(e) { setHeight(innerHeight - topBorderHeight - e.clientY); getSelection().removeAllRanges(); preview.frame.contentWindow.getSelection().removeAllRanges(); } function onUpConfirm(e) { GM_setValue('height', pvDoc.body.clientHeight); releaseResizeListeners(e); } function releaseResizeListeners() { $off('mouseup', releaseResizeListeners); $off('mousemove', onMoveResize); $on('mouseover', preview.frame, onOverAttach); onOverAttach({}); document.body.style.pointerEvents = ''; document.documentElement.style.cursor = ''; heightOnClick = 0; } } function setHeight(height) { const currentHeight = preview.frame.clientHeight; const borderHeight = preview.frame.offsetHeight - currentHeight; const newHeight = Math.max(MIN_HEIGHT, Math.min(innerHeight - borderHeight, height)); if (newHeight != currentHeight) preview.frame.style.height = newHeight + 'px'; } function $(selector, node = document) { return node.querySelector(selector); } function $$(selector, node = document) { return node.querySelectorAll(selector); } function $text(selector, node = document) { const e = typeof selector == 'string' ? node.querySelector(selector) : selector; return e ? e.textContent.trim() : ''; } function $$remove(selector, node = document) { node.querySelectorAll(selector).forEach(e => e.remove()); } function $appendChildren(newParent, elements) { const doc = newParent.ownerDocument; const fragment = doc.createDocumentFragment(); for (let e of elements) if (e) fragment.appendChild(e.ownerDocument == doc ? e : doc.importNode(e, true)); newParent.appendChild(fragment); } function $removeChildren(el) { if (el.children.length) el.innerHTML = ''; // the fastest as per https://jsperf.com/innerhtml-vs-removechild/256 } function $replaceOrCreate(options) { if (typeof options.map == 'function') return options.map($replaceOrCreate); const doc = (options.parent || options.before || options.after).ownerDocument; const el = doc.getElementById(options.id) || doc.createElement(options.tag || 'div'); for (let key of Object.keys(options)) { const value = options[key]; switch (key) { case 'tag': case 'parent': case 'before': case 'after': break; case 'dataset': for (let dataAttr of Object.keys(value)) if (el.dataset[dataAttr] != value[dataAttr]) el.dataset[dataAttr] = value[dataAttr]; break; case 'children': $removeChildren(el); $appendChildren(el, options[key]); break; default: if (key in el && el[key] != value) el[key] = value; } } if (!el.parentElement) (options.parent || (options.before || options.after).parentElement) .insertBefore(el, options.before || (options.after && options.after.nextElementSibling)); return el; } function $scriptIn(element) { return element.appendChild(element.ownerDocument.createElement('script')); } function $on(eventName, ...args) { // eventName, selector, node, callback, options // eventName, selector, callback, options // eventName, node, callback, options // eventName, callback, options let i = 0; const selector = typeof args[i] == 'string' ? args[i++] : null; const node = args[i].nodeType ? args[i++] : document; const callback = args[i++]; const options = args[i]; const actualNode = selector ? node.querySelector(selector) : node; const method = this == 'removeEventListener' ? this : 'addEventListener'; actualNode[method](eventName, callback, options); } function $off() { $on.apply('removeEventListener', arguments); } function hasKeyModifiers(e) { return e.ctrlKey || e.altKey || e.shiftKey || e.metaKey; } function log(...args) { console.log(GM_info.script.name, ...args); } function error(...args) { console.error(GM_info.script.name, ...args); console.trace(); } function tryCatch(fn) { try { return fn() } catch(e) {} } function initPolyfills(context = window) { for (let method of ['forEach', 'filter', 'map', 'every', 'some', context.Symbol.iterator]) if (!context.NodeList.prototype[method]) context.NodeList.prototype[method] = context.Array.prototype[method]; } function initStyles() { GM_addStyle(` #SEpreview { all: unset; box-sizing: content-box; width: 720px; /* 660px + 30px + 30px */ height: 33%; min-height: ${MIN_HEIGHT}px; position: fixed; opacity: 0; transition: opacity .25s cubic-bezier(.88,.02,.92,.66); right: 0; bottom: 0; padding: 0; margin: 0; background: white; box-shadow: 0 0 100px rgba(0,0,0,0.5); z-index: 999999; border-width: 8px; border-style: solid; border-color: transparent; } #SEpreview[SEpreview-type="question"].SEpreview-hasAnswers { border-image: linear-gradient(rgb(${COLORS.question.backRGB}) 66%, rgb(${COLORS.answer.backRGB})) 1 1; } ` + Object.keys(COLORS).map(s => ` #SEpreview[SEpreview-type="${s}"] { border-color: rgb(${COLORS[s].backRGB}); } `).join('') ); preview.stylesOverride = ` html, body { min-width: unset!important; box-shadow: none!important; padding: 0!important; margin: 0!important; background: unset!important;; } body { display: flex; flex-direction: column; height: 100vh; } #SEpreview-body a.SEpreviewable { text-decoration: underline !important; text-decoration-skip: ink; } #SEpreview-title { all: unset; display: block; padding: 20px 30px; font-weight: bold; font-size: 18px; line-height: 1.2; cursor: pointer; } #SEpreview-title:hover { text-decoration: underline; text-decoration-skip: ink; } #SEpreview-meta { position: absolute; top: .5ex; left: 30px; opacity: 0.5; } #SEpreview-title:hover + #SEpreview-meta { opacity: 1.0; } #SEpreview-close { position: absolute; top: 0; right: 0; flex: none; cursor: pointer; padding: .5ex 1ex; } #SEpreview-close:after { content: "x"; } #SEpreview-close:active { background-color: rgba(0,0,0,.1); } #SEpreview-close:hover { background-color: rgba(0,0,0,.05); } #SEpreview-body { position: relative; padding: 30px!important; overflow: auto; flex-grow: 2; } #SEpreview-body > .question-status { margin: -30px -30px 30px; padding-left: 30px; } #SEpreview-body .question-originals-of-duplicate { margin: -30px -30px 30px; padding: 15px 30px; } #SEpreview-body > .question-status h2 { font-weight: normal; } #SEpreview-answers { all: unset; display: block; padding: 10px 10px 10px 30px; font-weight: bold; line-height: 1.0; border-top: 4px solid rgba(${COLORS.answer.backRGB}, 0.37); background-color: rgba(${COLORS.answer.backRGB}, 0.37); color: ${COLORS.answer.fore}; word-break: break-word; } #SEpreview-answers:before { content: "Answers:"; margin-right: 1ex; font-size: 20px; line-height: 48px; } #SEpreview-answers a { color: ${COLORS.answer.fore}; text-decoration: none; font-size: 11px; font-family: monospace; width: 32px; display: inline-block; vertical-align: top; margin: 0 1ex 1ex 0; } #SEpreview-answers img { width: 32px; height: 32px; } .SEpreview-accepted { position: relative; } .SEpreview-accepted:after { content: "✔"; position: absolute; display: block; top: 1.3ex; right: -0.7ex; font-size: 32px; color: #4bff2c; text-shadow: 1px 2px 2px rgba(0,0,0,0.5); } #SEpreview-answers a.deleted-answer { color: ${COLORS.deleted.fore}; background: transparent; opacity: 0.25; } #SEpreview-answers a.deleted-answer:hover { opacity: 1.0; } #SEpreview-answers a:hover:not(.SEpreviewed) { text-decoration: underline; text-decoration-skip: ink; } #SEpreview-answers a.SEpreviewed { background-color: ${COLORS.answer.fore}; color: ${COLORS.answer.foreInv}; position: relative; } #SEpreview-answers a.SEpreviewed:before { display: block; content: " "; position: absolute; left: -4px; top: -4px; right: -4px; bottom: -4px; border: 4px solid ${COLORS.answer.fore}; } #SEpreview-body .comment-edit, #SEpreview-body .delete-tag, #SEpreview-body .comment-actions td:last-child { display: none; } #SEpreview-body .comments { border-top: none; } #SEpreview-body .comments tr:last-child td { border-bottom: none; } #SEpreview-body .comments .new-comment-highlight { -webkit-animation: highlight 9s cubic-bezier(0,.8,.37,.88); -moz-animation: highlight 9s cubic-bezier(0,.8,.37,.88); animation: highlight 9s cubic-bezier(0,.8,.37,.88); } #SEpreview-body .post-menu > span { opacity: .35; } #SEpreview-body #user-menu { position: absolute; } .SEpreview-userCard { position: absolute; display: none; transition: opacity .25s cubic-bezier(.88,.02,.92,.66) .5s; margin-top: -3rem; } #SEpreview-body .wmd-preview a:not(.post-tag), #SEpreview-body .post-text a:not(.post-tag), #SEpreview-body .comment-copy a:not(.post-tag) { border-bottom: none; } @-webkit-keyframes highlight { from {background-color: #ffcf78} to {background-color: none} } ` + Object.keys(COLORS).map(s => ` body[SEpreview-type="${s}"] #SEpreview-title { background-color: rgba(${COLORS[s].backRGB}, 0.37); color: ${COLORS[s].fore}; } body[SEpreview-type="${s}"] #SEpreview-body::-webkit-scrollbar { background-color: rgba(${COLORS[s].backRGB}, 0.1); } body[SEpreview-type="${s}"] #SEpreview-body::-webkit-scrollbar-thumb { background-color: rgba(${COLORS[s].backRGB}, 0.2); } body[SEpreview-type="${s}"] #SEpreview-body::-webkit-scrollbar-thumb:hover { background-color: rgba(${COLORS[s].backRGB}, 0.3); } body[SEpreview-type="${s}"] #SEpreview-body::-webkit-scrollbar-thumb:active { background-color: rgba(${COLORS[s].backRGB}, 0.75); } `).join('') + ['deleted', 'closed'].map(s => ` body[SEpreview-type="${s}"] #SEpreview-answers { border-top-color: rgba(${COLORS[s].backRGB}, 0.37); background-color: rgba(${COLORS[s].backRGB}, 0.37); color: ${COLORS[s].fore}; } body[SEpreview-type="${s}"] #SEpreview-answers a.SEpreviewed { background-color: ${COLORS[s].fore}; color: ${COLORS[s].foreInv}; } body[SEpreview-type="${s}"] #SEpreview-answers a.SEpreviewed:after { border-color: ${COLORS[s].fore}; } `).join(''); }