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() : "";