/* ------------------------------------------------------------------------* * Copyright 2013-2014 Arne F. Claassen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *-------------------------------------------------------------------------*/ var Josh = Josh || {}; Josh.Version = "0.2.10"; (function(root) { Josh.Keys = { Special: { Backspace: 8, Tab: 9, Enter: 13, Pause: 19, CapsLock: 20, Escape: 27, Space: 32, PageUp: 33, PageDown: 34, End: 35, Home: 36, Left: 37, Up: 38, Right: 39, Down: 40, Insert: 45, Delete: 46 } }; Josh.ReadLine = function(config) { config = config || {}; // instance fields var _console = config.console || (Josh.Debug && root.console ? root.console : { log: function() { } }); var _history = config.history || new Josh.History(); var _killring = config.killring || new Josh.KillRing(); var _boundToElement = config.element ? true : false; var _element = config.element || root; var _active = false; var _onActivate; var _onDeactivate; var _onCompletion; var _onEnter; var _onChange; var _onCancel; var _onEOT; var _onClear; var _onSearchStart; var _onSearchEnd; var _onSearchChange; var _inSearch = false; var _searchMatch; var _lastSearchText = ''; var _text = ''; var _cursor = 0; var _lastCmd; var _completionActive; var _cmdQueue = []; var _suspended = false; var _cmdMap = { complete: cmdComplete, done: cmdDone, noop: cmdNoOp, history_top: cmdHistoryTop, history_end: cmdHistoryEnd, history_next: cmdHistoryNext, history_previous: cmdHistoryPrev, end: cmdEnd, home: cmdHome, left: cmdLeft, right: cmdRight, cancel: cmdCancel, 'delete': cmdDeleteChar, backspace: cmdBackspace, kill_eof: cmdKillToEOF, kill_wordback: cmdKillWordBackward, kill_wordforward: cmdKillWordForward, yank: cmdYank, clear: cmdClear, search: cmdReverseSearch, wordback: cmdBackwardWord, wordforward: cmdForwardWord, yank_rotate: cmdRotate }; var _keyMap = { 'default': { 8: cmdBackspace, // Backspace 9: cmdComplete, // Tab 13: cmdDone, // Enter 27: cmdEsc, // Esc 33: cmdHistoryTop, // Page Up 34: cmdHistoryEnd, // Page Down 35: cmdEnd, // End 36: cmdHome, // Home 37: cmdLeft, // Left 38: cmdHistoryPrev, // Up 39: cmdRight, // Right 40: cmdHistoryNext, // Down 46: cmdDeleteChar, // Delete 10: cmdNoOp, // Pause 19: cmdNoOp, // Caps Lock 45: cmdNoOp // Insert }, control: { 65: cmdHome, // A 66: cmdLeft, // B 67: cmdCancel, // C 68: cmdDeleteChar, // D 69: cmdEnd, // E 70: cmdRight, // F 80: cmdHistoryPrev, // P 78: cmdHistoryNext, // N 75: cmdKillToEOF, // K 89: cmdYank, // Y 76: cmdClear, // L 82: cmdReverseSearch // R }, meta: { 8: cmdKillWordBackward, // Backspace 66: cmdBackwardWord, // B 68: cmdKillWordForward, // D 70: cmdForwardWord, // F 89: cmdRotate // Y } }; // public methods var self = { isActive: function() { return _active; }, activate: function() { _active = true; if(_onActivate) { _onActivate(); } }, deactivate: function() { _active = false; if(_onDeactivate) { _onDeactivate(); } }, bind: function(key, action) { var k = getKey(key); var cmd = _cmdMap[action]; if(!cmd) { return; } _keyMap[k.modifier][k.code]; }, unbind: function(key) { var k = getKey(key); delete _keyMap[k.modifier][k.code]; }, attach: function(el) { if(_element) { self.detach(); } _console.log("attaching"); _console.log(el); _element = el; _boundToElement = true; addEvent(_element, "focus", self.activate); addEvent(_element, "blur", self.deactivate); subscribeToKeys(); }, detach: function() { removeEvent(_element, "focus", self.activate); removeEvent(_element, "blur", self.deactivate); _element = null; _boundToElement = false; }, onActivate: function(completionHandler) { _onActivate = completionHandler; }, onDeactivate: function(completionHandler) { _onDeactivate = completionHandler; }, onChange: function(changeHandler) { _onChange = changeHandler; }, onClear: function(completionHandler) { _onClear = completionHandler; }, onEnter: function(enterHandler) { _onEnter = enterHandler; }, onCompletion: function(completionHandler) { _onCompletion = completionHandler; }, onCancel: function(completionHandler) { _onCancel = completionHandler; }, onEOT: function(completionHandler) { _onEOT = completionHandler; }, onSearchStart: function(completionHandler) { _onSearchStart = completionHandler; }, onSearchEnd: function(completionHandler) { _onSearchEnd = completionHandler; }, onSearchChange: function(completionHandler) { _onSearchChange = completionHandler; }, getLine: function() { return { text: _text, cursor: _cursor }; }, setLine: function(line) { _text = line.text; _cursor = line.cursor; refresh(); } }; // private methods function addEvent(element, name, callback) { if(element.addEventListener) { element.addEventListener(name, callback, false); } else if(element.attachEvent) { element.attachEvent('on' + name, callback); } } function removeEvent(element, name, callback) { if(element.removeEventListener) { element.removeEventListener(name, callback, false); } else if(element.detachEvent) { element.detachEvent('on' + name, callback); } } function getKeyInfo(e) { var code = e.keyCode || e.charCode; var c = String.fromCharCode(code); return { code: code, character: c, shift: e.shiftKey, control: e.controlKey, alt: e.altKey, isChar: true }; } function getKey(key) { var k = { modifier: 'default', code: key.keyCode }; if(key.metaKey || key.altKey) { k.modifier = 'meta'; } else if(key.ctrlKey) { k.modifier = 'control'; } if(key['char']) { k.code = key['char'].charCodeAt(0); } return k; } function queue(cmd) { if(_suspended) { _cmdQueue.push(cmd); return; } call(cmd); } function call(cmd) { _console.log('calling: ' + cmd.name + ', previous: ' + _lastCmd); if(_inSearch && cmd.name != "cmdKeyPress" && cmd.name != "cmdReverseSearch") { _inSearch = false; if(cmd.name == 'cmdEsc') { _searchMatch = null; } if(_searchMatch) { if(_searchMatch.text) { _cursor = _searchMatch.cursoridx; _text = _searchMatch.text; _history.applySearch(); } _searchMatch = null; } if(_onSearchEnd) { _onSearchEnd(); } } if(!_inSearch && _killring.isinkill() && cmd.name.substr(0, 7) != 'cmdKill') { _killring.commit(); } _lastCmd = cmd.name; cmd(); } function suspend(asyncCall) { _suspended = true; asyncCall(resume); } function resume() { var cmd = _cmdQueue.shift(); if(!cmd) { _suspended = false; return; } call(cmd); resume(); } function cmdNoOp() { // no-op, used for keys we capture and ignore } function cmdEsc() { // no-op, only has an effect on reverse search and that action was taken in call() } function cmdBackspace() { if(_cursor == 0) { return; } --_cursor; _text = remove(_text, _cursor, _cursor + 1); refresh(); } function cmdComplete() { if(!_onCompletion) { return; } suspend(function(resumeCallback) { _onCompletion(self.getLine(), function(completion) { if(completion) { _text = insert(_text, _cursor, completion); updateCursor(_cursor + completion.length); } _completionActive = true; resumeCallback(); }); }); } function cmdDone() { if(!_text) { return; } var text = _text; _history.accept(text); _text = ''; _cursor = 0; if(!_onEnter) { return; } suspend(function(resumeCallback) { _onEnter(text, function(text) { if(text) { _text = text; _cursor = _text.length; } if(_onChange) { _onChange(self.getLine()); } resumeCallback(); }); }); } function cmdEnd() { updateCursor(_text.length); } function cmdHome() { updateCursor(0); } function cmdLeft() { if(_cursor == 0) { return; } updateCursor(_cursor - 1); } function cmdRight() { if(_cursor == _text.length) { return; } updateCursor(_cursor + 1); } function cmdBackwardWord() { if(_cursor == 0) { return; } updateCursor(findBeginningOfPreviousWord()); } function cmdForwardWord() { if(_cursor == _text.length) { return; } updateCursor(findEndOfCurrentWord()); } function cmdHistoryPrev() { if(!_history.hasPrev()) { return; } getHistory(_history.prev); } function cmdHistoryNext() { if(!_history.hasNext()) { return; } getHistory(_history.next); } function cmdHistoryTop() { getHistory(_history.top); } function cmdHistoryEnd() { getHistory(_history.end); } function cmdDeleteChar() { if(_text.length == 0) { if(_onEOT) { _onEOT(); return; } } if(_cursor == _text.length) { return; } _text = remove(_text, _cursor, _cursor + 1); refresh(); } function cmdCancel() { if(_onCancel) { _onCancel(); } } function cmdKillToEOF() { _killring.append(_text.substr(_cursor)); _text = _text.substr(0, _cursor); refresh(); } function cmdKillWordForward() { if(_text.length == 0) { return; } if(_cursor == _text.length) { return; } var end = findEndOfCurrentWord(); if(end == _text.length - 1) { return cmdKillToEOF(); } _killring.append(_text.substring(_cursor, end)) _text = remove(_text, _cursor, end); refresh(); } function cmdKillWordBackward() { if(_cursor == 0) { return; } var oldCursor = _cursor; _cursor = findBeginningOfPreviousWord(); _killring.prepend(_text.substring(_cursor, oldCursor)); _text = remove(_text, _cursor, oldCursor); refresh(); } function cmdYank() { var yank = _killring.yank(); if(!yank) { return; } _text = insert(_text, _cursor, yank); updateCursor(_cursor + yank.length); } function cmdRotate() { var lastyanklength = _killring.lastyanklength(); if(!lastyanklength) { return; } var yank = _killring.rotate(); if(!yank) { return; } var oldCursor = _cursor; _cursor = _cursor - lastyanklength; _text = remove(_text, _cursor, oldCursor); _text = insert(_text, _cursor, yank); updateCursor(_cursor + yank.length); } function cmdClear() { if(_onClear) { _onClear(); } else { refresh(); } } function cmdReverseSearch() { if(!_inSearch) { _inSearch = true; if(_onSearchStart) { _onSearchStart(); } if(_onSearchChange) { _onSearchChange({}); } } else { if(!_searchMatch) { _searchMatch = {term: ''}; } search(); } } function updateCursor(position) { _cursor = position; refresh(); } function addText(c) { _text = insert(_text, _cursor, c); ++_cursor; refresh(); } function addSearchText(c) { if(!_searchMatch) { _searchMatch = {term: ''}; } _searchMatch.term += c; search(); } function search() { _console.log("searchtext: " + _searchMatch.term); var match = _history.search(_searchMatch.term); if(match != null) { _searchMatch = match; _console.log("match: " + match); if(_onSearchChange) { _onSearchChange(match); } } } function refresh() { if(_onChange) { _onChange(self.getLine()); } } function getHistory(historyCall) { _history.update(_text); _text = historyCall(); updateCursor(_text.length); } function findBeginningOfPreviousWord() { var position = _cursor - 1; if(position < 0) { return 0; } var word = false; for(var i = position; i > 0; i--) { var word2 = isWordChar(_text[i]); if(word && !word2) { return i + 1; } word = word2; } return 0; } function findEndOfCurrentWord() { if(_text.length == 0) { return 0; } var position = _cursor + 1; if(position >= _text.length) { return _text.length - 1; } var word = false; for(var i = position; i < _text.length; i++) { var word2 = isWordChar(_text[i]); if(word && !word2) { return i; } word = word2; } return _text.length - 1; } function isWordChar(c) { if(c == undefined) { return false; } var code = c.charCodeAt(0); return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122); } function remove(text, from, to) { if(text.length <= 1 || text.length <= to - from) { return ''; } if(from == 0) { // delete leading characters return text.substr(to); } var left = text.substr(0, from); var right = text.substr(to); return left + right; } function insert(text, idx, ins) { if(idx == 0) { return ins + text; } if(idx >= text.length) { return text + ins; } var left = text.substr(0, idx); var right = text.substr(idx); return left + ins + right; } function subscribeToKeys() { // set up key capture _element.onkeydown = function(e) { e = e || window.event; // return as unhandled if we're not active or the key is just a modifier key if(!_active || e.keyCode == 16 || e.keyCode == 17 || e.keyCode == 18 || e.keyCode == 91) { return true; } // check for some special first keys, regardless of modifiers _console.log("key: " + e.keyCode); var cmd = _keyMap['default'][e.keyCode]; // intercept ctrl- and meta- sequences (may override the non-modifier cmd captured above var mod; if(e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { mod = _keyMap.control[e.keyCode]; if(mod) { cmd = mod; } } else if((e.altKey || e.metaKey) && !e.ctrlKey && !e.shiftKey) { mod = _keyMap.meta[e.keyCode]; if(mod) { cmd = mod; } } if(!cmd) { return true; } queue(cmd); e.preventDefault(); e.stopPropagation(); e.cancelBubble = true; return false; }; _element.onkeypress = function(e) { if(!_active) { return true; } var key = getKeyInfo(e); if(key.code == 0 || e.defaultPrevented || e.metaKey || e.altKey || e.ctrlKey) { return false; } queue(function cmdKeyPress() { if(_inSearch) { addSearchText(key.character); } else { addText(key.character); } }); e.preventDefault(); e.stopPropagation(); e.cancelBubble = true; return false; }; } if(_boundToElement) { self.attach(_element); } else { subscribeToKeys(); } return self; }; })(this);