var weechat = angular.module('weechat'); weechat.directive('inputBar', function() { return { templateUrl: 'directives/input.html', scope: { inputId: '@inputId', command: '=command' }, controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', function($rootScope, $scope, $element, //XXX do we need this? don't seem to be using it $log, connection, //XXX we should eliminate this dependency and use signals instead models) { /* * Returns the input element */ $scope.getInputNode = function() { return document.querySelector('textarea#' + $scope.inputId); }; $scope.hideSidebar = function() { $rootScope.hideSidebar(); }; $scope.completeNick = function() { // input DOM node var inputNode = $scope.getInputNode(); // get current caret position var caretPos = inputNode.selectionStart; // get current active buffer var activeBuffer = models.getActiveBuffer(); // Empty input makes $scope.command undefined -- use empty string instead var input = $scope.command || ''; // complete nick var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate, activeBuffer.getNicklistByTime(), ':'); // remember iteration candidate $scope.iterCandidate = nickComp.iterCandidate; // update current input $scope.command = nickComp.text; // update current caret position setTimeout(function() { inputNode.focus(); inputNode.setSelectionRange(nickComp.caretPos, nickComp.caretPos); }, 0); }; // Send the message to the websocket $scope.sendMessage = function() { //XXX Use a signal here var ab = models.getActiveBuffer(); // It's undefined early in the lifecycle of the program. // Don't send empty commands if($scope.command !== undefined && $scope.command !== '') { // log to buffer history ab.addToHistory($scope.command); // Split the command into multiple commands based on line breaks _.each($scope.command.split(/\r?\n/), function(line) { connection.sendMessage(line); }); // Check for /clear command if ($scope.command === '/buffer clear' || $scope.command === '/c') { $log.debug('Clearing lines'); ab.clear(); } // Empty the input after it's sent $scope.command = ''; } $scope.getInputNode().focus(); }; //XXX THIS DOES NOT BELONG HERE! $rootScope.addMention = function(prefix) { // Extract nick from bufferline prefix var nick = prefix[prefix.length - 1].text; var newValue = $scope.command || ''; // can be undefined, in that case, use the empty string var addColon = newValue.length === 0; if (newValue.length > 0) { // Try to determine if it's a sequence of nicks var trimmedValue = newValue.trim(); if (trimmedValue.charAt(trimmedValue.length - 1) === ':') { // get last word var lastSpace = trimmedValue.lastIndexOf(' ') + 1; var lastWord = trimmedValue.slice(lastSpace, trimmedValue.length - 1); var nicklist = models.getActiveBuffer().getNicklistByTime(); // check against nicklist to see if it's a list of highlights for (var index in nicklist) { if (nicklist[index].name === lastWord) { // It's another highlight! newValue = newValue.slice(0, newValue.lastIndexOf(':')) + ' '; addColon = true; break; } } } // Add a space before the nick if there isn't one already // Last char might have changed above, so re-check if (newValue.charAt(newValue.length - 1) !== ' ') { newValue += ' '; } } // Add highlight to nicklist newValue += nick; if (addColon) { newValue += ': '; } $scope.command = newValue; $scope.getInputNode().focus(); }; // Handle key presses in the input bar $rootScope.handleKeyPress = function($event) { // don't do anything if not connected if (!$rootScope.connected) { return true; } var inputNode = $scope.getInputNode(); // Support different browser quirks var code = $event.keyCode ? $event.keyCode : $event.charCode; // any other key than Tab resets nick completion iteration var tmpIterCandidate = $scope.iterCandidate; $scope.iterCandidate = null; // Left Alt+[0-9] -> jump to buffer if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) { if (code === 48) { code = 58; } var bufferNumber = code - 48 - 1 ; // Map the buffers to only their numbers and IDs so we don't have to // copy the entire (possibly very large) buffer object, and then sort // the buffers according to their WeeChat number var sortedBuffers = _.map(models.getBuffers(), function(buffer) { return [buffer.number, buffer.id]; }).sort(function(left, right) { // By default, Array.prototype.sort() sorts alphabetically. // Pass an ordering function to sort by first element. return left[0] - right[0]; }); var activeBufferId = sortedBuffers[bufferNumber]; if (activeBufferId) { $scope.$parent.setActiveBuffer(activeBufferId[1]); $event.preventDefault(); } } // Tab -> nick completion if (code === 9 && !$event.altKey && !$event.ctrlKey) { $event.preventDefault(); $scope.iterCandidate = tmpIterCandidate; $scope.completeNick(); return true; } // Left Alt+n -> toggle nicklist if ($event.altKey && !$event.ctrlKey && code === 78) { $event.preventDefault(); $rootScope.toggleNicklist(); return true; } // Alt+A -> switch to buffer with activity if ($event.altKey && (code === 97 || code === 65)) { $event.preventDefault(); $rootScope.switchToActivityBuffer(); return true; } // Alt+L -> focus on input bar if ($event.altKey && (code === 76 || code === 108)) { $event.preventDefault(); inputNode.focus(); inputNode.setSelectionRange($scope.command.length, $scope.command.length); return true; } // Alt+< -> switch to previous buffer if ($event.altKey && (code === 60 || code === 226)) { var previousBuffer = models.getPreviousBuffer(); if (previousBuffer) { models.setActiveBuffer(previousBuffer.id); $event.preventDefault(); return true; } } // Double-tap Escape -> disconnect if (code === 27) { $event.preventDefault(); // Check if a modal is visible. If so, close it instead of disconnecting var modals = document.querySelectorAll('.gb-modal'); for (var modalId = 0; modalId < modals.length; modalId++) { if (modals[modalId].getAttribute('data-state') === 'visible') { modals[modalId].setAttribute('data-state', 'hidden'); return true; } } if (typeof $scope.lastEscape !== "undefined" && (Date.now() - $scope.lastEscape) <= 500) { // Double-tap connection.disconnect(); } $scope.lastEscape = Date.now(); return true; } // Alt+G -> focus on buffer filter input if ($event.altKey && (code === 103 || code === 71)) { $event.preventDefault(); if (!$scope.$parent.isSidebarVisible()) { $scope.$parent.showSidebar(); } document.getElementById('bufferFilter').focus(); return true; } var caretPos; // Arrow up -> go up in history if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) { caretPos = inputNode.selectionStart; if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) { return false; } $scope.command = models.getActiveBuffer().getHistoryUp($scope.command); // Set cursor to last position. Need 0ms timeout because browser sets cursor // position to the beginning after this key handler returns. setTimeout(function() { if ($scope.command) { inputNode.setSelectionRange($scope.command.length, $scope.command.length); } }, 0); return true; } // Arrow down -> go down in history if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) { caretPos = inputNode.selectionStart; if ($scope.command.slice(caretPos).indexOf("\n") !== -1) { return false; } $scope.command = models.getActiveBuffer().getHistoryDown($scope.command); // We don't need to set the cursor to the rightmost position here, the browser does that for us return true; } // Enter to submit, shift-enter for newline if (code == 13 && !$event.shiftKey && document.activeElement === inputNode) { $event.preventDefault(); $scope.sendMessage(); return true; } var bufferlines = document.getElementById("bufferlines"); var lines; var i; // Page up -> scroll up if ($event.type === "keydown" && code === 33 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) { lines = bufferlines.querySelectorAll("tr"); for (i = lines.length - 1; i >= 0; i--) { if ((lines[i].offsetTop-bufferlines.scrollTop) scroll down if ($event.type === "keydown" && code === 34 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) { lines = bufferlines.querySelectorAll("tr"); for (i = 0; i < lines.length; i++) { if ((lines[i].offsetTop-bufferlines.scrollTop)>bufferlines.clientHeight/2) { lines[i].scrollIntoView(true); break; } } return true; } // Some readline keybindings if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { // get current caret position caretPos = inputNode.selectionStart; // Ctrl-a if (code == 65) { inputNode.setSelectionRange(0, 0); // Ctrl-e } else if (code == 69) { inputNode.setSelectionRange($scope.command.length, $scope.command.length); // Ctrl-u } else if (code == 85) { $scope.command = $scope.command.slice(caretPos); setTimeout(function() { inputNode.setSelectionRange(0, 0); }); // Ctrl-k } else if (code == 75) { $scope.command = $scope.command.slice(0, caretPos); setTimeout(function() { inputNode.setSelectionRange($scope.command.length, $scope.command.length); }); // Ctrl-w } else if (code == 87) { var trimmedValue = $scope.command.slice(0, caretPos); var lastSpace = trimmedValue.lastIndexOf(' ') + 1; $scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length); setTimeout(function() { inputNode.setSelectionRange(lastSpace, lastSpace); }); } else { return false; } $event.preventDefault(); return true; } }; }] }; });