/*! * hnl.mobileConsole - javascript mobile console - v1.3.6 - 7/1/2020 * 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-2019 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 */ //Polyfills //Date.now polyfill if (!Date.now) { Date.now = function now() { return new Date().getTime(); }; } //Array.isArray polyfill if (typeof Array.isArray === 'undefined') { Array.isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; } //Array.filter polyfill if (!Array.prototype.filter) { Array.prototype.filter = function(fun/*, thisArg*/) { 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]; if (fun.call(thisArg, val, i, t)) { res.push(val); } } } return res; }; } //Function.bind polyfill if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { // closest thing possible to the ECMAScript 5 // internal IsCallable function throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function() {}, fBound = function() { return fToBind.apply(this instanceof fNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; if (this.prototype) { // Function.prototype doesn't have a prototype property fNOP.prototype = this.prototype; } fBound.prototype = new fNOP(); return fBound; }; } //Array.prototype.indexOf polyfill // Production steps of ECMA-262, Edition 5, 15.4.4.14 // Referentie: http://es5.github.io/#x15.4.4.14 if (!Array.prototype.indexOf) { Array.prototype.indexOf = function(searchElement, fromIndex) { var k; if (this == null) { throw new TypeError('"this" is null or not defined'); } var o = Object(this); var len = o.length >>> 0; if (len === 0) { return -1; } var n = +fromIndex || 0; if (Math.abs(n) === Infinity) { n = 0; } if (n >= len) { return -1; } k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); while (k < len) { if (k in o && o[k] === searchElement) { return k; } k++; } return -1; }; } //String.prototype.trim polyfill if (!String.prototype.trim) { String.prototype.trim = function () { return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); }; } //Array.prototype.map polyfill // Production steps of ECMA-262, Edition 5, 15.4.4.19 // Reference: http://es5.github.io/#x15.4.4.19 if (!Array.prototype.map) { Array.prototype.map = function(callback/*, thisArg*/) { var T, A, k; if (this == null) { throw new TypeError('this is null or not defined'); } var O = Object(this); var len = O.length >>> 0; if (typeof callback !== 'function') { throw new TypeError(callback + ' is not a function'); } if (arguments.length > 1) { T = arguments[1]; } A = new Array(len); k = 0; while (k < len) { var kValue, mappedValue; if (k in O) { kValue = O[k]; mappedValue = callback.call(T, kValue, k, O); A[k] = mappedValue; } k++; } return A; }; } // 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)); } //define console variable var console = window.console; var mobileConsole = (function () { 'use strict'; //options and other variable containers var options = { overrideAutorun: true, //set this to true to skip mobile-detection and run the console no matter what. startMinimized: false, version: '1.3.5', baseClass: 'mobileConsole_', animParams: 'all 200ms ease', browserinfo: { isMobile: (function (a) { return (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|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))); }(navigator.userAgent || navigator.vendor || window.opera)), 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' }, 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, //screen/console-ratio that determines the height of the console (reevaluated on every minimize/maximize). paddingLeft: 0, //used when grouping, no need to change as it will be reset. groupDepth: 0, //used when grouping, no need to change as it will be reset. truncate: 400 //hard limit for large strings. For speed/mem issues with consecutive logging of large strings }, 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 missingMethod = function() { return true; }, //method is not supported on this device's original console, return dummy originalConsole = { log: (console && typeof console.log === 'function') ? console.log.bind(console) : missingMethod, info: (console && typeof console.info === 'function') ? console.info.bind(console) : missingMethod, dir: (console && typeof console.dir === 'function') ? console.dir.bind(console) : missingMethod, group: (console && typeof console.group === 'function') ? console.group.bind(console) : missingMethod, groupEnd: (console && typeof console.groupEnd === 'function') ? console.groupEnd.bind(console) : missingMethod, warn: (console && typeof console.warn === 'function') ? console.warn.bind(console) : missingMethod, error: (console && typeof console.error === 'function') ? console.error.bind(console) : missingMethod, trace: (console && typeof console.trace === 'function') ? console.trace.bind(console) : missingMethod, clear: (console && typeof console.clear === 'function') ? console.clear.bind(console) : missingMethod }, // reference variables mobileConsole, consoleElement, commandLine; //helpers for all sub functions function setCSS(el, css) { var i; for (i in css) { if (css.hasOwnProperty(i)) { el.style[i] = css[i]; } } return el; } function htmlToString(html) { var string; try { string = String(html); } catch(e) { string = JSON.stringify(html); } //this should be done differently, but works for now return string.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/ /g, '\u00a0').replace(/(?:\r\n|\r|\n)/g, '
').trim(); } function createElem(type, className, css) { if (!type) { 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); } // elements var elements = { lines: [], acItems: [], base: createElem('div', 'base page-bg', { boxSizing: 'border-box', position: 'fixed', resize: 'none', fontSize: '12px', lineHeight: '14px', bottom: 0, top: 'auto', right: 0, width: '100%', zIndex: 1000000, padding: 0, paddingBottom: options.browserinfo.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: options.browserinfo.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: options.browserinfo.isMobile ? '8px 12px' : '4px 8px' }), tdRight : createElem('td', 'table_row_data', { border: '0 none', textAlign: 'left', padding: options.browserinfo.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: options.browserinfo.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: options.browserinfo.isMobile ? '34px' : '28px', height: options.browserinfo.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: options.browserinfo.isMobile ? '16px' : 'inherit', //prevents ios safari's zoom on focus fontFamily: 'Consolas, monaco, monospace', paddingLeft: '25px', margin: 0, height: options.browserinfo.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: options.browserinfo.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: options.browserinfo.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 = (typeof 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 = typeof 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 **==-- function displayConsole() { elements.base.style.display = "block"; } //initializes the console HTML element function initConsoleElement() { //reference var ref; //core function toggleScroll() { elements.scrollcontainer.scrollTop = elements.scrollcontainer.scrollHeight; elements.scrollcontainer.scrollLeft = 0; } function destroyConsole() { //conan the destroyer. Very basic; just removes the console element. mobileConsole will still 'pipe' console logging //don't see any reason for now to reverse that. /*elements.base.parentNode.removeChild(elements.base); status.initialized = false; console.warn( '--==## Mobile Console DESTROYED ##==--' + '\n' + 'To enable again: reload the page. Tip: use the minimize button instead of closing.' );*/ elements.base.style.display = "none"; } function assemble() { var i = options.methods.length, key; //add close button elements.buttons.closer = elements.button.cloneNode(false); elements.buttons.closer.innerHTML = '✕'; elements.buttons.closer.setAttribute('title', 'Close (destroy) console'); setCSS(elements.buttons.closer, { float: 'right', margin: '0'}); //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(e) { var button = e.currentTarget || e.srcElement; var logType = button.innerHTML.toLowerCase(); var elems = elements.lines[logType], i = elems.length; button.toggled = (typeof 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) { //hide buttons that we don't want setCSS(elements.buttons[methods[i]], { display: 'none' }); } } if (options.browserinfo.evtLstn) { elements.buttons.toggler.addEventListener('click', toggleHeight, false); elements.buttons.closer.addEventListener('click', destroyConsole, false); elements.buttons.clear.addEventListener('click', console.clear, false); } else { elements.buttons.toggler.attachEvent('onclick', toggleHeight); elements.buttons.closer.attachEvent('onclick', destroyConsole); elements.buttons.clear.attachEvent('onclick', console.clear); } } //init function init() { var element = assemble(); docReady(function () { setBinds(); attach(element); if (options.startMinimized) { toggleHeight(); } elements.base.style.display = "none"; }); //expose Public methods and variables return { toggleHeight : toggleHeight, toggleScroll : toggleScroll, destroy: destroyConsole }; } if (!ref) { ref = init(); } return ref; } //initializes the new console logger function initConsole() { //reference var ref; //sub helpers function isEmpty(obj) { for(var prop in obj) { if(obj.hasOwnProperty(prop)) return false; } return JSON.stringify(obj) === JSON.stringify({}); } 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 prop, output = ''; if (!isElement(object)) { for (prop in object) { if (!object.hasOwnProperty(prop)) { continue; } else if (typeof (object[prop]) === 'object') { output += prop + ((Array.isArray(object[prop])) ? ': Array(' + object[prop].length + ')' : ': {…}'); } else if (typeof (object[prop]) === 'function') { output += 'func: f'; } else { output += prop + ': "' + object[prop] + '"'; } output += ', '; } return '{' + output.slice(0, -2) + '}'; // 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 = (typeof trace !== 'undefined' && typeof 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+)/); if (lineAndColumn === null) continue; 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 (typeof trace === 'undefined') { return; } trace.reverse(); //reverse order of trace, just like 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 (typeof value === 'string') { valueColor = '#c54300'; if (value.length > options.truncate) { value = '"' + String(value).substring(0, options.truncate) + '" [...]
Note: string was truncated to ' + options.truncate + ' chars'; } else { value = '"' + value + '"'; } } else if (value === null) { valueColor = '#808080'; value = 'null'; } else if (typeof value === 'undefined' || value === undefined) { valueColor = '#808080'; value = 'undefined'; } else if (typeof value === 'object') { if (isEmpty(value)) { value = '{}'; } else { valueColor = ''; //iterate over object to create another table inside var tempTable = createElem('table', 'stackTraceSubTable', { border: '0 none', margin: 0, display: 'none', marginLeft: '10px', marginTop: options.browserinfo.isMobile ? '8px' : '4px', tableLayout: 'auto', maxWidth: '100%', color: '#333333' }), wrap = document.createElement('div'); wrap.appendChild(objectToTable(tempTable, value).cloneNode(true)); if (Array.isArray(value)) { value = 'Array(' + value.length + ')' + wrap.innerHTML; } else { value = wrap.innerHTML; } } } return '' + key + ': ' + value + ''; } function objectToTable(table, object) { var i, tdLeft, tr; if (isElement(object)){ tdLeft = elements.td.cloneNode(false); tr = elements.tr.cloneNode(false); tdLeft.innerHTML = htmlToString(object.outerHTML); tr.appendChild(tdLeft); table.appendChild(tr); } else { for (i in object) { if (object.hasOwnProperty(i)) { 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(e) { var button = e.currentTarget || e.srcElement, 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 newConsole2() { try { //get arguments, set vars var method = arguments[0], className, isHTMLElement, message = (typeof arguments[1].newMessage !== 'undefined') ? arguments[1].newMessage : undefined, stackTrace = (typeof arguments[1].newStackTrace !== 'undefined') ? arguments[1].newStackTrace : undefined; //if message emtpy or undefined, show empty message-message if (message === '' || typeof message === 'undefined' || message === undefined) { 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') { message = isElement(message) ? htmlToString(message.outerHTML.match(/<(.*?)>/g)[0] + '...' + message.outerHTML.match(/<(.*?)>/g).pop()) : //gets e.g.
...
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 && typeof 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(); } catch (e) { //not logging. why? throw error if (options.browserinfo.isMobile) { alert(e); } originalConsole.error('mobileConsole generated an error logging this event! (type: ' + typeof message + ')'); originalConsole.error(arguments); originalConsole.error(e); //try to re-log it as an error newConsole('error', e); } } function newConsole() { if (typeof arguments[1].newMessage === 'object') { var args = arguments[1].newMessage.slice(); var argString = []; for (var i in args) { if (typeof args[i] === 'object') { if (argString.length > 0) { arguments[1].newMessage = argString.join(' '); newConsole2(...arguments); argString = []; } arguments[1].newMessage = args[i]; newConsole2(...arguments); } else { argString.push(args[i]); } } if (argString.length > 0) { arguments[1].newMessage = argString.join(' '); newConsole2(...arguments); argString = []; } } else { newConsole2(...arguments); } //========================================================== //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); } } function interceptConsole(method) { var original = console ? console[method] : missingMethod(), i, stackTraceOrig; if (!console) { console = {}; } //create empty console if we have no console (IE?) 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') { try { elements.table.innerHTML = ''; } catch (e) { console.log('This browser does not allow clearing tables, the console cannot be cleared.'); return; } 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 args.newMessage = []; var i = 0; while (typeof args[i] !== 'undefined') { args.newMessage.push(args[i]); i ++; } 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); }; return { //nothing 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 (typeof e === 'undefined') { return; } //unset any already hovered elements var hovered = getFromArrayById('hover').element, target = e.target || e.srcElement, over; if (typeof hovered !== 'undefined') { setCSS(hovered, { color: '', backgroundColor: '' }).id = ''; } if (e.type === 'mouseover') { status.acHovered = true; over = true; } else { over = false; } setCSS(target, { color: over ? '#FFFFFF' : '', backgroundColor: over ? 'rgb(66,139,202)' : '' }).id = over ? 'hover' : ''; } function toggleAutoComplete(show) { var hidden = (elements.autocomplete.currentStyle ? elements.autocomplete.currentStyle.display : window.getComputedStyle(elements.autocomplete, null).display) === 'none'; show = (typeof show === 'undefined') ? hidden : show; setCSS(elements.autocomplete, {display: (show) ? 'inherit' : 'none'}); status.acActive = show; if (!show) { status.acHovered = false; } } function clickAutoComplete(e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } var tgt = e.target || e.srcElement; elements.consoleinput.value = tgt.innerHTML; elements.consoleinput.focus(); toggleAutoComplete(); } function autoComplete(command) { if (command.length < 1) { toggleAutoComplete(false); return; } var searchString = encodeURI(command), matches, match, row, i, maxAmount = options.browserinfo.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) { e = e || window.event; var tgt = e.target || e.srcElement; if (tgt === elements.consoleinput) { if ((e.key === 'Enter' || e.keyCode === 13)) { //enter if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } 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 if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } 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 = typeof hist[history.input.commandIdx] === 'undefined' ? '' : decodeURI(hist[history.input.commandIdx]); } } } if (e.keyCode === 27 && status.acActive) { toggleAutoComplete(false); } }; document.onkeyup = function (e) { e = e || window.event; var tgt = e.target || e.srcElement; if (tgt === 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() { assemble(); setBinds(); return { //nothing to expose }; } //return if (!ref) { ref = init(); } return ref; } function init() { if (!status.initialized) { if (consoleElement && mobileConsole) { console.error( 'Mobile Console cannot be reconstructed! Reload the page to enable Mobile Console again.' + '\n' + 'Tip: use the minimize button instead of closing.' ); return; } else { 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' ) + ' ##==--' ); } //autorun if mobile if (options.browserinfo.isMobile || options.overrideAutorun) { init(); } //expose the mobileConsole's methods return { init : init, displayConsole: displayConsole, about: about, toggle : toggleHeight, status : status, options : options }; }());