asdf98 commited on
Commit
3fdb1ce
·
verified ·
1 Parent(s): 3745156

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.__muse_hover_v9) return;
4
- window.__muse_hover_v9 = true;
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('__muse_badge_css')) return;
15
- var s = document.createElement('style'); s.id = '__muse_badge_css';
16
- s.textContent = '#__muse_add_badge{all:initial;position:fixed;z-index:2147483647;display:flex;align-items:center;gap:4px;padding:7px 14px;background:rgba(255,255,255,0.97);color:#111;border-radius:999px;box-shadow:0 4px 20px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.05);font:700 11px/1 -apple-system,BlinkMacSystemFont,"Inter",sans-serif;cursor:pointer;opacity:0;transform:translateY(4px) scale(0.92);transition:opacity .15s,transform .15s;pointer-events:none;letter-spacing:0.02em;user-select:none}#__muse_add_badge.v{opacity:1;transform:none;pointer-events:auto}#__muse_add_badge:hover{background:#fff;box-shadow:0 6px 28px rgba(0,0,0,0.35),0 0 0 1px rgba(0,0,0,0.08);transform:scale(1.05)}#__muse_add_badge:active{transform:scale(0.94)}#__muse_add_badge svg{width:14px;height:14px;stroke-width:2.5}#__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}#__mt7.s{opacity:1;transform:translateX(-50%) translateY(0)}';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 bestFromSrcset(srcset) {
24
- if (!srcset) return '';
25
- try {
 
26
  var best = '', bestScore = 0;
27
  srcset.split(',').forEach(function(part) {
28
- var bits = part.trim().split(/\s+/);
29
- var u = bits[0];
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
- return best ? absUrl(best) : '';
38
- } catch (_) { return ''; }
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'); }, 1500);
59
  }
60
 
61
- function addToBoard(url, title, w, h) {
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
- // 2) Fallback: registered custom protocol through image beacon.
69
- try {
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
- // 3) Extra fallback for platforms where fetch triggers scheme handlers.
82
- try { fetch(u).catch(function(){}); } catch (_) {}
 
 
83
  }
84
 
85
  function pos(el, img) {
86
  var r = img.getBoundingClientRect();
87
- var x = r.right - 78;
88
- var y = r.top + 8;
89
- if (x < r.left + 8) x = r.left + 8;
90
- if (x > innerWidth - 90) x = innerWidth - 90;
 
91
  if (y < 4) y = 4;
92
- if (y > innerHeight - 42) y = innerHeight - 42;
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> ADD';
112
- ['mousedown','pointerdown','pointerup'].forEach(function(ev){ badge.addEventListener(ev, function(e){ e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }, true); });
 
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
- if (!url || url.startsWith('data:') || url.startsWith('blob:')) { toast('Cannot capture this image'); return; }
121
- addToBoard(url, title, w, h);
122
  toast('✓ Added to Board');
 
123
  }, true);
124
- badge.onmouseenter = function () { clearTimeout(hideTimer); };
125
- badge.onmouseleave = function () { hideTimer = setTimeout(hide, 300); };
 
 
 
 
 
 
 
 
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) { clearTimeout(hideTimer); return; }
143
- clearTimeout(hideTimer); clearTimeout(showTimer);
144
- showTimer = setTimeout(function () { show(t); }, 120);
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
- if (badge && (badge === rel || badge.contains && badge.contains(rel))) return;
 
152
  clearTimeout(showTimer);
153
- hideTimer = setTimeout(hide, 300);
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
  })();