- // ==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());
- }