/**
* ticker-log<br>
* On-screen, ticker-tape-style logging tool<br><br>
*
* {@link http://jonbri.github.io/ticker-log}<br>
* {@link https://github.com/jonbri/ticker-log}<br>
* {@link https://www.npmjs.com/package/ticker-log}<br><br>
*
* @module ticker-log
* @author Jonathan Brink <jonathandavidbrink@gmail.com>
*/
(function ticker_go() {
// make sure dom is ready
if (!document.body) {
if (document.readyState !== 'loading') {
ticker_go();
} else {
document.addEventListener('DOMContentLoaded', ticker_go);
}
return;
}
//////////////////////////////////
// variables global to ticker
var
// embed starparam library
starparam = (function() {
var starparam,
bOrig__is_starparam_embedded = window.__is_starparam_embedded;
window.__is_starparam_embedded = true;
/* https://github.com/jonbri/starparam v1.0.1 Thu Dec 22 13:41:34 EST 2016 */
!function(){function r(r){return null===r||void 0===r}function n(n){var a;if(!r(n))return a=new RegExp("(^[^?&#]+)\\??([^#]*)#?(.*)$").exec(n),r(a)?{params:[]}:{prefix:a[1],params:a[2].split("&").filter(function(r){return""!==r}).map(function(r){return function(r){return{name:r[0],value:r[1]}}(r.split("="))}),hash:""===a[3]?void 0:a[3]}}function a(n){var a="";if(!r(n))return r(n.prefix)===!1&&(a+=n.prefix),r(n.params)===!1&&n.params.forEach(function(r,n){a+=0===n?"?":"&",a+=r.name+"="+r.value}),r(n.hash)===!1&&(a+="#"+n.hash),a}function e(r,a){return n(r).params.filter(function(r){return r.name===a})[0]}function t(n,a){var t;if(!r(n)&&!r(a))return t=e(n,a),r(t)?void 0:t.value}function i(t,i,u){var o;if(!r(t)&&!r(i))return r(u)&&(u=""),o=n(t),r(e(t,i))?o.params.push({name:i,value:u}):o.params=o.params.map(function(r){return r.name===i&&(r.value=u),r}),a(o)}function u(e,t){var i;if(!r(e))return r(t)?e:(i=n(e),i.params=i.params.filter(function(r){return r.name!==t}),a(i))}!function(){var e={parse:function(a){if(0===arguments.length&&(a=window.location.href),!r(a))return n(a)},stringify:function(r){return a(r)},get:function(n,a){var e;return a=a||{},e=a.url,r(e)&&(e=window.location.href),t(e,n)},set:function(n,a,e){var t,u;if(e=e||{},t=e.hasOwnProperty("url")?e.url:window.location.href,!r(t))return u=i(t,n,a)},remove:function(n,a){var e,t;if(a=a||{},e=a.hasOwnProperty("url")?a.url:window.location.href,!r(e))return t=u(e,n)}};window.__is_starparam_embedded===!0?window.__embedded_starparam=e:window.starparam=e}()}();
starparam = window.__embedded_starparam;
if (bOrig__is_starparam_embedded !== undefined) {
window.__is_starparam_embedded = bOrig__is_starparam_embedded;
} else {
delete window.__is_starparam_embedded;
}
return window.__embedded_starparam;
}()),
aChannels = ['log', 'debug', 'warn', 'error', 'trace'],
oChannels = {},
// default settings
oDEFAULTS = {
interval: 300,
logStartTop: 100,
align: 'left',
requireBackTick: true,
announceMacros: false,
channels: ['log']
},
// global (to ticker) config
oConfig = {
silentMode: false,
pauseMode: false,
adjustmentInterval: 25,
lastTextareaAction: undefined,
sMacro9Code: '// macro 9\r\r',
logStyle: {
position: 'fixed',
color: 'black',
'background-color': '#F2F2F2',
padding: '1px',
'z-index': 9998,
top: 0,
left: 0,
'font-family': 'monospace',
'font-size': '14px',
opacity: 0.85
}
},
// log buffer
aBuffer = [],
// buffer of logs still-to-be-rendered
aRenderBuffer = [],
// number returned from setInterval responsible for on-screen movement
render_interval,
// macros (0-8)
// (9 stored in oConfig.sMacro9Code)
aMacros = {},
// leave a 1/4 page buffer
iAllowedHeight = window.innerHeight * 1.25,
// the config settings that are configurable
aConfigurableKeys = [
'interval',
'logStartTop',
'align',
'requireBackTick'
// channels has special handling
// defaultBacktickKeys has special handling
],
// dom id of the "output" textarea
sTextareaId = 'tickerTextarea',
// whether or not the "`" key is pressed
keyIsDown = false,
// keep references to "addEventListener" functions for later removal
fnKeyDown, fnKeyUp,
// filter logging, populated via "filter" api
fnFilterFunction,
// help string
aHelp = [
'___________________________________',
'ticker-log_________________________',
'___________________________________',
'Enter commands: `key_______________',
'___________________________________',
'h__-> help_________________________',
't__-> test_________________________',
'p__-> pause (freeze output)________',
'k__-> kill (remove all)____________',
'o__-> output (show in textarea)____',
'l__-> output all (all past logging)',
'f__-> flip (reverse textarea text)_',
'd__-> dump (show config values)____',
'b__-> toggle api "`" requirement___',
'0-9-> invoke macros________________',
'm__-> enter macro 9________________',
'___________________________________',
'up______-> increase speed__________',
'down____-> decrease speed__________',
'right___-> move logs right_________',
'left____-> move logs left__________',
'pageup__-> increase starting point_',
'pagedown-> decrease starting point_',
'tab_____-> change console channel__',
'___________________________________',
'enter -> save configuration in url_',
'___________________________________',
'Print to the screen:_______________',
'__>> console.log("`", "hello...")__',
'___________________________________',
'-__________________________________',
'-__________________________________',
'-__________________________________'
],
// keycodes
KEYS = {
Tab: 9,
Enter: 13,
Esc: 27,
PageDown: 33,
PageUp: 34,
Left: 37,
Up: 38,
Right: 39,
Down: 40,
'0': 48,
'1': 49,
'2': 50,
'3': 51,
'4': 52,
'5': 53,
'6': 54,
'7': 55,
'8': 56,
'9': 57,
A: 65,
B: 66,
C: 67,
D: 68,
F: 70,
H: 72,
K: 75,
L: 76,
M: 77,
O: 79,
P: 80,
S: 83,
T: 84,
BackTick: 192
},
// the keys used as commands
// keys that that need to be key toggle friendly
// subset of KEYS
aActionKeys = [
KEYS.Tab,
KEYS.Esc,
KEYS.PageDown,
KEYS.PageUp,
KEYS.Left,
KEYS.Up,
KEYS.Right,
KEYS.Down,
KEYS['0'],
KEYS['1'],
KEYS['2'],
KEYS['3'],
KEYS['4'],
KEYS['5'],
KEYS['6'],
KEYS['7'],
KEYS['8'],
KEYS['9'],
KEYS.A,
KEYS.B,
KEYS.C,
KEYS.D,
KEYS.F,
KEYS.H,
KEYS.K,
KEYS.L,
KEYS.M,
KEYS.O,
KEYS.P,
KEYS.S,
KEYS.T
];
//////////////////////////////////
// util functions
/**
* used to iterate over querySelectorAll
* See link:
* {@link https://toddmotto.com/ditch-the-array-foreach-call-nodelist-hack}
* @param {array} array pseudo-array from querySelectorAll
* @param {function} callback invoke for each iteration
* @param {object} scope "this" scope for callback
*/
function pseudoForEach(array, callback, scope) {
for (var i = 0; i < array.length; i++) {
callback.call(scope, i, array[i]); // passes back stuff we need
}
}
/**
* overlay div's domelement style object
* @param {object} domNode dom element to apply style to
* @param {object} oStyle map of style declarations, e.g. color: 'green'
*/
function assignStyle(domNode, oStyle) {
for (var key in oStyle) {
if (oStyle.hasOwnProperty(key)) {
domNode.style[key] = oStyle[key];
}
}
}
/**
* determine whether passed-in object is an array
* @param {object} o potential array
* @returns {boolean} whether object is of type array
*/
function isArray(o) {
return Object.prototype.toString.call(o) === '[object Array]';
}
//////////////////////////////////
// api functions
/**
* Overlay object over configuration object.<br>
* Only an api function...doesn't map to a key.<br>
* Public+private way of setting configuration properties.<br><br>
*
* Example:<br>
* <pre>
* // change log speed to 400
* window.ticker.config({
* interval: 400
* });
* </pre>
*
* @param {object} o property/value map to apply
*
* @exports ticker-log
* @name config
* @public
* @function
*/
function config(o) {
var bChannels = isArray(o.channels);
if (bChannels) {
o.previousChannels = oConfig.channels;
}
for (var sKey in o) {
oConfig[sKey] = o[sKey];
}
if (bChannels) {
_listenToChannels();
}
}
/**
* Print log div to screen.<br>
* Only an api function...doesn't map (directly) to a key.<br><br>
*
* A configuration object can be passed as the second argument:
* <table>
* <tr>
* <th>key<th>value
* <tr>
* <th>overrideSilentMode</th><td>still print, even if silent mode is on
* <tr>
* <th>internal<td>do not track in aBuffer
* </table>
* <br>
*
* Example:<br>
* <pre>
* // show log on-screen
* window.ticker.print('lorum ipsum');
* </pre>
* <br>
*
* @param {string} text innerHTML for log dom ref
* @param {object} o configuration object
*
* @exports ticker-log
* @name print
* @public
* @function
*/
function print(text, o) {
o = o || {};
if (text === undefined || text === null) {
return;
}
if (oConfig.silentMode === true) {
return;
}
if (typeof fnFilterFunction === 'function' &&
fnFilterFunction(text) !== true) {
return;
}
if (o.textarea) {
_renderTextarea(text);
}
if (o.internal !== true) {
aBuffer.unshift(text);
}
aRenderBuffer.unshift(text);
_flushBuffer();
}
/**
* "t" api function.<br>
* Print out test log (plus date).
*
* @exports ticker-log
* @name test
* @public
* @function
*/
function test() {
if (oConfig.pauseMode === true) {
_nonSavedPrint('pauseMode');
return;
}
print('test: ' + new Date());
}
/**
* "h" api function.<br>
* Show help text on-screen as logs.
*
* @exports ticker-log
* @name help
* @public
* @function
*/
function help() {
oConfig.pauseMode = false;
kill();
oConfig.logStartTop = oDEFAULTS.logStartTop;
for (var i = 0; i < aHelp.length; i++) {
var text = aHelp[i];
text = text.replace(/\_/g, ' ');
print(text);
}
}
/**
* "k" api function.<br>
* Clear render buffer and remove all ticker log dom elements.
*
* @exports ticker-log
* @name kill
* @public
* @function
*/
function kill() {
oConfig.pauseMode = false;
killTextarea();
aRenderBuffer = [];
var aLogNodes = document.querySelectorAll('.ticker_log');
pseudoForEach(aLogNodes, function(i, oLogNode) {
oLogNode.parentNode.removeChild(oLogNode);
});
}
/**
* "p" api function.<br>
* Toggle pauseMode config prop boolean.
*
* @exports ticker-log
* @name pause
* @public
* @function
*/
function pause() {
if (oConfig.pauseMode) {
print('pause off');
} else {
print('paused');
}
oConfig.pauseMode = !oConfig.pauseMode;
}
/**
* "o" api function.<br>
* Show log text in the "output textarea".
*
* @param {boolean} bAll whether to show all logs ever,
* OR just the current on-screen ones (default: false)
*
* @exports ticker-log
* @name output
* @public
* @function
*/
function output(bAll) {
_flushBuffer();
if (bAll === undefined) {
bAll = false;
}
var sAllOutput='';
// get all output
if (bAll === true) {
aBuffer.forEach(function(s) {
sAllOutput += s + '\n';
});
} else {
// just show items on screen
var aLogNodes = document.querySelectorAll('.ticker_log');
if (aLogNodes.length > 0) {
pseudoForEach(aLogNodes, function(i, oLogNode) {
var string = oLogNode.innerHTML.trim();
string = string.replace(/ /g, '');
sAllOutput += string + '\n';
});
}
}
// remove final newline
sAllOutput = sAllOutput.replace(/\n$/, '');
_toggleTextarea({
text: sAllOutput,
source: KEYS.O
});
}
/**
* "l" (for "log") api function.<br>
* Api function to show all saved log messages.
*
* @exports ticker-log
* @name outputAll
* @public
* @function
*/
function outputAll() {
output(true);
}
/**
* "f" api function.<br>
* flip (reverse) order of textarea
*
* @exports ticker-log
* @name flip
* @public
* @function
*/
function flip() {
var textareaContainer = document.getElementById(sTextareaId),
textarea;
if (textareaContainer) {
textarea = textareaContainer.querySelectorAll('textarea')[0];
textarea.value = textarea.value.split('\n').reverse().join('\n');
}
}
/**
* "d" api function.<br>
* Show configuration properties in output textarea.
*
* @exports ticker-log
* @name dump
* @public
* @function
*/
function dump() {
var s = '';
aConfigurableKeys.forEach(function(sKey) {
s += (sKey + ': ' + oConfig[sKey]) + '\n';
});
s += 'listening to console: ' + oConfig.channels + '\n';
_toggleTextarea({
text: s,
source: KEYS.D
});
}
/**
* "s" api function.<br>
* Toggle silentMode config prop boolean.
*
* @exports ticker-log
* @name silent
* @public
* @function
*/
function silent() {
if (oConfig.silentMode === true) {
oConfig.silentMode = false;
print('silent mode off');
} else {
oConfig.silentMode = true;
}
}
/**
* "up" api function.<br>
* Decrease delay interval by half the adjustmentInterval.
*
* @exports ticker-log
* @name increaseSpeed
* @public
* @function
*/
function increaseSpeed() {
if (oConfig.pauseMode) {
_nonSavedPrint('pauseMode');
return;
}
oConfig.interval -= (oConfig.adjustmentInterval/2);
print('speed: ' + oConfig.interval);
}
/**
* "down" api function.<br>
* Increase delay interval by adjustmentInterval.
*
* @exports ticker-log
* @name decreaseSpeed
* @public
* @function
*/
function decreaseSpeed() {
if (oConfig.pauseMode) {
_nonSavedPrint('pauseMode');
return;
}
oConfig.interval += oConfig.adjustmentInterval;
print('speed: ' + oConfig.interval);
}
/**
* "right" api function.<br>
* Change log container position and alignment of log dom elements.
*
* @exports ticker-log
* @name moveRight
* @public
* @function
*/
function moveRight() {
oConfig.align = 'right';
_postConfigApply();
// move existing logs
var aLogNodes = document.querySelectorAll('.ticker_log');
pseudoForEach(aLogNodes, function(i, oLogNode) {
oLogNode.style.right = 0;
oLogNode.style.left = 'inherit';
oLogNode.style['text-align'] = 'right';
});
test();
}
/**
* "left" api function.<br>
* Change log container position and alignment of log dom elements.
*
* @exports ticker-log
* @name moveLeft
* @public
* @function
*/
function moveLeft() {
oConfig.align = 'left';
_postConfigApply();
// move existing logs
var aLogNodes = document.querySelectorAll('.ticker_log');
pseudoForEach(aLogNodes, function(i, oLogNode) {
oLogNode.style.left = 0;
oLogNode.style.right = 'inherit';
oLogNode.style['text-align'] = 'left';
});
test();
}
/**
* "enter" api function.<br>
* Update url (window.location) to "save state".<br>
* Only use config props that have changed.<br>
* Generate url-friendly, json string to use for "ticker" param.
*
* @exports ticker-log
* @name saveConfig
* @public
* @function
*/
function saveConfig() {
var url = _generateSaveUrl();
if (history.replaceState) {
window.history.replaceState({path:url},'',url);
} else {
window.location.replace(url);
}
}
/**
* "pageDown" api function.<br>
* Change starting vertical position (logStartTop) for on-screen logs.
*
* @exports ticker-log
* @name increaseLogStartTop
* @public
* @function
*/
function increaseLogStartTop() {
if (oConfig.pauseMode) {
_nonSavedPrint('pauseMode');
return;
}
kill();
oConfig.logStartTop += 5;
print('start: ' + oConfig.logStartTop);
}
/**
* "pageUp" api function.<br>
* Change starting vertical position (logStartTop) for on-screen logs.
*
* @exports ticker-log
* @name decreaseLogStartTop
* @public
* @function
*/
function decreaseLogStartTop() {
if (oConfig.pauseMode) {
_nonSavedPrint('pauseMode');
return;
}
kill();
oConfig.logStartTop -= 5;
print('start: ' + oConfig.logStartTop);
}
/**
* Register (overwrite) macro.<br>
* For macros 0-8.<br>
* Only an api function...doesn't map to a key.
*
* @param {int} iNumToRegister key in aMacros object to write to
* @param {function} fn callback function
*
* @exports ticker-log
* @name registerMacro
* @public
* @function
*/
function registerMacro(iNumToRegister, fn) {
if (iNumToRegister === 9) {
console.log('`', 'macro 9 reserved for interactive macro (`m)');
return;
}
if (oConfig.announceMacros === true) {
console.log('`', 'registering macro: ' + iNumToRegister);
}
aMacros[iNumToRegister] = fn;
}
/**
* "m" api function.<br>
* For macro 9.<br>
* Show a textarea where macro can be edited.<br>
* "save" macro when textarea is dismissed.
*
* @exports ticker-log
* @name macroEdit
* @public
* @function
*/
function macroEdit() {
var sDefaultText = oConfig.sMacro9Code;
_toggleTextarea({
text: sDefaultText,
source: KEYS.M,
buttons: {
clear: function() {
killTextarea();
macroEdit();
}
},
exit: function(sValue) {
oConfig.sMacro9Code = sValue;
if (oConfig.announceMacros === true) {
console.log('`', 'registering macro: 9');
}
aMacros[9] = function() {
/* eslint-disable no-eval */
/* jshint ignore:start */
eval(sValue);
/* jshint ignore:end */
/* eslint-enable no-eval */
};
}
});
}
/**
* "0-9" api function.<br>
* Also can be called directly.<br>
* Execute macro.
*
* @param {int} iMacroSlot macro in aMacros object to execute
*
* @exports ticker-log
* @name runMacro
* @public
* @function
*/
function runMacro(iMacroSlot) {
if (typeof aMacros[iMacroSlot] === 'function') {
if (oConfig.announceMacros === true) {
console.log('`', 'running macro: ' + iMacroSlot);
}
aMacros[iMacroSlot]();
} else {
console.log('`', 'macro empty');
}
}
/**
* "Tab" api function.<br>
* Switch to listen to next console channel ("log", "warn", etc).<br>
* Order is determined by aChannels.
*
* @exports ticker-log
* @name nextChannel
* @public
* @function
*/
function nextChannel() {
var i = 0,
sCurrentChannel = oConfig.channels[0];
// if there are multiple channels being used
// just set it up so "log" becomes the next channel
if (oConfig.channels.length > 1) {
sCurrentChannel = 'trace'; // trace + 1 => log
}
// set "i" to be the index of the array
for (; i < aChannels.length; i++) {
if (aChannels[i] === sCurrentChannel) {
break;
}
}
oConfig.previousChannels = oConfig.channels;
oConfig.channels = [aChannels[(i + 1) % aChannels.length]];
_listenToChannels();
// there will only be one channel at this point
print('listening to ' + oConfig.channels[0]);
}
/**
* "Esc" api function.<br>
* Remove textarea dom element.
*
* @exports ticker-log
* @name killTextarea
* @public
* @function
*/
function killTextarea() {
var oTickerTextarea = document.getElementById(sTextareaId);
if (oTickerTextarea) {
oTickerTextarea.parentNode.removeChild(oTickerTextarea);
}
}
/**
* "end all ticker operations" api function.<br>
* Stop ticker from doing anything.<br>
* Reset url param.<br>
* Reset console object.<br>
* Only an api function...doesn't map to a key.
*
* @exports ticker-log
* @name restoreAndExit
* @public
* @function
*/
function restoreAndExit() {
window.clearInterval(render_interval);
kill();
killTextarea();
document.body.removeEventListener('keydown', fnKeyDown);
document.body.removeEventListener('keyup', fnKeyUp);
window.ticker = undefined;
delete window.ticker;
// reset console object
aChannels.forEach(function(sChannel) {
console[sChannel] = oChannels[sChannel].fnOriginal;
});
}
/**
* Reset all settings.<br>
* Reverts everything ticker has modified and re-installs from scratch.<br>
* Only an api function...doesn't map to a key.
*
* @exports ticker-log
* @name reset
* @public
* @function
*/
function reset() {
restoreAndExit();
ticker_go();
}
/**
* "filter" api function.<br>
* Only an api function...doesn't map to a key.
*
* @param {regex|string} matcher either a string or regex to filter log by
*
* @exports ticker-log
* @name filter
* @public
* @function
*/
function filter(matcher) {
if (!matcher) {
return;
}
if (typeof matcher === 'string') {
fnFilterFunction = function(s) {
return s.indexOf(matcher) !== -1;
};
} else if (matcher instanceof RegExp) {
fnFilterFunction = function(s) {
return matcher.test(s);
};
} else if (typeof matcher === 'function') {
fnFilterFunction = matcher;
}
}
/**
* "listenToEverything" api function.<br>
* Only an api function...doesn't map to a key.<br><br>
*
* Listen to all console invocations:
* <ul>
* <li>all channels
* <li>regardless if backtick provided
* </ul>
*
* @exports ticker-log
* @name listenToEverything
* @public
* @function
*/
function listenToEverything() {
config({
requireBackTick: false,
channels: aChannels
});
_listenToChannels();
}
//////////////////////////////////
// domain/private functions
/**
* print but don't save to aBuffer
* uses "print" function's o.internal parameter
* @param {string} text text text to put in log dom ref
*/
function _nonSavedPrint(text) {
print(text, {
internal: true
});
}
/**
* start timeout loop
* for each iteration of the loop
* update the on-screen position of each log dom element
*/
function _startInterval() {
var moveUpOne = function() {
window.clearInterval(render_interval);
if (!oConfig.pauseMode) {
var aLogNodes = document.querySelectorAll('.ticker_log');
if (aLogNodes.length > 0) {
pseudoForEach(aLogNodes, function(iIndex, oLogNode) {
var iCurrentTop = parseInt(getComputedStyle(oLogNode).top, 10);
if (iCurrentTop <= 0) {
oLogNode.parentNode.removeChild(oLogNode);
oLogNode = null;
} else {
var sTop = (iCurrentTop - oLogNode.offsetHeight) + 'px';
oLogNode.style.top = sTop;
}
});
}
}
render_interval = setInterval(moveUpOne, oConfig.interval);
};
render_interval = setInterval(moveUpOne, oConfig.interval);
}
/**
* apply config properties
* used after oConfig is updated
*/
function _postConfigApply() {
function applyAlign() {
if (oConfig.align === 'right') {
oConfig.logStyle.right = 0;
oConfig.logStyle.left = 'inherit';
oConfig.logStyle['text-align'] = 'right';
} else {
oConfig.logStyle.right = 'inherit';
oConfig.logStyle.left = 0;
oConfig.logStyle['text-align'] = 'left';
}
}
applyAlign();
}
/**
* parse url parameter and populate oConfig
*/
function _loadConfigFromUrl() {
var o,
sUrlParam = starparam.get('ticker-log');
if (sUrlParam === null) {
return;
}
// read config from param
try {
o = JSON.parse(decodeURIComponent(sUrlParam));
} catch (e) {
}
// overlay url config onto global config object
if (typeof o === 'object') {
for (var key in o) {
if (key === 'channels') {
oConfig.channels = o[key].split(',');
} else if (key === 'defaultBacktickKeys') {
oConfig.defaultBacktickKeys = o[key].split(',').map(function(s) {
return parseInt(s);
});
} else {
oConfig[key] = o[key];
}
}
}
}
/**
* listen for when keys are pressed
* use both keydown and keyup to enable chording
*/
function _setupListeners() {
fnKeyDown = function(e) {
if (keyIsDown === false) {
// catch the ` (and potentially other modifier) key(s)
if (oConfig.defaultBacktickKeys.indexOf(e.keyCode) !== -1) {
keyIsDown = true;
}
}
if (keyIsDown !== true) {
return;
}
e.preventDefault();
var actionMap = {};
// toggle requireBackTick
actionMap[KEYS.B] = function() {
oConfig.requireBackTick = !!!oConfig.requireBackTick;
print('requireBackTick: ' + oConfig.requireBackTick);
};
actionMap[KEYS.D] = dump;
actionMap[KEYS.F] = flip;
actionMap[KEYS.S] = silent;
actionMap[KEYS.T] = test;
actionMap[KEYS.O] = output;
actionMap[KEYS.L] = outputAll;
actionMap[KEYS.P] = pause;
actionMap[KEYS.K] = kill;
actionMap[KEYS.H] = help;
actionMap[KEYS.M] = macroEdit;
actionMap[KEYS.Up] = increaseSpeed;
actionMap[KEYS.Down] = decreaseSpeed;
actionMap[KEYS.Right] = moveRight;
actionMap[KEYS.Left] = moveLeft;
actionMap[KEYS.PageDown] = decreaseLogStartTop;
actionMap[KEYS.PageUp] = increaseLogStartTop;
actionMap[KEYS.Enter] = saveConfig;
actionMap[KEYS.Tab] = nextChannel;
actionMap[KEYS.Esc] = killTextarea;
[0,1,2,3,4,5,6,7,8,9].forEach(function(i) {
actionMap[KEYS[i]] = function() {
runMacro(i);
};
});
if (typeof actionMap[e.keyCode] === 'function') {
actionMap[e.keyCode]();
}
};
fnKeyUp = function(e) {
if (keyIsDown === true && aActionKeys.indexOf(e.keyCode) === -1) {
keyIsDown=false;
}
};
document.body.addEventListener('keydown', fnKeyDown);
document.body.addEventListener('keyup', fnKeyUp);
}
/**
* determine "top" position of last log dom element
* @returns {int} top position value of dom ref
*/
function _calculateTop() {
var oLastNode =
Array.prototype.slice.call(document.querySelectorAll('.ticker_log'), 0).
reverse()[0];
if (!oLastNode) {
return oConfig.logStartTop;
} else {
return parseInt(oLastNode.style.top, 10) + (oLastNode.offsetHeight);
}
}
/**
* create and append to body a log dom element
* @param {string} sText the log text
*/
function _renderText(sText) {
var div = document.createElement('div');
assignStyle(div, oConfig.logStyle);
div.className += ' ticker_log';
div.innerHTML = sText;
var iTop = _calculateTop();
if (iTop < (oConfig.logStartTop / 2)) {
iTop = oConfig.logStartTop;
}
div.style.top = iTop + 'px';
// pause log on click
// div will be destroyed when it reaches off-screen
// which will release the event listener
div.addEventListener('click', function() {
pause();
});
document.body.appendChild(div);
}
/**
* if there is on-screen space available
* render as many log dom elements as possible from aRenderBuffer
*/
function _flushBuffer() {
while (aRenderBuffer.length > 0 && _calculateTop() < iAllowedHeight) {
_renderText(aRenderBuffer.pop());
}
}
/**
* "tune in" to the channel defined by configuration
* perform "console" overrides (log, warn, etc)
* cleanup previously listened to channel(s)
* overwrite "channels" config
*/
function _listenToChannels() {
// revert previous channels
if (isArray(oConfig.previousChannels)) {
oConfig.previousChannels.forEach(function(sChannel) {
if (typeof oChannels[sChannel].fnOriginal === 'function') {
console[sChannel] = oChannels[sChannel].fnOriginal;
}
});
}
oConfig.channels.forEach(function(sChannel) {
// monkey-patch and chain console function
console[sChannel] = function(firstArg, secondArg) {
var sText = firstArg;
if (firstArg === '`') {
sText = secondArg;
}
if (oConfig.requireBackTick === false) {
print(sText);
} else if (oConfig.requireBackTick === true && firstArg === '`') {
print(sText);
} else {
oChannels[sChannel].fnOriginal.apply(this, Array.prototype.slice.call(arguments));
}
};
});
}
/**
* create textarea container div and textarea
* position, fill with sText, and render
* @param {string} sText text to place in textarea
*/
function _renderTextarea(sText) {
var heightOfPage = window.innerHeight,
widthOfPage = window.innerWidth,
textareaDiv;
if (document.getElementById(sTextareaId)) {
killTextarea();
}
textareaDiv = document.createElement('div');
textareaDiv.id = sTextareaId;
textareaDiv.style.position = 'fixed';
textareaDiv.style.left = 0;
textareaDiv.style.top = 0;
textareaDiv.style['z-index'] = 9999;
textareaDiv.style.width = (widthOfPage/3) + 'px';
textareaDiv.style.height = (heightOfPage-10) + 'px';
var textarea = document.createElement('textarea');
textarea.style.height = '100%';
textarea.style.width = '100%';
textarea.innerHTML = sText;
textareaDiv.appendChild(textarea);
document.body.appendChild(textareaDiv);
}
/**
* manage showing/hiding textarea div container
* @param {object} o customize the textarea div
* keys:
* - text: (string) the text to show in the textarea
* - source: (string) id that identifies the invoking keyboard key
* - buttons: (object) map of buttons, with their labels as keys
* - exit: (fn) callback function when textarea is closed,
* the text inside of the textarea is passed along
*/
function _toggleTextarea(o) {
// if it's a new action, clear slate and render
if (oConfig.lastTextareaAction !== o.source) {
killTextarea();
}
oConfig.lastTextareaAction = o.source;
var textareaContainer;
if (document.getElementById(sTextareaId)) {
oConfig.pauseMode = false;
if (typeof o.exit === 'function') {
textareaContainer = document.getElementById(sTextareaId);
var textarea = textareaContainer.querySelectorAll('textarea')[0];
o.exit(textarea.value);
}
killTextarea();
} else {
oConfig.pauseMode = true;
_renderTextarea(o.text);
textareaContainer = document.getElementById(sTextareaId);
if (typeof o.buttons === 'object') {
var buttonContainer = document.createElement('div');
buttonContainer.style.position = 'absolute';
buttonContainer.style.bottom = 0;
buttonContainer.style.left = 0;
buttonContainer.style.height = '20px';
buttonContainer.style.borderTopWidth = '1px';
buttonContainer.style.borderTopStyle = 'solid';
buttonContainer.style.width = '100%';
buttonContainer.style.paddingTop = '5px';
for (var key in o.buttons) {
if (o.buttons.hasOwnProperty(key)) {
var button = document.createElement('button');
button.innerHTML = key;
button.onclick = o.buttons[key];
button.style.float = 'left';
buttonContainer.appendChild(button);
}
}
textareaContainer.appendChild(buttonContainer);
}
}
}
/**
* Returns a serialized json map representing
* certain property values that have changed state
* @returns {string} serialized config object
*/
function _generateConfigSerialization() {
var s = '{';
aConfigurableKeys.forEach(function(sKey) {
// don't include if default
if (oDEFAULTS[sKey] !== undefined &&
oDEFAULTS[sKey] === oConfig[sKey]) {
return;
}
s += '%22' + sKey + '%22:';
if (typeof oConfig[sKey] === 'string') {
s += '%22' + oConfig[sKey] + '%22';
} else {
s += oConfig[sKey];
}
s += ',';
});
// channels
if (oConfig.channels.length !== 1 && oConfig.channels[0] !== 'log') {
s += '%22channels%22:';
s += '%22' + oConfig.channels + '%22';
}
// defaultBacktickKeys
if (!(oConfig.defaultBacktickKeys.length === 1 &&
oConfig.defaultBacktickKeys[0] === KEYS.BackTick)) {
s += '%22defaultBacktickKeys%22:';
s += '%22' + oConfig.defaultBacktickKeys + '%22';
}
s += '}';
s = s.replace(/,}/, '}');
return s;
}
/**
* Return a url string for saving configuration state.<br />
* Apply config string to url and account for any past url state
* @param {string} sPrefix the starting url
* @returns {string} url string representing current state
*/
function _generateSaveUrl() {
return starparam.set('ticker-log', _generateConfigSerialization());
}
//////////////////////////////////
// execution starts
// until this time, everything in this file
// has just been variable and function declarations
// additional settings tweaks
// user can override using '`'
// as the main keyboard interface key
oDEFAULTS.defaultBacktickKeys = [KEYS.BackTick];
// fill oChannels object
aChannels.forEach(function(sChannel) {
oChannels[sChannel] = {
fnOriginal: console[sChannel]
};
});
// init config
// load default config
for (var sKey in oDEFAULTS) {
oConfig[sKey] = oDEFAULTS[sKey];
}
_loadConfigFromUrl();
_postConfigApply();
// manage proxying of console
_listenToChannels();
// keep polling to see if flushing the
// log buffer to screen is possible
setInterval(function() {
_flushBuffer();
}, 250);
// start job that "moves the ticker tape"
_startInterval();
// listen for keyboard events
_setupListeners();
// expose api to global namespace
(function() {
var ticker = {};
ticker.config = config;
ticker.test = test;
ticker.help = help;
ticker.kill = kill;
ticker.silent = silent;
ticker.pause = pause;
ticker.output = output;
ticker.outputAll = outputAll;
ticker.flip = flip;
ticker.dump = dump;
ticker.moveDown = increaseLogStartTop;
ticker.moveUp = decreaseLogStartTop;
ticker.moveLeft = moveLeft;
ticker.moveRight = moveRight;
ticker.increaseSpeed = increaseSpeed;
ticker.decreaseSpeed = decreaseSpeed;
ticker.nextChannel = nextChannel;
ticker.print = print;
ticker.registerMacro = registerMacro;
ticker.runMacro = runMacro;
ticker.filter = filter;
ticker.listenToEverything = listenToEverything;
ticker.macroEdit = macroEdit;
ticker.restoreAndExit = restoreAndExit;
ticker.reset = reset;
ticker.flush = _flushBuffer;
// private
ticker._oConfig = oConfig;
ticker._generateConfigSerialization = _generateConfigSerialization;
window.ticker = ticker;
}());
}());