// ==UserScript==
// @name SE Preview on hover
// @description Shows preview of the linked questions/answers on hover
// @version 0.5.9
// @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://greasyfork.org/scripts/12228/code/setMutationHandler.js
// @require https://greasyfork.org/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, .question-summary .answered, .question-summary .answered-accepted', onLinkAdded, {processExisting: true});
setTimeout(cleanupCache, 10000);
/**************************************************************/
function onLinkAdded(links) {
for (let i = 0, link; (link = links[i++]); ) {
if (link.localName != 'a' || 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);
if (link.localName != 'a')
link.href = $('a', link.closest('.question-summary')).href;
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 showAnswers = link.localName != 'a';
const cached = readCache(link.href);
if (cached)
return showPreview(Object.assign(cached, {showAnswers}));
preview.timerCursor = setTimeout(() => {
preview.timerCursor = 0;
link.style.cursor = 'wait';
}, BUSY_CURSOR_DELAY);
doXHR(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, showAnswers}),
};
}).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, showAnswers}) {
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 = !showAnswers
? finalUrl.match(/questions\/\d+\/[^\/]+\/(\d+)/)
: [0, ($('[id^="answer-"]', doc) || {id:''}).id.replace(/^answer-/, '')];
const isQuestion = !answerIdMatch;
const postNumber = isQuestion ? finalUrl.match(/\d+/)[0] : answerIdMatch[1];
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 = +$('[data-remaining-comments-count]', 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('.post-layout')).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
$('.post-menu', pvDoc).insertAdjacentHTML('beforeend',
'<span class="lsep">|</span>' +
`<a href="/posts/${postNumber}/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 list = $(`#${comments.id} .comments-list`, pvDoc);
const oldIds = new Set([...list.children].map(e => e.id));
list.innerHTML = r.responseText;
list.closest('.comments').style.display = 'block';
for (const cmt of list.children)
if (!oldIds.has(cmt.id))
cmt.classList.add('new-comment-highlight');
markPreviewableLinks(list);
markHoverableUsers(list);
});
}
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 = typeof options == 'string' ? {url: options} : 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 {
options.url = options.url.replace(/^http:/, '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 .comment-text {
-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('');
}