Greasy Fork is available in English.

SE Preview on hover

Shows preview of the linked questions/answers on hover

Version au 19/02/2017. Voir la dernière version.

// ==UserScript==
// @name           SE Preview on hover
// @description    Shows preview of the linked questions/answers on hover
// @version        0.3.6
// @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=|#q|search).*/
// @require        https://greasyfork.org/scripts/12228/code/setMutationHandler.js
// @require        https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.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 = 200;
const CACHE_DURATION = 1 * 60 * 1000; // 1 minute for the recently active posts, scales up logarithmically
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;
let preview = {
	frame: null,
	link: null,
	hover: {x:0, y:0},
	timer: 0,
	stylesOverride: '',
};
const lockScroll = {};

const rxPreviewable = 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 (hasKeyModifier(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;
		$off('mousemove', link, onLinkMouseMove);
		if (link.matches(':hover'))
			downloadPreview(link.href);
	}, 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();
}

function releaseLinkListeners(link = preview.link) {
	$off('mousemove', link, onLinkMouseMove);
	$off('mouseout', link, abortPreview);
	$off('mousedown', link, abortPreview);
	clearTimeout(preview.timer);
}

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, function done() {
				$off('transitionend', element, 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(url) {
	const cached = readCache(url);
	if (cached) {
		if (preview.frame)
			preview.frame.style.transitionDuration = '0.1s';
		showPreview(cached);
	} else {
		if (preview.frame)
			preview.frame.style.transitionDuration = '';
		xhr = GM_xmlhttpRequest({
			method: 'GET',
			url: httpsUrl(url),
			onload: r => {
				const html = r.responseText;
				const lastActivity = +showPreview({finalUrl: r.finalUrl, html});
				if (!lastActivity)
					return;
				const inactiveDays = Math.max(0, (Date.now() - lastActivity) / (24 * 3600 * 1000));
				const cacheDuration = CACHE_DURATION * Math.pow(Math.log(inactiveDays + 1) + 1, 2);
				writeCache({url, finalUrl: r.finalUrl, html, cacheDuration});
			},
		});
	}
}

function initPreview() {
	preview.frame = document.createElement('iframe');
	preview.frame.id = 'SEpreview';
	document.body.appendChild(preview.frame);

	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('mouseout', document, lockScroll.detach);
		$off('scroll', document, lockScroll.run);
	};
}

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();

	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');
	onFrameReady(preview.frame).then(
		() => {
			pvDoc = preview.frame.contentDocument;
			pvWin = preview.frame.contentWindow;
			initPolyfills(pvWin);
		})
		.then(addStyles)
		.then(render)
		.then(show);
	return 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 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(
			$replaceOrCreate($$('link[rel="stylesheet"]', doc).map(e => ({
				id: e.href.replace(/\W+/g, ''),
				tag: 'link', before: SEpreviewStyles, className: 'SEpreview-reuse',
				href: e.href, rel: 'stylesheet',
			})))
		);
	}

	function render() {
		pvDoc.body.setAttribute('SEpreview-type', preview.frame.getAttribute('SEpreview-type'));

		$replaceOrCreate([{
		// 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],
		}]);

		// 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
		const answers = $$('.answer', doc);
		if (answers.length > (isQuestion ? 0 : 1)) {
			$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)', 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$/, '&nbsp;') + ' ' +
				(!gravatar ? '' : gravatar.src ? `<img src="${gravatar.src}">` : gravatar.outerHTML) +
			'</a>');
	}

	function show() {
		pvDoc.onmouseover = lockScroll.attach;
		pvDoc.onclick = onClick;
		pvDoc.onkeydown = e => !hasKeyModifier(e) && e.keyCode == 27 ? hide() : null;
		pvWin.onmessage = e => e.data == 'SEpreview-hidden' ? hide({fade: true}) : null;
		$$('.user-info a img', pvDoc).forEach(e => e.onmouseover = loadUserDetails);
		$('#SEpreview-body', pvDoc).scrollTop = 0;
		preview.frame.style.opacity = '1';
		preview.frame.focus();
	}

	function hide({fade = false} = {}) {
		releaseLinkListeners();
		releasePreviewListeners();
		const maybeZap = () => preview.frame.style.opacity == '0' && $removeChildren(pvDoc.body);
		if (fade)
			fadeOut(preview.frame).then(maybeZap);
		else {
			preview.frame.style.opacity = '0';
			preview.frame.style.display = 'none';
			maybeZap();
		}
	}

	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 || hasKeyModifier(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.href);
	}

	function loadComments() {
		GM_xmlhttpRequest({
			method: 'GET',
			url: new URL(finalUrl).origin + '/posts/' + comments.id.match(/\d+/)[0] + '/comments',
			onload: r => {
				let tbody = $(`#${comments.id} tbody`, pvDoc);
				let 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);
			},
		});
	}

	function loadUserDetails(e, ready) {
		if (ready !== true)
			return setTimeout(loadUserDetails, PREVIEW_DELAY, e, true);
		$$('#user-menu', pvDoc).forEach(e => e.id = '');
		const userId = e.target.closest('a').pathname.match(/\d+/)[0];
		const existing = $(`.SEpreview-user[data-id="${userId}"]`, pvDoc);
		if (existing) {
			existing.id = 'user-menu';
			fadeIn(existing);
			return;
		}
		GM_xmlhttpRequest({
			method: 'GET',
			url: new URL(finalUrl).origin + '/users/user-info/' + userId,
			onload: r => {
				let userMenu = $replaceOrCreate({
					id: 'user-menu',
					dataset: {id: userId},
					parent: e.target.closest('.user-info'),
					className: 'SEpreview-user',
					innerHTML: r.responseText,
				});
				userMenu.onmouseout = e => e.target == userMenu ? fadeOut(userMenu) : undefined;
				userMenu.onmouseover = e => e.target == userMenu ? (userMenu.style.opacity = '1') : undefined;
				fadeIn(userMenu);
			},
		});
	}
}

