glowing-bear/js/inputbar.js

626 lines
28 KiB
JavaScript

(function() {
'use strict';
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', 'imgur', 'models', 'IrcUtils', 'settings', 'utils', 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
imgur,
models,
IrcUtils,
settings,
utils) {
// Expose utils to be able to check if we're on a mobile UI
$scope.utils = utils;
// Emojify input. E.g. Turn :smile: into the unicode equivalent, but
// don't do replacements in the middle of a word (e.g. std::io::foo)
$scope.inputChanged = function() {
var emojiRegex = /^(?:[\uD800-\uDBFF][\uDC00-\uDFFF])+$/, // *only* emoji
changed = false, // whether a segment was modified
inputNode = $scope.getInputNode(),
caretPos = inputNode.selectionStart,
position = 0; // current position in text
// use capturing group in regex to include whitespace in output array
var segments = $scope.command.split(/(\s+)/);
for (var i = 0; i < segments.length; i ++) {
if (/\s+/.test(segments[i]) || emojiRegex.test(segments[i])) {
// ignore whitespace and emoji-only segments
position += segments[i].length;
continue;
}
// emojify segment
var emojified = emojione.shortnameToUnicode(segments[i]);
if (emojiRegex.test(emojified)) {
// If result consists *only* of emoji, adjust caret
// position and replace segment with emojified version
caretPos = caretPos - segments[i].length + emojified.length;
segments[i] = emojified;
changed = true;
}
position += segments[i].length;
}
if (changed) { // Only re-assemble if something changed
$scope.command = segments.join('');
setTimeout(function() {
inputNode.setSelectionRange(caretPos, caretPos);
});
}
};
/*
* 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 completion_suffix = models.wconfig['weechat.completion.nick_completer'];
var add_space = models.wconfig['weechat.completion.nick_add_space'];
var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate,
activeBuffer.getNicklistByTime(),
completion_suffix, add_space);
// 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);
};
$rootScope.insertAtCaret = function(toInsert) {
// caret position in the input bar
var inputNode = $scope.getInputNode(),
caretPos = inputNode.selectionStart;
var prefix = $scope.command.substring(0, caretPos),
suffix = $scope.command.substring(caretPos, $scope.command.length);
// Add spaces if missing
if (prefix.length > 0 && prefix[prefix.length - 1] !== ' ') {
prefix += ' ';
}
if (suffix.length > 0 && suffix[0] !== ' ') {
suffix = ' '.concat(suffix);
}
$scope.command = prefix + toInsert + suffix;
setTimeout(function() {
inputNode.focus();
var pos = $scope.command.length - suffix.length;
inputNode.setSelectionRange(pos, pos);
// force refresh?
$scope.$apply();
}, 0);
};
$scope.uploadImage = function($event, files) {
// Send image url after upload
var sendImageUrl = function(imageUrl) {
// Send image
if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl));
}
};
if(typeof files !== "undefined" && files.length > 0) {
// Loop through files
for (var i = 0; i < files.length; i++) {
// Process image
imgur.process(files[i], sendImageUrl);
}
}
};
// 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) {
// Ask before a /quit
if (line === '/quit' || line.indexOf('/quit ') === 0) {
if (!window.confirm("Are you sure you want to quit WeeChat? This will prevent you from connecting with Glowing Bear until you restart WeeChat on the command line!")) {
// skip this line
return;
}
}
connection.sendMessage(line);
});
// Check for /clear command
if ($scope.command === '/buffer clear' || $scope.command === '/c') {
$log.debug('Clearing lines');
ab.clear();
}
// Check against a list of commands that opens a new
// buffer and save the name of the buffer so we can
// also automatically switch to the new buffer in gb
var opencommands = ['/query', '/join', '/j', '/q'];
var spacepos = $scope.command.indexOf(' ');
var firstword = $scope.command.substr(0, spacepos);
var index = opencommands.indexOf(firstword);
if (index >= 0) {
var queryName = $scope.command.substring(spacepos + 1);
// Cache our queries so when a buffer gets opened we can open in UI
models.outgoingQueries.push(queryName);
}
// Empty the input after it's sent
$scope.command = '';
}
// New style clearing requires this, old does not
if (settings.hotlistsync && models.version[0] >= 1) {
connection.sendHotlistClear();
}
$scope.getInputNode().focus();
};
//XXX THIS DOES NOT BELONG HERE!
$rootScope.addMention = function(bufferline) {
if (!bufferline.showHiddenBrackets) {
// the line is a notice or action or something else that doesn't belong
return;
}
var prefix = bufferline.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;
// A KeyboardEvent property representing the physical key that was pressed, ignoring the keyboard layout and ignoring whether any modifier keys were active.
// Not supported in Edge or Safari at the time of writing this, but supported in Firefox and Chrome.
var key = $event.code;
// Safari doesn't implement DOM 3 input events yet as of 8.0.6
var altg = $event.getModifierState ? $event.getModifierState('AltGraph') : false;
// Mac OSX behaves differntly for altgr, so we check for that
if (altg) {
// We don't handle any anything with altgr
return false;
}
// reset quick keys display
$rootScope.showQuickKeys = false;
// any other key than Tab resets nick completion iteration
var tmpIterCandidate = $scope.iterCandidate;
$scope.iterCandidate = null;
var bufferNumber;
var sortedBuffers;
var filteredBufferNum;
var activeBufferId;
// if Alt+J was pressed last...
if ($rootScope.showJumpKeys) {
var cleanup = function() { // cleanup helper
$rootScope.showJumpKeys = false;
$rootScope.jumpDecimal = undefined;
$scope.$parent.search = '';
$scope.$parent.search_placeholder = 'Search';
$rootScope.refresh_filter_predicate();
};
// ... we expect two digits now
if (!$event.altKey && (code > 47 && code < 58)) {
// first digit
if ($rootScope.jumpDecimal === undefined) {
$rootScope.jumpDecimal = code - 48;
$event.preventDefault();
$scope.$parent.search = $rootScope.jumpDecimal;
$rootScope.refresh_filter_predicate();
// second digit, jump to correct buffer
} else {
bufferNumber = ($rootScope.jumpDecimal * 10) + (code - 48);
$scope.$parent.setActiveBuffer(bufferNumber, '$jumpKey');
$event.preventDefault();
cleanup();
}
} else {
// Not a decimal digit, abort
cleanup();
}
}
// Left Alt+[0-9] -> jump to buffer
if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58) && settings.enableQuickKeys) {
if (code === 48) {
code = 58;
}
bufferNumber = code - 48 - 1 ;
// quick select filtered entries
if (($scope.$parent.search.length || $scope.$parent.onlyUnread) && $scope.$parent.filteredBuffers.length) {
filteredBufferNum = $scope.$parent.filteredBuffers[bufferNumber];
if (filteredBufferNum !== undefined) {
activeBufferId = [filteredBufferNum.number, filteredBufferNum.id];
}
} else {
// 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
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];
});
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+Arrow up/down -> switch to prev/next adjacent buffer
if ($event.altKey && !$event.ctrlKey && (code === 38 || code === 40)) {
$event.preventDefault();
var direction = code - 39;
$rootScope.switchToAdjacentBuffer(direction);
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
// https://w3c.github.io/uievents-code/#code-IntlBackslash
if ($event.altKey && (code === 60 || code === 226 || key === "IntlBackslash")) {
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();
}
setTimeout(function() {
document.getElementById('bufferFilter').focus();
});
return true;
}
// Alt-h -> Toggle all as read
if ($event.altKey && !$event.ctrlKey && code === 72) {
var buffers = models.getBuffers();
_.each(buffers, function(buffer) {
buffer.unread = 0;
buffer.notification = 0;
});
var servers = models.getServers();
_.each(servers, function(server) {
server.unread = 0;
});
connection.sendHotlistClearAll();
}
// Alt+J -> Jump to buffer
if ($event.altKey && (code === 106 || code === 74)) {
$event.preventDefault();
// reset search state and show jump keys
$scope.$parent.search = '';
$scope.$parent.search_placeholder = 'Number';
$rootScope.showJumpKeys = true;
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) {
if (bufferlines.scrollTop === 0) {
if (!$rootScope.loadingLines) {
$scope.$parent.fetchMoreLines();
}
return true;
}
lines = bufferlines.querySelectorAll("tr");
for (i = lines.length - 1; i >= 0; i--) {
if ((lines[i].offsetTop-bufferlines.scrollTop)<bufferlines.clientHeight/2) {
lines[i].scrollIntoView(false);
break;
}
}
return true;
}
// Page down -> 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 (settings.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.replace(/\s+$/, '').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;
}
// Alt key down -> display quick key legend
if ($event.type === "keydown" && code === 18 && !$event.ctrlKey && !$event.shiftKey && settings.enableQuickKeys) {
$rootScope.showQuickKeys = true;
}
};
$rootScope.handleKeyRelease = function($event) {
// Alt key up -> remove quick key legend
if ($event.keyCode === 18) {
if ($rootScope.quickKeysTimer !== undefined) {
clearTimeout($rootScope.quickKeysTimer);
}
$rootScope.quickKeysTimer = setTimeout(function() {
if ($rootScope.showQuickKeys) {
$rootScope.showQuickKeys = false;
$rootScope.$apply();
}
delete $rootScope.quickKeysTimer;
}, 1000);
return true;
}
};
$scope.handleCompleteNickButton = function($event) {
$event.preventDefault();
$scope.completeNick();
setTimeout(function() {
$scope.getInputNode().focus();
}, 0);
return true;
};
$scope.inputPasted = function(e) {
if (e.clipboardData && e.clipboardData.files && e.clipboardData.files.length) {
e.stopPropagation();
e.preventDefault();
var sendImageUrl = function(imageUrl) {
if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl));
}
};
for (var i = 0; i < e.clipboardData.files.length; i++) {
imgur.process(e.clipboardData.files[i], sendImageUrl);
}
return false;
}
return true;
};
}]
};
});
})();