PageSourceSearch

https://thecompdesk.com/js/header-attorney-cta.js

js thecompdesk.com collected 2026-10-01 14:30:04 UTC 8,709 bytes, 203 lines download raw bytes

1/**
2 * header-attorney-cta.js — Universal "Contact an attorney" header CTA
3 *
4 * Self-bootstrapping. On load, injects a neutral, directory-style
5 * "Contact an attorney" button next to the site brand/wordmark in the
6 * header, plus an embedded attorney-connect wizard modal (the same flow
7 * served by /connect-with-attorney.html?embed=1). Guest-facing; no auth.
8 *
9 * Mirrors the app's entry point (the round-robin attorney-connect flow),
10 * surfaced as a persistent header CTA rather than a buried link.
11 *
12 * Brand detection covers every header variant on the site:
13 *   - static  .tcd-wordmark        (phase-1 skinned nav, ~32 pages)
14 *   - async   #app-nav .nav-logo   (nav.js renderPublicNav, calculators/tools)
15 *   - misc    .nav-logo / .wordmark
16 * A MutationObserver handles navs injected after initial load.
17 *
18 * The brand + button are wrapped in an inline-flex group so the button
19 * sits immediately beside the brand without disturbing the header's
20 * space-between layout (links / audience switcher stay in the right slot).
21 *
22 * Suppression: set `<body data-no-attorney-cta="true">` on any page that
23 * shouldn't get the CTA (e.g. the connect wizard itself, attorney-facing
24 * recruitment pages, authenticated dashboards).
25 *
26 * Styling uses the active skin tokens (var(--skin-*)) with safe fallbacks
27 * so it adapts to the worker/attorney skins and degrades on unskinned pages.
28 */
29(function injectAttorneyCta() {
30  'use strict';
31
32  var LABEL = 'Contact an attorney';
33  // Neutral balance-scale glyph (currentColor) — reads "legal", not salesy.
34  var ICON =
35    '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" ' +
36    'stroke="currentColor" stroke-width="1.7" stroke-linecap="round" ' +
37    'stroke-linejoin="round" aria-hidden="true" focusable="false">' +
38    '<path d="M12 3v18M7 21h10M5 7h14M12 7l-4 6a3 3 0 0 0 6 0l-4-6zm0 0l4 6a3 3 0 0 1-6 0l4-6z"/>' +
39    '</svg>';
40
41  // Header/nav-scoped brand selectors first, so a generic `.logo` elsewhere
42  // on the page never wins over the real header brand. Covers every header
43  // variant on the site: skinned nav, renderPublicNav, and the legacy navs.
44  var BRAND_SELECTORS = [
45    '.tcd-wordmark',
46    '#app-nav .nav-logo',
47    'nav .nav-logo', 'nav .nav-brand', 'nav .logo', 'nav .wordmark',
48    'header .nav-logo', 'header .nav-brand', 'header .logo', 'header .wordmark',
49    '.nav-logo', '.nav-brand', '.wordmark', '.logo'
50  ];
51
52  function suppressed() {
53    return document.body && document.body.getAttribute('data-no-attorney-cta') === 'true';
54  }
55
56  // ── Embed src — carry UTM params through to the wizard ──────────────
57  function buildEmbedSrc() {
58    var qs = new URLSearchParams(window.location.search);
59    var out = new URLSearchParams();
60    out.set('embed', '1');
61    ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(function (k) {
62      if (qs.get(k)) out.set(k, qs.get(k));
63    });
64    return '/connect-with-attorney.html?' + out.toString();
65  }
66
67  // ── Modal (built lazily on first open) ──────────────────────────────
68  var overlay = null;
69  var frame = null;
70
71  function buildModal() {
72    if (overlay) return;
73    overlay = document.createElement('div');
74    overlay.setAttribute('data-tcd', 'attorney-connect-overlay');
75    overlay.setAttribute('role', 'dialog');
76    overlay.setAttribute('aria-modal', 'true');
77    overlay.setAttribute('aria-label', 'Connect with a Workers’ Compensation Attorney');
78    overlay.style.cssText =
79      'display:none;position:fixed;inset:0;z-index:100000;' +
80      'background:rgba(7,16,31,0.72);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);';
81
82    var closeBtn = document.createElement('button');
83    closeBtn.type = 'button';
84    closeBtn.setAttribute('aria-label', 'Close');
85    closeBtn.innerHTML = '&times;';
86    closeBtn.style.cssText =
87      'position:fixed;top:16px;right:16px;z-index:100001;cursor:pointer;' +
88      'width:40px;height:40px;border-radius:8px;font-size:24px;line-height:1;' +
89      'display:flex;align-items:center;justify-content:center;' +
90      'background:rgba(20,28,44,0.92);border:1px solid rgba(255,255,255,0.18);color:#fff;';
91    closeBtn.addEventListener('click', closeModal);
92
93    frame = document.createElement('iframe');
94    frame.title = 'Connect with a Workers’ Compensation Attorney';
95    frame.setAttribute('allow', 'clipboard-write');
96    frame.setAttribute('referrerpolicy', 'same-origin');
97    frame.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent;';
98
99    overlay.appendChild(closeBtn);
100    overlay.appendChild(frame);
101    overlay.addEventListener('click', function (e) { if (e.target === overlay) closeModal(); });
102    document.body.appendChild(overlay);
103  }
104
105  function openModal() {
106    buildModal();
107    var src = buildEmbedSrc();
108    if (frame.getAttribute('src') !== src) frame.setAttribute('src', src);
109    overlay.style.display = 'block';
110    document.body.style.overflow = 'hidden';
111  }
112
113  function closeModal() {
114    if (!overlay) return;
115    overlay.style.display = 'none';
116    document.body.style.overflow = '';
117    if (frame) frame.setAttribute('src', 'about:blank');
118  }
119
120  document.addEventListener('keydown', function (e) {
121    if (e.key === 'Escape' && overlay && overlay.style.display === 'block') closeModal();
122  });
123  window.addEventListener('message', function (e) {
124    if (e && e.data && e.data.type === 'compdesk:closeConnectMod
124al') closeModal();
125  });
126
127  // Expose for any page that wants to trigger the same flow.
128  window.tcdOpenAttorneyConnect = openModal;
129
130  // ── Button ──────────────────────────────────────────────────────────
131  function buildButton() {
132    var btn = document.createElement('button');
133    btn.type = 'button';
134    btn.setAttribute('data-tcd', 'attorney-cta');
135    btn.setAttribute('aria-label', LABEL);
136    btn.innerHTML = ICON + '<span>' + LABEL + '</span>';
137    btn.style.cssText = [
138      'display:inline-flex', 'align-items:center', 'gap:7px',
139      'padding:8px 15px', 'border-radius:999px',
140      'font-family:inherit', 'font-weight:600', 'font-size:13.5px', 'line-height:1',
141      'white-space:nowrap', 'cursor:pointer',
142      'background:transparent',
143      'color:var(--skin-text, #f4f6fa)',
144      'border:1.5px solid var(--skin-divider, rgba(255,255,255,0.20))',
145      'transition:background .18s ease, border-color .18s ease, transform .18s ease'
146    ].join(';') + ';';
147    btn.addEventListener('mouseenter', function () {
148      btn.style.background = 'var(--skin-surface-elev, rgba(255,255,255,0.07))';
149      btn.style.borderColor = 'var(--skin-accent, #4f8ff7)';
150      btn.style.transform = 'translateY(-1px)';
151    });
152    btn.addEventListener('mouseleave', function () {
153      btn.style.background = 'transparent';
154      btn.style.borderColor = 'var(--skin-divider, rgba(255,255,255,0.20))';
155      btn.style.transform = 'none';
156    });
157    btn.addEventListener('click', openModal);
158    return btn;
159  }
160
161  function findBrand() {
162    for (var i = 0; i < BRAND_SELECTORS.length; i++) {
163      var el = document.querySelector(BRAND_SELECTORS[i]);
164      if (el) return el;
165    }
166    return null;
167  }
168
169  function placeButton() {
170    if (suppressed()) return true; // treat as "done" — nothing to place
171    if (document.querySelector('[data-tcd="attorney-cta"]')) return true; // idempotent
172    var brand = findBrand();
173    if (!brand || !brand.parentNode) return false;
174
175    // Wrap brand + button together so the button stays beside the brand
176    // inside a space-between header (links / switcher keep the right slot).
177    var group = document.createElement('span');
178    group.setAttribute('data-tcd', 'attorney-cta-group');
179    group.style.cssText = 'display:inline-flex;align-items:center;gap:14px;min-width:0;';
180    brand.parentNode.insertBefore(group, brand);
181    group.appendChild(brand);
182    group.appendChild(buildButton());
183    return true;
184  }
185
186  function start() {
187    if (placeButton()) return;
188    // Header not in the DOM yet (e.g. nav.js renderPublicNav runs async).
189    // Watch for it, then place once and stop.
190    var obs = new MutationObserver(function () {
191      if (placeButton()) obs.disconnect();
192    });
193    obs.observe(document.body, { childList: true, subtree: true });
194    // Safety: stop observing after 10s regardless.
195    setTimeout(function () { obs.disconnect(); }, 10000);
196  }
197
198  if (document.readyState === 'loading') {
199    document.addEventListener('DOMContentLoaded', start);
200  } else {
201    start();
202  }
203})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.