Merge pull request #1800 from thelounge/xpaw/textcomplete

Update textcomplete library and rewrite tabcomplete
This commit is contained in:
Jérémie Astori 2017-12-06 18:29:49 -05:00 committed by GitHub
commit b662764caa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 286 deletions

View File

@ -2,11 +2,23 @@
const $ = require("jquery"); const $ = require("jquery");
const fuzzy = require("fuzzy"); const fuzzy = require("fuzzy");
const Mousetrap = require("mousetrap");
const emojiMap = require("./libs/simplemap.json"); const emojiMap = require("./libs/simplemap.json");
const options = require("./options");
const constants = require("./constants"); 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 chat = $("#chat");
const sidebar = $("#sidebar"); const sidebar = $("#sidebar");
@ -138,31 +150,84 @@ const backgroundColorStrategy = {
index: 2, index: 2,
}; };
const input = $("#input") function enableAutocomplete() {
.tab((word) => completeNicks(word, false), {hint: false}) let tabCount = 0;
.on("autocomplete:on", function() { let currentMatches = [];
enableAutocomplete();
input.on("input.tabcomplete", () => {
tabCount = 0;
currentMatches = [];
}); });
if (options.autocomplete) { Mousetrap(input.get(0)).bind("tab", (e) => {
enableAutocomplete(); if (input.data("autocompleting")) {
} return;
}
function enableAutocomplete() { e.preventDefault();
input.textcomplete([
emojiStrategy, nicksStrategy, chanStrategy, commandStrategy, const text = input.val();
foregroundColorStrategy, backgroundColorStrategy,
], { if (input.get(0).selectionStart !== text.length) {
dropdownClassName: "textcomplete-menu", return;
placement: "top", }
}).on({
"textComplete:show": function() { let lastWord;
$(this).data("autocompleting", true);
}, if (tabCount === 0) {
"textComplete:hide": function() { lastWord = text.split(/\s/).pop();
$(this).data("autocompleting", false);
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) { function fuzzyGrep(term, array) {

View File

@ -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 <input> and <textarea> 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(
$("<div>").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);

View File

@ -17,7 +17,6 @@ const render = require("./render");
require("./socket-events"); require("./socket-events");
const storage = require("./localStorage"); const storage = require("./localStorage");
const utils = require("./utils"); const utils = require("./utils");
require("./autocompletion");
require("./webpush"); require("./webpush");
require("./keybinds"); require("./keybinds");
require("./clipboard"); require("./clipboard");

View File

@ -1,11 +1,11 @@
"use strict"; "use strict";
const $ = require("jquery"); const $ = require("jquery");
require("jquery-textcomplete");
const escapeRegExp = require("lodash/escapeRegExp"); const escapeRegExp = require("lodash/escapeRegExp");
const userStyles = $("#user-specified-css"); const userStyles = $("#user-specified-css");
const storage = require("./localStorage"); const storage = require("./localStorage");
const tz = require("./libs/handlebars/tz"); const tz = require("./libs/handlebars/tz");
const autocompletion = require("./autocompletion");
const windows = $("#windows"); const windows = $("#windows");
const chat = $("#chat"); const chat = $("#chat");
@ -156,9 +156,9 @@ module.exports.initialize = () => {
chat.toggleClass("show-seconds", self.prop("checked")); chat.toggleClass("show-seconds", self.prop("checked"));
} else if (name === "autocomplete") { } else if (name === "autocomplete") {
if (self.prop("checked")) { if (self.prop("checked")) {
$("#input").trigger("autocomplete:on"); autocompletion.enable();
} else { } else {
$("#input").textcomplete("destroy"); autocompletion.disable();
} }
} else if (name === "desktopNotifications") { } else if (name === "desktopNotifications") {
if ($(this).prop("checked") && Notification.permission !== "granted") { if ($(this).prop("checked") && Notification.permission !== "granted") {

View File

@ -76,7 +76,6 @@
"handlebars-loader": "1.6.0", "handlebars-loader": "1.6.0",
"intersection-observer": "0.5.0", "intersection-observer": "0.5.0",
"jquery": "3.2.1", "jquery": "3.2.1",
"jquery-textcomplete": "1.8.4",
"jquery-ui": "1.12.1", "jquery-ui": "1.12.1",
"mocha": "4.0.1", "mocha": "4.0.1",
"mousetrap": "1.6.1", "mousetrap": "1.6.1",
@ -85,6 +84,7 @@
"socket.io-client": "2.0.4", "socket.io-client": "2.0.4",
"stylelint": "8.3.1", "stylelint": "8.3.1",
"stylelint-config-standard": "18.0.0", "stylelint-config-standard": "18.0.0",
"textcomplete": "0.14.5",
"webpack": "3.10.0" "webpack": "3.10.0"
} }
} }