您需要先安装一个扩展,例如 篡改猴、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.2
- // @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 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,
- 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();
- }
- function releaseLinkListeners(link = preview.link) {
- $off('mousemove', link, onLinkMouseMove);
- $off('mouseout', link, abortPreview);
- $off('mousedown', link, abortPreview);
- if (preview.timer)
- 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, 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);
- doXHR({url: httpsUrl(link.href)}).then(r => {
- const html = r.responseText;
- const lastActivity = link.matches(':hover') ? +showPreview({finalUrl: r.finalUrl, html}) : Date.now();
- 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);
- setTimeout(writeCache, 1000, {url: link.href, finalUrl: r.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);
- onFrameReady(preview.frame).then(
- () => {
- pvDoc = preview.frame.contentDocument;
- pvWin = preview.frame.contentWindow;
- initPolyfills(pvWin);
- preview.killInvaders.stop();
- })
- .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 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) {
- 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);
- }
- 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;
- }
- #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 {
- 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;
- }
- #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};
- }
- #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('');
- }