function getCacheableUrl(url) {
	// strips querys 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) {
	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.replace(/[?#].*/, '');
	const keyUrl = getCacheableUrl(url);
	const keyFinalUrl = getCacheableUrl(finalUrl);
	const expires = Date.now() + cacheDuration;
	if (!tryCatch(() => localStorage[keyFinalUrl + '\thtml'] = LZString.compressToUTF16(html))) {
		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(linkElements) {
	let retryCount = 0;
	return new Promise(function retry(resolve) {
		if (linkElements.every(e => e.sheet && e.sheet.href == e.href))
			resolve();
		else if (retryCount++ > 10)
			resolve();
		else
			setTimeout(retry, 0, resolve);
	});
}

function getURLregexForMatchedSites() {
	return new RegExp('https?://(\\w*\\.)*(' + GM_info.script.matches.map(m =>
		m.match(/^.*?\/\/\W*(\w.*?)\//)[1].replace(/\./g, '\\.')
	).join('|') + ')/(questions|q|a|posts\/comments)/\\d+');
}

function isLinkPreviewable(link) {
	const inPreview = preview.frame && link.ownerDocument == preview.frame.contentDocument;
	if (!rxPreviewable.test(link.href) || link.matches('.short-link'))
		return false;
	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 $(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)
		return;
	const range = new Range();
	range.selectNodeContents(el);
	range.deleteContents();
}

function $replaceOrCreate(options) {
	if (typeof options.map == 'function')
		return options.map($replaceOrCreate);
    const doc = (options.parent || options.before).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':
				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.parentElement).insertBefore(el, options.before);
	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
	const selector = typeof args[0] == 'string' ? args[0] : null;
	const node = args[0].nodeType ? args[0] : args[1].nodeType ? args[1] : document;
	const callback = args[typeof args[0] == 'function' ? 0 : typeof args[1] == 'function' ? 1 : 2];
	const options = args[args.length - 1] != callback ? args[args.length - 1] : undefined;
	const method = this == 'removeEventListener' ? this : 'addEventListener';
	(selector ? node.querySelector(selector) : node)[method](eventName, callback, options);
}

function $off(eventName, ...args) {
	$on.apply('removeEventListener', arguments);
}

function hasKeyModifier(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);
}

function tryCatch(fn) {
	try { return fn() }
	catch(e) {}
}

function initPolyfills(context = window) {
	for (let method of ['forEach', 'filter', 'map', 'every', 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: 400px;
			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;
		}
	`
	+ Object.keys(COLORS).map(s => `
		#SEpreview[SEpreview-type="${s}"] {
			border-color: rgb(${COLORS[s].backRGB});
		}
	`).join('')
	);

	preview.stylesOverride = `
		body, html {
			min-width: unset!important;
			box-shadow: none!important;
			padding: 0!important;
			margin: 0!important;
		}
		html, body {
			background: unset!important;;
		}
		body {
			display: flex;
			flex-direction: column;
			height: 100vh;
		}
		#SEpreview-body a.SEpreviewable {
			text-decoration: underline !important;
		}
		#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;
		}
		#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 {
			padding: 30px!important;
			overflow: auto;
			flex-grow: 2;
		}
		#SEpreview-body .post-menu {
			display: none!important;
		}
		#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;
		}
		#SEpreview-answers a.SEpreviewed {
			background-color: ${COLORS.answer.fore};
			color: ${COLORS.answer.foreInv};
			position: relative;
		}
		#SEpreview-answers a.SEpreviewed:after {
			display: block;
			content: " ";
			position: absolute;
			left: -4px;
			top: -4px;
			right: -4px;
			bottom: -4px;
			border: 4px solid ${COLORS.answer.fore};
		}

		.comment-edit,
		.delete-tag,
		.comment-actions td:last-child {
			display: none;
		}
		.comments {
			border-top: none;
		}
		.comments tr:last-child td {
			border-bottom: none;
		}
		.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);
		}

		.user-info {
			position: relative;
		}
		#user-menu {
			position: absolute;
		}
		.SEpreview-user {
			position: absolute;
			right: -1em;
			top: -2em;
			transition: opacity .25s ease-in-out;
			opacity: 0;
			display: 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('');
}