// ==UserScript==
// @name SE Preview on hover
// @description Shows preview of the linked questions/answers on hover while Ctrl key is held
// @version 0.0.3
// @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/*
// @require https://greasyfork.org/scripts/12228/code/setMutationHandler.js
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @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 = 100;
const COLORS = {
question: {
back: '80, 133, 195',
fore: '#265184',
},
answer: {
back: '112, 195, 80',
fore: '#3f7722',
},
};
var xhr;
var preview;
var previewLink;
var previewTimer;
var previewCSScache = {};
var hovering = {stoppedAt: {x:0, y:0}};
const rx = getURLregexForMatchedSites();
const thisPageBaseUrl = (location.href.match(rx) || [])[0];
const thisPageBaseUrlShort = thisPageBaseUrl ? thisPageBaseUrl.replace('/questions/', '/q/') : undefined;
const stylesOverride = `<style>
body, html {
min-width: unset!important;
box-shadow: none!important;
}
html, body {
background: unset!important;;
}
body {
display: flex;
flex-direction: column;
height: 100%;
}
#SEpreviewTitle {
all: unset;
display: block;
padding: 20px 30px;
font-weight: bold;
font-size: 20px;
line-height: 1.3;
background-color: rgba(${COLORS.question.back}, 0.37);
color: ${COLORS.question.fore};
}
#SEpreviewTitle:hover {
text-decoration: underline;
}
#SEpreviewBody {
padding: 30px!important;
overflow: auto;
}
#SEpreviewBody::-webkit-scrollbar {
background-color: rgba(${COLORS.question.back}, 0.1);
}
#SEpreviewBody::-webkit-scrollbar-thumb {
background-color: rgba(${COLORS.question.back}, 0.2);
}
#SEpreviewBody::-webkit-scrollbar-thumb:hover {
background-color: rgba(${COLORS.question.back}, 0.3);
}
#SEpreviewBody::-webkit-scrollbar-thumb:active {
background-color: rgba(${COLORS.question.back}, 0.75);
}
body.SEpreviewIsAnswer #SEpreviewTitle {
background-color: rgba(${COLORS.answer.back}, 0.37);
color: ${COLORS.answer.fore};
}
body.SEpreviewIsAnswer #SEpreviewBody::-webkit-scrollbar {
background-color: rgba(${COLORS.answer.back}, 0.1);
}
body.SEpreviewIsAnswer #SEpreviewBody::-webkit-scrollbar-thumb {
background-color: rgba(${COLORS.answer.back}, 0.2);
}
body.SEpreviewIsAnswer #SEpreviewBody::-webkit-scrollbar-thumb:hover {
background-color: rgba(${COLORS.answer.back}, 0.3);
}
body.SEpreviewIsAnswer #SEpreviewBody::-webkit-scrollbar-thumb:active {
background-color: rgba(${COLORS.answer.back}, 0.75);
}
#SEpreviewAnswers {
all: unset;
display: block;
padding: 10px 30px;
font-weight: bold;
font-size: 20px;
line-height: 1.3;
border-top: 4px solid rgba(${COLORS.answer.back}, 0.37);
background-color: rgba(${COLORS.answer.back}, 0.37);
color: ${COLORS.answer.fore};
}
</style>`;
GM_addStyle(`
#SEpreview {
all: unset;
box-sizing: content-box;
width: 720px; /* 660px + 30px + 30px */
height: 33%;
min-height: 200px;
position: fixed;
transition: opacity .25s ease-in-out;
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: 8px solid rgb(${COLORS.question.back});
}
#SEpreview.SEpreviewIsAnswer {
border-color: rgb(${COLORS.answer.back});
}
`);
processExistingAndSetMutationHandler('a', onLinkAdded);
/**************************************************************/
function onLinkAdded(links) {
for (var i = 0, link; (link = links[i++]); ) {
if (rx.test(link.href) &&
!link.matches('.short-link') &&
!link.href.startsWith(thisPageBaseUrl) &&
!link.href.startsWith(thisPageBaseUrlShort)
) {
link.removeAttribute('title');
link.addEventListener('mouseover', onLinkHovered);
}
}
}
function onLinkHovered(e) {
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey)
return;
previewLink = this;
previewLink.addEventListener('mousemove', onLinkMouseMove);
previewLink.addEventListener('mouseout', abortPreview);
previewLink.addEventListener('mousedown', abortPreview);
restartPreviewTimer();
}
function onLinkMouseMove(e) {
if (Math.abs(hovering.stoppedAt.x - e.clientX) < 2 && Math.abs(hovering.stoppedAt.y - e.clientY) < 2)
return;
hovering.stoppedAt.x = e.clientX;
hovering.stoppedAt.y = e.clientY;
restartPreviewTimer();
}
function restartPreviewTimer() {
clearTimeout(previewTimer);
previewTimer = setTimeout(() => {
previewTimer = 0;
if (!previewLink.matches(':hover'))
return;
downloadPage();
}, PREVIEW_DELAY);
}
function abortPreview() {
previewLink.removeEventListener('mousemove', onLinkMouseMove);
previewLink.removeEventListener('mouseout', abortPreview);
previewLink.removeEventListener('mousedown', abortPreview);
clearTimeout(previewTimer);
previewTimer = setTimeout(() => {
previewTimer = 0;
if (preview && !preview.matches(':hover'))
hidePreview();
}, 500);
if (xhr)
xhr.abort();
}
function downloadPage() {
xhr = GM_xmlhttpRequest({
method: 'GET',
url: previewLink.href,
onload: showPreview,
});
}
function showPreview(data) {
var doc = new DOMParser().parseFromString(data.responseText, 'text/html');
if (!doc || !doc.head) {
console.error(GM_info.script.name, 'empty document received:', data);
return;
}
if (!$(doc, 'base'))
doc.head.insertAdjacentHTML('afterbegin', `<base href="${data.finalUrl}">`);
var answerIdMatch = data.finalUrl.match(/questions\/.+?\/(\d+)/);
var postId = answerIdMatch ? '#answer-' + answerIdMatch[1] : '#question';
var post = $(doc, postId + ' .post-text');
if (!post)
return;
var title = $(doc, 'meta[property="og:title"]').content;
var comments = $(doc, postId + ' .comments');
var answers; // = answerIdMatch ? null : $$(doc, '.answer');
$$remove(doc, 'script, .post-menu');
var externalsReady = [stylesOverride];
var stylesToGet = new Set();
var afterBodyHtml = '';
fetchExternals();
maybeRender();
function fetchExternals() {
var codeBlocks = $$(post, 'pre code');
if (codeBlocks.length) {
codeBlocks.forEach(e => e.parentElement.classList.add('prettyprint'));
externalsReady.push(
'<script> StackExchange = {}; </script>',
'<script src="https://cdn.sstatic.net/Js/prettify-full.en.js"></script>'
);
afterBodyHtml = '<script> prettyPrint(); </script>';
}
$$(doc, 'style, link[rel="stylesheet"]').forEach(e => {
if (e.localName == 'style')
externalsReady.push(e.outerHTML);
else if (e.href in previewCSScache)
externalsReady.push(previewCSScache[e.href]);
else {
stylesToGet.add(e.href);
GM_xmlhttpRequest({
method: 'GET',
url: e.href,
onload: data => {
externalsReady.push(previewCSScache[e.href] = '<style>' + data.responseText + '</style>');
stylesToGet.delete(e.href);
maybeRender();
},
});
}
});
}
function maybeRender() {
if (stylesToGet.size)
return;
if (!preview) {
preview = document.createElement('iframe');
preview.id = 'SEpreview';
preview.sandbox = 'allow-same-origin allow-scripts';
}
preview.classList.toggle('SEpreviewIsAnswer', !!answerIdMatch);
document.body.appendChild(preview);
var headHtml = externalsReady.join('');
var bodyHtml = [post.parentElement, comments].map(e => e ? e.outerHTML || e : '').join('');
var pvDoc = preview.contentDocument;
pvDoc.open();
pvDoc.write(`
<head>${headHtml}</head>
<body${answerIdMatch ? ' class="SEpreviewIsAnswer"' : ''}>
<a id="SEpreviewTitle" href="${data.finalUrl}">${title}</a>
<div id="SEpreviewBody">${bodyHtml}</div>
${!answers ? '' : '<div id="SEpreviewAnswers">' + answers.length + ' answer' + (answers.length > 1 ? 's' : '') + '</div>'}
${afterBodyHtml}
</body>`);
pvDoc.close();
pvDoc.addEventListener('mouseover', retainMainScrollPos);
if (answers)
$(pvDoc, '#SEpreviewAnswers').addEventListener('click', revealAnswers);
}
}
function retainMainScrollPos(e) {
var scrollPos = {x:scrollX, y:scrollY};
document.addEventListener('scroll', preventScroll);
document.addEventListener('mouseover', releaseScrollLock);
function preventScroll(e) {
scrollTo(scrollPos.x, scrollPos.y);
}
function releaseScrollLock(e) {
document.removeEventListener('mouseout', releaseScrollLock);
document.removeEventListener('scroll', preventScroll);
}
}
function hidePreview() {
preview.remove();
}
function getURLregexForMatchedSites() {
return new RegExp('https?://(\\w*\\.)*(' + GM_info.script.matches.map(m =>
m.match(/^.*?\/\/\W*(\w.*?)\//)[1].replace(/\./g, '\\.')
).join('|') + ')/(questions|q|a)/\\d+');
}
function $(node__optional, selector) {
// or $(selector) {
return (node__optional || document).querySelector(selector || node__optional);
}
function $$(node__optional, selector) {
// or $$(selector) {
return (node__optional || document).querySelectorAll(selector || node__optional);
}
function $$remove(node__optional, selector) {
// or $$remove(selector) {
(node__optional || document).querySelectorAll(selector || node__optional)
.forEach(e => e.remove());
}