/** * Takes a div element and runs a console simulator within. */ (function () { var noop = function () { return false; }, // ignore keypress defaultKeyMappings = { // keyed by modifiers, each either 1 or 0: shift || ctrl || alt '000': { 'ArrowLeft': function () { this.moveLeft(); return false; }, 'ArrowRight': function () { this.moveRight(); return false; }, 'ArrowUp': function () { this.showPreviousHistory(); return false; }, 'ArrowDown': function () { this.showNextHistory(); return false; }, 'Backspace': function () { this.deleteCharacterLeft(); return false; }, 'Delete': function () { this.deleteCharacterRight(); return false; }, 'End': function () { this.moveToEndOfLine(); return false; }, 'Home': function () { this.moveToStartOfLine(); return false; }, 'Enter': function () { return true; }, // if it returns false in line-buffered mode, then a line will never be formed -> no input ever 'Tab': noop } }; // Conversion table from legacy key codes for assorted keys // For details see: // * https://w3c.github.io/uievents/ // * http://www.w3.org/TR/2015/WD-DOM-Level-3-Events-code-20150428/ // * http://www.w3.org/TR/2015/WD-DOM-Level-3-Events-key-20150427/ // and their references. var legacyKeyConversion = { 0x03: {'code': 'Cancel'}, 0x06: {'code': 'Help'}, 0x08: {'code': 'Backspace'}, 0x09: {'code': 'Tab'}, 0x0c: {'code': 'Clear'}, 0x0d: {'code': 'Enter'}, 0x13: {'code': 'Pause'}, 0x1b: {'code': 'Escape'}, 0x20: {'code': 'Space', 'key': ' '}, 0x21: {'code': 'PageUp'}, 0x22: {'code': 'PageDown'}, 0x23: {'code': 'End'}, 0x24: {'code': 'Home'}, 0x25: {'code': 'ArrowLeft'}, 0x26: {'code': 'ArrowUp'}, 0x27: {'code': 'ArrowRight'}, 0x28: {'code': 'ArrowDown'}, 0x2c: {'code': 'PrintScreen'}, 0x2d: {'code': 'Insert'}, 0x2e: {'code': 'Delete'}, 0x6a: {'code': 'NumpadMultiply', 'key': '*'}, 0x6b: {'code': 'NumpadAdd', 'key': '+'}, 0x6c: {'code': 'NumpadComma', 'key': ','}, 0x6d: {'code': 'NumpadSubtract', 'key': '-'}, 0x6e: {'code': 'NumpadDecimal', 'key': '.'}, 0x6f: {'code': 'NumpadDivide', 'key': '/'}, 0x90: {'code': 'NumLock'}, 0x91: {'code': 'ScrollLock'}, 0xba: {'code': 'Semicolon', 'key': ';'}, 0xbb: {'code': 'Equal', 'key': '='}, 0xbc: {'code': 'Comma', 'key': ','}, 0xbd: {'code': 'Minus', 'key': '-'}, 0xbe: {'code': 'Period', 'key': '.'}, 0xbf: {'code': 'Slash', 'key': '/'}, 0xc0: {'code': 'Backquote', 'key': '`'}, 0xdb: {'code': 'BracketLeft', 'key': '['}, 0xdc: {'code': 'Backslash', 'key': '\\'}, 0xdd: {'code': 'BracketRight', 'key': ']'}, 0xde: {'code': 'Quote', 'key': "'"} }; // generate the rest of the mappings (function (table) { var userAgent = navigator.userAgent, i; // digits for (i = 0; i < 10; i++) table[0x30 + i] = {'code': 'Digit' + i, 'key': '' + i}; // characters: 'key' is the lower-cased character for (i = 0; i < 26; i++) table[0x41 + i] = {'code': 'Key' + String.fromCharCode(0x41 + i), 'key': String.fromCharCode(0x61 + i)}; // numpad for (i = 0; i < 10; i++) table[0x60 + i] = {'code': 'Numpad' + i, 'key': '' + i}; // function keys F1 to F24 for (i = 1; i < 25; i++) table[0x6f + i] = {'code': 'F' + i}; // override tables if (userAgent.indexOf('Gecko/') >= 0) { // Firefox table[0x3b] = {'code': 'Semicolon', 'key': ';'}; table[0x3d] = {'code': 'Equal', 'key': '='}; table[0x6b] = {'code': 'Equal', 'key': '='}; table[0x6d] = {'code': 'Minus', 'key': '-'}; table[0xbb] = {'code': 'NumpadAdd', 'key': '+'}; table[0xbd] = {'code': 'NumpadSubtract', 'key': '-'}; if (navigator.platform.indexOf('Mac') >= 0) { table[0x0c] = {'code': 'NumLock'}; table[0xad] = {'code': 'Minus', 'key': '-'}; } else if (navigator.platform.indexOf('Win') >= 0) { table[0xad] = {'code': 'Minus', 'key': '-'}; } } if (navigator.vendor && (navigator.vendor.indexOf('Apple') >= 0)) { // Safari table[0x03] = {'code': 'Enter'}; table[0x19] = {'code': 'Tab'}; } if ((userAgent.indexOf('iPad') >= 0) || (userAgent.indexOf('iPod') >= 0) || (userAgent.indexOf('iPhone') >= 0)) { // ios table[0x0a] = {'code': 'Enter'}; } })(legacyKeyConversion); // some (older) browsers specify incorrect key codes, this is a fix-up mapping var codeFixTable = { 'Esc': 'Escape', 'Left': 'ArrowLeft', 'Up': 'ArrowUp', 'Right': 'ArrowRight', 'Down': 'ArrowDown', 'Del': 'Delete' }; /** * Takes the given DOM Keyboard Event and returns a normalized representation. * @param event */ var processKeyboardEvent = function (event) { var keyCode, converted, result, c; if (('code' in event) && (('key' in event) || ('keyIdentifier') in event)) { // we have DOM3 KeyboardEvent, no conversion needed result = {'code': codeFixTable[event.code] || event.code, 'key': event.key || event.keyIdentifier}; } else { keyCode = event.keyCode || event.which; converted = legacyKeyConversion[keyCode]; if (converted) { result = {'code': converted.code}; if (converted.key) result.key = converted.key; else result.key = converted.code; // as per DOM3 spec: unprintables have the key the same as the code TODO: not true for all of them, e.g. AltLeft -> Alt } else { // unknown key, try to set at least some info result = {'code': ''}; c = String.fromCharCode(keyCode); if (c !== '') result.key = c; else result.key = 'Unidentified'; } } // char is set only in keypress events, never in keydown and keyup // (keypress is not a part of any standard) if ('char' in event) { result.char = event.char; } else if ('charCode' in event) { result.char = String.fromCharCode(event.charCode); } else if (event.type == 'keypress') { // TODO: generate result.char somehow? Probably not needed, we should use the input event instead of the keypress event anyway. } return result; }; var instanceCounter = 1; // for generating unique console instance numbers codeq.makeConsole = function (jqElt, options) { var lines = [],// console content, line descriptors of the form {'content': 'string', 'classNames': [], 'jqLine': jQuery object, 'row': number} maxLines = (options && options.maxLines) || 10000, // how many lines to display at most currentRow = 0, // cursor position currentCol = 0, jqContent = $('
'), // the rendering of the console jqInput = $(''), // for receiving keypresses, clipboard content inputDisabled = false, // whether to ignore input lineBuffered = true, // whether we are line-buffered or not buffered autoHistory = (options && options.autoHistory) || false, // whether to auto-decide what goes into the history buffer history = [], // the list of entered lines, it is only appended currentHistory = [], // the list of entered lines, some possibly modified (it is copied from history on each new line editing) currentHistoryIndex = -1, instanceNumber = instanceCounter++, // unique instace number, to be used with stuff tied to this instance, e.g.: jqElt events, life-cycle debug output renderSpan = function (lineDescriptor, startCol, length) { var jqLine = lineDescriptor.jqLine, content = lineDescriptor.content, classes = lineDescriptor.classNames, endCol = startCol + length, N = classes ? classes.length : 0, i = 0, spans = [], // what to render, items are of the form {'content': string, 'cssClass': string} entry, classSpan, jq, span, s, prefixDelta, suffixDelta, e; // seek out the first css class while (i < N) { entry = classes[i]; if (((entry.start + entry.length) > startCol) && (entry.start < endCol)) break; i++; } if (i < N) { // render the first span e = entry.start + entry.length; // end column of this css class prefixDelta = entry.start < startCol ? startCol - entry.start : 0; suffixDelta = e > endCol ? e - endCol : 0; classSpan = entry.length - prefixDelta - suffixDelta; span = {'content': content.substr(startCol, classSpan), 'cssClass': entry.name}; spans.push(span); startCol += classSpan; i++; // render the rest while ((startCol < endCol) && (i < N)) { entry = classes[i]; e = entry.start + entry.length; // end column of this css class prefixDelta = entry.start < startCol ? startCol - entry.start : 0; suffixDelta = e > endCol ? e - endCol : 0; classSpan = entry.length - prefixDelta - suffixDelta; s = content.substr(startCol, classSpan); if (entry.name === span.cssClass) { span.content += s; // join content where the class is the same } else { span = {'content': s, 'cssClass': entry.name}; spans.push(span); } startCol += classSpan; i++; } } // render any leftover if (startCol < endCol) { entry = makeClassDescriptor('', startCol, endCol - startCol); spans.push({'content': content.substring(startCol, endCol), 'cssClass': ''}); } // render spans for (i = 0; i < spans.length; i++) { span = spans[i]; jq = $(''); jqLine.append(jq); jq.text(span.content); } return entry; // return the last entry used }, renderLine = function (lineDescriptor) { var jqLine = lineDescriptor.jqLine, content = lineDescriptor.content, jqCursor, classDescriptor; if (!jqLine) { jqLine = $(''); if (lineDescriptor.row == 0) { jqContent.prepend(jqLine); } else if ((lineDescriptor.row < lines.length) && lines[lineDescriptor.row - 1].jqLine) { lines[lineDescriptor.row - 1].jqLine.after(jqLine); } else { // bail-out line, we should never end up here jqContent.append(jqLine); } lineDescriptor.jqLine = jqLine; } else { jqLine.empty(); } if (currentRow == lineDescriptor.row) { // mark the cursor in the current line if (currentCol >= content.length) { // cursor after the last character renderSpan(lineDescriptor, 0, content.length); jqCursor = $(' '); jqLine.append(jqCursor); } else if (currentCol <= 0) { // cursor at the first character classDescriptor = lineDescriptor.classNames[0] || makeClassDescriptor('', 0, content.length); jqCursor = $(''); jqLine.append(jqCursor); jqCursor.text(content.charAt(0)); renderSpan(lineDescriptor, 1, content.length - 1); } else { // cursor somewhere in between classDescriptor = renderSpan(lineDescriptor, 0, currentCol); jqCursor = $(''); jqLine.append(jqCursor); jqCursor.text(content.charAt(currentCol)); renderSpan(lineDescriptor, currentCol + 1, content.length - currentCol - 1); } } else { renderSpan(lineDescriptor, 0, content.length); } }, render = function () { var i, lineDescriptor, n = lines.length; jqContent.empty(); for (i = 0; i < n; i++) { lineDescriptor = lines[i]; lineDescriptor.row = i; if (lineDescriptor.jqLine) { lineDescriptor.jqLine.remove(); lineDescriptor.jqLine = null; } renderLine(lineDescriptor); } }, formNewLines = function (newText) { var newLines, newTextLines; if (!newText) newLines = []; else if (newText instanceof Array) { newLines = []; for (i = 0; i < newText.length; i++) { newTextLines = ('' + newText[i]).split('\n'); for (j = 0; j < newTextLines.length; j++) newLines.push(newTextLines[j]); } } else { newLines = ('' + newText).split('\n'); } return newLines; }, makeClassDescriptor = function (name, start, length) { return {'name': name, 'start': start, 'length': length}; }, indexOfClassAtOrAfterColumn = function (classes, col) { var N = classes.length, i; for (i = 0; i < N; i++) { if (classes[i].start >= col) return i; } return N; }, insertClass = function (classes, newClassName, startingCol, length) { var newEntry = makeClassDescriptor(newClassName, startingCol, length), endCol = startingCol + length, i, N, prevClass, pos, auxEntry, delta; // seek out the position where to insert the new class i = indexOfClassAtOrAfterColumn(classes, startingCol); prevClass = i > 0 ? classes[i-1] : null; // insert the new class classes.splice(i, 0, newEntry); // if needed, adjust the length of the previous class if (prevClass) { pos = prevClass.start + prevClass.length; if (pos > startingCol) { // split the previous class in two, the new class gets inserted in between delta = pos - startingCol; auxEntry = makeClassDescriptor(prevClass.name, endCol, delta); classes.splice(i+1, 0, auxEntry); prevClass.length = prevClass.length - delta; } } // readjust positions of the classes after the new one N = classes.length; for (i++; i < N; i++) { auxEntry = classes[i]; auxEntry.start = endCol; endCol += auxEntry.length; } }, removeClasses = function (classes, startCol, length) { var i, entry, endCol, e, prefixDelta, suffixDelta, N; if (!length) return; endCol = startCol + length; for (i = classes.length - 1; i >= 0; i--) { entry = classes[i]; e = entry.start + entry.length; if ((e > startCol) && (entry.start < endCol)) { prefixDelta = entry.start < startCol ? startCol - entry.start : 0; suffixDelta = e > endCol ? e - endCol : 0; if ((prefixDelta == 0) && (suffixDelta == 0)) classes.splice(i, 1); else entry.length = prefixDelta + suffixDelta; } } // readjust positions N = classes.length; endCol = 0; for (i = 0; i < N; i++) { entry = classes[i]; entry.start = endCol; endCol += entry.length; } }, removeClassesBefore = function (classes, col) { var i = indexOfClassAtOrAfterColumn(classes, col); if (i == 0) return; // nothing to remove classes.splice(0, i); }, replaceClassesBeforeColumn = function (classes, col, newClassName, newLength) { var i = indexOfClassAtOrAfterColumn(classes, col), newEntry = makeClassDescriptor(newClassName, 0, newLength); classes.splice(0, i, newEntry); }, classesBetween = function (classes, startCol, endCol) { var N = classes.length, result = [], i, e, entry, prefixDelta, suffixDelta; for (i = 0; i < N; i++) { entry = classes[i]; e = entry.start + entry.length; // end column of this css class if ((e > startCol) && (entry.start < endCol)) { prefixDelta = entry.start < startCol ? startCol - entry.start : 0; suffixDelta = e > endCol ? e - endCol : 0; result.push(makeClassDescriptor(entry.name, entry.start + prefixDelta, entry.length - prefixDelta - suffixDelta)); } } return result; }, mergeClasses = function () { var result = [], i, arg, j, entry, endCol; for (i = 0; i < arguments.length; i++) { arg = arguments[i]; if (arg instanceof Array) { for (j = 0; j < arg.length; j++) result.push(arg[j]); } else result.push(arg); } // adjust positions of css classes endCol = 0; for (i = 0; i < result.length; i++) { entry = result[i]; entry.start = endCol; endCol += entry.length; } return result; }, scrollCursorIntoView = function () { var dh = jqContent.height() - jqElt.height(); if (dh > 0) jqElt.scrollTop(dh); }, stashCurrentHistory = function () { var leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0, lineDescriptor = lines[currentRow], line = lineDescriptor.content.substring(leftmost); if (currentHistoryIndex < 0) { currentHistoryIndex = currentHistory.length; currentHistory.push(line); } else { currentHistory[currentHistoryIndex] = line; } return lineDescriptor; }, // the handler object that is returned handler = { 'setLineBuffered': function () { lineBuffered = true; }, 'setNotBuffered': function () { lineBuffered = false; }, 'onInput': false, // the caller should assign a function here, that takes input as parameter, and optionally returns a promise which is then waited to be resolved before further input is accepted 'onKeypress': false, // same, but it takes a keycode, and returns a keycode 'inputEnable': function () { inputDisabled = false; }, 'inputDisable': function () { inputDisabled = true; }, 'keyMaps': {}, // will be filled in later 'leftmostCol': 0, // the column from where editing is possible; it is not possible to delete or change a character to the left of this column 'reflow': function () { var startRow, endRow, i, lineDescriptor; if (arguments.length == 0) { // no arguments, reflow all lines startRow = 0; endRow = lines.length - 1; } else if (arguments.length == 1) { // only the starting row given, reflow all lines from there on startRow = arguments[0]; endRow = lines.length - 1; } else { // both arguments are given startRow = arguments[0]; endRow = arguments[1]; } for (i = startRow; i <= endRow; i++) { lineDescriptor = lines[i]; lineDescriptor.row = i; renderLine(lineDescriptor); } }, 'removeRow': function (row, deferReflow) { var lineDescriptor; if (row >= lines.length) return; // already absent lineDescriptor = lines[row]; lines.splice(row, 1); if (lineDescriptor.jqLine) { lineDescriptor.jqLine.remove(); lineDescriptor.jqLine = null; } if (!deferReflow) this.reflow(row); return lineDescriptor; }, 'insertRow': function (row, content, className, deferReflow) { // inserts a new row at the position specified var lineDescriptor, i; if (!className) className = 'output'; for (i = lines.length; i < row; i++) { // insert any empty rows lineDescriptor = {content: '', classNames: [], row: i}; lines.push(lineDescriptor); if (!deferReflow) renderLine(lineDescriptor); } lineDescriptor = {content: content, classNames: content.length > 0 ? [makeClassDescriptor(className, 0, content.length)] : [], row: row}; if (row >= lines.length) lines.push(lineDescriptor); else lines.splice(row, 0, lineDescriptor); if (!deferReflow) this.reflow(row); return lineDescriptor; }, 'splice': function (startRow, startCol, endRow, endCol, newText, className) { var i, j, n, startLine, endLine, newLines, part1, part2, s, auxLine; if (!className) className = 'output'; // pre-process newText, convert it to newLines: an array of strings without newlines newLines = formNewLines(newText); if (newLines.length == 0) newLines.push(''); // there must at least be an empty "glue" if (lines.length > 0) { // there is something to remove // verify sanity if (startRow < 0) startRow = 0; else if (startRow >= lines.length) startRow = lines.length - 1; if (endRow < 0) endRow = 0; else if (endRow >= lines.length) endRow = lines.length - 1; if (endRow < startRow) { // swap the meaning of the start and end coordinates i = startRow; startRow = endRow; endRow = i; i = startCol; startCol = endCol; endCol = i; } if (startRow == endRow) { if (endCol < startCol) { i = startCol; startCol = endCol; endCol = i; } } startLine = lines[startRow]; endLine = lines[endRow]; if (startCol < 0) startCol = 0; else if (startCol > startLine.length) startCol = startLine.length; if (endCol < 0) endCol = 0; else if (endCol > endLine.length) endCol = endLine.length; // execute splice part1 = startLine.content.substring(0, startCol); // the remaining content on the first and last lines part2 = endLine.content.substring(endCol); // modify the first and last lines, and make the list of the old lines to remove j = startRow + 1; // where to start removing lines n = endRow - startRow; // how many lines to remove if (newLines.length > 1) { // the first and the last new lines are separate existing lines, modify them startLine.content = part1 + newLines[0]; startLine.classNames = mergeClasses(classesBetween(startLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length)); if (startRow == endRow) { // we need to create one additional line to hold the compositum of the last line and part2 auxLine = this.insertRow(j, newLines[newLines.length - 1], className, true); insertClass(auxLine.classNames, auxLine.content.length, part2.length); auxLine.content = auxLine.content + part2; j++; } else { s = newLines[newLines.length - 1]; endLine.content = s + part2; replaceClassesBeforeColumn(endLine.classNames, endCol, className, s.length); n--; // retain the last line } } else { // the first and the last new lines are the same existing line startLine.content = part1 + newLines[0] + part2; startLine.classNames = mergeClasses(classesBetween(startLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length), classesBetween(endLine.classNames, endCol, endCol + part2.length)); } // remove the rest of the old lines while (n > 0) { this.removeRow(j, true); n--; } // insert new lines in between j = newLines.length - 1; // the first and the last lines have already been set up for (i = 1; i < j; i++) { this.insertRow(startRow + i, newLines[i], className, true); } // optimization step: if only one line changed, then only reflow the line if ((n == 0) && (newLines.length == 1)) { renderLine(startLine); } else { this.reflow(startRow); } } else { // no content exists, add all lines for (i = 0; i < newLines.length; i++) { this.insertRow(i, newLines[i], className, true); } this.reflow(); } }, 'insert': function (text, row, col, className) { this.splice(row, col, row, col, text, className || 'output'); }, 'append': function (text, className, dontMoveCursor) { // we add to the end, so optimize: do not call splice(), it is too heavy for this purpose var newLines = formNewLines(text), originalCurrentRow = currentRow, startRow = lines.length - 1, i, lineDescriptor; if (newLines.length == 0) return; // nothing to do if (typeof className !== 'string') className = 'output'; lineDescriptor = lines[startRow]; insertClass(lineDescriptor.classNames, className, lineDescriptor.content.length, newLines[0].length); lineDescriptor.content = lineDescriptor.content + newLines[0]; for (i = 1; i < newLines.length; i++) { lineDescriptor = this.insertRow(startRow + i, newLines[i], className, true); } if (!dontMoveCursor) { currentRow = lines.length - 1; currentCol = lines[currentRow].content.length; } this.reflow(startRow < originalCurrentRow ? startRow : originalCurrentRow); }, 'insertAtCursor': function (text, className, dontMoveCursor) { var startRow = currentRow, currentLine = lines[currentRow], currentContent = currentLine.content, part1 = currentContent.substring(0, currentCol), part2 = currentContent.substring(currentCol), newLines = formNewLines(text), i, n, auxLine; if (typeof className !== 'string') className = 'output'; if (newLines.length == 0) return; // nothing to do else if (newLines.length == 1) { insertClass(currentLine.classNames, className, part1.length, newLines[0].length); currentLine.content = part1 + newLines[0] + part2; if (!dontMoveCursor) currentCol += newLines[0].length; renderLine(currentLine); } else { currentLine.classNames = mergeClasses(classesBetween(currentLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length)); currentLine.content = part1 + newLines[0]; n = newLines.length - 1; for (i = 1; i < n; i++) { this.insertRow(startRow + i, newLines[i], className, true); } auxLine = this.insertRow(startRow + n, newLines[n], className, true); insertClass(auxLine.classNames, auxLine.content.length, part2.length); auxLine.content = auxLine.content + part2; if (!dontMoveCursor) { currentRow = startRow + n; currentCol = newLines[n].length; } this.reflow(startRow); } }, 'moveLeft': function () { var leftmost = typeof this.leftmostCol === 'number' && this.leftmostCol || 0; if (currentCol > leftmost) { currentCol--; renderLine(lines[currentRow]); } }, 'moveRight': function () { var lineDescriptor = lines[currentRow]; if (currentCol < lineDescriptor.content.length) { currentCol++; renderLine(lineDescriptor); } }, 'deleteCharacterLeft': function () { var lineDescriptor = lines[currentRow], content = lineDescriptor.content, leftmost = typeof this.leftmostCol === 'number' && this.leftmostCol || 0; if ((currentCol > leftmost) && (currentCol <= content.length)) { currentCol--; removeClasses(lineDescriptor.classNames, currentCol, 1); lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1); renderLine(lineDescriptor); } }, 'deleteCharacterRight': function () { var lineDescriptor = lines[currentRow], content = lineDescriptor.content; if ((currentCol >= 0) && (currentCol < content.length)) { removeClasses(lineDescriptor.classNames, currentCol, 1); lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1); renderLine(lineDescriptor); } }, 'moveToEndOfLine': function () { var lineDescriptor = lines[currentRow], n = lineDescriptor.content.length; if (currentCol != n) { currentCol = n; renderLine(lineDescriptor); } }, 'moveToStartOfLine': function () { var leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0, lineDescriptor = lines[currentRow]; if (currentCol != leftmost) { currentCol = leftmost; renderLine(lineDescriptor); } }, 'destroy': function () { var n = lines.length, i, lineDescriptor; if (blinkTimer !== null) { clearInterval(blinkTimer); blinkTimer = null; } // detach all jQuery objects from internal structures for (i = 0; i < n; i++) { lineDescriptor = lines[i]; if (lineDescriptor.jqLine) { lineDescriptor.jqLine.remove(); lineDescriptor.jqLine = null; } } lines.length = 0; // clear and detach the rest render = null; renderLine = null; jqContent.remove(); jqContent = null; jqInput.remove(); jqInput = null; for (i in handler) { if (handler.hasOwnProperty(i)) { delete handler[i]; } } jqElt.off('click.console' + instanceNumber); jqElt.off('blur.console' + instanceNumber); jqElt.empty(); jqElt = null; options = null; }, 'addHistory': function (line) { history.push(line); }, 'currentInput': function () { var leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0; return lines[currentRow].content.substring(leftmost); }, 'resetCurrentHistory': function () { var i; currentHistory.length = 0; for (i = 0; i < history.length; i++) currentHistory.push(history[i]); currentHistoryIndex = -1; }, 'showPreviousHistory': function () { var leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0, lineDescriptor = stashCurrentHistory(); if (currentHistoryIndex > 0) { currentHistoryIndex--; lineDescriptor.content = lineDescriptor.content.substring(0, leftmost) + currentHistory[currentHistoryIndex]; currentCol = lineDescriptor.content.length; lineDescriptor.classNames = mergeClasses(classesBetween(lineDescriptor.classNames, 0, leftmost), makeClassDescriptor('input', leftmost, currentCol - leftmost)); renderLine(lineDescriptor); } }, 'showNextHistory': function () { var leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0, lineDescriptor = stashCurrentHistory(); if (currentHistoryIndex < (currentHistory.length - 1)) { currentHistoryIndex++; lineDescriptor.content = lineDescriptor.content.substring(0, leftmost) + currentHistory[currentHistoryIndex]; currentCol = lineDescriptor.content.length; lineDescriptor.classNames = mergeClasses(classesBetween(lineDescriptor.classNames, 0, leftmost), makeClassDescriptor('input', leftmost, currentCol - leftmost)); renderLine(lineDescriptor); } } }, internallyDisabled = false, handleInput = function (chars) { var isPastedFromClipboard = clipboardPasteInProgress || !aKeyIsDown, cookedChars = chars, // the default cookedPromise, cookedLines, lastCookedLineIndex, i; if (inputDisabled || internallyDisabled) { // flatly ignore clipboardPasteInProgress = false; return; } internallyDisabled = true; cookedPromise = Q(); // first cook the input if (typeof handler.onKeypress === 'function') { // only invoke the handler if the newLine is not an empty string, otherwise cookedChars = '' try { cookedChars = cookedChars && handler.onKeypress(cookedChars, isPastedFromClipboard); } catch (e) { codeq.log.error('Error during invocation of terminal onKeypress: ' + e, e); } } if (typeof cookedChars !== 'string') cookedLines = ['']; else cookedLines = cookedChars.split('\n'); lastCookedLineIndex = cookedLines.length - 1; // now serve the cooking for (i = 0; i <= lastCookedLineIndex; i++) { cookedPromise = cookedPromise .then(scrollCursorIntoView) // scroll to bottom on input .then((function (cookedLine, cookedLineIndex) {return function () { var thisRow = currentRow, leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0, line; if (cookedLine) handler.insertAtCursor(cookedLine, 'input'); // append what we have to the display if (cookedLineIndex < lastCookedLineIndex) { // press on "enter" must be simulated -> jump to the start of the next line, which is first added currentRow++; currentCol = 0; handler.insertRow(currentRow, '', 'input', true); handler.reflow(currentRow - 1); // the previous line must be rendered without a cursor, and the new line must be rendered completely } if (typeof handler.onInput === 'function') { // now tell the owner what we've done if (lineBuffered) { if (thisRow < currentRow) { // in line-buffered mode emit each line separately, except for the last (current) line try { line = lines[thisRow].content.substring(leftmost); if (autoHistory) handler.addHistory(line); // auto-history works only in line-buffered mode handler.resetCurrentHistory(); return handler.onInput(line); } catch (e) { codeq.log.error('Error while invoking terminal onInput: ' + e, e); } } } else if (cookedLine) { try { return handler.onInput(cookedLine); } catch (e) { codeq.log.error('Error while invoking terminal onInput: ' + e, e); } } } };})(cookedLines[i], i)) .fail(function (e) { codeq.log.error('Error in handleInput loop: ' + e, e); }); } cookedPromise.fin(function () { internallyDisabled = false; scrollCursorIntoView(); }).done(); clipboardPasteInProgress = false; }, clipboardPasteInProgress = false, // whether the previous keydown was a CTRL+V or shift+insert aKeyIsDown = false, // whether a key is currently pressed: if it's not and there is input, then we got a paste via mouse cursorBlinkRate = (options && options.cursorBlinkRate) || 750, // default: 750 ms blinkTimer = null, i, j, a, b, c; // copy default key handlers a = handler.keyMaps; for (i in defaultKeyMappings) { if (!defaultKeyMappings.hasOwnProperty(i)) continue; b = defaultKeyMappings[i]; c = {}; a[i] = c; for (j in b) { if (!b.hasOwnProperty(j)) continue; c[j] = b[j]; } } a = null; b = null; c = null; // initialize the DOM container jqElt.empty(); jqElt.append(jqContent); jqElt.append(jqInput); jqElt.on('click.console' + instanceNumber, function () { // set focus to the textarea so we pick up keypresses jqInput.css('position', 'fixed').focus(); }); jqElt.on('blur.console' + instanceNumber, function () { jqInput.css('position', 'absolute'); }); // register key handlers jqInput.on('keydown', function (evt) { var modifiers = ['' + (0 + evt.shiftKey), '' + (0 + (evt.ctrlKey || evt.metaKey)), '' + (0 + evt.altKey)].join(''), handlersForModifiers = handler.keyMaps[modifiers], acceptKeydown = !(inputDisabled || internallyDisabled), // the default value eventInfo = processKeyboardEvent(evt), handlerForKeydown = handlersForModifiers && handlersForModifiers[eventInfo.code]; if (acceptKeydown && (typeof handlerForKeydown === 'function')) { try { acceptKeydown = handlerForKeydown.apply(handler, [eventInfo]); } catch (e) { // ignore errors, TODO: log errors } } if (!acceptKeydown) { evt.preventDefault(); evt.stopPropagation(); return false; } // paste: CTRL+V or shift+insert clipboardPasteInProgress = ((modifiers === '010') && (eventInfo.code === 'KeyV')) || ((modifiers === '100') && (eventInfo.code === 'Insert')); aKeyIsDown = true; return true; }); jqInput.on('keyup', function (evt) { aKeyIsDown = false; }); if ('oninput' in jqInput[0]) { // the element supports input events, use them in preference to keypress events jqInput.on('input', function (evt) { // this event cannot be prevented/cancelled in the DOM scheme of things, but we can simulate prevention, because we have a "detached" display var t = jqInput.val(); jqInput.val(''); handleInput(t); }); } else { // the element does not support the "modern" input events, so it ought to support the old keypress events jqInput.on('keypress', function (evt) { // this event cannot be prevented/cancelled in the DOM scheme of things, but we can simulate prevention, because we have a "detached" display var keyChar; try { if ('char' in evt) keyChar = evt.char; else if ('charCode' in evt) keyChar = String.fromCharCode(evt.charCode); else return; // ignore input handleInput(keyChar); } finally { jqInput.val(''); } }); } // start the cursor blinking if so instructed if ((typeof cursorBlinkRate === 'number') && (cursorBlinkRate >= 50)) { // have some sense of sanity blinkTimer = setInterval(function () { jqContent.find('.cq-con-cursor').toggleClass('inverted'); }, cursorBlinkRate); } // emit greeting if provided if (options && options.greeting) { lines.push({content: options.greeting, className: 'greeting'}); currentRow++; } lines.push({content: '', classNames: []}); // initial input line render(); return handler; }; })();