diff --git a/client/js/autocompletion.js b/client/js/autocompletion.js index 87c173f2..ff1fb259 100644 --- a/client/js/autocompletion.js +++ b/client/js/autocompletion.js @@ -2,19 +2,22 @@ const $ = require("jquery"); const fuzzy = require("fuzzy"); +const Mousetrap = require("mousetrap"); const emojiMap = require("./libs/simplemap.json"); const constants = require("./constants"); -require("./libs/jquery/tabcomplete"); const input = $("#input"); const Textcomplete = require("textcomplete/lib/textcomplete").default; const Textarea = require("textcomplete/lib/textarea").default; -const editor = new Textarea(input.get(0)); let textcomplete; module.exports = { enable: enableAutocomplete, - disable: () => textcomplete.destroy(false), + disable: () => { + input.unbind("input.tabcomplete"); + Mousetrap(input.get(0)).unbind("tab", "keydown"); + textcomplete.destroy(); + }, }; const chat = $("#chat"); @@ -147,13 +150,53 @@ const backgroundColorStrategy = { index: 2, }; -input - .tab((word) => completeNicks(word, false), {hint: false}) - .on("autocomplete:on", function() { - enableAutocomplete(); +function enableAutocomplete() { + let tabCount = 0; + let currentMatches = []; + + input.on("input.tabcomplete", () => { + tabCount = 0; + currentMatches = []; }); -function enableAutocomplete() { + Mousetrap(input.get(0)).bind("tab", (e) => { + if (input.data("autocompleting")) { + return; + } + + e.preventDefault(); + + const text = input.val(); + + if (input.get(0).selectionStart !== text.length) { + return; + } + + let lastWord; + + if (tabCount === 0) { + lastWord = text.split(/\s/).pop(); + + if (lastWord.length === 0) { + return; + } + + currentMatches = completeNicks(lastWord, false); + + if (currentMatches.length === 0) { + return; + } + } else { + lastWord = nicksStrategy.replace([0, currentMatches[(tabCount - 1) % currentMatches.length]]); + } + + const matchedNick = currentMatches[tabCount % currentMatches.length]; + input.val(text.substr(0, input.get(0).selectionStart - lastWord.length) + nicksStrategy.replace([0, matchedNick])); + + tabCount++; + }, "keydown"); + + const editor = new Textarea(input.get(0)); textcomplete = new Textcomplete(editor, { dropdown: { className: "textcomplete-menu", diff --git a/client/js/libs/jquery/tabcomplete.js b/client/js/libs/jquery/tabcomplete.js deleted file mode 100644 index 5d04794c..00000000 --- a/client/js/libs/jquery/tabcomplete.js +++ /dev/null @@ -1,258 +0,0 @@ -import jQuery from "jquery"; - -/*! - * tabcomplete - * http://github.com/erming/tabcomplete - * v1.3.1 - */ -(function($) { - var keys = { - backspace: 8, - tab: 9, - up: 38, - down: 40 - }; - - $.tabcomplete = {}; - $.tabcomplete.defaultOptions = { - after: "", - arrowKeys: false, - caseSensitive: false, - hint: "placeholder", - minLength: 1 - }; - - $.fn.tab = // Alias - $.fn.tabcomplete = function(args, options) { - if (this.length > 1) { - return this.each(function() { - $(this).tabcomplete(args, options); - }); - } - - // Only enable the plugin on and elements. - var tag = this.prop("tagName"); - if (tag != "INPUT" && tag != "TEXTAREA") { - return; - } - - // Set default options. - options = $.extend( - $.tabcomplete.defaultOptions, - options - ); - - // Remove any leftovers. - // This allows us to override the plugin if necessary. - this.unbind(".tabcomplete"); - this.prev(".hint").remove(); - - var self = this; - var backspace = false; - var i = -1; - var words = []; - var last = ""; - - var hint = $.noop; - - // Determine what type of hinting to use. - switch (options.hint) { - case "placeholder": - hint = placeholder; - break; - - case "select": - hint = select; - break; - } - - this.on("input.tabcomplete", function() { - var input = self.val(); - var word = input.split(/ |\n/).pop(); - - // Reset iteration. - i = -1; - last = ""; - words = []; - - // Check for matches if the current word is the last word. - if (self[0].selectionStart == input.length - && word.length) { - if (typeof args === "function") { - // If the user supplies a function, invoke it - // and keep the result. - words = args(word); - } else { - // Otherwise, call the .match() function. - words = match(word, args, options.caseSensitive); - } - - // Append 'after' to each word. - if (options.after) { - words = $.map(words, function(w) { return w + options.after; }); - } - } - - // Emit the number of matching words with the 'match' event. - self.trigger("match", words.length); - - if (options.hint) { - if (!(options.hint == "select" && backspace) && word.length >= options.minLength) { - // Show hint. - hint.call(self, words[0]); - } else { - // Clear hinting. - // This call is needed when using backspace. - hint.call(self, ""); - } - } - - if (backspace) { - backspace = false; - } - }); - - this.on("keydown.tabcomplete", function(e) { - var key = e.which; - if (key == keys.tab && !e.ctrlKey - || (options.arrowKeys && (key == keys.up || key == keys.down))) { - - // Don't lose focus on tab click. - e.preventDefault(); - - // Iterate the matches with tab and the up and down keys by incrementing - // or decrementing the 'i' variable. - if (key != keys.up) { - i++; - } else { - if (i == -1) return; - if (i == 0) { - // Jump to the last word. - i = words.length - 1; - } else { - i--; - } - } - - // Get next match. - var word = words[i % words.length]; - if (!word) { - return; - } - - var value = self.val(); - last = last || value.split(/ |\n/).pop(); - - // Return if the 'minLength' requirement isn't met. - if (last.length < options.minLength) { - return; - } - - // Update element with the completed text. - var text = value.substr(0, self[0].selectionStart - last.length) + word; - self.val(text); - - // Put the cursor at the end after completion. - // This isn't strictly necessary, but solves an issue with - // Internet Explorer. - if (options.hint == "select") { - self[0].selectionStart = text.length; - } - - // Remember the word until next time. - last = word; - - // Emit event. - self.trigger("tabcomplete", last); - - if (options.hint) { - // Turn off any additional hinting. - hint.call(self, ""); - } - } else if (e.which == keys.backspace) { - // Remember that backspace was pressed. This is used - // by the 'input' event. - backspace = true; - - // Reset iteration. - i = -1; - last = ""; - } - }); - - if (options.hint) { - // If enabled, turn on hinting. - hint.call(this, ""); - } - - return this; - } - - // Simple matching. - // Filter the array and return the items that begins with 'word'. - function match(word, array, caseSensitive) { - return $.grep( - array, - function(w) { - if (caseSensitive) { - return !w.indexOf(word); - } else { - return !w.toLowerCase().indexOf(word.toLowerCase()); - } - } - ); - } - - // Show placeholder text. - // This works by creating a copy of the input and placing it behind - // the real input. - function placeholder(word) { - var input = this; - var clone = input.prev(".hint"); - - input.css({ - backgroundColor: "transparent", - position: "relative", - }); - - // Lets create a clone of the input if it does - // not already exist. - if (!clone.length) { - input.wrap( - $("").css({position: "relative", height: input.css("height")}) - ); - clone = input - .clone() - .attr("tabindex", -1) - .removeAttr("id name placeholder") - .addClass("hint") - .insertBefore(input); - clone.css({ - position: "absolute", - }); - } - - var hint = ""; - if (typeof word !== "undefined") { - var value = input.val(); - hint = value + word.substr(value.split(/ |\n/).pop().length); - } - - clone.val(hint); - } - - // Hint by selecting part of the suggested word. - function select(word) { - var input = this; - var value = input.val(); - if (word) { - input.val( - value - + word.substr(value.split(/ |\n/).pop().length) - ); - - // Select hint. - input[0].selectionStart = value.length; - } - } -})(jQuery);