// ==UserScript==
// @name Twitter Image Zoom on Hover
// @name:ja Twitter Image Zoom on Hover
// @name:zh-cn Twitter Image Zoom on Hover
// @name::zh-tw Twitter Image Zoom on Hover
// @description Show untrimmed image on hover.
// @description:ja トリミングされていない画像を表示する。
// @description:zh-cn 鼠标悬停时显示完整的图片。
// @description:zh-tw 滑鼠懸停時顯示完整的圖片。
// @version 0.61
// @author AMANE
// @namespace none
// @match https://twitter.com/*
// @grant none
// @license MIT
// ==/UserScript==
/* jshint esversion: 6 */
const twimg_zoom = (function () {
let is_deck, is_keep, is_wait, is_load, is_show, show_timeout, hide_timeout;
let popup, popup_bg, popup_img, css_popup_pos, css_popup_bg;
return {
init: function () {
is_deck = document.location.href.indexOf('tweetdeck.twitter.com') >= 0;
document.head.insertAdjacentHTML('beforeend', '<style>' + this.css + '</style>');
popup = document.createElement('div');
popup.innerHTML = '<svg viewBox="0,0,24,24"><circle cx="12" cy="12" r="10" fill="none" stroke="#1DA1F2" stroke-width="4" opacity="0.4" /><path d="M12,2 a10,10 0 0 1 10,10" fill="none" stroke="#1DA1F2" stroke-width="4" stroke-linecap="round" /></svg>';
let container = document.createElement('div');
popup_bg = document.createElement('div');
popup_img = document.createElement('img');
container.appendChild(popup_bg);
container.appendChild(popup_img);
popup.appendChild(container);
popup.classList.add('twimg_popup', 'hide');
document.body.appendChild(popup);
let observer = new MutationObserver(ms => ms.forEach(m => m.addedNodes.forEach(node => this.detect(node))));
observer.observe(document.body, {childList: true, subtree: true});
},
detect: function(node) {
if (node.tagName == 'DIV' || node.tagName == 'ARTICLE' || node.tagName == 'LI') {
let photos = node.dataset.testid == 'tweetPhoto' ? [node] : node.querySelectorAll('div[data-testid="tweetPhoto"], div:not(.is-video) > a.js-media-image-link');
if (photos && photos.length) this.inject(photos);
let listitems = node.tagName == 'LI' && node.getAttribute('role') == 'listitem' && [node.firstChild] || node.tagName == 'DIV' && node.querySelectorAll('li[role="listitem"] > div:first-child');
if (listitems && listitems.length) this.inject(listitems);
}
},
inject: function (photos) {
photos.forEach(photo => {
if (photo.querySelector('div[data-testid="previewInterstitial"]')) return;
photo.onmouseenter = () => this.popup(photo);
photo.onmouseleave = () => this.close(200);
});
},
popup: function (photo) {
let pos = photo.parentNode.getBoundingClientRect();
let img = photo.querySelector('img');
if (is_deck || img.naturalWidth > Math.ceil(pos.width) || img.naturalHeight > Math.ceil(pos.height) || img.width > Math.ceil(pos.width) || img.height > Math.ceil(pos.height)) {
is_wait = true;
if (is_load || is_show) {
clearTimeout(hide_timeout);
this.close(0);
}
show_timeout = setTimeout(() => {
popup.classList.remove('hide');
let img_src = (is_deck ? photo.style.backgroundImage.slice(5, -2) : img.src).replace(/120x120|240x240|360x360/, 'small');
let pos = photo.parentNode.getBoundingClientRect();
pos.cx = pos.left + pos.width / 2;
pos.cy = pos.top + pos.height / 2 + window.pageYOffset;
css_popup_pos = 'left: ' + pos.cx + 'px; top: ' + pos.cy + 'px; width: ' + pos.width + 'px; height: ' + pos.height + 'px;';
css_popup_bg = photo.style.cssText + 'background-image: url(' + img_src + ')';
popup.style.cssText = css_popup_pos;
popup_bg.style.cssText = css_popup_bg;
popup_img.src = img_src;
let load_failed = 0;
(function load_n_show() {
show_timeout = setTimeout(() => {
if (popup_img.naturalWidth > 0 && popup_img.naturalHeight > 0) {
let zoom = {}, padding = 6;
zoom.width_max = Math.min(popup_img.naturalWidth, 680, document.body.clientWidth - padding * 2);
zoom.height_max = Math.min(popup_img.naturalHeight, 680, is_deck ? document.body.clientHeight - padding * 2 : 9999);
zoom.width = popup_img.naturalWidth >= popup_img.naturalHeight ? zoom.width_max : (popup_img.naturalWidth >= zoom.width_max ? zoom.width_max : popup_img.naturalWidth) * (zoom.height_max / popup_img.naturalHeight);
zoom.height = popup_img.naturalHeight >= popup_img.naturalWidth ? zoom.height_max : (popup_img.naturalHeight >= zoom.height_max ? zoom.height_max : popup_img.naturalHeight) * (zoom.width_max / popup_img.naturalWidth);
let pos = photo.parentNode.getBoundingClientRect();
zoom.cx = pos.left + pos.width / 2;
zoom.cy = pos.top + pos.height / 2 + window.pageYOffset;
zoom.cx_min = zoom.width / 2 + padding;
zoom.cy_min = zoom.height / 2 + padding;
zoom.cx_max = document.body.clientWidth - zoom.width / 2 - padding;
zoom.cy_max = document.body.clientHeight - zoom.height / 2 - padding + window.pageYOffset;
if (zoom.cx < zoom.cx_min) popup.style.left = zoom.cx_min + 'px';
if (zoom.cx > zoom.cx_max) popup.style.left = zoom.cx_max + 'px';
if (zoom.cy < zoom.cy_min) popup.style.top = zoom.cy_min + 'px';
if (is_deck && zoom.cy > zoom.cy_max) popup.style.top = zoom.cy_max + 'px';
popup_bg.style.margin = '';
popup.style.width = zoom.width + 'px';
popup.style.height = zoom.height + 'px';
popup.classList.add('show');
is_wait = false;
is_load = false;
is_show = true;
if (load_failed > 0) popup.classList.remove('load');
} else {
is_load = true;
if (load_failed == 0) popup.classList.add('load');
load_failed++;
if (load_failed < 14) load_n_show();
else popup.classList.remove('load');
}
}, is_keep ? 250 + 250 * load_failed : 750);
})();
is_keep = true;
}, 250);
}
},
close: function (wait) {
if (is_wait) clearTimeout(show_timeout);
if (is_load || is_show) {
hide_timeout = setTimeout(() => {
popup.classList.remove('load', 'show');
popup.style.cssText = css_popup_pos;
popup_bg.style.cssText = css_popup_bg;
is_load = false;
is_show = false;
setTimeout(() => {
if (!is_wait && !is_show) {
is_keep = false;
popup.classList.add('hide');
}
}, 1000);
}, wait);
}
},
css: `
.twimg_popup {
position: absolute; transform: translate(-50%, -50%);
display: flex; align-items: center; justify-content: center;
pointer-events: none; transition: 0.2s; z-index: 999;
}
.twimg_popup.hide {
display: none;
}
.twimg_popup > svg {
display: none; position: absolute; width: 24px; height: 24px;
}
.twimg_popup.load > svg {
display: block; animation: spin 1s linear infinite;
}
@keyframes spin {
0% {transform: rotate(0deg);}
100% {transform: rotate(360deg);}
}
.twimg_popup > div {
position: relative; width: 100%; height: 100%;
}
.twimg_popup > div > div {
position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px;
background-size: cover; background-repeat: no-repeat; background-position: center center;
opacity: 0; transition: 0.2s;
}
.twimg_popup.show > div > div {
opacity: 1;
}
.twimg_popup > div > img {
width: 0; height: 0; opacity: 0;
}
/* #### padding and shadow #### */
.twimg_popup.show {
padding: 6px; border-radius: 6px;
background-color: rgba(255, 255, 255);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
html.dark .twimg_popup.show {
background-color: rgba(21, 32, 43);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
}
body[style*="#FFFFFF"] .twimg_popup.show, body[style*="rgb(255, 255, 255)"] .twimg_popup.show {
background-color: rgba(255, 255, 255);
box-shadow: rgba(101, 119, 134, 0.2) 0px 0px 15px, rgba(101, 119, 134, 0.15) 0px 0px 3px 1px;
}
body[style*="#15202B"] .twimg_popup.show, body[style*="rgb(21, 32, 43)"] .twimg_popup.show {
background-color: rgba(21, 32, 43);
box-shadow: rgba(136, 153, 166, 0.2) 0px 0px 15px, rgba(136, 153, 166, 0.15) 0px 0px 3px 1px;
}
body[style*="#000000"] .twimg_popup.show, body[style*="rgb(0, 0, 0)"] .twimg_popup.show {
background-color: rgba(0, 0, 0);
box-shadow: rgba(255, 255, 255, 0.2) 0px 0px 15px, rgba(255, 255, 255, 0.15) 0px 0px 3px 1px;
}
`
};
})();
twimg_zoom.init();