/*! * hnl.mobileConsole - javascript mobile console - v1.2.6 - 26/10/2016 * Adds html console to webpage. Especially useful for debugging JS on mobile devices. * Supports 'log', 'trace', 'info', 'warn', 'error', 'group', 'groupEnd', 'table', 'assert', 'clear' * Inspired by code by jakub fiala (https://gist.github.com/jakubfiala/8fe3461ab6508f46003d) * Licensed under the MIT license * * Original author: @hnldesign * Further changes, comments: @hnldesign * Copyright (c) 2014-2016 HN Leussink * Dual licensed under the MIT and GPL licenses. * * Info: http://www.hnldesign.nl/work/code/javascript-mobile-console/ * Demo: http://code.hnldesign.nl/demo/hnl.MobileConsole.html */ var console = window.console; var mobileConsole = (function () { 'use strict'; //stop if there is no console in this browser if (!console) { alert('mobileConsole not supported on this browser'); return; } //polyfills if (!Date.now) { Date.now = function now() { return new Date().getTime(); }; } if (!Array.prototype.filter) { Array.prototype.filter = function(fun/*, thisArg*/) { 'use strict'; if (this === void 0 || this === null) { throw new TypeError(); } var t = Object(this); var len = t.length >>> 0; if (typeof fun !== 'function') { throw new TypeError(); } var res = []; var thisArg = arguments.length >= 2 ? arguments[1] : void 0; for (var i = 0; i < len; i++) { if (i in t) { var val = t[i]; // NOTE: Technically this should Object.defineProperty at // the next index, as push can be affected by // properties on Object.prototype and Array.prototype. // But that method's new, and collisions should be // rare, so use the more-compatible alternative. if (fun.call(thisArg, val, i, t)) { res.push(val); } } } return res; }; } //options and other variable containers var options = { overrideAutorun: false, version : '1.2.6', baseClass : 'mobileConsole_', animParams: 'all 200ms ease', browserinfo: { browserChrome: /chrome/.test(navigator.userAgent.toLowerCase()), ffox: /firefox/.test(navigator.userAgent.toLowerCase()) && !/chrome/.test(navigator.userAgent.toLowerCase()), safari: /safari/.test(navigator.userAgent.toLowerCase()) && !/chrome/.test(navigator.userAgent.toLowerCase()), trident: /trident/.test(navigator.userAgent.toLowerCase()), evtLstn: typeof window.addEventListener === 'function', isCrap: document.querySelectorAll === undefined }, methods : ['log', 'trace', 'info', 'warn', 'error', 'group', 'groupCollapsed', 'groupEnd', 'table', 'assert', 'time', 'timeEnd', 'clear'], hideButtons : ['group', 'groupCollapsed', 'groupEnd', 'table', 'assert', 'time', 'timeEnd'], ratio: 0.4, paddingLeft: 0, groupDepth: 0 }, messages = { clear : 'Console was cleared', empty: '(Empty string)' }, status = { initialized: false, acActive : false, acHovered : false, acInput : '', timers : {} }, history = { output : { prevMsg : '', prevMethod : '', counter : 0 }, input : { commands : window.sessionStorage ? (sessionStorage.getItem('mobileConsoleCommandHistory') ? JSON.parse(sessionStorage.getItem('mobileConsoleCommandHistory')) : []) : [], commandIdx: window.sessionStorage ? (sessionStorage.getItem('mobileConsoleCommandHistory') ? JSON.parse(sessionStorage.getItem('mobileConsoleCommandHistory')).length : 0) : 0, acIdx: 0, acHovered: false } }, //'backup' original console for reference & internal debugging originalConsole = { log: (typeof console.log === 'function') ? console.log.bind(console) : null, info: (typeof console.info === 'function') ? console.info.bind(console) : null, dir: (typeof console.dir === 'function') ? console.dir.bind(console) : null, group: (typeof console.group === 'function') ? console.group.bind(console) : null, groupEnd: (typeof console.groupEnd === 'function') ? console.groupEnd.bind(console) : null, warn: (typeof console.warn === 'function') ? console.warn.bind(console) : null, error: (typeof console.error === 'function') ? console.error.bind(console) : null, trace: (typeof console.trace === 'function') ? console.trace.bind(console) : null, clear: (typeof console.clear === 'function') ? console.clear.bind(console) : null }, // reference variables mobileConsole, consoleElement, commandLine; if(options.browserinfo.isCrap) { console.error( '--==## Error: Browser not supported by Mobile Console ##==--' + '\n' + 'MobileConsole v' + options.version + ', running on ' + navigator.userAgent.toLowerCase() ); return false; } //helpers for all sub functions function isMobile() { var check = false; (function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) { check = true; } }(navigator.userAgent || navigator.vendor || window.opera)); return check; } function setCSS(el, css) { var i; for (i in css) { if (css.hasOwnProperty(i)) { el.style[i] = css[i]; } } return el; } function htmlToString(html) { return String(html).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/ /g, '\u00a0').replace(/(?:\r\n|\r|\n)/g, '
').trim(); } function createElem(type, className, css) { if (!type || typeof setCSS !== 'function') { return; } var element = setCSS(document.createElement(type), css); if (className) { element.className = options.baseClass + className; } return setCSS(element, css); } function storeCommand(command) { if (history) { history.input.commands.push(encodeURI(command.trim())); history.input.commandIdx = history.input.commands.length; if (window.sessionStorage) { sessionStorage.setItem('mobileConsoleCommandHistory', JSON.stringify(history.input.commands)); } } } function valBetween(val, min, max) { return (Math.min(max, Math.max(min, val))); } function getMaxHeight() { return valBetween(Math.floor((window.innerHeight || document.documentElement.clientHeight) * options.ratio), 55, 300); } function getClass(item) { var returnVal = ''; if (item && item.constructor) { returnVal = item.constructor.name; } else { returnVal = Object.prototype.toString.call(item); } return String(returnVal); } // DocReady - Fires supplied function when document is ready if (typeof 'docReady' !== 'function') { (function (funcName, baseObj) { // The public function name defaults to window.docReady // but you can pass in your own object and own function name and those will be used // if you want to put them in a different namespace funcName = funcName || 'docReady'; baseObj = baseObj || window; var i, len, readyList = [], readyFired = false, readyEventHandlersInstalled = false; // call this when the document is ready // this function protects itself against being called more than once function ready() { if (!readyFired) { // this must be set to true before we start calling callbacks readyFired = true; for (i = 0, len = readyList.length; i < len; i = i + 1) { // if a callback here happens to add new ready handlers, // the docReady() function will see that it already fired // and will schedule the callback to run right after // this event loop finishes so all handlers will still execute // in order and no new ones will be added to the readyList // while we are processing the list readyList[i].fn.call(window, readyList[i].ctx); } // allow any closures held by these functions to free readyList = []; } } function readyStateChange() { if (document.readyState === 'complete') { ready(); } } // This is the one public interface // docReady(fn, context); // the context argument is optional - if present, it will be passed // as an argument to the callback baseObj[funcName] = function (callback, context) { // if ready has already fired, then just schedule the callback // to fire asynchronously, but right away if (readyFired) { setTimeout(function () {callback(context); }, 1); return; } // add the function and context to the list readyList.push({fn: callback, ctx: context}); // if document already ready to go, schedule the ready function to run if (document.readyState === 'complete') { setTimeout(ready, 1); } else if (!readyEventHandlersInstalled) { // otherwise if we don't have event handlers installed, install them if (document.addEventListener) { // first choice is DOMContentLoaded event document.addEventListener('DOMContentLoaded', ready, false); // backup is window load event window.addEventListener('load', ready, false); } else { // must be IE document.attachEvent('onreadystatechange', readyStateChange); window.attachEvent('onload', ready); } readyEventHandlersInstalled = true; } }; }('docReady', window)); } // elements var elements = { lines: [], acItems: [], base: createElem('div', 'base', { boxSizing: 'border-box', position: 'fixed', resize: 'none', fontSize: '12px', lineHeight: '14px', bottom: 0, top: 'auto', right: 0, width: '100%', zIndex: 10000, padding: 0, paddingBottom: isMobile() ? '35px' : '25px', margin: 0, border: '0 none', borderTop: '1px solid #808080', backgroundColor: '#ffffff' }), topbar : createElem('div', 'topbar', { boxSizing: 'border-box', position: 'absolute', height: '28px', left: 0, right: 0, display: 'block', padding: '0 2px', overflow: 'hidden', webkitOverflowScrolling: 'touch', color: '#444444', backgroundColor: '#f3f3f3', border: '0 none', borderTop: '1px solid #a3a3a3', borderBottom: '1px solid #a3a3a3', whiteSpace: 'nowrap', overflowX: 'auto' }), scrollcontainer : createElem('div', 'scroller', { boxSizing: 'border-box', border: '0 none', fontFamily: 'Consolas, monaco, monospace', position: 'relative', display: 'block', height: getMaxHeight() + 'px', overflow: 'auto', webkitOverflowScrolling: 'touch', '-webkit-transition': options.animParams, '-moz-transition': options.animParams, '-o-transition': options.animParams, 'transition': options.animParams }), table : createElem('table', 'table', { border: '0 none', margin: 0, position: 'relative', tableLayout: 'auto', width: '100%', borderCollapse: 'collapse' }), stackTraceTable : createElem('table', 'stackTraceTable', { border: '0 none', margin: 0, display: 'none', marginLeft: '10px', marginTop: isMobile() ? '8px' : '4px', tableLayout: 'auto', maxWidth: '100%', color: '#333333' }), tr : createElem('tr', 'table_row', { verticalAlign: 'top' }), td : createElem('td', 'table_row', { border: '0 none', padding: '2px 4px', verticalAlign: 'top' }), msgContainer : createElem('span', 'msgContainer', { border: '0 none', margin: 0, display: 'inline', overflow: 'hidden' }), tdLeft : createElem('td', 'table_row_data', { border: '0 none', textAlign: 'left', padding: isMobile() ? '8px 12px' : '4px 8px' }), tdRight : createElem('td', 'table_row_data', { border: '0 none', textAlign: 'left', padding: isMobile() ? '8px 12px' : '4px 8px', whiteSpace: 'nowrap', overflow: 'hidden' }), link : createElem('a', 'link', { color: '#1155cc', textDecoration: 'underline' }), dot : createElem('div', 'table_row_data_dot', { display: 'inline', borderRadius: '50%', fontSize: '80%', fontWeight: 'bold', padding: '2px 5px', textAlign: 'center', marginRight: '5px', backgroundColor: '#333333', color: '#ffffff' }), button : createElem('button', 'button', { display: 'inline-block', fontFamily: '"Helvetica Neue",Helvetica,Arial,sans-serif', fontWeight: 'normal', textTransform: 'capitalize', fontSize: '12px', lineHeight: '26px', height: '26px', padding: '0 8px', margin: 0, textAlign: 'center', marginRight: '5px', border: '0 none', backgroundColor: 'transparent', color: 'inherit', cursor: 'pointer' }), buttons : { }, input : createElem('div', 'input', { boxSizing: 'border-box', height: isMobile() ? '35px' : '29px', fontFamily: 'Consolas, monaco, monospace', position: 'absolute', bottom: 0, left: 0, right: 0, margin: 0, border: '0 none', borderTop: '1px solid #EEEEEE' }), gt : createElem('DIV', 'gt', { position: 'absolute', bottom: 0, width: '25px', lineHeight: isMobile() ? '34px' : '28px', height: isMobile() ? '34px' : '28px', textAlign: 'center', fontSize: '16px', fontFamily: 'Consolas, monaco, monospace', fontWeight: 'bold', color: '#3577B1', zIndex: 2 }), consoleinput : createElem('input', 'consoleinput', { boxSizing: 'border-box', position: 'absolute', bottom: 0, width : '100%', fontSize: isMobile() ? '16px' : 'inherit', //prevents ios safari's zoom on focus fontFamily: 'Consolas, monaco, monospace', paddingLeft: '25px', margin: 0, height: isMobile() ? '35px' : '25px', border: '0 none', outline: 'none', outlineWidth: 0, boxShadow: 'none', '-moz-appearance': 'none', '-webkit-appearance': 'none', backgroundColor: 'transparent', color: '#000000', zIndex: 1 }), autocomplete : createElem('div', 'autocomplete', { display: 'none', position: 'absolute', bottom: isMobile() ? '35px' : '28px', left: 0, boxShadow: '1px 2px 5px rgba(0,0,0,0.1)', color: '#000000', backgroundColor: '#FFFFFF', border: '1px solid #b5b5b5' }), autocompleteItem : createElem('a', 'autocompleteitem', { display: 'block', textDecoration: 'none', fontSize: isMobile() ? '16px' : 'inherit', padding: '5px 8px', wordWrap: 'break-word', whiteSpace: 'nowrap' }), arrowUp: '', arrowDown: '', arrowRight: '' }; //shared functions var setLineStyle = (function () { var lineStyles = function (style) { switch (style) { case 'log': return { text : { borderBottom: '1px solid #DDDDDD', color: '#000000' }, dot : { color: '#FFFFFF', backgroundColor: '#8097bd' } }; case 'info': return { text : { borderBottom: '1px solid #DDDDDD', color: '#1f3dc4' }, dot : { color: '#FFFFFF', backgroundColor: '#367AB4' } }; case 'warn': return { text : { borderBottom: '1px solid #DDDDDD', color: '#CE8724', backgroundColor : '#fff6e0' }, dot : { color: '#FFFFFF', backgroundColor: '#e8a400' } }; case 'error': case 'table': return { text : { borderBottom: '1px solid #DDDDDD', color: '#FF0000', backgroundColor : '#ffe5e5' }, dot : { color: '#FFFFFF', backgroundColor: '#FF0000' } }; case 'assert': return { text : { borderBottom: '1px solid #DDDDDD', color: '#FF0000', backgroundColor : '#ffe5e5' }, dot : { color: '#FFFFFF', backgroundColor: '#FF0000' } }; case 'trace': return { text : { borderBottom: '1px solid #DDDDDD', color: '#000000' }, dot : { //will not happen } }; case 'time': case 'timeEnd': return { text : { borderBottom: '1px solid #DDDDDD', color: '#0000ff' }, dot : { color: '#FFFFFF', backgroundColor: '#0000ff' } }; default: return { text : { borderBottom: '1px solid #DDDDDD', color: '#000000' }, dot : { color: '#FFFFFF', backgroundColor: '#8097bd' } }; } }; var color, dot; return function (element, type, msg) { if (status.initialized) { color = (msg === 'undefined' || msg === htmlToString(messages.empty)) ? {color: '#808080'} : ((msg === htmlToString(messages.clear)) ? {color: '#808080', fontStyle: 'italic'} : (lineStyles(type) !== undefined ? lineStyles(type).text : lineStyles.log.text)); dot = lineStyles(type) !== undefined ? lineStyles(type).dot : lineStyles.log.dot; setCSS(element, color); //has dot? if (element.childNodes[0].childNodes[0].className.indexOf('dot') !== -1) { setCSS(element.childNodes[0].childNodes[0], lineStyles(type).dot); } } }; }()), getLink = function (href, textString) { var HTMLurl = elements.link.cloneNode(false); if (href) { HTMLurl.setAttribute('href', href); HTMLurl.setAttribute('target', '_blank'); } HTMLurl.innerHTML = textString || href.split('\\').pop().split('/').filter(Boolean).pop(); return HTMLurl; }, toggleHeight = function () { if (status.initialized) { var existingPadding = parseInt(document.body.style.paddingBottom, 10) - Math.abs(elements.base.offsetHeight + elements.topbar.offsetHeight); var newHeight = (elements.base.minimized) ? getMaxHeight() + 'px' : '0px'; setCSS(elements.scrollcontainer, { height: newHeight }); setCSS(document.body, { paddingBottom: existingPadding + Math.abs(parseInt(newHeight, 10) + elements.topbar.offsetHeight) + 'px' }); elements.buttons.toggler.innerHTML = (elements.base.minimized) ? elements.arrowDown : elements.arrowUp; elements.buttons.toggler.setAttribute('title', (elements.base.minimized) ? 'Minimize console' : 'Maximize console'); elements.base.minimized = !elements.base.minimized; return elements.base.minimized; } return 'Not built!'; }, about = (function () { return function () { console.info( '--==## Mobile Console ' + (status.initialized ? 'active' : 'inactive') + ' ##==--' + '\n' + '--===============================--' + '\n' + 'MobileConsole v' + options.version + ', running on ' + navigator.userAgent.toLowerCase() ); }; }()); // --==** sub functions start here **==-- //initializes the console HTML element function initConsoleElement() { //reference var ref; //core function toggleScroll() { elements.scrollcontainer.scrollTop = elements.scrollcontainer.scrollHeight; elements.scrollcontainer.scrollLeft = 0; } function assemble() { var i = options.methods.length, key; //add buttons while (i--) { elements.buttons[options.methods[i]] = elements.button.cloneNode(false); elements.buttons[options.methods[i]].innerHTML = options.methods[i].charAt(0).toUpperCase() + options.methods[i].slice(1); elements.buttons[options.methods[i]].setAttribute('title', (options.methods[i] !== 'clear') ? 'Toggle the display of ' + options.methods[i] + ' messages' : 'Clear the console'); } //add min/maximize button elements.buttons.toggler = elements.button.cloneNode(false); elements.buttons.toggler.innerHTML = elements.arrowDown; elements.buttons.toggler.setAttribute('title', 'Minimize console'); //assemble everything for (key in elements.buttons) { if (elements.buttons.hasOwnProperty(key)) { elements.topbar.insertBefore(elements.buttons[key], elements.topbar.firstChild); } } elements.scrollcontainer.appendChild(elements.table); elements.base.appendChild(elements.topbar); elements.base.appendChild(elements.scrollcontainer); status.initialized = true; return elements.base; } function attach(console) { document.body.appendChild(console); setCSS(elements.topbar, { top: -Math.abs(elements.topbar.offsetHeight) + 'px' }); var existingPadding = isNaN(parseInt(document.body.style.paddingBottom, 10)) ? 0 : parseInt(document.body.style.paddingBottom, 10); setCSS(document.body, { paddingBottom: existingPadding + Math.abs(console.offsetHeight + elements.topbar.offsetHeight) + 'px' }); elements.scrollcontainer.scrollTop = elements.scrollcontainer.scrollHeight; return elements.base; } function toggleLogType() { //togglelogtype is a click handler; 'this' is the button that was clicked var button = this; var logType = button.innerHTML.toLowerCase(); var elems = elements.lines[logType], i = elems.length; button.toggled = (button.toggled === undefined) ? true : !button.toggled; setCSS(button, { opacity: (button.toggled) ? '0.5' : '' }); while (i--) { setCSS(elems[i], { display: (button.toggled) ? 'none' : '' }); } toggleScroll(); button.blur(); return button; } function setBinds() { var methods = options.methods, i = methods.length; while (i--) { if (methods[i] !== 'clear') { if (options.browserinfo.evtLstn) { elements.buttons[methods[i]].addEventListener('click', toggleLogType, false); } else { elements.buttons[methods[i]].attachEvent('onclick', toggleLogType); } } if (options.hideButtons.indexOf(methods[i]) !== -1) { setCSS(elements.buttons[methods[i]], { display: 'none' }); } } if (options.browserinfo.evtLstn) { elements.buttons.toggler.addEventListener('click', toggleHeight, false); elements.buttons.clear.addEventListener('click', console.clear, false); } else { elements.buttons.toggler.attachEvent('onclick', toggleHeight); elements.buttons.clear.attachEvent('onclick', console.clear); } } //init function init() { var element = assemble(); docReady(function () { setBinds(); attach(element); }); //expose Public methods and variables return { toggleHeight : toggleHeight, toggleScroll : toggleScroll }; } if (!ref) { ref = init(); } return ref; } //initializes the new console logger function initConsole() { //reference var ref; //sub helpers function isElement(o) { return ( typeof HTMLElement === 'object' ? o instanceof HTMLElement : //DOM2 o && typeof o === 'object' && o !== null && o.nodeType === 1 && typeof o.nodeName === 'string' ); } function objectToString(object) { var simpleObject = {}, prop, classname = getClass(object); if (!isElement(object)) { for (prop in object) { if (!object.hasOwnProperty(prop) || (typeof (object[prop]) === 'object') || (typeof (object[prop]) === 'function')) { continue; } simpleObject[prop] = object[prop]; } return '' + classname + ' ' + JSON.stringify(simpleObject) + ''; // returns cleaned up JSON } return htmlToString(object.outerHTML); } function urlFromString(string) { string = String(string); //searches for url in string, returns url as string var match, uriPattern = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig; try { match = string.match(uriPattern)[0]; return match; } catch (e) { return ''; } } function filterOut(array, match) { return array.filter(function(item){ return typeof item === 'string' && item.indexOf(match) === -1; }); } function preFilterTrace(array) { var newArray = array.split('\n').filter(Boolean), //filter cleans out empty values isCommandLine = false, stealthThese, i; if (newArray[0].indexOf('http') === -1) { newArray.shift(); } //remove first line if contains no 'http' (Chrome starts with 'Error', Firefox doesn't..) if (newArray[0].indexOf('console.') !== -1 || newArray[0].indexOf('console[method]') !== -1) { newArray.shift(); } if (newArray.length > 0) { isCommandLine = newArray[newArray.length - 1].indexOf('keydown') !== -1; newArray = newArray.filter(function(item){ return item !== ''; }); if (isCommandLine) { stealthThese = ['submitCommand', 'eval', 'setBinds', 'interceptConsole', 'newConsole']; newArray.pop(); //remove last index, as it is the keydown event. i = stealthThese.length; while(i--) { newArray = filterOut(newArray, stealthThese[i]); } } } if (isCommandLine || newArray.length === 0) { newArray.push('(anonymous function) console:1:1'); } return newArray; } //core function formatStackTrace(trace, origtrace) { var callStack = []; //original stack is hidden inside trace object, if specified var stackTraceOrig = (trace !== undefined && trace[4] !== undefined) ? trace[4].stack : undefined; //if the first line contains this, skip it. Meant for browsers that begin the stack with the error message itself (already captured before formatStackTrace) var traceToProcess = (origtrace && origtrace !== '') ? origtrace : stackTraceOrig, i, lines, url, txt, thisLine, lineAndColumn, caller, separator = options.browserinfo.ffox ? '@' : '()'; //stop if no source trace can be determined if (!traceToProcess) { return; } lines = preFilterTrace(traceToProcess); //pre filters all lines by filtering out all mobileConsole's own methods so mobileConsole runs Stealth and unobtrusive i = lines.length; while (i--) { thisLine = lines[i].trim(); lineAndColumn = thisLine.match(/(?::)(\d+)(?::)(\d+)/); url = urlFromString(thisLine).replace(lineAndColumn[0], '').split('#')[0] || ''; caller = htmlToString(thisLine.replace(urlFromString(thisLine), '').replace(separator, '').replace('at ', '').trim()); if (caller === '' || caller === lineAndColumn[0]) { continue; } if (url[url.length - 1] === '/') { txt = '(index)'; } else { txt = url.split('\\').pop().split('/').filter(Boolean).pop() || caller; } callStack.push({ caller: caller, url: url ? url.split(':')[0] + ':' + url.split(':')[1] : caller, linkText: txt + lineAndColumn[0], line: lineAndColumn[1], col: lineAndColumn[2], originalLine: thisLine }); } return callStack; } function traceToTable(table, trace) { var i, tdLeft, tdRight, tr; if (trace === undefined) { return; } trace.reverse(); //reverse order of trace, as it is in a browser's console i = trace.length; while (i--) { tdLeft = elements.td.cloneNode(false); tdRight = elements.td.cloneNode(false); tr = elements.tr.cloneNode(false); tdLeft.innerHTML = trace[i].caller; tdRight.innerHTML = ' @ '; tdRight.appendChild(getLink((trace[i].url || ''), trace[i].linkText)); tr.appendChild(tdLeft); tr.appendChild(tdRight); table.insertBefore(tr, table.firstChild); } return table; } function colorizeData(key, value) { var valueColor = '#3c53da', keyColor = '#ae33b7', classname = getClass(value); if (value && classname.indexOf('HTML') !== -1) { value = htmlToString(value.outerHTML); valueColor = '#ad8200'; } else if (key === 'innerHTML' || key === 'outerHTML') { value = htmlToString(value); valueColor = '#ad8200'; } if (value === null) { valueColor = '#808080'; } if (typeof value === 'string') { valueColor = '#c54300'; //HARD limit, for speed/mem issues with consecutive logging of large strings if (value.length > 400) { value = '"' + String(value).substring(0, 400) + '" [...]
Note: string was truncated to 400 chars'; } else { value = '"' + value + '"'; } } return '' + key + ': ' + value + ''; } function objectToTable(table, object) { var i; for (i in object) { var tdLeft = elements.td.cloneNode(false), tr = elements.tr.cloneNode(false); tdLeft.innerHTML = colorizeData(i, object[i]); tr.appendChild(tdLeft); table.appendChild(tr); } return table; } function toggleDetails() { //toggleDetails is a click handler; 'this' is the button that was clicked var button = this, i, hidden; if (button.getAttribute('toggles') === 'table') { var tables = button.parentElement.getElementsByTagName('table'); i = tables.length; while (i--) { hidden = (tables[i].currentStyle ? tables[i].currentStyle.display : window.getComputedStyle(tables[i], null).display) === 'none'; button.innerHTML = button.innerHTML.replace((hidden ? elements.arrowRight : elements.arrowDown), (hidden ? elements.arrowDown : elements.arrowRight)); setCSS(tables[i], { display: hidden ? 'table' : 'none' }); } } } function isRepeat(message, method) { return (history.output.prevMsg === message && history.output.prevMethod === method) && (typeof message !== 'object') && (method !== 'trace') && (method !== 'group') && (method !== 'groupCollapsed') && (method !== 'groupEnd'); } function newConsole() { try { //get arguments, set vars var method = arguments[0], className, message = (arguments[1].newMessage !== undefined) ? arguments[1].newMessage : undefined, stackTrace = (arguments[1].newStackTrace !== undefined) ? arguments[1].newStackTrace : undefined; //if message emtpy, show empty message-message if (message === '') { message = messages.empty; } if (isRepeat(message, method) && method.indexOf('time') === -1) { // up the counter and add the dot history.output.counter = history.output.counter + 1; elements.table.lastChild.countDot = elements.table.lastChild.countDot || elements.dot.cloneNode(false); elements.table.lastChild.firstChild.insertBefore(elements.table.lastChild.countDot, elements.table.lastChild.firstChild.firstChild).innerHTML = history.output.counter; setLineStyle(elements.table.lastChild, method, message); } else { history.output.prevMsg = message; history.output.prevMethod = method; history.output.counter = 1; //an object requires some more handling if (typeof message === 'object' && method !== 'assert' && method !== 'timeEnd') { className = getClass(message); if (className.indexOf('HTML') !== -1 && className !== 'HTMLDocument') { message = htmlToString(message.outerHTML.match(/<(.*?)>/g)[0] + '...' + message.outerHTML.match(/<(.*?)>/g).pop()); //gets first and last tag, adds '...' in middle. e.g.
...
} else { message = objectToString(message); } } else if (method !== 'assert' && method.indexOf('time') === -1) { message = htmlToString(message); } var detailTable, stackTable, msgContainer = elements.msgContainer.cloneNode(false), lineContainer = elements.tr.cloneNode(false), leftContainer = elements.tdLeft.cloneNode(true), rightContainer = elements.tdRight.cloneNode(false), arrows = stackTrace ? elements.arrowRight + ' ' : ''; switch (method) { case 'assert': if (message[0] === false) { msgContainer.innerHTML = arrows + 'Assertion failed: ' + message[1]; } stackTable = traceToTable(elements.stackTraceTable.cloneNode(false), stackTrace); method = 'error'; //groups it under 'error' and is thus toggleable in view break; case 'log': case 'debug': case 'info': case 'warn': if (typeof arguments[1].newMessage === 'object') { detailTable = objectToTable(elements.stackTraceTable.cloneNode(false), arguments[1].newMessage); msgContainer.innerHTML = elements.arrowRight + ' ' + message; } else { msgContainer.innerHTML = message; } break; case 'error': case 'trace': case 'dir': case 'table': //left side if (method === 'table' || typeof arguments[1].newMessage === 'object') { detailTable = objectToTable(elements.stackTraceTable.cloneNode(false), arguments[1].newMessage); msgContainer.innerHTML = elements.arrowRight + ' ' + message; } else if (method === 'trace') { message = 'console.trace()'; msgContainer.innerHTML = arrows + message; } else { msgContainer.innerHTML = arrows + message; } stackTable = traceToTable(elements.stackTraceTable.cloneNode(false), stackTrace); break; case 'group': case 'groupCollapsed': case 'groupEnd': if (method !== 'groupEnd') { options.groupDepth = options.groupDepth + 1; msgContainer.innerHTML = '' + message + ''; msgContainer.setAttribute('toggles', 'group_' + options.groupDepth); } else { options.groupDepth = valBetween(options.groupDepth - 1, 0, 99); history.output.prevMsg = ''; } if (options.groupDepth > 0) { options.paddingLeft = (options.groupDepth * 23) + 'px'; } else { options.paddingLeft = 0; } break; case 'time': case 'timeEnd': var timerName = arguments[1].newMessage || 'default', now, passed; if (method === 'time') { status.timers[timerName] = Date.now(); if (typeof arguments[1].original === 'function') { arguments[1].original.apply(console, arguments[1].originalArguments); //make sure we still call the original console.time to start the browser's console timer } return; } now = Date.now(); if (!status.timers[timerName]) { console.warn('Timer "' + timerName + '" does not exist.'); return; } passed = now - (status.timers[timerName] || 0); message = timerName + ': ' + passed + 'ms'; msgContainer.innerHTML = message; delete status.timers[timerName]; break; default: msgContainer.innerHTML = message; } if (!msgContainer.innerHTML) { return; } leftContainer.appendChild(msgContainer); if (detailTable || stackTable) { setCSS(msgContainer, {cursor : 'pointer'}); leftContainer.appendChild(detailTable || stackTable); msgContainer.setAttribute('toggles', 'table'); } //populate right side if (stackTrace && stackTrace[stackTrace.length - 1] !== undefined) { rightContainer.appendChild(setCSS(getLink(stackTrace[0].url, stackTrace[0].linkText), {color: '#808080'})); } //add to line lineContainer.appendChild(leftContainer); lineContainer.appendChild(rightContainer); //set colors setCSS(lineContainer, { display: (elements.buttons[method].toggled ? 'none' : '') }); setLineStyle(lineContainer, method, message); //set binds if (options.browserinfo.evtLstn) { msgContainer.addEventListener('click', toggleDetails, false); } else { msgContainer.attachEvent('onclick', toggleDetails); } //store the lines in the object corresponding to the method used elements.lines[method].push(lineContainer); //handle grouping (group and groupEnd if (options.paddingLeft !== 0) { setCSS(leftContainer, {paddingLeft: options.paddingLeft}); setCSS(msgContainer, {borderLeft: '1px solid #808080', paddingLeft: '5px'}); } //add the line to the table elements.table.appendChild(lineContainer); } //scroll consoleElement.toggleScroll(); //========================================================== //make sure we still call the original method, if applicable (not window.onerror) if (typeof arguments[1].original === 'function') { arguments[1].original.apply(console, arguments[1].originalArguments); } } catch (e) { //not logging. why? throw error if (isMobile()) { alert(e); } originalConsole.error('mobileConsole generated an error logging this event!'); originalConsole.error(arguments); originalConsole.error(e); //try to re-log it as an error newConsole('error', e); } } function interceptConsole(method) { var original = console[method], i, stackTraceOrig; console[method] = function () { var args = Array.prototype.slice.call(arguments); args.original = original; args.originalArguments = arguments; args.newMessage = (method === 'assert') ? [args[0], args[1]] : args[0]; //create an Error and get its stack trace and format it try { throw new Error(); } catch (e) { stackTraceOrig = e.stack; } args.newStackTrace = formatStackTrace(args.newStackTrace, stackTraceOrig); if (method === 'clear') { elements.table.innerHTML = ''; history.output.prevMethod = ''; i = options.methods.length; while (i--) { elements.lines[options.methods[i]] = []; } options.groupDepth = 0; options.paddingLeft = 0; console.log(messages.clear); originalConsole.clear(); return; } //Handle the new console logging newConsole(method, args); }; } //init function init() { //Intercept all original console methods including trace. Register the event type as a line type. var i = options.methods.length; while (i--) { elements.lines[options.methods[i]] = []; interceptConsole(options.methods[i]); } //Bind to window.onerror window.onerror = function() { var args = Array.prototype.slice.call(arguments); args.newMessage = args[0]; args.newStackTrace = formatStackTrace(arguments); newConsole('error', args); }; //expose Public methods and variables return { //nothing yet to expose }; } //return if (!ref) { ref = init(); } return ref; } //initialize the console commandline function initCommandLine() { //reference var ref; //sub helpers function getFromArrayById(id) { var pos = elements.acItems.map(function(x) {return x.id; }).indexOf(id); return { position: pos, element: (pos !== -1) ? elements.acItems[pos] : undefined }; } function findInArray(array, match) { return array.filter(function(item, index, self){ return (typeof item === 'string' && item.indexOf(match) > -1) && (index === self.indexOf(item)); }); } //core function assemble() { elements.consoleinput.setAttribute('type', 'text'); elements.consoleinput.setAttribute('autocapitalize', 'off'); elements.consoleinput.setAttribute('autocorrect', 'off'); elements.autocompleteItem.setAttribute('href', '#'); elements.gt.innerHTML = '>'; elements.input.appendChild(elements.gt); elements.input.appendChild(elements.consoleinput); elements.input.appendChild(elements.autocomplete); elements.base.appendChild(elements.input); return elements.base; } function submitCommand(command) { if (command !== '') { storeCommand(command); var result; try { result = eval.call(window, command.trim()); console.log.call(window, result); } catch(e) { console.error(e.message); } finally { elements.consoleinput.value = ''; } } } function hoverAutoComplete(e) { if (e === undefined) { return; } //unset any already hovered elements var hovered = getFromArrayById('hover').element, target = e.target, over; if (hovered !== undefined) { setCSS(hovered, { color: '', backgroundColor: 'rgba(0, 0, 0, 0)' }).id = ''; } if (e.type === 'mouseover') { status.acHovered = true; over = true; } else { over = false; } setCSS(target, { color: over ? '#FFFFFF' : '', backgroundColor: over ? 'rgba(66, 139, 202, 1)' : 'rgba(0, 0, 0, 0)' }).id = over ? 'hover' : ''; } function toggleAutoComplete(show) { var hidden = (elements.autocomplete.currentStyle ? elements.autocomplete.currentStyle.display : window.getComputedStyle(elements.autocomplete, null).display) === 'none'; show = (show === undefined) ? hidden : show; setCSS(elements.autocomplete, {display: (show) ? 'inherit' : 'none'}); status.acActive = show; if (!show) { status.acHovered = false; } } function clickAutoComplete(e) { e.preventDefault(); elements.consoleinput.value = e.target.innerHTML; elements.consoleinput.focus(); toggleAutoComplete(); } function autoComplete(command) { if (command.length < 1) { toggleAutoComplete(false); return; } var searchString = encodeURI(command), matches, match, row, i, maxAmount = isMobile() ? 3 : 5; elements.autocomplete.innerHTML = ''; elements.acItems = []; matches = findInArray(history.input.commands, searchString); matches = matches.slice(Math.max(matches.length - maxAmount, 0)); i = matches.length; while (i--) { match = decodeURI(matches[i]); row = elements.autocompleteItem.cloneNode(false); row.innerHTML = match; row.onmouseover = hoverAutoComplete; elements.autocomplete.insertBefore(row, elements.autocomplete.firstChild); elements.acItems.unshift(row); } toggleAutoComplete(matches.length > 0); } function setBinds() { if (options.browserinfo.evtLstn) { elements.autocomplete.addEventListener('click', clickAutoComplete, false); } else { elements.autocomplete.attachEvent('onclick', clickAutoComplete); } document.onkeydown = function (e) { if (e.target === elements.consoleinput) { if ((e.key === 'Enter' || e.keyCode === 13)) { //enter e.preventDefault(); if(!status.acHovered) { submitCommand(elements.consoleinput.value); } else { elements.consoleinput.value = getFromArrayById('hover').element.innerHTML; elements.consoleinput.focus(); } toggleAutoComplete(false); status.acInput = ''; } else if ((e.keyCode === 38 || e.keyCode === 40)) { //up and down arrows for history browsing e.preventDefault(); var up = (e.keyCode === 40); if(status.acActive) { //autocomplete window is opened //get id of currently hovered element var hovered = getFromArrayById('hover').position; var counter = (hovered === -1) ? elements.acItems.length : hovered; //hover new (in- or decreased number) one counter = valBetween((counter += (up) ? 1 : -1), 0, elements.acItems.length - 1); hoverAutoComplete({target : elements.acItems[counter], type : 'mouseover'}); } else { //autocompete window not opened var hist = history.input.commands; history.input.commandIdx += (up) ? 1 : -1; history.input.commandIdx = valBetween(history.input.commandIdx, 0, hist.length); elements.consoleinput.value = hist[history.input.commandIdx] === undefined ? '' : decodeURI(hist[history.input.commandIdx]); } } } if (e.keyCode === 27 && status.acActive) { toggleAutoComplete(false); } }; document.onkeyup = function (e) { if (e.target === elements.consoleinput && status.acInput !== elements.consoleinput.value && (e.keyCode !== 38 && e.keyCode !== 40 && e.keyCode !== 27 && e.key !== 'Enter' && e.keyCode !== 13)) { status.acInput = elements.consoleinput.value.trim(); autoComplete(elements.consoleinput.value); } }; } //init function init() { var element = assemble(); setBinds(); //expose Public methods and variables return { //nothing yet to expose }; } //return if (!ref) { ref = init(); } return ref; } function init() { if (!status.initialized) { status.initialized = true; //populate references if (!mobileConsole) { //taps into native console and adds new functionality mobileConsole = initConsole(); } if (!consoleElement && mobileConsole) { //creates the new HTML console element and attaches it to document consoleElement = initConsoleElement(); } if (!commandLine && consoleElement && mobileConsole) { //creates an HTML commandline and attaches it to existing console element commandLine = initCommandLine(); } //log a 'welcome' message console.info( '--==## Mobile Console v' + options.version + ' ' + (status.initialized ? 'active' : 'inactive' ) + ' ##==--' ); } else if (options.browserinfo.isCrap) { console.error( '--==## Error: Browser not supported by Mobile Console ##==--' + '\n' + '--===============================--' + '\n' + 'MobileConsole v' + options.version + ', running on ' + navigator.userAgent.toLowerCase() ); } } //autorun if mobile if (isMobile() || options.overrideAutorun) { init(); } //expose the mobileConsole return { init : init, about: about, toggle : toggleHeight, status : status, options : options }; }());