(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 (isIdentifyChrome(e.target)) return;
if (swallowNextClick) { swallowNextClick = false; e.preventDefault(); e.stopPropagation(); return; }
if (isIdentifyMode) { e.preventDefault(); e.stopPropagation(); }
}
function onMouseCapture(e){
if (!isIdentifyMode || isIdentifyChrome(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