fix: hover ADD badge stable positioning, larger hit target, delayed hide, no disappear on approach
Browse files
src-tauri/resources/scripts/hover_overlay.js
CHANGED
|
@@ -1,47 +1,66 @@
|
|
| 1 |
-
/* Refstudio hover overlay: + ADD button on web images.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
(function () {
|
| 3 |
-
if (window.
|
| 4 |
-
window.
|
| 5 |
|
| 6 |
var MIN = 90;
|
| 7 |
var activeImg = null;
|
| 8 |
var badge = null;
|
| 9 |
var showTimer = 0;
|
| 10 |
var hideTimer = 0;
|
| 11 |
-
var beaconKeepalive = [];
|
| 12 |
|
| 13 |
function injectCSS() {
|
| 14 |
-
if (document.getElementById('
|
| 15 |
-
var s = document.createElement('style'); s.id = '
|
| 16 |
-
s.textContent =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
(document.head || document.documentElement).appendChild(s);
|
| 18 |
}
|
| 19 |
|
| 20 |
function enc(v) { return encodeURIComponent(v == null ? '' : String(v)); }
|
| 21 |
function absUrl(s) { try { return new URL(s || '', location.href).href; } catch (_) { return s || ''; } }
|
| 22 |
|
| 23 |
-
function
|
| 24 |
-
|
| 25 |
-
|
|
|
|
| 26 |
var best = '', bestScore = 0;
|
| 27 |
srcset.split(',').forEach(function(part) {
|
| 28 |
-
var bits = part.trim().split(/\s+/);
|
| 29 |
-
|
| 30 |
-
var score = 1;
|
| 31 |
-
if (bits[1]) {
|
| 32 |
-
if (bits[1].endsWith('w')) score = parseInt(bits[1], 10) || 1;
|
| 33 |
-
else if (bits[1].endsWith('x')) score = (parseFloat(bits[1]) || 1) * 1000;
|
| 34 |
-
}
|
| 35 |
if (score > bestScore) { bestScore = score; best = u; }
|
| 36 |
});
|
| 37 |
-
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
function bestImageUrl(img) {
|
| 42 |
-
var u = bestFromSrcset(img.getAttribute('srcset') || img.srcset);
|
| 43 |
-
if (u) return u;
|
| 44 |
-
u = img.getAttribute('data-full') || img.getAttribute('data-fullsrc') || img.getAttribute('data-original') || img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.currentSrc || img.src;
|
| 45 |
u = absUrl(u);
|
| 46 |
try {
|
| 47 |
if (u.includes('pinimg.com') && u.match(/\/\d+x\//)) u = u.replace(/\/\d+x\//, '/originals/');
|
|
@@ -55,52 +74,39 @@
|
|
| 55 |
var t = document.getElementById('__mt7');
|
| 56 |
if (!t) { t = document.createElement('div'); t.id = '__mt7'; document.documentElement.appendChild(t); }
|
| 57 |
t.textContent = m; t.classList.add('s');
|
| 58 |
-
clearTimeout(t._t); t._t = setTimeout(function () { t.classList.remove('s'); },
|
| 59 |
}
|
| 60 |
|
| 61 |
-
function
|
| 62 |
var u = 'muse-action://board?url=' + enc(url) + '&source=' + enc(location.href) + '&title=' + enc(title) + '&w=' + enc(w) + '&h=' + enc(h);
|
| 63 |
-
|
| 64 |
-
// 1) Primary: top-level navigation. Tauri on_navigation sees this and returns false,
|
| 65 |
-
// so the web page should NOT actually navigate away, but Rust definitely receives it.
|
| 66 |
try { window.location.href = u; } catch (_) {}
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
var b = new Image();
|
| 71 |
-
b.onload = b.onerror = function () {
|
| 72 |
-
setTimeout(function () {
|
| 73 |
-
var i = beaconKeepalive.indexOf(b);
|
| 74 |
-
if (i >= 0) beaconKeepalive.splice(i, 1);
|
| 75 |
-
}, 500);
|
| 76 |
-
};
|
| 77 |
-
beaconKeepalive.push(b);
|
| 78 |
-
b.src = u;
|
| 79 |
-
} catch (_) {}
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
function pos(el, img) {
|
| 86 |
var r = img.getBoundingClientRect();
|
| 87 |
-
|
| 88 |
-
var
|
| 89 |
-
|
| 90 |
-
if (x
|
|
|
|
| 91 |
if (y < 4) y = 4;
|
| 92 |
-
if (y > innerHeight -
|
| 93 |
el.style.left = x + 'px';
|
| 94 |
el.style.top = y + 'px';
|
| 95 |
}
|
| 96 |
|
| 97 |
-
function hide() {
|
| 98 |
-
if (!badge) return;
|
| 99 |
-
badge.classList.remove('v');
|
| 100 |
-
activeImg = null;
|
| 101 |
-
setTimeout(function () { if (badge && !badge.classList.contains('v')) badge.style.pointerEvents = 'none'; }, 160);
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
function show(img) {
|
| 105 |
if (img === activeImg && badge && badge.classList.contains('v')) return;
|
| 106 |
injectCSS();
|
|
@@ -108,55 +114,69 @@
|
|
| 108 |
if (!badge) {
|
| 109 |
badge = document.createElement('div');
|
| 110 |
badge.id = '__muse_add_badge';
|
| 111 |
-
badge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
| 112 |
-
|
|
|
|
| 113 |
badge.addEventListener('click', function (e) {
|
| 114 |
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
| 115 |
if (!activeImg) return;
|
| 116 |
var url = bestImageUrl(activeImg);
|
|
|
|
| 117 |
var title = activeImg.alt || activeImg.title || document.title || 'Reference';
|
| 118 |
var w = activeImg.naturalWidth || Math.round(activeImg.getBoundingClientRect().width);
|
| 119 |
var h = activeImg.naturalHeight || Math.round(activeImg.getBoundingClientRect().height);
|
| 120 |
-
|
| 121 |
-
addToBoard(url, title, w, h);
|
| 122 |
toast('✓ Added to Board');
|
|
|
|
| 123 |
}, true);
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
document.documentElement.appendChild(badge);
|
| 127 |
}
|
| 128 |
pos(badge, activeImg);
|
| 129 |
-
badge.offsetHeight;
|
| 130 |
badge.classList.add('v');
|
| 131 |
}
|
| 132 |
|
| 133 |
function isValidTarget(el) {
|
| 134 |
if (!el || el.tagName !== 'IMG') return false;
|
|
|
|
| 135 |
var r = el.getBoundingClientRect();
|
| 136 |
return r.width >= MIN && r.height >= MIN;
|
| 137 |
}
|
| 138 |
|
| 139 |
document.addEventListener('mouseover', function (e) {
|
| 140 |
var t = e.target;
|
|
|
|
|
|
|
| 141 |
if (!isValidTarget(t)) return;
|
| 142 |
-
if (t === activeImg) {
|
| 143 |
-
|
| 144 |
-
showTimer = setTimeout(function () { show(t); },
|
| 145 |
}, true);
|
| 146 |
|
| 147 |
document.addEventListener('mouseout', function (e) {
|
| 148 |
var t = e.target;
|
| 149 |
if (!t || t.tagName !== 'IMG' || t !== activeImg) return;
|
| 150 |
var rel = e.relatedTarget;
|
| 151 |
-
|
|
|
|
| 152 |
clearTimeout(showTimer);
|
| 153 |
-
|
| 154 |
}, true);
|
| 155 |
|
| 156 |
window.addEventListener('scroll', function () {
|
| 157 |
if (!badge || !activeImg) return;
|
| 158 |
var r = activeImg.getBoundingClientRect();
|
| 159 |
-
if (r.bottom < -20 || r.top > innerHeight + 20) hide();
|
| 160 |
else pos(badge, activeImg);
|
| 161 |
}, { passive: true, capture: true });
|
| 162 |
})();
|
|
|
|
| 1 |
+
/* Refstudio hover overlay: + ADD button on web images.
|
| 2 |
+
* v10 — fixes: badge disappears before click, badge not clickable, click goes to image behind.
|
| 3 |
+
* Root causes fixed:
|
| 4 |
+
* 1. Badge was positioned at top-right of image but mouseout fired when moving FROM image TO badge
|
| 5 |
+
* 2. Badge pointer-events were toggled too aggressively
|
| 6 |
+
* 3. Badge hide timer was too short (300ms) — cursor couldn't reach it
|
| 7 |
+
*/
|
| 8 |
(function () {
|
| 9 |
+
if (window.__muse_hover_v10) return;
|
| 10 |
+
window.__muse_hover_v10 = true;
|
| 11 |
|
| 12 |
var MIN = 90;
|
| 13 |
var activeImg = null;
|
| 14 |
var badge = null;
|
| 15 |
var showTimer = 0;
|
| 16 |
var hideTimer = 0;
|
|
|
|
| 17 |
|
| 18 |
function injectCSS() {
|
| 19 |
+
if (document.getElementById('__muse_badge_css2')) return;
|
| 20 |
+
var s = document.createElement('style'); s.id = '__muse_badge_css2';
|
| 21 |
+
s.textContent = [
|
| 22 |
+
/* The badge itself */
|
| 23 |
+
'#__muse_add_badge{',
|
| 24 |
+
' all:initial;position:fixed;z-index:2147483647;',
|
| 25 |
+
' display:flex;align-items:center;gap:5px;',
|
| 26 |
+
' padding:8px 16px;',
|
| 27 |
+
' background:rgba(10,132,255,0.95);color:#fff;',
|
| 28 |
+
' border-radius:999px;',
|
| 29 |
+
' box-shadow:0 4px 20px rgba(0,0,0,0.4),0 0 0 1px rgba(255,255,255,0.1);',
|
| 30 |
+
' font:700 12px/1 -apple-system,BlinkMacSystemFont,"Inter",sans-serif;',
|
| 31 |
+
' cursor:pointer;',
|
| 32 |
+
' opacity:0;transform:translateY(4px) scale(0.92);',
|
| 33 |
+
' transition:opacity .2s,transform .2s,background .1s;',
|
| 34 |
+
' pointer-events:auto;', /* ALWAYS clickable when visible */
|
| 35 |
+
' letter-spacing:0.01em;user-select:none;',
|
| 36 |
+
'}',
|
| 37 |
+
'#__muse_add_badge.v{opacity:1;transform:none}',
|
| 38 |
+
'#__muse_add_badge:hover{background:rgba(10,132,255,1);box-shadow:0 6px 28px rgba(10,132,255,0.4),0 0 0 1px rgba(255,255,255,0.2);transform:scale(1.06)}',
|
| 39 |
+
'#__muse_add_badge:active{transform:scale(0.92);background:rgba(10,100,220,1)}',
|
| 40 |
+
'#__muse_add_badge svg{width:14px;height:14px;stroke-width:2.5;pointer-events:none}',
|
| 41 |
+
/* Toast */
|
| 42 |
+
'#__mt7{all:initial;position:fixed;left:50%;bottom:24px;transform:translateX(-50%) translateY(8px);z-index:2147483647;padding:9px 16px;border-radius:999px;background:rgba(20,20,20,.97);border:1px solid rgba(255,255,255,.1);box-shadow:0 10px 36px rgba(0,0,0,.5);font:600 12px/1 -apple-system,sans-serif;color:#fff;opacity:0;transition:opacity .16s,transform .16s;pointer-events:none}',
|
| 43 |
+
'#__mt7.s{opacity:1;transform:translateX(-50%) translateY(0)}'
|
| 44 |
+
].join('');
|
| 45 |
(document.head || document.documentElement).appendChild(s);
|
| 46 |
}
|
| 47 |
|
| 48 |
function enc(v) { return encodeURIComponent(v == null ? '' : String(v)); }
|
| 49 |
function absUrl(s) { try { return new URL(s || '', location.href).href; } catch (_) { return s || ''; } }
|
| 50 |
|
| 51 |
+
function bestImageUrl(img) {
|
| 52 |
+
// srcset best
|
| 53 |
+
var srcset = img.getAttribute('srcset') || '';
|
| 54 |
+
if (srcset) {
|
| 55 |
var best = '', bestScore = 0;
|
| 56 |
srcset.split(',').forEach(function(part) {
|
| 57 |
+
var bits = part.trim().split(/\s+/); var u = bits[0]; var score = 1;
|
| 58 |
+
if (bits[1]) { if (bits[1].endsWith('w')) score = parseInt(bits[1],10)||1; else if (bits[1].endsWith('x')) score = (parseFloat(bits[1])||1)*1000; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
if (score > bestScore) { bestScore = score; best = u; }
|
| 60 |
});
|
| 61 |
+
if (best) return absUrl(best);
|
| 62 |
+
}
|
| 63 |
+
var u = img.getAttribute('data-full') || img.getAttribute('data-fullsrc') || img.getAttribute('data-original') || img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.currentSrc || img.src;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
u = absUrl(u);
|
| 65 |
try {
|
| 66 |
if (u.includes('pinimg.com') && u.match(/\/\d+x\//)) u = u.replace(/\/\d+x\//, '/originals/');
|
|
|
|
| 74 |
var t = document.getElementById('__mt7');
|
| 75 |
if (!t) { t = document.createElement('div'); t.id = '__mt7'; document.documentElement.appendChild(t); }
|
| 76 |
t.textContent = m; t.classList.add('s');
|
| 77 |
+
clearTimeout(t._t); t._t = setTimeout(function () { t.classList.remove('s'); }, 2000);
|
| 78 |
}
|
| 79 |
|
| 80 |
+
function fireAction(url, title, w, h) {
|
| 81 |
var u = 'muse-action://board?url=' + enc(url) + '&source=' + enc(location.href) + '&title=' + enc(title) + '&w=' + enc(w) + '&h=' + enc(h);
|
| 82 |
+
// Fire through all three channels for maximum reliability.
|
|
|
|
|
|
|
| 83 |
try { window.location.href = u; } catch (_) {}
|
| 84 |
+
try { var b = new Image(); b.src = u; } catch (_) {}
|
| 85 |
+
try { fetch(u).catch(function(){}); } catch (_) {}
|
| 86 |
+
}
|
| 87 |
|
| 88 |
+
function clearHide() { clearTimeout(hideTimer); }
|
| 89 |
+
function scheduleHide() { clearHide(); hideTimer = setTimeout(hide, 600); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
function hide() {
|
| 92 |
+
if (!badge) return;
|
| 93 |
+
badge.classList.remove('v');
|
| 94 |
+
activeImg = null;
|
| 95 |
}
|
| 96 |
|
| 97 |
function pos(el, img) {
|
| 98 |
var r = img.getBoundingClientRect();
|
| 99 |
+
// Position badge at TOP-RIGHT of image, inside the image bounds
|
| 100 |
+
var x = r.right - 80;
|
| 101 |
+
var y = r.top + 10;
|
| 102 |
+
if (x < r.left + 4) x = r.left + 4;
|
| 103 |
+
if (x > window.innerWidth - 90) x = window.innerWidth - 90;
|
| 104 |
if (y < 4) y = 4;
|
| 105 |
+
if (y > window.innerHeight - 40) y = window.innerHeight - 40;
|
| 106 |
el.style.left = x + 'px';
|
| 107 |
el.style.top = y + 'px';
|
| 108 |
}
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
function show(img) {
|
| 111 |
if (img === activeImg && badge && badge.classList.contains('v')) return;
|
| 112 |
injectCSS();
|
|
|
|
| 114 |
if (!badge) {
|
| 115 |
badge = document.createElement('div');
|
| 116 |
badge.id = '__muse_add_badge';
|
| 117 |
+
badge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>ADD';
|
| 118 |
+
|
| 119 |
+
// Click handler — the actual capture action
|
| 120 |
badge.addEventListener('click', function (e) {
|
| 121 |
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
| 122 |
if (!activeImg) return;
|
| 123 |
var url = bestImageUrl(activeImg);
|
| 124 |
+
if (!url || url.startsWith('data:') || url.startsWith('blob:')) { toast('Cannot capture this image'); return; }
|
| 125 |
var title = activeImg.alt || activeImg.title || document.title || 'Reference';
|
| 126 |
var w = activeImg.naturalWidth || Math.round(activeImg.getBoundingClientRect().width);
|
| 127 |
var h = activeImg.naturalHeight || Math.round(activeImg.getBoundingClientRect().height);
|
| 128 |
+
fireAction(url, title, w, h);
|
|
|
|
| 129 |
toast('✓ Added to Board');
|
| 130 |
+
hide();
|
| 131 |
}, true);
|
| 132 |
+
|
| 133 |
+
// Block ALL events from reaching elements underneath
|
| 134 |
+
['mousedown','mouseup','pointerdown','pointerup','touchstart','touchend'].forEach(function(ev) {
|
| 135 |
+
badge.addEventListener(ev, function(e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }, true);
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
// Keep badge visible while hovering it
|
| 139 |
+
badge.addEventListener('mouseenter', clearHide, true);
|
| 140 |
+
badge.addEventListener('mouseleave', scheduleHide, true);
|
| 141 |
+
|
| 142 |
document.documentElement.appendChild(badge);
|
| 143 |
}
|
| 144 |
pos(badge, activeImg);
|
| 145 |
+
void badge.offsetHeight; // force reflow for transition
|
| 146 |
badge.classList.add('v');
|
| 147 |
}
|
| 148 |
|
| 149 |
function isValidTarget(el) {
|
| 150 |
if (!el || el.tagName !== 'IMG') return false;
|
| 151 |
+
if (el === badge) return false;
|
| 152 |
var r = el.getBoundingClientRect();
|
| 153 |
return r.width >= MIN && r.height >= MIN;
|
| 154 |
}
|
| 155 |
|
| 156 |
document.addEventListener('mouseover', function (e) {
|
| 157 |
var t = e.target;
|
| 158 |
+
// If hovering badge, keep it alive
|
| 159 |
+
if (badge && (t === badge || badge.contains(t))) { clearHide(); return; }
|
| 160 |
if (!isValidTarget(t)) return;
|
| 161 |
+
if (t === activeImg) { clearHide(); return; }
|
| 162 |
+
clearHide(); clearTimeout(showTimer);
|
| 163 |
+
showTimer = setTimeout(function () { show(t); }, 150);
|
| 164 |
}, true);
|
| 165 |
|
| 166 |
document.addEventListener('mouseout', function (e) {
|
| 167 |
var t = e.target;
|
| 168 |
if (!t || t.tagName !== 'IMG' || t !== activeImg) return;
|
| 169 |
var rel = e.relatedTarget;
|
| 170 |
+
// Don't hide if moving to the badge
|
| 171 |
+
if (badge && rel && (badge === rel || badge.contains(rel))) { clearHide(); return; }
|
| 172 |
clearTimeout(showTimer);
|
| 173 |
+
scheduleHide();
|
| 174 |
}, true);
|
| 175 |
|
| 176 |
window.addEventListener('scroll', function () {
|
| 177 |
if (!badge || !activeImg) return;
|
| 178 |
var r = activeImg.getBoundingClientRect();
|
| 179 |
+
if (r.bottom < -20 || r.top > window.innerHeight + 20) hide();
|
| 180 |
else pos(badge, activeImg);
|
| 181 |
}, { passive: true, capture: true });
|
| 182 |
})();
|