diff --git a/.gitignore b/.gitignore index c2658d7d..1ca95717 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +npm-debug.log diff --git a/client/css/style.css b/client/css/style.css index 857d8237..6108e4b8 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -200,6 +200,9 @@ button::-moz-focus-inner { padding: 0 10px; width: 100%; } +#chat form .hint { + color: #bdc3c7; +} #chat .lobby .messages, #chat .query .messages { right: 0; diff --git a/client/index.html b/client/index.html index e01c9661..e830aced 100644 --- a/client/index.html +++ b/client/index.html @@ -80,6 +80,7 @@
+
{{/each}} diff --git a/client/js/chat.js b/client/js/chat.js index 9d6fe611..0a4d6f12 100644 --- a/client/js/chat.js +++ b/client/js/chat.js @@ -49,13 +49,13 @@ $(function() { switch (e) { case "join": chat.append(render("windows", {windows: [data.chan]})) - .find(".messages") + .find(".window") .last() + .find(".messages") .scrollGlue({speed: 400}) .end() - .end() .find(".input") - .tabComplete({list: commands}); + .tabComplete(commands); $("#network-" + data.id) .append(render("channels", {channels: [data.chan]})) @@ -74,7 +74,7 @@ $(function() { var channels = $.map(data.networks, function(n) { return n.channels; }); chat.html(render("windows", {windows: channels})) .find(".input") - .tabComplete({list: commands}) + .tabComplete(commands) .end() .find(".hidden") .prev(".show-more") @@ -122,15 +122,12 @@ $(function() { if (!target) { return; } - location.hash = target; - sidebar.find(".active").removeClass("active"); button.addClass("active") .find(".badge") .removeClass("highlight") .empty(); - var window = $(target) .css("z-index", z++) .find("input") @@ -179,7 +176,7 @@ $(function() { chat.on("submit", "form", function() { var form = $(this); - var input = form.find(".input"); + var input = form.find(".input:not(.hint)"); var text = input.val(); if (text == "") { return; diff --git a/client/js/jquery.plugins.js b/client/js/jquery.plugins.js index 529eb4a6..4ad0f84e 100644 --- a/client/js/jquery.plugins.js +++ b/client/js/jquery.plugins.js @@ -71,7 +71,7 @@ * Copyright (c) 2014 Mattias Erming * Licensed under the MIT License. * - * Version 1.2.0 + * Version 1.2.1 */ (function($) { $.fn.scrollGlue = function(options) { @@ -161,75 +161,164 @@ * Copyright (c) 2014 Mattias Erming * Licensed under the MIT License. * - * Version 0.2.4 + * Version 1.0.0-alpha2 */ (function($) { - $.fn.tabComplete = function(options) { - var settings = $.extend({ - after: '', - caseSensitive: false, - list: [], - }, options); - + var defaults = { + after: "", + caseSensitive: false, + hint: true, + minLength: 1, + }; + + $.fn.tabComplete = function(args, options) { var self = this; - if (self.size() > 1) { - return self.each(function() { - $(this).tabComplete(options); + options = $.extend( + {}, defaults, options + ); + + if (this.length > 1) { + return this.each(function() { + $(this).tabComplete(args, options); }); } - // Keep the list stored in the DOM via jQuery.data() - self.data('list', settings.list); + if (options.hint) { + // Lets turn on hinting. + hint.call(self, ""); + } - var match = []; - self.on('keydown', function(e) { - var key = e.which; - if (key != 9) { - match = []; - return; - } + // Unbind namespace. + // This allows us to override the plugin if necessary. + this.unbind(".tabComplete"); + + var i = 0; + var words = []; + var last = ""; + + this.on("input.tabComplete", function(e) { + var input = self.val(); + var word = input.split(/ |\n/).pop(); - var text = self.val().trim().split(' '); - var last = text.splice(-1)[0]; - - if (!match.length) { - match = []; - $.each(self.data('list'), function(i, w) { - var l = last; - if (l == '') { - return; - } else if (typeof w === "function") { - var words = w(l); - if (words) { - match = match.concat(words); - } - } else if (!settings.caseSensitive) { - if (0 == w.toLowerCase().indexOf(l.toLowerCase())) { - match.push(w); - } - } else { - if (0 == w.indexOf(l)) { - match.push(w); - } - } - }); - } - - var i = match.indexOf(last) + 1; - if (i == match.length) { + if (!word) { i = 0; + words = []; + last = ""; + } else 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(args, word, options.caseSensitive); } - if (match.length) { - text.push(match[i]); - self.val(text.join(' ') + settings.after); + if (options.hint) { + if (word.length >= options.minLength) { + hint.call(self, words[0]); + } else { + // Clear hinting. + // This call is needed when using backspace. + hint.call(self, ""); + } + } + }); + + this.on("keydown.tabComplete", function(e) { + var key = e.which; + if (key == 9) { + // Don't lose focus on tab click. + e.preventDefault(); + + // Get next match. + var word = words[i++ % words.length]; + if (!word) { + return; + } + + var input = self.val().trim(); + last = last || input.split(/ |\n/).pop(); + + if (last.length < options.minLength) { + return; + } + + self.val( + input.substr(0, input.lastIndexOf(last)) + + word + + options.after + ); + + // Remember the word until next time. + last = word; + + if (options.hint) { + // Turn off any additional hinting. + hint.call(self, ""); + } } - - return false; }); return this; - }; + } + + // Simple matching. + // Filter the array and return the items that begins with `word`. + function match(array, word, caseSensitive) { + return $.grep( + array, + function(w) { + if (caseSensitive) { + return !w.indexOf(word); + } else { + return !w.toLowerCase().indexOf(word.toLowerCase()); + } + } + ); + } + + // Add input hinting. + // This works by creating a copy of the input and placing it behind + // the real input. + function hint(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"}) + ); + clone = input + .clone() + .prop("disabled", true) + .removeAttr("id name placeholder") + .addClass("hint") + .insertBefore(input); + clone.css({ + position: "absolute", + }); + } + + var hint = ""; + if (typeof word !== "undefined") { + var text = input.val(); + hint = text + word.substr(last(text).length); + } + + clone.val(hint); + } + + // Get the last word of a string. + function last(str) { + return str.split(/ |\n/).pop(); + } })(jQuery); /*! diff --git a/lib/server.js b/lib/server.js index 06808141..2e9e613e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -121,6 +121,9 @@ function input(data) { var id = data.id; var text = data.text; + if (!text) { + return; + } var args = text.replace(/^\//, '').split(" "); var cmd = text.charAt(0) == "/" ? args[0].toLowerCase() : "";