[object Object]

← back to Rentv

crm: open external action links in a reusable popup window instead of a right-dock iframe (many sites block embedding via X-Frame-Options/CSP)

78864166b5e78af0e7a77946ea06ccc71897468f · 2026-08-13 07:31:43 -0700 · Steve Abrams

Files touched

Diff

commit 78864166b5e78af0e7a77946ea06ccc71897468f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 07:31:43 2026 -0700

    crm: open external action links in a reusable popup window instead of a right-dock iframe (many sites block embedding via X-Frame-Options/CSP)
---
 public/crm.html | 80 +++++++++++++++++----------------------------------------
 1 file changed, 23 insertions(+), 57 deletions(-)

diff --git a/public/crm.html b/public/crm.html
index 0432f12d..ccf234b3 100644
--- a/public/crm.html
+++ b/public/crm.html
@@ -130,22 +130,7 @@
   .tbl .tav{width:34px;height:34px;border-radius:7px;object-fit:cover;object-position:center top;border:1px solid var(--line);display:block}
   .tbl a.act{color:#8fb7ff;text-decoration:none}.tbl a.act:hover{text-decoration:underline}
   .xref{background:#12161c;border:1px solid var(--red);color:#e7ecf1;border-radius:20px;padding:3px 9px;font:800 11px 'Inter';cursor:pointer;white-space:nowrap}.xref:hover{background:var(--red);color:#fff}
-  /* ── inline preview panel (right dock) ── opens web action links in an iframe instead of a new tab */
-  .pv-scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:.2s;z-index:970}.pv-scrim.open{opacity:1;pointer-events:auto}
-  .pv{position:fixed;top:0;right:0;bottom:0;width:46vw;min-width:380px;max-width:820px;background:#0b0e12;border-left:1px solid var(--line);transform:translateX(102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:971;display:flex;flex-direction:column;box-shadow:-2px 0 44px rgba(0,0,0,.5)}
-  .pv.open{transform:none}
-  .pv .pvh{display:flex;align-items:center;gap:9px;padding:10px 12px;border-bottom:1px solid var(--line);background:#0d1014}
-  .pv .pvh .t{font-weight:800;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:60px}
-  .pv .pvh .u{font-size:11px;color:#6b747d;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:34%}
-  .pv .pvh a,.pv .pvh button{background:#12161c;border:1px solid #2a333d;border-radius:8px;color:#dbe1e7;font:700 12px 'Inter';padding:6px 10px;cursor:pointer;white-space:nowrap;text-decoration:none;line-height:1}.pv .pvh a:hover,.pv .pvh button:hover{border-color:var(--red);color:#fff}
-  .pv .pvbody{position:relative;flex:1;background:#fff;overflow:hidden}
-  .pv iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#fff}
-  .pv .pvstate{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;gap:14px;text-align:center;padding:30px;background:#0d1014;color:#c3cbd3;z-index:2}
-  .pv .pvbody.loading .pvload,.pv .pvbody.blocked .pvblock{display:flex}
-  .pv .pvstate .big{font-size:16px;font-weight:800}.pv .pvstate .sm{font-size:12.5px;color:#8b939b;max-width:360px;line-height:1.55}
-  .pv .pvstate a.open{background:var(--red);border:0;border-radius:9px;color:#fff;font:800 13px 'Inter';padding:11px 18px;text-decoration:none}.pv .pvstate a.open:hover{filter:brightness(1.08)}
-  .pv .spin{width:26px;height:26px;border:3px solid #232a32;border-top-color:var(--red);border-radius:50%;animation:pvspin .8s linear infinite}@keyframes pvspin{to{transform:rotate(360deg)}}
-  @media(max-width:900px){.pv{width:100vw;max-width:none;min-width:0}.pv .pvh .u{display:none}}
+  /* external action links open in a reusable popup window (window.open) — no inline iframe panel */
 </style>
 </head>
 <body>
@@ -209,25 +194,7 @@
   </div>
 </div>
 
-<!-- inline preview: right-dock iframe panel -->
-<div class="pv-scrim" id="pvScrim"></div>
-<aside class="pv" id="pv" aria-hidden="true">
-  <div class="pvh">
-    <span class="t" id="pvTitle">Preview</span>
-    <span class="u" id="pvUrl"></span>
-    <a id="pvOpen" href="#" target="_blank" rel="noopener noreferrer">Open ↗</a>
-    <button id="pvClose" aria-label="Close preview">✕</button>
-  </div>
-  <div class="pvbody" id="pvBody">
-    <div class="pvstate pvload"><div class="spin"></div><div class="sm">Loading inline preview…</div></div>
-    <div class="pvstate pvblock">
-      <div class="big">🔒 This site blocks inline preview</div>
-      <div class="sm">Some sites (LinkedIn, Google, Facebook/X) refuse to load inside an embedded frame for security. Open it in a new tab instead — the CRM stays here behind it.</div>
-      <a class="open" id="pvBlockOpen" href="#" target="_blank" rel="noopener noreferrer">Open in new tab ↗</a>
-    </div>
-    <iframe id="pvFrame" referrerpolicy="strict-origin-when-cross-origin" allow="fullscreen" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"></iframe>
-  </div>
-</aside>
+<!-- external action links open in a reusable popup window (see pvOpen) — no inline iframe -->
 <script>
 const $=s=>document.getElementById(s), enc=encodeURIComponent;
 const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
@@ -568,31 +535,30 @@ $('dens').oninput=e=>{ S.dens=+e.target.value; setLS('dens',S.dens); applyDensit
 document.querySelectorAll('#modeToggle button').forEach(b=>b.onclick=()=>{ S.mode=b.dataset.mode; setLS('mode',S.mode); render(); });
 $('facetClear').onclick=()=>{ FACETS.forEach(f=>S.facet[f.key]=[]); setLS('facet',JSON.stringify(S.facet)); S.sel=null; render(); };
 
-// ── inline preview panel ── route web action links into a right-dock iframe instead of a new tab
-const PV={t:null};
-function pvEmbed(u){ try{const m=u.match(/[?&]query=([^&]+)/);if(/google\.[^/]+\/maps\/search/i.test(u)&&m)return 'https://maps.google.com/maps?q='+m[1]+'&z=14&output=embed';}catch(e){} return u; }
-const PV_HARD=[/google\.[a-z.]+\/search/i,/linkedin\.com/i,/facebook\.com/i,/(^|\/\/)(www\.)?(x|twitter)\.com/i,/instagram\.com/i];
-function pvHardBlocked(u){return PV_HARD.some(re=>re.test(u));}
-function pvClose(){clearTimeout(PV.t);$('pvFrame').onload=null;$('pv').classList.remove('open');$('pvScrim').classList.remove('open');$('pv').setAttribute('aria-hidden','true');$('pvFrame').src='about:blank';$('pvBody').classList.remove('loading','blocked');}
-function pvOpen(href,label){
-  const body=$('pvBody'),fr=$('pvFrame');
-  $('pvTitle').textContent=label||'Preview';
-  try{$('pvUrl').textContent=new URL(href).hostname.replace(/^www\./,'');}catch(e){$('pvUrl').textContent='';}
-  $('pvOpen').href=href;$('pvBlockOpen').href=href;
-  clearTimeout(PV.t);fr.onload=null;body.classList.remove('loading','blocked');
-  $('pv').classList.add('open');$('pvScrim').classList.add('open');$('pv').setAttribute('aria-hidden','false');
-  if(pvHardBlocked(href)){fr.src='about:blank';body.classList.add('blocked');return;}
-  body.classList.add('loading');
-  fr.onload=()=>{clearTimeout(PV.t);body.classList.remove('loading','blocked');};
-  fr.src=pvEmbed(href);
-  PV.t=setTimeout(()=>{if(body.classList.contains('loading')){body.classList.remove('loading');body.classList.add('blocked');}},4200);
+// ── external action links open in a single reusable POPUP WINDOW ──
+// Most sites (LinkedIn, Google, Facebook/X, Instagram, many vendor pages) refuse to
+// load inside an <iframe> via X-Frame-Options / CSP frame-ancestors, so the old
+// right-dock preview rendered blank. A window.open() popup is a TOP-LEVEL browsing
+// context — those anti-framing headers don't apply — so it always renders, and the
+// user just closes it to return to the CRM. One named window ('crmPreview') is
+// reused across clicks so repeat opens don't litter the desktop.
+let PVWIN=null;
+function pvOpen(href){
+  try{
+    const w=Math.min(1100,Math.round(screen.availWidth*0.62)),
+          h=Math.round(screen.availHeight*0.86),
+          x=Math.max(screen.availLeft,screen.availLeft+screen.availWidth-w-24),
+          y=screen.availTop+Math.round((screen.availHeight-h)/2);
+    const feat=`popup=yes,width=${w},height=${h},left=${x},top=${y},scrollbars=yes,resizable=yes`;
+    if(PVWIN&&!PVWIN.closed){ PVWIN.location.href=href; }      // reuse the open popup
+    else { PVWIN=window.open(href,'crmPreview',feat); }
+    if(PVWIN){ PVWIN.focus(); } else { window.open(href,'_blank','noopener'); } // popup blocked → new tab
+  }catch(e){ window.open(href,'_blank','noopener'); }
 }
-$('pvClose').onclick=pvClose;$('pvScrim').onclick=pvClose;
-document.addEventListener('keydown',e=>{if(e.key==='Escape'&&$('pv').classList.contains('open'))pvClose();});
-document.addEventListener('click',e=>{ // plain left-click on an action link → inline panel; cmd/ctrl/middle-click still opens a native new tab
+document.addEventListener('click',e=>{ // action link → reusable popup window (cmd/ctrl/middle-click still does a native new tab)
   const a=e.target.closest('a.act[data-pv]');if(!a)return;
   if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0)return;
-  e.preventDefault();e.stopPropagation();pvOpen(a.getAttribute('href'),(a.textContent||'').trim());
+  e.preventDefault();e.stopPropagation();pvOpen(a.getAttribute('href'));
 },true);
 
 load();

← 4317e810 auto-data-snapshot: 2026-08-13T07:20:35 (7 data files) — dat  ·  back to Rentv  ·  auto-data-snapshot: 2026-08-13T07:51:09 (7 data files) — dat de85e39a →