/* === Advally Bug Report: tokens (XF style properties; the fallback chain covers styles that have variations disabled and therefore emit no --xf-* custom properties) === */ .advally-bug-ui { --advally-bug-primary: hsl(var(--xf-buttonPrimaryBg)); --advally-bug-mask: hsla(var(--xf-overlayMaskColor)); --advally-bug-outline: hsl(var(--xf-errorFeatureColor)); --advally-bug-content: hsl(var(--xf-contentBg)); --advally-bug-text: hsl(var(--xf-textColor)); --advally-bug-muted: hsl(var(--xf-textColorMuted)); --advally-bug-border: hsl(var(--xf-borderColor)); --advally-bug-highlightBg: hsl(var(--xf-contentHighlightBg)); } .advally-bug-ui [hidden] { display: none !important; } .advally-bug-mark { display: block; border-radius: 50%; object-fit: cover; } /* === Bubble (bottom-right, collapses to an edge tab) === */ #advally-bug-root { position: fixed; bottom: 80px; right: 20px; z-index: 99999; display: flex; align-items: center; gap: 4px; transition: opacity .2s, transform .2s; } #advally-bug-root.is-hidden { opacity: 0; pointer-events: none; transform: translateX(20px); visibility: hidden; transition: opacity .2s, transform .2s, visibility 0s .2s; } /* The launcher is TARS alone: 50px, arm down at rest, saluting when the pointer or keyboard focus arrives, with the words in a tooltip beside him (Doug, 2026-09-30). */ #advally-bug-bubble { width: 50px; height: 50px; box-sizing: border-box; padding: 0; border-radius: 50%; border: 0; background: transparent; box-shadow: 0 4px 12px rgba(0,0,0,.3); cursor: pointer; display: flex; align-items: center; justify-content: center; overflow: hidden; transition: transform .2s, box-shadow .2s; } #advally-bug-bubble:hover { transform: scale(1.08); box-shadow: 0 6px 16px rgba(0,0,0,.4); } #advally-bug-bubble:active { transform: scale(.95); } #advally-bug-bubble .advally-bug-tars { display: block; width: 50px; height: 50px; max-width: none; flex: none; } /* The arm is two bars on two joints. At rest the shoulder turns it straight down and the elbow straightens; hover and focus bring both back to zero, which is the salute as drawn. */ #advally-bug-root .advally-bug-tars-upper { transform: rotate(-90deg); transition: transform .42s cubic-bezier(.34,1.4,.5,1); } #advally-bug-root .advally-bug-tars-fore { transform: rotate(-147deg); transition: transform .42s cubic-bezier(.34,1.4,.5,1) .04s; } /* Only TARS himself raises the salute and the tooltip: his own hover, or keyboard focus on him. The hide arrow does not, and neither does the focus the form hands back when it closes (4.2.4 keyed both to the whole launcher, so they stuck after a mouse close). */ #advally-bug-bubble:hover .advally-bug-tars-upper, #advally-bug-bubble:hover .advally-bug-tars-fore, #advally-bug-bubble:focus-visible .advally-bug-tars-upper, #advally-bug-bubble:focus-visible .advally-bug-tars-fore { transform: rotate(0deg); } /* The tooltip sits to the left of the launcher and only exists where there is a pointer to hover with. */ /* The label is a quiet chip that hugs TARS. Its ground is the page's text colour and its ink is the page's surface, so it stands off any theme without a border, and it sets its own typeface: the 4.2.4 box borrowed the board's panel, border and font and read as a 1990s tooltip. */ #advally-bug-root .advally-bug-tip { position: absolute; right: calc(100% + 10px); top: 50%; transform: translate(4px, -50%); white-space: nowrap; padding: 7px 11px; border-radius: 999px; background: var(--advally-bug-text, CanvasText); color: var(--advally-bug-content, Canvas); box-shadow: 0 4px 14px rgba(0,0,0,.22); font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 12px; font-weight: 500; font-style: normal; letter-spacing: .01em; line-height: 1; text-transform: none; opacity: 0; pointer-events: none; transition: opacity .16s ease-out, transform .16s ease-out; } #advally-bug-bubble:hover ~ .advally-bug-tip, #advally-bug-bubble:focus-visible ~ .advally-bug-tip { opacity: 1; transform: translate(0, -50%); } @media (hover: none) { #advally-bug-root .advally-bug-tip { display: none; } } @media (prefers-reduced-motion: reduce) { #advally-bug-root .advally-bug-tars-upper, #advally-bug-root .advally-bug-tars-fore, #advally-bug-root .advally-bug-tip { transition: none; } } /* The hide control is a small badge on TARS's far shoulder, in the chip's colours: clear of the saluting arm, and nothing sits between him and his label. Its ::after widens the target to 30px. A touch screen has no hover to reveal it, so there it always shows. */ #advally-bug-collapse-btn { position: absolute; top: -7px; right: -7px; z-index: 1; width: 22px; height: 22px; box-sizing: border-box; padding: 0; border-radius: 50%; border: 0; background: var(--advally-bug-text, CanvasText); color: var(--advally-bug-content, Canvas); box-shadow: 0 1px 4px rgba(0,0,0,.3); cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity .2s; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 1; } #advally-bug-collapse-btn::after { content: ""; position: absolute; inset: -4px; border-radius: 50%; } #advally-bug-root:hover #advally-bug-collapse-btn, #advally-bug-collapse-btn:focus-visible { opacity: 1; } @media (hover: none) { #advally-bug-collapse-btn { opacity: 1; } } #advally-bug-tab { position: fixed; bottom: 100px; right: 0; z-index: 99999; width: 32px; height: 48px; box-sizing: border-box; padding: 0; border: 0; border-radius: 8px 0 0 8px; background: var(--advally-bug-primary, Highlight); box-shadow: -2px 2px 8px rgba(0,0,0,.2); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: opacity .2s, transform .2s, width .2s; } #advally-bug-tab.is-hidden { opacity: 0; pointer-events: none; transform: translateX(100%); visibility: hidden; transition: opacity .2s, transform .2s, width .2s, visibility 0s .2s; } #advally-bug-tab:hover { width: 40px; } #advally-bug-tab .advally-bug-mark { width: 26px; height: 26px; max-width: none; flex: none; } /* === Modal === */ #advally-bug-modal { position: fixed; inset: 0; z-index: 99998; display: none; align-items: center; justify-content: center; padding: 16px; box-sizing: border-box; background-color: var(--advally-bug-mask, rgba(0,0,0,.5)); background-image: linear-gradient(rgba(0,0,0,.45), rgba(0,0,0,.45)); } #advally-bug-modal.is-open { display: flex; } /* The launcher sits above the mask (z 99999) and would cover the sheet's buttons on a phone; the approved mockup shows no bubble while the card is open. */ .advally-bug-modal-open #advally-bug-root, .advally-bug-modal-open #advally-bug-tab { display: none; } /* The card paints its own surface. It borrowed .block-container's until 4.1.3, and the Newschoolers theme ships `.block-container { background: transparent }` (IT 2352): the form floated over the page with no card at all. The fallbacks are system colours, so a style that emits no --xf-* tokens (AudioKarma, 4Runner) still gets a readable card. */ #advally-bug-modal .advally-bug-card { margin: 0; width: 100%; max-width: 560px; max-height: 92vh; overflow: auto; box-sizing: border-box; background: var(--advally-bug-content, Canvas); color: var(--advally-bug-text, CanvasText); border: 1px solid var(--advally-bug-border, ButtonBorder); border-radius: 8px; box-shadow: 0 16px 48px rgba(0,0,0,.35); } /* Sent: the form folds away and only the confirmation, the ticket link and Close remain, so the state reads as finished (IT 2352). */ #advally-bug-modal .advally-bug-card.is-sent .advally-bug-label, #advally-bug-modal .advally-bug-card.is-sent textarea.input, #advally-bug-modal .advally-bug-card.is-sent .advally-bug-pick, #advally-bug-modal .advally-bug-card.is-sent .formRow-explain, #advally-bug-modal .advally-bug-card.is-sent .advally-bug-footer-note, #advally-bug-modal .advally-bug-card.is-sent .advally-bug-submit { display: none; } #advally-bug-modal .advally-bug-card.is-sent form { display: flex; flex-direction: column; } #advally-bug-modal .advally-bug-card.is-sent .advally-bug-message { order: 1; margin-top: 0; font-size: 16px; line-height: 1.5; } #advally-bug-modal .advally-bug-card.is-sent .advally-bug-actions { order: 2; margin-top: 16px; } #advally-bug-modal .block-header { display: flex; align-items: center; gap: 8px; margin: 0; } #advally-bug-modal .block-header .advally-bug-mark { width: 24px; height: 24px; flex: none; } #advally-bug-modal .advally-bug-label { display: block; font-weight: 600; margin-bottom: 6px; } #advally-bug-modal textarea.input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 72px; } #advally-bug-modal .advally-bug-pick { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; } #advally-bug-modal .advally-bug-pick .button svg { width: 14px; height: 14px; vertical-align: -2px; margin-right: 4px; } #advally-bug-modal .advally-bug-chip { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; padding: 3px 4px 3px 10px; border-radius: 999px; border: 1px solid var(--advally-bug-border, ButtonBorder); background: var(--advally-bug-highlightBg, Canvas); font-size: 13px; line-height: 1.4; } #advally-bug-modal .advally-bug-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; } #advally-bug-modal .advally-bug-chip-clear { border: 0; background: transparent; color: inherit; cursor: pointer; font-size: 16px; line-height: 1; padding: 0 5px; border-radius: 50%; } #advally-bug-modal .advally-bug-chip-clear:hover { background: var(--advally-bug-border, ButtonBorder); } #advally-bug-modal .formRow-explain { margin-top: 6px; } #advally-bug-modal .advally-bug-footer-note { display: flex; align-items: flex-start; gap: 6px; margin-top: 12px; font-size: 12px; color: var(--advally-bug-muted, GrayText); } #advally-bug-modal .advally-bug-footer-note svg { width: 14px; height: 14px; flex: none; margin-top: 2px; } #advally-bug-modal .advally-bug-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; } #advally-bug-modal .advally-bug-message { margin-top: 12px; font-size: 13px; } #advally-bug-modal .advally-bug-message--error, #advally-bug-modal .advally-bug-message--empty { color: var(--advally-bug-outline, currentColor); } #advally-bug-modal .advally-bug-proof { margin-top: 4px; color: var(--advally-bug-muted, GrayText); } #advally-bug-modal .advally-bug-spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; margin-right: 6px; vertical-align: -3px; animation: advally-bug-spin .8s linear infinite; } @keyframes advally-bug-spin { to { transform: rotate(360deg); } } @media (max-width: 640px) { #advally-bug-modal { align-items: flex-end; padding: 0; } #advally-bug-modal .advally-bug-card { width: 100%; max-width: none; border-radius: 12px 12px 0 0; max-height: 92vh; overflow: auto; } #advally-bug-modal .advally-bug-chip-label { max-width: 160px; } } @media (prefers-reduced-motion: reduce) { #advally-bug-root, #advally-bug-root.is-hidden, #advally-bug-tab, #advally-bug-tab.is-hidden, #advally-bug-bubble, #advally-bug-collapse-btn { transition: none; } #advally-bug-modal .advally-bug-spinner { animation-duration: 1.6s; } } /* === Identify mode (one fixed highlight box; nothing is added to live elements) === */ .advally-bug-identify-mode, .advally-bug-identify-mode * { cursor: crosshair !important; } .advally-bug-identify-mode #advally-bug-root, .advally-bug-identify-mode #advally-bug-tab { display: none; } #advally-bug-highlight { position: fixed; display: none; pointer-events: none; z-index: 99997; box-sizing: border-box; border-radius: 2px; outline: 3px solid var(--advally-bug-outline, currentColor); outline-offset: 2px; } #advally-bug-highlight.is-active { display: block; } #advally-bug-identify-overlay { position: fixed; inset: 0; z-index: 99996; display: none; pointer-events: none; } #advally-bug-identify-overlay.is-active { display: block; } #advally-bug-identify-overlay .advally-bug-identify-mask { position: absolute; inset: 0; background: var(--advally-bug-mask, rgba(0,0,0,.5)); opacity: .5; } /* The bar borrows block-container like the card, so it paints the card's own surface: a theme that blanks .block-container (Newschoolers) left it see-through, black text on the black header (IT 2352). */ #advally-bug-identify-overlay .advally-bug-identify-instruction { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); max-width: calc(100% - 32px); box-sizing: border-box; margin: 0; pointer-events: auto; display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,.3); font-size: 14px; cursor: default; background: var(--advally-bug-content, Canvas); color: var(--advally-bug-text, CanvasText); border: 1px solid var(--advally-bug-border, ButtonBorder); }
{{ phrase('advally_feedback_widget_tooltip') }}
(function(){ function init(){ var MAX_OUTER_HTML = 1500, MAX_SELECTOR_DEPTH = 8, MAX_ANCESTOR_PATH = 5, STORAGE_KEY = 'advallyBugReporterCollapsed'; var MAX_CONSOLE = 100, MAX_NETWORK = 50, MAX_MESSAGE = 1000, MAX_JSON_BYTES = 100000; var MAX_SHOT_BYTES = 900000, SHOT_QUALITY = 0.7, SHOT_RETRY_QUALITY = 0.5, SHOT_TIMEOUT_MS = 15000, SHOT_LIB_WAIT_MS = 5000; /* How long the library may wait on ONE resource before it gives up on that resource and carries on. Its own default is 30s, which is longer than SHOT_TIMEOUT_MS above, so a single stubborn asset ran out OUR budget and the report filed with no screenshot at all. Measured on the culturecoma front page, 1280x900, 9 ad iframes (IT #2009): 33.5s at the default, 6.5s at 3s, byte-for-byte the same 66 KB JPEG. Keep it well under SHOT_TIMEOUT_MS -- the library returning a placeholder for one slow image beats us aborting the whole capture. It is NOT only a per-resource cap, which is why captureScreenshot below checks the canvas: the library spends this same value on the final SVG->canvas decode, and that wait resolves on the timer whether the image decoded or not (IT #2009). */ var SHOT_RESOURCE_TIMEOUT_MS = 3000; var SHOT_BELOW_MARGIN_PX = 200, SHOT_ABOVE_MARGIN_PX = 200; /* The longest the capture may hold the main thread before it hands it back. The capture starts as the form opens, and run in one piece it held the page for 1.3-2.6s on the culturecoma front page, so the reporter's first keystrokes queued behind it (IT 2438). */ var SHOT_SLICE_MS = 12; /* A box whose children can go while it keeps its size: the library copies every element's computed width and height onto its clone, and these are the displays that honour them. An inline box does not, so emptying one would reflow the text it sits in (IT 2352). */ var SHOT_SIZED_DISPLAYS = { 'block': 1, 'flow-root': 1, 'list-item': 1, 'flex': 1, 'grid': 1, 'inline-block': 1, 'inline-flex': 1, 'inline-grid': 1 }; var UI_CLASS = 'advally-bug-ui', TAP_SLOP = 10; var root = document.getElementById('advally-bug-root'), tab = document.getElementById('advally-bug-tab'), collapseBtn = document.getElementById('advally-bug-collapse-btn'); var bubble = document.getElementById('advally-bug-bubble'), modal = document.getElementById('advally-bug-modal'), form = document.getElementById('advally-bug-form'); if (!root || !tab || !bubble || !modal || !form) return; var card = modal.querySelector('.advally-bug-card'); var textarea = document.getElementById('advally-bug-what-wrong'), pageUrlInput = form.querySelector('[name=page_url]'); var identifyBtn = document.getElementById('advally-bug-identify-btn'), chip = document.getElementById('advally-bug-chip'), chipLabel = chip.querySelector('.advally-bug-chip-label'), chipClear = document.getElementById('advally-bug-chip-clear'); var cancelBtn = form.querySelector('.advally-bug-cancel'), cancelText = cancelBtn.querySelector('.button-text') || cancelBtn, submitBtn = form.querySelector('.advally-bug-submit'); var msgBox = form.querySelector('.advally-bug-message'), msgSending = msgBox.querySelector('.advally-bug-message--sending'), msgSuccess = msgBox.querySelector('.advally-bug-message--success'), msgError = msgBox.querySelector('.advally-bug-message--error'), msgEmpty = msgBox.querySelector('.advally-bug-message--empty'); var ticketLink = msgSuccess.querySelector('.advally-bug-ticket-link'), proofLine = msgSuccess.querySelector('.advally-bug-proof'), errorDetail = msgError.querySelector('.advally-bug-error-detail'); var overlay = document.getElementById('advally-bug-identify-overlay'), cancelIdentifyBtn = document.getElementById('advally-bug-cancel-identify'), highlight = document.getElementById('advally-bug-highlight'); var ds = modal.dataset || {}; var selected = null, isIdentifyMode = false, openedAt = null, busy = false, sent = false, lastFocus = null, openedByPointer = false; var pointerStart = null, hoverTarget = null, swallowNextClick = false, swallowTimer = null, shotLibPromise = null; /* ----- element labelling + selector path ----- */ function elLabel(el){ if (!el) return 'unknown element'; if (el.tagName === 'LABEL') return (el.textContent||'').trim().replace(/\*$/,'').trim(); if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') { if (el.id) { var lbl = document.querySelector('label[for="'+el.id+'"]'); if (lbl) return (lbl.textContent||'').trim().replace(/\*$/,'').trim() + ' field'; } return (el.placeholder || el.name || 'Input') + ' field'; } if (el.tagName === 'BUTTON' || (el.tagName === 'A' && el.classList.contains('button'))) return ((el.textContent||'').trim() || el.title || el.getAttribute('aria-label') || 'Button') + ' button'; if (el.tagName === 'TD') { var tbl = el.closest('table'); if (tbl) { var th = tbl.querySelector('thead th:nth-child('+(el.cellIndex+1)+')'); if (th) return (th.textContent||'').trim() + ' column'; } return 'Table cell'; } if (el.tagName === 'TH') return (el.textContent||'').trim() + ' column header'; if (el.tagName === 'IMG') return (el.alt || el.title || 'Image').trim().substring(0, 50) + ' image'; if (/^H[1-6]$/.test(el.tagName)) return (el.textContent||'').trim(); if (el.classList.contains('p-nav-list-item') || el.classList.contains('menu-linkRow')) return (el.textContent||'').trim() + ' menu item'; var t = (el.textContent||'').trim().replace(/\s+/g, ' ').substring(0,50); if (t && t.length > 2) return t + (t.length >= 50 ? '...' : ''); return el.tagName.toLowerCase() + ' element'; } function cssSelectorFor(el){ if (!el || el.nodeType !== 1) return null; if (el === document.body) return 'body'; var parts = [], cur = el; while (cur && cur !== document.body && parts.length < MAX_SELECTOR_DEPTH) { var p = cur.tagName.toLowerCase(); if (cur.id) { parts.unshift('#' + cur.id); break; } if (cur.className && typeof cur.className === 'string') { var classes = cur.className.trim().split(/\s+/).filter(function(c){ return c && c.indexOf('advally-bug-') !== 0; }).slice(0,3).join('.'); if (classes) p += '.' + classes; } var par = cur.parentElement; if (par) { var sib = Array.prototype.filter.call(par.children, function(c){ return c.tagName === cur.tagName; }); if (sib.length > 1) p += ':nth-of-type(' + (sib.indexOf(cur)+1) + ')'; } parts.unshift(p); cur = par; } return parts.join(' > '); } function stripTokens(root){ /* XF renders the reporter's live CSRF token into pickable markup: every XF form opens with a hidden _xfToken input, the account menu Log out row carries ?t=, some GET action links carry ?_xfToken=, and the html element carries data-csrf. None of it belongs in a ticket. ?t= is matched by the token's shape (time, comma, hash), so XF search's own ?t=post stays. The controller scrubs the same things again (scrubOuterHtml). */ var nodes = [root].concat(Array.prototype.slice.call(root.querySelectorAll('*'))); nodes.forEach(function(n){ if (n.tagName.toUpperCase() === 'INPUT' && n.getAttribute('name') === '_xfToken') { if (n === root) n.setAttribute('value', ''); else if (n.parentNode) n.parentNode.removeChild(n); return; } if (n.hasAttribute('data-csrf')) n.setAttribute('data-csrf', ''); ['href', 'action', 'formaction', 'data-href'].forEach(function(a){ var v = n.getAttribute(a); if (v === null) return; var s = v.replace(/([?&])(?:(?:_xfToken=|t=\d+(?:,|%2[Cc]))[^&#]*&?)+/g, function(m, lead){ return /&$/.test(m) ? lead : ''; }); if (s !== v) n.setAttribute(a, s); }); }); } function truncateOuterHtml(el){ try { var clone = el.cloneNode(true); (function strip(n){ if (n.nodeType !== 1) return; if (n.classList && n.classList.length) { var rm = Array.prototype.filter.call(n.classList, function(c){ return c.indexOf('advally-bug-') === 0; }); rm.forEach(function(c){ n.classList.remove(c); }); if (n.classList.length === 0 && n.hasAttribute('class')) n.removeAttribute('class'); } for (var i=0;i TAP_SLOP || Math.abs(e.clientY - start.y) > TAP_SLOP) return; /* a scroll or drag, not a tap */ var t = targetFromPoint(e.clientX, e.clientY) || hoverTarget; if (t) pick(t); } function onPointerCancel(){ pointerStart = null; } function armClickSwallow(){ swallowNextClick = true; if (swallowTimer) clearTimeout(swallowTimer); swallowTimer = setTimeout(function(){ swallowNextClick = false; }, 600); } function onClickCapture(e){ if (isWidgetNode(e.target)) return; if (swallowNextClick) { swallowNextClick = false; e.preventDefault(); e.stopPropagation(); return; } if (isIdentifyMode) { e.preventDefault(); e.stopPropagation(); } } function onMouseCapture(e){ if (!isIdentifyMode || isWidgetNode(e.target)) return; e.preventDefault(); e.stopPropagation(); } function onScrollReposition(){ if (isIdentifyMode && hoverTarget) positionHighlight(hoverTarget); } function pick(el){ var r = el.getBoundingClientRect(); selected = { label: elLabel(el), selector: cssSelectorFor(el), path: ancestorPath(el), outer_html: truncateOuterHtml(el), rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } }; exitIdentify(); } function enterIdentify(){ if (isIdentifyMode) return; isIdentifyMode = true; pointerStart = null; hoverTarget = null; document.body.classList.add('advally-bug-identify-mode'); closeModal(true); overlay.classList.add('is-active'); overlay.setAttribute('aria-hidden', 'false'); positionHighlight(null); document.addEventListener('pointermove', onPointerMove, true); document.addEventListener('pointerdown', onPointerDown, true); document.addEventListener('pointerup', onPointerUp, true); document.addEventListener('pointercancel', onPointerCancel, true); document.addEventListener('mousedown', onMouseCapture, true); document.addEventListener('mouseup', onMouseCapture, true); window.addEventListener('scroll', onScrollReposition, true); } function exitIdentify(){ if (!isIdentifyMode) return; isIdentifyMode = false; pointerStart = null; hoverTarget = null; document.body.classList.remove('advally-bug-identify-mode'); overlay.classList.remove('is-active'); overlay.setAttribute('aria-hidden', 'true'); positionHighlight(null); document.removeEventListener('pointermove', onPointerMove, true); document.removeEventListener('pointerdown', onPointerDown, true); document.removeEventListener('pointerup', onPointerUp, true); document.removeEventListener('pointercancel', onPointerCancel, true); document.removeEventListener('mousedown', onMouseCapture, true); document.removeEventListener('mouseup', onMouseCapture, true); window.removeEventListener('scroll', onScrollReposition, true); openModal(); } document.addEventListener('click', onClickCapture, true); /* ----- modal show/hide ----- */ function renderChip(){ if (selected && selected.label) { chipLabel.textContent = selected.label; chip.hidden = false; } else { chipLabel.textContent = ''; chip.hidden = true; } } function hideMessages(){ msgSending.hidden = true; msgSuccess.hidden = true; msgError.hidden = true; msgEmpty.hidden = true; } function showMsg(kind, detail){ hideMessages(); if (kind === 'sending') msgSending.hidden = false; else if (kind === 'error') { errorDetail.textContent = detail ? ' ' + detail : ''; msgError.hidden = false; } else if (kind === 'empty') msgEmpty.hidden = false; } function setBusy(b){ busy = b; submitBtn.disabled = b; identifyBtn.disabled = b; textarea.readOnly = b; } function resetForm(){ sent = false; selected = null; card.classList.remove('is-sent'); textarea.value = ''; hideMessages(); cancelText.textContent = ds.labelCancel || 'Cancel'; renderChip(); } function openModal(){ if (sent) resetForm(); lastFocus = document.activeElement; pageUrlInput.value = window.location.href; if (!openedAt) openedAt = new Date().toISOString(); renderChip(); modal.classList.add('is-open'); document.body.classList.add('advally-bug-modal-open'); modal.setAttribute('aria-hidden', 'false'); /* Each open gets its own capture. The shot is of the viewport as it is right now, and the reporter has usually scrolled since the last open -- above all on the "Point at it" round trip, which closes this modal and reopens it once they have picked the element (IT #2009). */ dropScreenshot(); beginScreenshot(); setTimeout(function(){ try { textarea.focus(); } catch(_){} }, 0); } function closeModal(keepFocus){ modal.classList.remove('is-open'); document.body.classList.remove('advally-bug-modal-open'); modal.setAttribute('aria-hidden', 'true'); // A capture belongs to the open that started it, so closing the form // drops it: whatever comes next captures its own viewport (IT #2009). dropScreenshot(); if (keepFocus) return; if (lastFocus && typeof lastFocus.focus === 'function' && !isWidgetNode(lastFocus)) { try { lastFocus.focus(); } catch(_){} } // A keyboard open gets its focus back on the launcher. A mouse or touch open does not: // focus parked there reads as a hover that never cleared (4.2.4, IT 2352 QA). else if (!openedByPointer) { try { bubble.focus(); } catch(_){} } else { try { var af = document.activeElement; if (af && af !== document.body && isWidgetNode(af)) af.blur(); } catch(_){} } } /* ----- collapse / expand bubble ----- */ function setCollapsed(c){ if (c) { root.classList.add('is-hidden'); tab.classList.remove('is-hidden'); } else { root.classList.remove('is-hidden'); tab.classList.add('is-hidden'); } try { localStorage.setItem(STORAGE_KEY, c ? '1' : '0'); } catch(_){} } try { if (localStorage.getItem(STORAGE_KEY) === '1') setCollapsed(true); } catch(_){} // The control that hides cannot hold focus, so a keyboard activation (click count 0) moves to the one that replaced it. collapseBtn.addEventListener('click', function(e){ e.stopPropagation(); setCollapsed(true); if (e.detail === 0) { try { tab.focus(); } catch(_){} } }); tab.addEventListener('click', function(e){ setCollapsed(false); if (e.detail === 0) { try { bubble.focus(); } catch(_){} } }); /* ----- screenshot library (loaded once, on first modal open; submit awaits the same promise) ----- */ function ensureScreenshotLib(){ if (shotLibPromise) return shotLibPromise; shotLibPromise = new Promise(function(resolve){ if (window.modernScreenshot) { resolve(); return; } var src = ds.shotSrc; if (!src) { resolve(); return; } try { var s = document.createElement('script'); s.src = src; s.async = true; s.onload = function(){ resolve(); }; s.onerror = function(){ resolve(); }; (document.head || document.body).appendChild(s); } catch(_) { resolve(); } }); return shotLibPromise; } // A 1x1 transparent GIF, the same placeholder the library falls back to // when an image cannot be read. The embed pass skips data: urls, so this // is what a third-party image becomes: no request, same box. var BLANK_PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // Names a non-ad iframe by its host, so the ticket reader can tell a // YouTube player from a reCAPTCHA frame where the grey box sits. An // iframe with no usable src (about:blank, srcdoc) falls back to a // neutral word -- never 'AD' (IT #2009). function embedLabel(el){ try { var src = el.getAttribute('src') || el.getAttribute('data-src') || ''; if (!src) return 'EMBED'; var host = new URL(src, document.baseURI || window.location.href).hostname || ''; host = host.replace(/^www\./, ''); return host ? host.toUpperCase() : 'EMBED'; } catch(_) { return 'EMBED'; } } function sanitizeClone(cloned){ if (!cloned || !cloned.querySelectorAll) return; var doc = cloned.ownerDocument || document; var sx = shotScrollX, sy = shotScrollY; if (cloned.style && (sx || sy)) { cloned.style.marginTop = (-sy) + 'px'; cloned.style.marginLeft = (-sx) + 'px'; } Array.prototype.forEach.call(cloned.querySelectorAll('input[type=password]'), function(el){ try { el.value = ''; el.setAttribute('value', ''); el.style.visibility = 'hidden'; } catch(_){} }); Array.prototype.forEach.call(cloned.querySelectorAll('.advally-mask'), function(el){ try { el.style.visibility = 'hidden'; if ('value' in el) el.value = ''; } catch(_){} }); // A grey box of the same size stands in for what cannot be drawn, and // the caption on it is load-bearing. An ad slot is labelled AD; a forum // media embed is NOT an ad, and XF renders every BB-code media site as // an iframe (youtube, vimeo, giphy, spotify, dailymotion), as does a // reCAPTCHA. Captioning those AD tells whoever reads the ticket that the // very thing the reporter is pointing at is an advertisement. So the two // passes are kept apart: only the ad-container selectors say AD (IT #2009). function placeholderBox(el, label){ var box = doc.createElement('div'); box.textContent = label; var w = el.style.width || (el.getAttribute('width') ? el.getAttribute('width') + 'px' : '100%'); var h = el.style.height || (el.getAttribute('height') ? el.getAttribute('height') + 'px' : '90px'); box.style.cssText = 'display:flex;align-items:center;justify-content:center;overflow:hidden;box-sizing:border-box;background:silver;color:dimgray;font:bold 12px sans-serif;letter-spacing:.1em;width:' + w + ';height:' + h + ';'; return box; } // [id^="AdvallyTag-"] is the plugin's own slot container, and it has to // be here: GPT renders into a same-origin frame, which the pass below // can only caption EMBED. Boxing the wrapper is what gets our own // slots the AD they have earned (IT #2009). Array.prototype.forEach.call(cloned.querySelectorAll('[data-ad], .adv-ad, .advally-ad, [id^="div-gpt-ad"], [id^="AdvallyTag-"], ins.adsbygoogle'), function(el){ try { if (!el.parentNode) return; el.parentNode.replaceChild(placeholderBox(el, 'AD'), el); } catch(_){} }); // Queried AFTER the ad pass: an ad container's own iframe has already // left the tree with it, so it is never captioned twice and never // loses the AD label it earned. Array.prototype.forEach.call(cloned.querySelectorAll('iframe'), function(el){ try { if (!el.parentNode) return; el.parentNode.replaceChild(placeholderBox(el, embedLabel(el)), el); } catch(_){} }); // A same-origin ("friendly") iframe never reaches the pass above. The // library does not keep such a frame as an