diff --git a/client/js/autocompletion.js b/client/js/autocompletion.js index dad11900..ff1fb259 100644 --- a/client/js/autocompletion.js +++ b/client/js/autocompletion.js @@ -2,11 +2,23 @@ const $ = require("jquery"); const fuzzy = require("fuzzy"); +const Mousetrap = require("mousetrap"); const emojiMap = require("./libs/simplemap.json"); -const options = require("./options"); const constants = require("./constants"); -require("jquery-textcomplete"); -require("./libs/jquery/tabcomplete"); + +const input = $("#input"); +const Textcomplete = require("textcomplete/lib/textcomplete").default; +const Textarea = require("textcomplete/lib/textarea").default; +let textcomplete; + +module.exports = { + enable: enableAutocomplete, + disable: () => { + input.unbind("input.tabcomplete"); + Mousetrap(input.get(0)).unbind("tab", "keydown"); + textcomplete.destroy(); + }, +}; const chat = $("#chat"); const sidebar = $("#sidebar"); @@ -138,31 +150,84 @@ const backgroundColorStrategy = { index: 2, }; -const input = $("#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 = []; }); -if (options.autocomplete) { - enableAutocomplete(); -} + Mousetrap(input.get(0)).bind("tab", (e) => { + if (input.data("autocompleting")) { + return; + } -function enableAutocomplete() { - input.textcomplete([ - emojiStrategy, nicksStrategy, chanStrategy, commandStrategy, - foregroundColorStrategy, backgroundColorStrategy, - ], { - dropdownClassName: "textcomplete-menu", - placement: "top", - }).on({ - "textComplete:show": function() { - $(this).data("autocompleting", true); - }, - "textComplete:hide": function() { - $(this).data("autocompleting", false); + 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", + placement: "top", }, }); + + textcomplete.register([ + emojiStrategy, + nicksStrategy, + chanStrategy, + commandStrategy, + foregroundColorStrategy, + backgroundColorStrategy, + ]); + + // Activate the first item by default + // https://github.com/yuku-t/textcomplete/issues/93 + textcomplete.on("rendered", () => { + if (textcomplete.dropdown.items.length > 0) { + textcomplete.dropdown.items[0].activate(); + } + }); + + textcomplete.on("show", () => { + input.data("autocompleting", true); + }); + + textcomplete.on("hidden", () => { + input.data("autocompleting", false); + }); } function fuzzyGrep(term, array) { 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); diff --git a/client/js/lounge.js b/client/js/lounge.js index 3adf2881..0aaa9b76 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -17,7 +17,6 @@ const render = require("./render"); require("./socket-events"); const storage = require("./localStorage"); const utils = require("./utils"); -require("./autocompletion"); require("./webpush"); require("./keybinds"); require("./clipboard"); diff --git a/client/js/options.js b/client/js/options.js index 2ea08808..a7c3e4e1 100644 --- a/client/js/options.js +++ b/client/js/options.js @@ -1,11 +1,11 @@ "use strict"; const $ = require("jquery"); -require("jquery-textcomplete"); const escapeRegExp = require("lodash/escapeRegExp"); const userStyles = $("#user-specified-css"); const storage = require("./localStorage"); const tz = require("./libs/handlebars/tz"); +const autocompletion = require("./autocompletion"); const windows = $("#windows"); const chat = $("#chat"); @@ -156,9 +156,9 @@ module.exports.initialize = () => { chat.toggleClass("show-seconds", self.prop("checked")); } else if (name === "autocomplete") { if (self.prop("checked")) { - $("#input").trigger("autocomplete:on"); + autocompletion.enable(); } else { - $("#input").textcomplete("destroy"); + autocompletion.disable(); } } else if (name === "desktopNotifications") { if ($(this).prop("checked") && Notification.permission !== "granted") { diff --git a/package.json b/package.json index c4521de5..d0d6de5e 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "handlebars-loader": "1.6.0", "intersection-observer": "0.5.0", "jquery": "3.2.1", - "jquery-textcomplete": "1.8.4", "jquery-ui": "1.12.1", "mocha": "4.0.1", "mousetrap": "1.6.1", @@ -85,6 +84,7 @@ "socket.io-client": "2.0.4", "stylelint": "8.3.1", "stylelint-config-standard": "18.0.0", + "textcomplete": "0.14.5", "webpack": "3.10.0" } }