/** * A log you can read inside the app. * * The app ships without developer tools, so when something goes wrong the * usual answer — "open the console and tell me what it says" — isn't * available. This captures everything the console would have shown, plus the * things that turn out to matter when a control looks fine and does nothing, * and puts it behind Cmd-Shift-L with a Copy button. * * Loaded before the app so it catches errors thrown during start-up. */ ( function () { "use strict"; var LIMIT = 800; var lines = []; var panel = null; var output = null; function stamp() { var now = new Date(); function two( n ) { return ( n < 10 ? "0" : "" ) + n; } return two( now.getHours() ) + ":" + two( now.getMinutes() ) + ":" + two( now.getSeconds() ) + "." + ( "00" + now.getMilliseconds() ).slice( -3 ); } /** Anything at all, as one short readable string. */ function describe( value ) { if ( value === null ) { return "null"; } if ( value === undefined ) { return "undefined"; } if ( typeof value === "string" ) { return value; } if ( typeof value !== "object" ) { return String( value ); } if ( value instanceof Error ) { return value.name + ": " + value.message + ( value.stack ? "\n " + String( value.stack ).split( "\n" ).slice( 1, 4 ) .join( "\n " ) : "" ); } if ( value.nodeType === 1 ) { return element( value ); } try { var json = JSON.stringify( value ); return json && json.length > 300 ? json.slice( 0, 300 ) + "…" : String( json ); } catch ( e ) { return Object.prototype.toString.call( value ); } } /** An element as you would point at it: tag, class, and its data hooks. */ function element( node ) { if ( ! node || node.nodeType !== 1 ) { return String( node ); } var out = node.tagName.toLowerCase(); if ( node.id ) { out += "#" + node.id; } var cls = typeof node.className === "string" ? node.className : ( node.getAttribute && node.getAttribute( "class" ) ) || ""; if ( cls ) { out += "." + cls.trim().split( /\s+/ ).join( "." ); } var hooks = []; if ( node.getAttributeNames ) { node.getAttributeNames().forEach( function ( name ) { if ( name.indexOf( "data-bwfa" ) === 0 ) { var v = node.getAttribute( name ); hooks.push( v ? name + "=" + v : name ); } } ); } if ( hooks.length ) { out += " [" + hooks.join( " " ) + "]"; } return out; } function add( kind, text ) { lines.push( stamp() + " " + kind + " " + text ); if ( lines.length > LIMIT ) { lines.shift(); } if ( output ) { render(); } } // Console, kept working as well as captured. [ "log", "info", "warn", "error" ].forEach( function ( name ) { var original = window.console && window.console[ name ]; window.console[ name ] = function () { var parts = Array.prototype.slice.call( arguments ).map( describe ); add( name.toUpperCase(), parts.join( " " ) ); if ( original ) { original.apply( window.console, arguments ); } }; } ); window.addEventListener( "error", function ( e ) { if ( e.error ) { add( "THROWN", describe( e.error ) ); } else { add( "THROWN", e.message + " (" + e.filename + ":" + e.lineno + ")" ); } } ); window.addEventListener( "unhandledrejection", function ( e ) { add( "REJECTED", describe( e.reason ) ); } ); /** * Every click, with what was actually hit. * * This is the line that matters when a button looks alive and isn't: the * thing under the pointer is not the button you aimed at, either because * something covers it or because it has been squashed to no width at all. */ document.addEventListener( "click", function ( e ) { var hit = e.target; var button = hit && hit.closest ? hit.closest( "button" ) : null; var note = "hit " + element( hit ); if ( button && button !== hit ) { note += " inside " + element( button ); } if ( ! button ) { note += " (no button in the ancestry)"; } add( "CLICK", note ); }, true ); /** * Measures the controls, because a control can be visible and unclickable. * For each one: its box, and whatever the browser says is on top at the * middle of that box. Those two disagreeing is the whole diagnosis. */ function probe() { var selectors = [ "[data-bwfa-mixer-open]", "[data-bwfa-spectro-open]", "[data-bwfa-player-edit]", "[data-bwfa-player-export]", "[data-bwfa-player-playpause]" ]; add( "PROBE", "measuring the player's controls" ); // This panel covers the bottom of the window, so measuring with it on // screen reports the panel as the topmost thing over every control. // Ask it to step out of the way first. var wasShowing = panel && panel.style.display !== "none"; if ( wasShowing ) { panel.style.display = "none"; } selectors.forEach( function ( selector ) { var node = document.querySelector( selector ); if ( ! node ) { add( "PROBE", selector + " MISSING from the page" ); return; } var box = node.getBoundingClientRect(); var size = Math.round( box.width ) + "x" + Math.round( box.height ) + " at " + Math.round( box.left ) + "," + Math.round( box.top ); // elementFromPoint is the whole point of this, but it does not // exist everywhere the tests run. var mid = document.elementFromPoint ? document.elementFromPoint( box.left + box.width / 2, box.top + box.height / 2 ) : null; var reaches = ! document.elementFromPoint || ( mid && ( mid === node || ( mid.closest && mid.closest( selector ) ) ) ); add( "PROBE", selector + " " + size + ( box.width < 4 || box.height < 4 ? " SQUASHED" : "" ) + " topmost: " + element( mid ) + ( reaches ? " reachable" : " NOT REACHABLE — clicks land elsewhere" ) ); } ); if ( wasShowing ) { panel.style.display = "flex"; } var modal = document.querySelector( "[data-bwfa-mixer]" ); add( "PROBE", "mixer modal in the page: " + ( modal ? "yes, hidden=" + modal.hidden + ", open class=" + modal.classList.contains( "open" ) : "NO — markup missing" ) ); // Unhidden is not the same as visible, which is exactly the trap that // made this look like a dead button. Report what is actually painted. add( "PROBE", "mixer modal display: " + ( modal ? window.getComputedStyle( modal ).display : "n/a" ) ); add( "PROBE", "page built " + ( window.BWFA_BUILD || "unknown" ) ); } function render() { output.textContent = lines.join( "\n" ); output.scrollTop = output.scrollHeight; } function build() { panel = document.createElement( "div" ); // Inline styles throughout: this has to work when the app's own // stylesheet is the thing that's broken. panel.setAttribute( "style", [ "position:fixed", "left:0", "right:0", "bottom:0", "height:45vh", "z-index:2147483647", "background:#1c1c1e", "color:#e8e8ed", "font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace", "display:flex", "flex-direction:column", "box-shadow:0 -8px 24px rgba(0,0,0,.4)" ].join( ";" ) ); var bar = document.createElement( "div" ); bar.setAttribute( "style", [ "display:flex", "align-items:center", "gap:8px", "padding:8px 12px", "border-bottom:1px solid #3a3a3c", "flex:none" ].join( ";" ) ); var title = document.createElement( "strong" ); title.textContent = "BWF Analyser log"; title.setAttribute( "style", "margin-right:auto;font-weight:600" ); bar.appendChild( title ); function chip( label, onClick ) { var b = document.createElement( "button" ); b.type = "button"; b.textContent = label; b.setAttribute( "style", [ "font:inherit", "padding:3px 10px", "border-radius:6px", "border:1px solid #5a5a5e", "background:#2c2c2e", "color:inherit", "cursor:pointer" ].join( ";" ) ); b.addEventListener( "click", onClick ); bar.appendChild( b ); return b; } chip( "Copy", function () { var text = lines.join( "\n" ); if ( navigator.clipboard && navigator.clipboard.writeText ) { navigator.clipboard.writeText( text ).then( function () { title.textContent = "BWF Analyser log — copied"; }, function () { fallbackCopy( text, title ); } ); } else { fallbackCopy( text, title ); } } ); chip( "Measure controls", function () { probe(); } ); chip( "Clear", function () { lines = []; render(); } ); chip( "Close", function () { toggle( false ); } ); output = document.createElement( "pre" ); output.setAttribute( "style", [ "margin:0", "padding:10px 12px", "overflow:auto", "flex:1", "white-space:pre-wrap", "word-break:break-word" ].join( ";" ) ); panel.appendChild( bar ); panel.appendChild( output ); document.body.appendChild( panel ); } function fallbackCopy( text, title ) { var area = document.createElement( "textarea" ); area.value = text; document.body.appendChild( area ); area.select(); try { document.execCommand( "copy" ); title.textContent = "BWF Analyser log — copied"; } catch ( e ) { title.textContent = "BWF Analyser log — select the text and copy it"; } area.remove(); } function toggle( wanted ) { if ( ! panel ) { build(); } var show = wanted === undefined ? panel.style.display === "none" : wanted; panel.style.display = show ? "flex" : "none"; if ( show ) { probe(); render(); } } document.addEventListener( "keydown", function ( e ) { if ( ( e.metaKey || e.ctrlKey ) && e.shiftKey && String( e.key ).toLowerCase() === "l" ) { e.preventDefault(); toggle(); } } ); window.BWFA_DIAG = { lines: function () { return lines.slice(); }, open: function () { toggle( true ); }, close: function () { toggle( false ); }, probe: probe, describe: element }; }() );