Displays mouseover-only tooltips (likely bloated by various extensions) in a draggable float window that stays visible at your preferred position.
// ==UserScript==
// @name MWI Tooltip Float Window
// @namespace http://tampermonkey.net/
// @version 8.0
// @description Displays mouseover-only tooltips (likely bloated by various extensions) in a draggable float window that stays visible at your preferred position.
// @author syabusyabukun
// @match https://www.milkywayidle.com/*
// @match https://test.milkywayidle.com/*
// @match https://www.milkywayidlecn.com/*
// @match https://test.milkywayidlecn.com/*
// @grant GM_addStyle
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const FLOAT_WINDOW_ID = 'mwi-tooltip-float-window';
const STORAGE_KEY_POS = 'mwi-tooltip-float-pos';
const HIDE_CLASS = 'mwi-float-hidden-tooltip';
GM_addStyle(`
/* 元tooltipの一時非表示用クラス
visibility:hidden でレイアウトを壊さずに視覚的に消す。
MUIのstyle属性(position/transform等)には干渉しない。 */
.${HIDE_CLASS} {
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
#${FLOAT_WINDOW_ID} {
position: fixed;
z-index: 100000;
min-width: 200px;
max-width: 90vw;
max-height: 85vh;
background: var(--color-midnight-900, #1a1f36);
border: 1px solid var(--color-neutral-200, #555);
border-radius: 6px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
display: none;
flex-direction: column;
overflow: hidden;
font-family: "Roboto", sans-serif;
color: var(--color-text-dark-mode, #e0e0e0);
}
#${FLOAT_WINDOW_ID}.visible {
display: flex;
}
#${FLOAT_WINDOW_ID} .mwi-float-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
background: var(--color-space-700, #2a2f4a);
cursor: move;
user-select: none;
flex-shrink: 0;
min-height: 24px;
border-bottom: 1px solid var(--color-neutral-200, #555);
}
#${FLOAT_WINDOW_ID} .mwi-float-header .title-text {
font-size: 11px;
font-weight: 600;
color: var(--color-neutral-200, #aaa);
pointer-events: none;
}
#${FLOAT_WINDOW_ID} .mwi-float-close-btn {
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
color: #ccc;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.15s ease;
flex-shrink: 0;
}
#${FLOAT_WINDOW_ID} .mwi-float-close-btn:hover {
background: #e74c3c;
color: #fff;
}
#${FLOAT_WINDOW_ID} .mwi-float-body {
overflow-y: auto;
overflow-x: hidden;
flex-grow: 1;
padding: 0;
font-size: var(--font-size-base, 0.875rem);
line-height: 1.4;
user-select: text;
-webkit-user-select: text;
cursor: auto;
}
#${FLOAT_WINDOW_ID} .mwi-float-body * {
user-select: text !important;
-webkit-user-select: text !important;
}
#${FLOAT_WINDOW_ID} .mwi-float-resize {
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
z-index: 1;
}
#${FLOAT_WINDOW_ID} .mwi-float-resize::after {
content: '';
position: absolute;
bottom: 3px;
right: 3px;
width: 8px;
height: 8px;
border-right: 2px solid rgba(255,255,255,0.3);
border-bottom: 2px solid rgba(255,255,255,0.3);
}
`);
// ─── 状態 ───
let floatWindow = null;
let isFloatVisible = false;
let syncObserver = null;
let trackedTooltip = null;
let captureTimer = null;
let frozen = false;
let lastClickTime = 0;
const CLICK_THRESHOLD_MS = 600;
document.addEventListener('mousedown', (e) => {
if (e.target.closest?.(`#${FLOAT_WINDOW_ID}`)) return;
lastClickTime = Date.now();
}, true);
// ─── 非hover tooltip除外判定 ───
function shouldExclude(popperNode) {
if (Date.now() - lastClickTime < CLICK_THRESHOLD_MS) return true;
const buttons = popperNode.querySelectorAll(
'button, [role="button"], [role="menuitem"], input[type="button"], input[type="submit"], .MuiButton-root'
);
if (buttons.length >= 2) return true;
if (popperNode.querySelector('[class*="ItemSelector"]')) return true;
if (popperNode.querySelector('[class*="Item_clickable"]')) return true;
const itemContainers = popperNode.querySelectorAll('[class*="Item_itemContainer"]');
if (itemContainers.length >= 2) return true;
if (popperNode.querySelector('input[type="text"], input:not([type])')) return true;
return false;
}
// ─── フロートウィンドウ作成 ───
function createFloatWindow() {
const win = document.createElement('div');
win.id = FLOAT_WINDOW_ID;
win.innerHTML = `
<div class="mwi-float-header">
<span class="title-text">Tooltip</span>
<span class="mwi-float-close-btn" title="Close">×</span>
</div>
<div class="mwi-float-body"></div>
<div class="mwi-float-resize"></div>
`;
document.body.appendChild(win);
win.querySelector('.mwi-float-close-btn').addEventListener('click', (e) => {
e.stopPropagation();
hideFloatWindow();
});
setupDrag(win);
setupResize(win);
restorePosition(win);
return win;
}
function setupDrag(win) {
const header = win.querySelector('.mwi-float-header');
let isDragging = false;
let startX, startY, origLeft, origTop;
header.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('mwi-float-close-btn')) return;
e.preventDefault();
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = win.getBoundingClientRect();
origLeft = rect.left;
origTop = rect.top;
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
let newLeft = origLeft + (e.clientX - startX);
let newTop = origTop + (e.clientY - startY);
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - win.offsetWidth));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - 30));
win.style.left = newLeft + 'px';
win.style.top = newTop + 'px';
win.style.right = 'auto';
win.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
savePosition(win);
}
});
}
function setupResize(win) {
const handle = win.querySelector('.mwi-float-resize');
let isResizing = false;
let startX, startY, origW, origH;
handle.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
isResizing = true;
startX = e.clientX;
startY = e.clientY;
origW = win.offsetWidth;
origH = win.offsetHeight;
win.dataset.manualResize = 'true';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
e.preventDefault();
win.style.width = Math.max(200, origW + (e.clientX - startX)) + 'px';
win.style.height = Math.max(100, origH + (e.clientY - startY)) + 'px';
});
document.addEventListener('mouseup', () => {
if (isResizing) isResizing = false;
});
}
function savePosition(win) {
try {
const rect = win.getBoundingClientRect();
localStorage.setItem(STORAGE_KEY_POS, JSON.stringify({ left: rect.left, top: rect.top }));
} catch (e) { }
}
function restorePosition(win) {
try {
const saved = localStorage.getItem(STORAGE_KEY_POS);
if (saved) {
const pos = JSON.parse(saved);
win.style.left = Math.max(0, Math.min(pos.left, window.innerWidth - 100)) + 'px';
win.style.top = Math.max(0, Math.min(pos.top, window.innerHeight - 50)) + 'px';
} else {
win.style.left = (window.innerWidth - 450) + 'px';
win.style.top = '60px';
}
win.style.right = 'auto';
win.style.bottom = 'auto';
} catch (e) {
win.style.left = '100px';
win.style.top = '100px';
}
}
// ─── スナップショット ───
function snapshotToFloat(sourceEl) {
if (!floatWindow) floatWindow = createFloatWindow();
const body = floatWindow.querySelector('.mwi-float-body');
const scrollTop = body.scrollTop;
const clone = sourceEl.cloneNode(true);
clone.classList.remove(HIDE_CLASS);
clone.style.cssText = `
position: static !important;
transform: none !important;
top: auto !important; left: auto !important;
right: auto !important; bottom: auto !important;
width: auto !important; max-width: 100% !important;
visibility: visible !important; opacity: 1 !important;
pointer-events: auto !important; z-index: auto !important;
`;
body.innerHTML = '';
body.appendChild(clone);
body.scrollTop = scrollTop;
delete floatWindow.dataset.manualResize;
floatWindow.style.width = '';
floatWindow.style.height = '';
isFloatVisible = true;
floatWindow.classList.add('visible');
}
// ─── 同期 ───
function startSync(sourceEl) {
stopSync();
trackedTooltip = sourceEl;
frozen = false;
syncObserver = new MutationObserver(() => {
if (frozen) return;
const text = sourceEl.textContent || '';
if (text.trim().length < 5) {
frozen = true;
stopSync();
return;
}
snapshotToFloat(sourceEl);
});
syncObserver.observe(sourceEl, {
childList: true,
subtree: true,
characterData: true
});
}
function stopSync() {
if (syncObserver) {
syncObserver.disconnect();
syncObserver = null;
}
trackedTooltip = null;
}
function hideFloatWindow() {
isFloatVisible = false;
frozen = false;
stopSync();
if (captureTimer) {
clearTimeout(captureTimer);
captureTimer = null;
}
if (floatWindow) {
floatWindow.classList.remove('visible');
floatWindow.querySelector('.mwi-float-body').innerHTML = '';
delete floatWindow.dataset.manualResize;
floatWindow.style.width = '';
floatWindow.style.height = '';
}
}
// ─── body直下のtooltip出現を監視 ───
const skippedPoppers = new WeakSet();
const bodyObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (node.id === FLOAT_WINDOW_ID) continue;
let tooltipWrapper = null;
if (node.getAttribute?.('role') === 'tooltip') {
tooltipWrapper = node;
} else if (node.querySelector?.('[role="tooltip"]')) {
tooltipWrapper = node;
}
if (!tooltipWrapper) continue;
if (skippedPoppers.has(tooltipWrapper)) continue;
// ★ 即座にCSSクラスで非表示(インラインstyleに干渉しない)
tooltipWrapper.classList.add(HIDE_CLASS);
if (captureTimer) clearTimeout(captureTimer);
const target = tooltipWrapper;
captureTimer = setTimeout(() => {
captureTimer = null;
if (!target.isConnected) {
target.classList.remove(HIDE_CLASS);
return;
}
if (shouldExclude(target)) {
// ★ hover tooltipではない → クラスを外すだけで完全に元通り
target.classList.remove(HIDE_CLASS);
skippedPoppers.add(target);
return;
}
// hover tooltipと確定
snapshotToFloat(target);
startSync(target);
// 元のtooltipは非表示クラスを付けたまま維持
}, 150);
}
for (const node of mutation.removedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (trackedTooltip && (node === trackedTooltip || node.contains?.(trackedTooltip))) {
frozen = true;
stopSync();
}
}
}
});
// ─── 初期化 ───
function init() {
const waitForGame = setInterval(() => {
const root = document.getElementById('root');
if (root && root.children.length > 0) {
clearInterval(waitForGame);
floatWindow = createFloatWindow();
bodyObserver.observe(document.body, { childList: true, subtree: false });
console.log('[MWI Tooltip Float v8] Initialized');
}
}, 500);
}
init();
})();