NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name FastJSLogger // @namespace FastJSLogger // @description Intercepts console.log calls to display log messages in a div floating on the page. Tries to be a replacement for normal browser Error Consoles (which can be a little slow to open). // @include * // @version 1.2.7 // @grant none // ==/UserScript== (function(){ // You may configure FastJSLogger before loading it, by putting options in // window.FJSL = { startHidden: false, displaySearchFilter: true }; // var FJSL = {}; if (!window.FJSL) { window.FJSL = {}; } var FJSL = window.FJSL; // @concern Don't double-load // I haven't actually had any problems from double loading, but it seems silly to have two loggers. if (FJSL.loaded) { console.log("FJSL refusing to load a second time."); return; } FJSL.loaded = true; FJSL.defaults = { autoHide: true, // hides some time after displaying, even if you are focused on it! startHidden: true, logTimeouts: false, logEvents: true, // Log any activity over the wrapped listeners. logCommonMouseEvents: false, // These can be triggered a lot! logChangesToGlobal: true, // Logs any new properties which are added to window watchWindowForErrors: true, // Catches and reports DOM error events, including syntax errors from scripts loaded later, and missing images. BUG: It can hide the line number from Chrome's console - sometimes worth disabling to get that back. interceptTimeouts: true, // Wraps calls to setTimeout, so any errors thrown may be reported. interceptEvents: true, // Wraps any listeners registered later, so errors can be caught and reported. displaySearchFilter: false, // This messes up the size of the textbox on Firefox and Konqueror but not Chrome. // Hack to avoid infinite loops. Disabled since they have stopped! :) // Mute console.log messages repeated to the browser console. // (Display them only in the fastlogger, keep the browser log clear for // warnings, errors and info messages.) // TODO: Similarly, we may want to mute log messages in the FJSL, and only // show info/warn/errors. muteLogRepeater: 0, logNodeInsertions: false, // Watch for and report DOMNodeInserted events. bringBackTheStatusBar: true // TODO: add fake statusbar div, watch window.status for change? }; for (var k in FJSL.defaults) { if (FJSL[k] === undefined) { FJSL[k] = FJSL.defaults[k]; } } if (this.localStorage) { // TODO: Recall and store FJSL preferences in localStorage. // UNWANTED: During testing, I am toggling this setting. // FJSL.interceptTimeouts = !Boolean(localStorage['fastjslogger_interceptTimeouts']); localStorage['fastjslogger_interceptTimeouts'] = FJSL.interceptTimeouts; } // This fails to intercept GM_log calls from other userscripts. // However it intercepts everything when we run bookmarklets. // Partly DONE: We may be able to capture errors by overriding setTimeout, // setInterval, XMLHttpRequest and any other functions which use callbacks, // with our own versions which attempt the callback within a try-catch which // logs and throws any intercepted exceptions. // Unfortunately wrapping a try-catch around the main scripts in the document // (which have alread run) is a bit harder from here. ;) // Aha! Turns out we can catch those errors with window.onerror! // TODO: No thanks to Wikipedia's pre styling(?) I can't set a good default, so we need font zoom buttons! // TODO: Add ability to minimize but popup again on new log message. // TODO: Report file/line-numbers where possible. (Split output into table? :E) // TODO: Option to watch for new globals. // TODO: Options to toggle logging of any intercepted setTimeouts, XMLHttpRequest's etc, in case the reader is interested. // TESTING: all DOM events! TODO: XHR. // TESTING: We could even put intercepts on generic functions, in case an error occurs inside them, in a context which we had otherwise failed to intercept. // TODO: If the FJSL is wanted for custom logging, not normal console.log // logging, we will need to expose a function (FJSL.log()?), and *not* // intercept console.log. // TODO: Perhaps instead of creating a new console, we should just replace // functions in the existing one (and leave anything we haven't altered // intact). // TODO: See https://github.com/h5bp/html5-boilerplate/blob/master/js/plugins.js // for more log actions. var loadFJSL = function(){ // I did have two running happily in parallel (Chrome userscript and page // script) but it is rarely useful. Perhaps we should close the older one? if (document.getElementById("fastJSLogger") != null) { return; } // GUI creation library functions function addStyle(css) { // Konqueror 3.5 does not act on textValue, it requires textContent. // innerHTML doesn't work for selectors containing '>' var st = newNode("style", { type: "text/css", innerHTML: css } ); document.getElementsByTagName('head')[0].appendChild(st); } function newNode(tag,data) { var elem = document.createElement(tag); if (data) { for (var prop in data) { elem[prop] = data[prop]; } } return elem; } function newSpan(text) { return newNode("span",{textContent:text}); } function addCloseButtonTo(elem) { var b = document.createElement("button"); b.textContent = "X"; b.style.zIndex = 2000; b.onclick = function() { //GM_log("[",b,"] Destroying:",elem); elem.style.display = 'none'; elem.parentNode.removeChild(elem); }; b.style.float = 'right'; // BUG: not working in FF4! elem.appendChild(b); } // == Main Feature - Present floating popup for log messages == // Cleaner to expose the div via id? It may have been removed from DOM! // We could fetch logDiv from FJSL when needed? var logDiv = null; var logContainer = null; var autoHideTimer = null; var oldSetTimeout = window.setTimeout; function createGUI() { var css = ""; css += " .fastJSLogger { position: fixed; right: 8px; top: 8px; width: 40%; /*max-height: 90%; height: 320px;*/ background-color: #333; color: white; border: 1px solid #666; border-radius: 5px; padding: 2px 4px; z-index: 10000; } "; css += " .fastJSLogger > span { max-height: 10%; padding: 0 0.3em; }"; css += " .fastJSLogger > .fjsl-title { font-weight: bold; }"; // css += " .fastJSLogger > pre { max-height: 90%; overflow: auto; }"; //// On the pre, max-height: 90% is not working, but specifying px does. var maxHeight = window.innerHeight * 0.8 | 0; css += " .fastJSLogger > pre { max-height: "+maxHeight+"px; overflow: auto; word-wrap: break-word; }"; css += " .fastJSLogger > pre { padding: 0px; margin: 0.4em 0.2em; }"; // Must set the colors again, in case the page defined its own bg or fg color for pres that conflicts ours. css += " .fastJSLogger > pre { /* background-color: #ffffcc; */ background-color: #888; color: black; }"; css += " .fastJSLogger > pre { font-family: Sans; }"; // css += " .fastJSLogger > pre > input { width: 100%, background-color: #888888; }"; css += " .fastJSLogger > pre > div { border-top: 1px solid #888; padding: 0px 2px; "; css += " white-space: normal; word-break: break-all; }"; css += " .fastJSLogger > pre > .log { background-color: #eee; color: #555; }"; css += " .fastJSLogger > pre > .info { background-color: #aaf; }"; css += " .fastJSLogger > pre > .warn { background-color: #ff4; }"; css += " .fastJSLogger > pre > .error { background-color: #f99; }"; css += " .fastJSLogger { opacity: 0.1; transition: opacity 1s ease-out; } "; css += " .fastJSLogger:hover { opacity: 1.0; transition: opacity 400ms ease-out; } "; css += " .fastJSLogger.notifying { opacity: 1.0; transition: opacity 200ms ease-out; } "; if (document.location.host.match(/wikipedia/)) css += " .fastJSLogger > pre { font-size: 60%; }"; else css += " .fastJSLogger > pre { font-size: 70%; } "; addStyle(css); // Add var before logDiv to break hideLogger()! logDiv = newNode("div",{ id: 'fastJSLogger', className: 'fastJSLogger' }); if (FJSL.startHidden) { hideLogger(); } document.body.appendChild(logDiv); // logDiv.style.position = 'fixed'; // logDiv.style.top = '20px'; // logDiv.style.right = '20px'; // @todo refactor // I/O: logDiv, logContainer var heading = newSpan("FastJSLogger"); heading.className = "fjsl-title"; logDiv.appendChild(heading); // addCloseButtonTo(logDiv); var closeButton = newSpan("[X]"); closeButton.style.float = 'right'; closeButton.style.cursor = 'pointer'; closeButton.style.paddingLeft = '5px'; closeButton.onclick = function() { logDiv.parentNode.removeChild(logDiv); }; logDiv.appendChild(closeButton); var logContainer = newNode("pre"); var rollupButton = newSpan("[--]"); rollupButton.style.float = 'right'; rollupButton.style.cursor = 'pointer'; rollupButton.style.paddingLeft = '10px'; rollupButton.onclick = function() { if (logContainer.style.display == 'none') { logContainer.style.display = ''; rollupButton.textContent = "[--]"; } else { logContainer.style.display = 'none'; rollupButton.textContent = "[+]"; } }; logDiv.appendChild(rollupButton); if (FJSL.displaySearchFilter) { function createSearchFilter(logDiv,logContainer) { var searchFilter = document.createElement("input"); searchFilter.type = 'text'; searchFilter.style.float = 'right'; searchFilter.style.paddingLeft = '5px'; searchFilter.onchange = function(evt) { var searchText = this.value; // console.log("Searching for "+searchText); var logLines = logContainer.childNodes; for (var i=0;i<logLines.length;i++) { if (logLines[i].textContent.indexOf(searchText) >= 0) { logLines[i].style.display = ''; } else { logLines[i].style.display = 'none'; } } }; return searchFilter; } var searchFilter = createSearchFilter(logDiv,logContainer); // logDiv.appendChild(document.createElement("br")); logDiv.appendChild(searchFilter); } logDiv.appendChild(logContainer); return [logDiv,logContainer]; } // @parameter channel String // @parameter args Array<*> // Do not pass more than two parameters; addToFastJSLog will ignore any extra. function addToFastJSLog(channel, args) { // Make FJSL visible if hidden showLogger(); if (logContainer) { var out = ""; for (var i=0; i<args.length; i++) { var obj = args[i]; var str = "" + obj; if (obj && obj.constructor === "Array") { str = "[" + obj.map(showObject).join(", ") + "]"; } // Non-standard: inform type if toString() is dull. if (str === "[object Object]") { str = ""; if (typeof obj !== 'object') { // Surely this is incredibly unlikely! Apart from the obvious case of a String. str += "(" + (typeof obj) + ")"; } if (obj.constructor && obj.constructor.name) { str = "[" + obj.constructor.name + "]"; if (obj.constructor.name === "Object") { str = ""; // Because showObject will give it {}s, this is implied. // Although Chrome's console does actually print: "Object { ... }" } } str += showObject(obj); } else { } str = shortenString(str); var gap = (i>0?' ':''); out += gap + str; } var d = document.createElement("div"); d.className = channel; d.textContent = out; if (logContainer.childNodes.length >= 1000) { logContainer.removeChild(logContainer.firstChild); } logContainer.appendChild(d); // Scroll to bottom // TODO: This is undesirable if the scrollbar was not already at the bottom, i.e. the user has scrolled up manually and is trying to read earlier log entries! logContainer.scrollTop = logContainer.scrollHeight; } if (autoHideTimer !== null) { clearTimeout(autoHideTimer); autoHideTimer = null; } if (FJSL.autoHide) { // Never log this setTimeout! That produces an infinite loop! autoHideTimer = oldSetTimeout(hideLogger,15 * 1000); } return d; } function debounce(duration, fn) { var timer = null; return function() { clearTimeout(timer); timer = setTimeout(fn, duration); }; } var clearOpacity = debounce(3000, function(){ //logDiv.style.opacity = ''; logDiv.className = logDiv.className.replace(/(^|\s)notifying(\s|$)/, ''); }); function showLogger() { if (logDiv) { // logDiv.style.display = ''; //// BUG: Transition is not working! Perhaps it only works when switching between CSS classes. // logDiv.style._webkit_transition_property = 'opacity'; // logDiv.style._webkit_transition_duration = '2s'; //logDiv.style.opacity = 1.0; if (!logDiv.className.match(/(^|\s)notifying(\s|$)/)) { logDiv.className += " notifying"; } clearOpacity(); } } function hideLogger() { if (logDiv) { // logDiv.style.display = 'none'; // logDiv.style._webkit_transition_property = 'opacity'; // logDiv.style._webkit_transition_duration = '2s'; // logDiv.style.opacity = 0.0; clearOpacity(); } } var k = createGUI(); logDiv = k[0]; logContainer = k[1]; // target.console.log("FastJSLogger loaded this="+this+" GM_log="+typeof this.GM_log); // console.log("interceptTimeouts is "+(FJSL.interceptTimeouts?"ENABLED":"DISABLED")); // Intercept messages for console.log if it exists. // Create console.log if it does not exist. var oldConsole = this.console; // When running as a userscript in Chrome, cannot see this.console! // TODO: We should probably remove all this GM_ stuff. // We aren't using it even if we do find it. var oldGM_log = this.GM_log; var target = ( this.unsafeWindow ? this.unsafeWindow : window ); // Replace the old console target.console = {}; var lastArgs = []; target.console.log = function(a,b,c) { // I tried disabling this and regretted it! // My Console bookmarklet can cause an infloop with FJSL if you want to test it. var args = Array.prototype.slice.call(arguments, 0); if (arraysEqual(args, lastArgs)) { return; } lastArgs = args; // Replicate to the old loggers we intercepted (overrode) if (oldConsole && oldConsole.log && !FJSL.muteLogRepeater) { // Some browsers dislike use of .call and .apply here, e.g. GM in FF4. // So to avoid "oldConsole.log is not a function": try { oldConsole.log.apply(oldConsole,arguments); } catch (e) { // Ugly chars to signify that sucky fallback is being used oldConsole.log("[noapply]",a,b,c); } } /* //// WARNING: *This* is the cause of infinite console.logging, if it has been implemented by FallbackGMAPI. //// Solution: FBGMAPI should take a reference to console when creating it's GM_log, not waiting until later to look for the console. if (oldGM_log) { oldGM_log(a,b,c); } */ addToFastJSLog("log", arguments); }; //// NOT TESTED. Intercept Greasemonkey log messages. (Worth noting we have disabled calls *to* GM_log above.) // this.GM_log = target.console.log; // == MAIN SCRIPT ENDS == // The rest is all optional so may be stripped for minification. /* Provide/intercept console.info/warn/error(). */ target.console.error = function() { // We can get away with this in Chrome! //var args = Array.prototype.slice.call(arguments,0); //args.unshift("[ERROR]"); oldConsole.error.apply(oldConsole, arguments); addToFastJSLog("error", arguments); // ,'\n'+getStack(2,20).join("\n")); // Report stacktrace if we were passed an error. if (arguments[0] instanceof Error) { //target.console.error("" + arguments[0].stack); addToFastJSLog("error", "" + arguments[0].stack); } }; // Could generalise the two functions below: //interceptLogLevel("warn"); //interceptLogLevel("info"); target.console.warn = function() { //var args = Array.prototype.slice.call(arguments,0); //args.push(""+getCallerFromStack()); oldConsole.warn.apply(oldConsole, arguments); addToFastJSLog("warn", arguments); //// This was quite useful on one occasion. But not in the presence of lots of warnings! // logStack(getStackFromCaller()); }; target.console.info = function() { oldConsole.info.apply(oldConsole, arguments); addToFastJSLog("info", arguments); }; /* target.console.error = function(e) { // target.console.log("[ERROR]",e,e.stack); var newArgs = []; newArgs.push("[ERROR]"); for (var i=0;i<arguments.length;i++) { newArgs.push(arguments[i]); } try { newArgs.push("(reported from function "+arguments.callee.caller.name+")"); // console.error("caller = "+arguments.callee.caller); // console.error("stack = "+e.stack); } catch (e) { } target.console.log.apply(target.console,newArgs); target.console.log.apply(target.console,["Stacktrace:\n",e.stack]); }; */ // Some more library functions: function arraysEqual(a, b) { for (var i=0; i<a.length; i++) { if (a[i] !== b[i]) { return false; } } return true; } function tryToDo(fn, target, args) { try { return fn.apply(target, args); } catch (e) { // Actually hard to read! // var prettyFn = fn; // ("" + fn) .replace(/\n/g,'\\n'); var fnName = fn && fn.name; if (!fnName) { fnName = "<anonymous>"; } // console.log("[ERR]", e, prettyFn); // console.log("[Exception]", e, "from " + fnName + "()"); if (e.stack) { console.error("!! " + e.stack); } else if (e.lineNumber) { console.error("!! " + e + " on " + e.lineNumber + ")"); } else { console.error("!!", e); } // var prettyFn = ("" + fn).replace(/\n/,/ /, 'g'); var prettyFn = shortenString(fn); console.error("occurred when calling: " + prettyFn); // If we rethrow the error, browser devtools will show the throw as coming from here! // But we should throw it anyway. That is what the function's caller expects. // At one point we tried to reproduce the error by calling fn again, but not catching it. // However this is a dangerous tactic. What if fn() creates a setTimeout before throwing its exception? Then calling it again would produce a second setTimeout! //return fn.apply(target, args); throw e; } } // TODO: Unify showObject and shortenString with replay.js's toDetailedString and toSimpleString. function showObject(obj) { return "{ " + Object.keys(obj).map(function(prop) { return prop + ": " + shortenString(obj[prop]); }).join(", ") + " }"; } function getXPath(node) { var parent = node.parentNode; if (!parent) { return ''; } var siblings = parent.childNodes; var totalCount = 0; var thisCount = -1; for (var i=0;i<siblings.length;i++) { var sibling = siblings[i]; if (sibling.nodeType == node.nodeType) { totalCount++; } if (sibling == node) { thisCount = totalCount; break; } } return getXPath(parent) + '/' + node.nodeName.toLowerCase() + (totalCount>1 ? '[' + thisCount + ']' : '' ); } function getStack(drop,max) { var stack; try { throw new Error("Dummy for getStack"); } catch (e) { stack = e.stack.split('\n').slice(drop).slice(0,max); } return stack; } // What frame/function called the function which called us? function getCallerFromStack() { return getStack(4,1); } // The frame/function which called our caller, and all above it. function getStackFromCaller() { return getStack(4,-1); } function logStack(stack) { for (var i=0;i<stack.length;i++) { console.log(""+stack[i]); } } // == Error Interceptors == // Whenever a callback is placed, we should wrap it! // DONE: // event listeners (added after we run) // setTimeout // TODO: // setInterval // XMLHttpRequest // We could even wrap any standard functions which are common sources of // Errors, in case we fail to catch them any other way. if (FJSL.watchWindowForErrors) { // Registers a window.onerror event handler, which catches DOM Errors like // img elements which failed to load their src resource. function handleError(evt) { if (!FJSL.firstWindowErrorEvent) { FJSL.firstWindowErrorEvent = evt; } //// Expose this event for inspection FJSL.lastWindowErrorEvent = evt; // console.log("Error caught by FJSL:"); // console.log(evt); // console.log(Object.keys(evt)); // target.console.error(evt.filename+":"+evt.lineno+" "+evt.message+" ["+evt.srcElement+"]",evt); // In fact repeating these to the browser log is redundant, since the // browser is likely to report these errors anyway. // Therefore, we could use addToFastJSLog instead of console.log below. // Chrome sometimes gives us a message, sometimes doesn't. if (evt.message) { // This is what Chrome provides for an error thrown by a page script. // In Chrome this event object contains neither an Error nor a stack-trace. // Also the current stack-trace is uninformative (see nothing about the call to handleError.) // So we don't use the following: , evt, getStack(0,99).join('\n') console.error("[Caught Error] "+evt.message+" "+evt.filename+":"+evt.lineno); } else { var report = '[Caught Unknown Error] '; report += "type="+evt.type+" "; // For some errors neither Firefox nor Chrome give us a message. // But sometimes we can peek into the element that fired the error. // If it was an image, then its src attribute may be useful. var elem = evt.srcElement || evt.target; if (elem) { report += "From element "+elem+" "; try { report += "with src="+elem.src+" "; } catch (e) { } } /* if (elem == window) { FJSL.lastEE = evt; } */ /* if (!elem) { // Firefox 14 was providing only a constructor function in the event, // so I wonder if calling it will reproduce the error for us. Either // that, or it will try to construct an event object. :P // I will probably remove this if I see it again and confirm that it's useless. // I did get a HUGE string from Firefox which was pretty interesting. // But it's not what we were looking for. if (typeof evt.constructor == "function") { try { var result = evt.constructor(); if (result) { console.error(report + "Constructor result: "+result); } } catch (e) { // I think in Firefox this gives us useful information. // TODO: But I have seen us reach here in Chrome, with an error "cannot call DOM Object constructor" presumably meaning out evt.constructor() call was forbidden, and we should look elsewhere for information. report += "(Constructor error: \""+e+"\") "; // Dirty cheat expose it for dev/debugger: evt.FJSLconstructorError = e; // I have come to the conclusion that although they are different, the errors I get in both Firefox and Chrome are errors about the way I have called the constructor, and not any useful error I was after :P // FF: Timestamp: 29/07/12 18:10:14 // Error: NS_ERROR_XPC_NOT_ENOUGH_ARGS: Not enough arguments console.log("[Constructor Error]",e); throw e; } } } */ console.error(report,evt); } } window.addEventListener("error",handleError,true); // document.body.addEventListener("error",handleError,true); } // Split into shortenString, escapeString, compressString, and toSimpleString. function shortenString(s) { s = ""+s; s = s.replace(/\n[ \t]+/,'\\n ','g'); s = s.replace(/\n/,'\\n','g'); s = s.replace(/\t/,' ','g'); if (s.length > 202) { s = s.substring(0,202) + "..."; } return s; } if (FJSL.interceptTimeouts) { window.setTimeout = function(fn, ms) { if (FJSL.logTimeouts) { // TODO: We used to pass ,fn to log here. That was nice in Chrome, because we can fold it open or closed, but too spammy for plain FJSL. Recommendation: Refactor out prettyShow() fn which will determine browser and provide raw object for chrome or shortened string for noob browsers? console.info("[EVENT] setTimeout() called: "+ms+", "+shortenString(fn)); } var wrappedFn = function(){ // setTimeout can be passed a string. But we want a function so we can call it later. if (typeof fn === 'string') { var str = fn; fn = function(){ eval(str); }; } // CONSIDER: Of dubious value if (typeof fn !== 'function') { console.error("[FJSL] setTimeout was not given a function!",fn); return; } // We don't really need to return here. setTimeout won't do anything with the returned value. // Similarly it probably hasn't passed us any arguments. The context object 'this' is probably 'window'. return tryToDo(fn, this, arguments); }; return oldSetTimeout(wrappedFn, ms); }; } // == General Info Logging Utilities == if (FJSL.logNodeInsertions) { document.body.addEventListener('DOMNodeInserted', function(evt) { if (evt.target === logContainer || evt.target.parentNode === logContainer) { return; } // console.log("[DOMNodeInserted]: target=",evt.target," event=",evt); // console.log("[DOMNodeInserted]:",evt,evt.target,"path="+getXPath(evt.target),"html="+evt.target.outerHTML); console.log("[DOMNodeInserted]: path="+getXPath(evt.target)+" html="+evt.target.outerHTML,evt); }, true); } if (FJSL.interceptEvents) { // This store helps us to remove the wrapped event listeners which we add var wrapped_and_unwrapped = []; var realAddEventListener = HTMLElement.prototype.addEventListener; // HTMLElement.prototype.oldAddEventListener = realAddEventListener; HTMLElement.prototype.addEventListener = function(type,handler,capture,other){ if (FJSL.logEvents) { // Note niceFunc is re-used later, so don't remove this! var niceFunc = handler.name || (""+handler).substring(0,80).replace(/\n/," ",'g').replace(/[ \t]*/," ",'g')+"..."; /* console.info("[EVENTS] Listening for "+type+" events on "+getXPath(this)+" with handler "+niceFunc); console.info(getStack(3,3).join("\n")); */ } var newHandler = function(evt) { if (FJSL.logEvents) { var isSpammyEvent = ["mousemove","mouseover","mouseout","mousedown","mouseup","keydown","keyup"].indexOf(evt.type) >= 0; if (!isSpammyEvent || FJSL.logCommonMouseEvents) { if (evt.target.parentNode == logContainer) { // Do not log events in the console's log area, such as DOMNodeInserted! } else { // console.log("("+type+") on "+evt.target); // console.info("[EVENT] "+type+" on "+getXPath(evt.target)); // console.log("[EVENT] "+type+" on "+getXPath(evt.target)+" evt="+showObject(evt)); console.info("[EVENT] "+type+" on "+getXPath(evt.target)+" being handled by "+niceFunc); } } } return tryToDo(handler, this, arguments); // return handler.apply(this, arguments); }; wrapped_and_unwrapped.push({ unwrapped: handler, wrapped: newHandler, }); return realAddEventListener.call(this,type,newHandler,capture,other); }; // We cannot remove the original handler, because it was never added. Instead we need to remove the wrapper that we did add. var realRemoveEventListener = HTMLElement.prototype.removeEventListener; // HTMLElement.prototype.oldRemoveEventListener = realAddEventListener; HTMLElement.prototype.removeEventListener = function(type,handler,capture,other){ //console.log("removeEventListener was called with:",type,handler); //try { throw new Error("dummy for stacktrace"); } catch (e) { console.log("At:",e); } for (var i = wrapped_and_unwrapped.length; i--;) { var both = wrapped_and_unwrapped[i]; if (handler === both.unwrapped) { handler = both.wrapped; wrapped_and_unwrapped.splice(i,1); break; } } return realRemoveEventListener.call(this,type,handler,capture,other); }; } if (FJSL.logChangesToGlobal) { function newChangeWatcher() { var checkSpeed = 4000; var watcher = {}; var global = window; var knownKeys = {}; var firstCheck = true; function checkGlobal() { if (FJSL.logChangesToGlobal) { for (var key in global) { if (knownKeys[key] === undefined) { if (!firstCheck) { var obj = global[key]; console.log("[New global detected] "+key+" =",obj); } knownKeys[key] = 1; } } firstCheck = false; } oldSetTimeout(checkGlobal,checkSpeed); } oldSetTimeout(checkGlobal,checkSpeed); return watcher; }; var changeWatcher = newChangeWatcher(); } }; if (document.body) { loadFJSL(); } else { // doc bod may not exist if we are loaded in the HEAD! Better wait till later... setTimeout(loadFJSL,1000); // But we would quite like to start doing stuff now anyway! // TODO: Load what we can immediately, delay only things we must. // E.g. start capturing text, even if we can't display it yet. } })();