// JQuery Console 1.0
// Sun Feb 21 20:28:47 GMT 2010
//
// Copyright 2010 Chris Done, Simon David Pratt. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// 1. Redistributions of source code must retain the above
// copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
// TESTED ON
// Internet Explorer 6
// Opera 10.01
// Chromium 4.0.237.0 (Ubuntu build 31094)
// Firefox 3.5.8, 3.6.2 (Mac)
// Safari 4.0.5 (6531.22.7) (Mac)
// Google Chrome 5.0.375.55 (Mac)
(function($) {
var isWebkit = !!~navigator.userAgent.indexOf(' AppleWebKit/');
$.fn.console = function(config) {
////////////////////////////////////////////////////////////////////////
// Constants
// Some are enums, data types, others just for optimisation
var keyCodes = {
// left
37: moveBackward,
// right
39: moveForward,
// up
38: previousHistory,
// down
40: nextHistory,
// backspace
8: backDelete,
// delete
46: forwardDelete,
// end
35: moveToEnd,
// start
36: moveToStart,
// return
13: commandTrigger,
// tab
18: doNothing,
// tab
9: doComplete
};
var ctrlCodes = {
// C-a
65: moveToStart,
// C-e
69: moveToEnd,
// C-d
68: forwardDelete,
// C-n
78: nextHistory,
// C-p
80: previousHistory,
// C-b
66: moveBackward,
// C-f
70: moveForward,
// C-k
75: deleteUntilEnd,
// C-l
76: clearScreen,
// C-u
85: clearCurrentPrompt
};
if (config.ctrlCodes) {
$.extend(ctrlCodes, config.ctrlCodes);
}
var altCodes = {
// M-f
70: moveToNextWord,
// M-b
66: moveToPreviousWord,
// M-d
68: deleteNextWord
};
var shiftCodes = {
// return
13: newLine,
};
var cursor = ' ';
////////////////////////////////////////////////////////////////////////
// Globals
var container = $(this);
var inner = $('
');
// erjiang: changed this from a text input to a textarea so we
// can get pasted newlines
var typer = $('');
// Prompt
var promptBox;
var prompt;
var continuedPromptLabel = config && config.continuedPromptLabel ?
config.continuedPromptLabel : "> ";
var column = 0;
var promptText = '';
var restoreText = '';
var continuedText = '';
var fadeOnReset = config.fadeOnReset !== undefined ? config.fadeOnReset : true;
// Prompt history stack
var history = [];
var ringn = 0;
// For reasons unknown to The Sword of Michael himself, Opera
// triggers and sends a key character when you hit various
// keys like PgUp, End, etc. So there is no way of knowing
// when a user has typed '#' or End. My solution is in the
// typer.keydown and typer.keypress functions; I use the
// variable below to ignore the keypress event if the keydown
// event succeeds.
var cancelKeyPress = 0;
// When this value is false, the prompt will not respond to input
var acceptInput = true;
// When this value is true, the command has been canceled
var cancelCommand = false;
// External exports object
var extern = {};
////////////////////////////////////////////////////////////////////////
// Main entry point
(function() {
extern.promptLabel = config && config.promptLabel ? config.promptLabel : "> ";
container.append(inner);
inner.append(typer);
typer.css({ position: 'absolute', top: 0, left: '-9999px' });
if (config.welcomeMessage)
message(config.welcomeMessage, 'jquery-console-welcome');
newPromptBox();
if (config.autofocus) {
inner.addClass('jquery-console-focus');
typer.focus();
setTimeout(function() {
inner.addClass('jquery-console-focus');
typer.focus();
}, 100);
}
extern.inner = inner;
extern.typer = typer;
extern.scrollToBottom = scrollToBottom;
extern.report = report;
extern.showCompletion = showCompletion;
extern.clearScreen = clearScreen;
})();
////////////////////////////////////////////////////////////////////////
// Reset terminal
extern.reset = function() {
var welcome = (typeof config.welcomeMessage != 'undefined');
var removeElements = function() {
inner.find('div').each(function() {
if (!welcome) {
$(this).remove();
} else {
welcome = false;
}
});
};
if (fadeOnReset) {
inner.parent().fadeOut(function() {
removeElements();
newPromptBox();
inner.parent().fadeIn(focusConsole);
});
} else {
removeElements();
newPromptBox();
focusConsole();
}
};
var focusConsole = function() {
inner.addClass('jquery-console-focus');
typer.focus();
};
extern.focus = function() {
focusConsole();
}
////////////////////////////////////////////////////////////////////////
// Reset terminal
extern.notice = function(msg, style) {
var n = $('').append($('').text(msg))
.css({ visibility: 'hidden' });
container.append(n);
var focused = true;
if (style == 'fadeout')
setTimeout(function() {
n.fadeOut(function() {
n.remove();
});
}, 4000);
else if (style == 'prompt') {
var a = $('
');
n.append(a);
focused = false;
a.click(function() {
n.fadeOut(function() {
n.remove();
inner.css({ opacity: 1 })
});
});
}
var h = n.height();
n.css({ height: '0px', visibility: 'visible' })
.animate({ height: h + 'px' }, function() {
if (!focused) inner.css({ opacity: 0.5 });
});
n.css('cursor', 'default');
return n;
};
////////////////////////////////////////////////////////////////////////
// Make a new prompt box
function newPromptBox() {
column = 0;
promptText = '';
ringn = 0; // Reset the position of the history ring
enableInput();
promptBox = $('');
var label = $('');
var labelText = extern.continuedPrompt ? continuedPromptLabel : extern.promptLabel;
promptBox.append(label.text(labelText).show());
label.html(label.html().replace(' ', ' '));
prompt = $('');
promptBox.append(prompt);
inner.append(promptBox);
updatePromptDisplay();
};
////////////////////////////////////////////////////////////////////////
// Handle setting focus
container.click(function() {
// Don't mess with the focus if there is an active selection
if (window.getSelection().toString()) {
return false;
}
inner.addClass('jquery-console-focus');
inner.removeClass('jquery-console-nofocus');
if (isWebkit) {
typer.focusWithoutScrolling();
} else {
typer.css('position', 'fixed').focus();
}
scrollToBottom();
return false;
});
////////////////////////////////////////////////////////////////////////
// Handle losing focus
typer.blur(function() {
inner.removeClass('jquery-console-focus');
inner.addClass('jquery-console-nofocus');
});
////////////////////////////////////////////////////////////////////////
// Bind to the paste event of the input box so we know when we
// get pasted data
typer.bind('paste', function(e) {
// wipe typer input clean just in case
typer.val("");
// this timeout is required because the onpaste event is
// fired *before* the text is actually pasted
setTimeout(function() {
typer.consoleInsert(typer.val());
typer.val("");
}, 0);
});
//helper function to figure out new character added to input
difference = function(value1, value2) {
var output = [];
for (i = 0; i < value2.length; i++) {
if (value1[i] !== value2[i]) {
output.push(value2[i]);
}
}
return output.join("");
}
////////////////////////////////////////////////////////////////////////
// Handle key hit before translation
// For picking up control characters like up/left/down/right
typer.keydown(function(e) {
typer.oldValue = typer.val();
oldValue = typer.val();
cancelKeyPress = 0;
var keyCode = e.which || e.code;
// C-c: cancel the execution
if (e.ctrlKey && keyCode == 67) {
cancelKeyPress = keyCode;
cancelExecution();
return false;
}
if (acceptInput) {
if (e.shiftKey && keyCode in shiftCodes) {
cancelKeyPress = keyCode;
(shiftCodes[keyCode])();
return false;
} else if (e.altKey && keyCode in altCodes) {
cancelKeyPress = keyCode;
(altCodes[keyCode])();
return false;
} else if (e.ctrlKey && keyCode in ctrlCodes) {
cancelKeyPress = keyCode;
(ctrlCodes[keyCode])();
return false;
} else if (keyCode in keyCodes) {
cancelKeyPress = keyCode;
(keyCodes[keyCode])();
return false;
}
}
if (isIgnorableKey(e)) {
return false;
}
// C-v: don't insert on paste event
if ((e.ctrlKey || e.metaKey) && String.fromCharCode(keyCode).toLowerCase() == 'v') {
return true;
}
if (acceptInput && cancelKeyPress != keyCode && keyCode >= 32) {
if (cancelKeyPress) return false;
}
});
//after input event get new string, figure out the difference and add the character to the typer.
typer.on("input", function(e) {
var newValue = typer.val();
typer.consoleInsert(difference(typer.oldValue, newValue));
typer.val("");
})
function isIgnorableKey(e) {
// for now just filter alt+tab that we receive on some platforms when
// user switches windows (goes away from the browser)
return ((e.keyCode == keyCodes.tab || e.keyCode == 192) && e.altKey);
};
////////////////////////////////////////////////////////////////////////
// Rotate through the command history
function rotateHistory(n) {
if (history.length == 0) return;
ringn += n;
if (ringn < 0) ringn = history.length;
else if (ringn > history.length) ringn = 0;
var prevText = promptText;
if (ringn == 0) {
promptText = restoreText;
} else {
promptText = history[ringn - 1];
}
if (config.historyPreserveColumn) {
if (promptText.length < column + 1) {
column = promptText.length;
} else if (column == 0) {
column = promptText.length;
}
} else {
column = promptText.length;
}
updatePromptDisplay();
};
function previousHistory() {
rotateHistory(-1);
};
function nextHistory() {
rotateHistory(1);
};
// Add something to the history ring
function addToHistory(line) {
history.push(line);
restoreText = '';
};
// Delete the character at the current position
function deleteCharAtPos() {
if (column < promptText.length) {
promptText =
promptText.substring(0, column) +
promptText.substring(column + 1);
restoreText = promptText;
return true;
} else return false;
};
function backDelete() {
if (moveColumn(-1)) {
deleteCharAtPos();
updatePromptDisplay();
}
};
function forwardDelete() {
if (deleteCharAtPos()) {
updatePromptDisplay();
}
};
function deleteUntilEnd() {
while (deleteCharAtPos()) {
updatePromptDisplay();
}
};
function clearCurrentPrompt() {
extern.promptText("");
};
function clearScreen() {
inner.children(".jquery-console-prompt-box, .jquery-console-message").slice(0, -1).remove();
extern.report(" ");
extern.focus();
};
function deleteNextWord() {
// A word is defined within this context as a series of alphanumeric
// characters.
// Delete up to the next alphanumeric character
while (
column < promptText.length &&
!isCharAlphanumeric(promptText[column])
) {
deleteCharAtPos();
updatePromptDisplay();
}
// Then, delete until the next non-alphanumeric character
while (
column < promptText.length &&
isCharAlphanumeric(promptText[column])
) {
deleteCharAtPos();
updatePromptDisplay();
}
};
function newLine() {
var lines = promptText.split("\n");
var last_line = lines.slice(-1)[0];
var spaces = last_line.match(/^(\s*)/g)[0];
var new_line = "\n" + spaces;
promptText += new_line;
moveColumn(new_line.length);
updatePromptDisplay();
};
////////////////////////////////////////////////////////////////////////
// Validate command and trigger it if valid, or show a validation error
function commandTrigger() {
var line = promptText;
if (typeof config.commandValidate == 'function') {
var ret = config.commandValidate(line);
if (ret == true || ret == false) {
if (ret) {
handleCommand();
}
} else {
commandResult(ret, "jquery-console-message-error");
}
} else {
handleCommand();
}
};
// Scroll to the bottom of the view
function scrollToBottom() {
var version = jQuery.fn.jquery.split('.');
var major = parseInt(version[0]);
var minor = parseInt(version[1]);
// check if we're using jquery > 1.6
if ((major == 1 && minor > 6) || major > 1) {
inner.prop({ scrollTop: inner.prop("scrollHeight") });
} else {
inner.attr({ scrollTop: inner.attr("scrollHeight") });
}
};
function cancelExecution() {
if (typeof config.cancelHandle == 'function') {
config.cancelHandle();
}
}
////////////////////////////////////////////////////////////////////////
// Handle a command
function handleCommand() {
if (typeof config.commandHandle == 'function') {
disableInput();
addToHistory(promptText);
var text = promptText;
if (extern.continuedPrompt) {
if (continuedText)
continuedText += '\n' + promptText;
else continuedText = promptText;
} else continuedText = undefined;
if (continuedText) text = continuedText;
var ret = config.commandHandle(text, function(msgs) {
commandResult(msgs);
});
if (extern.continuedPrompt && !continuedText)
continuedText = promptText;
if (typeof ret == 'boolean') {
if (ret) {
// Command succeeded without a result.
commandResult();
} else {
commandResult(
'Command failed.',
"jquery-console-message-error"
);
}
} else if (typeof ret == "string") {
commandResult(ret, "jquery-console-message-success");
} else if (typeof ret == 'object' && ret.length) {
commandResult(ret);
} else if (extern.continuedPrompt) {
commandResult();
}
}
};
////////////////////////////////////////////////////////////////////////
// Disable input
function disableInput() {
acceptInput = false;
};
// Enable input
function enableInput() {
acceptInput = true;
}
////////////////////////////////////////////////////////////////////////
// Reset the prompt in invalid command
function commandResult(msg, className) {
column = -1;
updatePromptDisplay();
if (typeof msg == 'string') {
message(msg, className);
} else if ($.isArray(msg)) {
for (var x in msg) {
var ret = msg[x];
message(ret.msg, ret.className);
}
} else { // Assume it's a DOM node or jQuery object.
inner.append(msg);
}
newPromptBox();
};
////////////////////////////////////////////////////////////////////////
// Report some message into the console
function report(msg, className) {
var text = promptText;
promptBox.remove();
commandResult(msg, className);
extern.promptText(text);
};
////////////////////////////////////////////////////////////////////////
// Display a message
function message(msg, className) {
var mesg = $('');
if (className) mesg.addClass(className);
mesg.filledText(msg).hide();
inner.append(mesg);
mesg.show();
};
////////////////////////////////////////////////////////////////////////
// Handle normal character insertion
// data can either be a number, which will be interpreted as the
// numeric value of a single character, or a string
typer.consoleInsert = function(data) {
// TODO: remove redundant indirection
var text = (typeof data == 'number') ? String.fromCharCode(data) : data;
var before = promptText.substring(0, column);
var after = promptText.substring(column);
promptText = before + text + after;
moveColumn(text.length);
restoreText = promptText;
updatePromptDisplay();
};
////////////////////////////////////////////////////////////////////////
// Move to another column relative to this one
// Negative means go back, positive means go forward.
function moveColumn(n) {
if (column + n >= 0 && column + n <= promptText.length) {
column += n;
return true;
} else return false;
};
function moveForward() {
if (moveColumn(1)) {
updatePromptDisplay();
return true;
}
return false;
};
function moveBackward() {
if (moveColumn(-1)) {
updatePromptDisplay();
return true;
}
return false;
};
function moveToStart() {
if (moveColumn(-column))
updatePromptDisplay();
};
function moveToEnd() {
if (moveColumn(promptText.length - column))
updatePromptDisplay();
};
function moveToNextWord() {
while (
column < promptText.length &&
!isCharAlphanumeric(promptText[column]) &&
moveForward()
) {}
while (
column < promptText.length &&
isCharAlphanumeric(promptText[column]) &&
moveForward()
) {}
};
function moveToPreviousWord() {
// Move backward until we find the first alphanumeric
while (
column - 1 >= 0 &&
!isCharAlphanumeric(promptText[column - 1]) &&
moveBackward()
) {}
// Move until we find the first non-alphanumeric
while (
column - 1 >= 0 &&
isCharAlphanumeric(promptText[column - 1]) &&
moveBackward()
) {}
};
function isCharAlphanumeric(charToTest) {
if (typeof charToTest == 'string') {
var code = charToTest.charCodeAt();
return (code >= 'A'.charCodeAt() && code <= 'Z'.charCodeAt()) ||
(code >= 'a'.charCodeAt() && code <= 'z'.charCodeAt()) ||
(code >= '0'.charCodeAt() && code <= '9'.charCodeAt());
}
return false;
};
function doComplete() {
if (typeof config.completeHandle == 'function') {
doCompleteDirectly();
} else {
issueComplete();
}
};
function doCompleteDirectly() {
if (typeof config.completeHandle == 'function') {
var completions = config.completeHandle(promptText);
var len = completions.length;
if (len === 1) {
extern.promptText(promptText + completions[0]);
} else if (len > 1 && config.cols) {
var prompt = promptText;
// Compute the number of rows that will fit in the width
var max = 0;
for (var i = 0; i < len; i++) {
max = Math.max(max, completions[i].length);
}
max += 2;
var n = Math.floor(config.cols / max);
var buffer = "";
var col = 0;
for (i = 0; i < len; i++) {
var completion = completions[i];
buffer += completions[i];
for (var j = completion.length; j < max; j++) {
buffer += " ";
}
if (++col >= n) {
buffer += "\n";
col = 0;
}
}
commandResult(buffer, "jquery-console-message-value");
extern.promptText(prompt);
}
}
};
function issueComplete() {
if (typeof config.completeIssuer == 'function') {
config.completeIssuer(promptText);
}
};
function showCompletion(promptText, completions) {
var len = completions.length;
if (len === 1) {
extern.promptText(promptText + completions[0]);
} else if (len > 1 && config.cols) {
var prompt = promptText;
// Compute the number of rows that will fit in the width
var max = 0;
for (var i = 0; i < len; i++) {
max = Math.max(max, completions[i].length);
}
max += 2;
var n = Math.floor(config.cols / max);
var buffer = "";
var col = 0;
for (i = 0; i < len; i++) {
var completion = completions[i];
buffer += completions[i];
for (var j = completion.length; j < max; j++) {
buffer += " ";
}
if (++col >= n) {
buffer += "\n";
col = 0;
}
}
commandResult(buffer, "jquery-console-message-value");
extern.promptText(prompt);
}
};
function doNothing() {};
extern.promptText = function(text) {
if (typeof text === 'string') {
promptText = text;
column = promptText.length;
updatePromptDisplay();
}
return promptText;
};
////////////////////////////////////////////////////////////////////////
// Update the prompt display
function updatePromptDisplay() {
var line = promptText;
var html = '';
if (column > 0 && line == '') {
// When we have an empty line just display a cursor.
html = cursor;
} else if (column == promptText.length) {
// We're at the end of the line, so we need to display
// the text *and* cursor.
html = htmlEncode(line) + cursor;
} else {
// Grab the current character, if there is one, and
// make it the current cursor.
var before = line.substring(0, column);
var current = line.substring(column, column + 1);
if (current) {
current =
'' +
htmlEncode(current) +
'';
}
var after = line.substring(column + 1);
html = htmlEncode(before) + current + htmlEncode(after);
}
prompt.html(html);
scrollToBottom();
};
// Simple HTML encoding
// Simply replace '<', '>' and '&'
// TODO: Use jQuery's .html() trick, or grab a proper, fast
// HTML encoder.
function htmlEncode(text) {
return (
text.replace(/&/g, '&')
.replace(/')
);
};
return extern;
};
// Simple utility for printing messages
$.fn.filledText = function(txt) {
$(this).text(txt);
$(this).html($(this).html().replace(/\t/g, ' ').replace(/\n/g, '
'));
return this;
};
// Alternative method for focus without scrolling
$.fn.focusWithoutScrolling = function() {
var x = window.scrollX,
y = window.scrollY;
$(this).focus();
window.scrollTo(x, y);
};
})(jQuery);