Skip to content

Instantly share code, notes, and snippets.

@xxx
Created August 19, 2010 18:36
Show Gist options
  • Save xxx/538566 to your computer and use it in GitHub Desktop.
Save xxx/538566 to your computer and use it in GitHub Desktop.
if (typeof Relife !== "object") {
Relife = {};
}
Relife.max_chat_lines = 100;
Relife.chat_interval = null;
Relife.current_channel = null;
Relife.ajaxQueue = {
_queue: [],
processing: false,
head: function () {
if (!this._queue.length) {
return false;
} else {
return this._queue[0];
}
},
_handler: function (successful, data, status) {
var head = this.head();
if (successful) {
head.onSuccess(data);
} else {
head.onFail();
}
this._shift();
},
execute: function () {
if (this.processing || this._queue.length === 0) {
return;
}
this.processing = true;
this.head().execute(this._handler.bind(this));
},
_shift: function () {
this.processing = false;
this._queue.shift();
this.execute();
},
push: function (item) {
item.onPush();
this._queue.push(item);
this.execute();
},
empty: function () {
this._queue = [];
}
};
Relife.getMessagesCommand = function (data) {
this.url = '/chat';
this.httpMethod = 'GET';
this.dataType = 'json';
this.sendData = data || {};
this.onPush = function () {};
this.execute = function (callback) {
var self = this;
$.ajax({
url: self.url,
data: self.sendData,
dataType: self.dataType,
success: function (data, status) {
callback(true, data, status);
},
error: function (xhr, textstatus, error) {
callback(false, textstatus, error);
},
type: self.httpMethod
});
};
this.onSuccess = function (data) {
var msg,
output,
_remove_excess_lines,
_handle_background_tab_activity,
_do_output,
_handle_global,
_do_private_output,
_handle_private,
_handle_message;
_remove_excess_lines = function (output) {
var kids = output.children('div'),
i = kids.length - Relife.max_chat_lines;
$(kids.slice(0, i)).remove();
};
_handle_background_tab_activity = function (search_text) {
var tab = $('#chat-output ul li:contains("' + search_text + '")');
if (!tab.is('.ui-tabs-selected')) {
tab.addClass('background-chat-activity');
}
};
_do_output = function (msg, klass, chan) {
var output = $('#' + chan + '-output');
output.append('<div class="' + klass + '-chat-message">' + msg + '</div>');
Relife.scroll_output(output);
_remove_excess_lines(output);
_handle_background_tab_activity(chan);
};
_handle_global = function (item) {
var msg = 'Global Notice: ' + item.text;
_do_output(msg, 'global', 'global');
};
_do_private_output = function (msg, sender, recip, output_owner, type) {
var output = $('#private-' + output_owner + '-output');
output.append('<div class="' + type + '-chat-message">' + msg + '</div>');
Relife.scroll_output(output);
_remove_excess_lines(output);
_handle_background_tab_activity("[" + output_owner + "]");
};
_handle_private = function (item) {
var snd = item.sender,
output_owner = (snd === $('#chat-output').attr('data-username') ? item.recipient : snd),
output = $('#private-' + output_owner + '-output'),
msg;
if (!output.length) {
$('#chat-pane').fn('newPrivateTab', output_owner, false);
}
snd = (item.type === "private" ?
"<span class='chat-speaker'>" + snd + ":" :
"<span class='chat-actor'>" + snd) +
"</span> ";
msg = snd + item.text;
_do_private_output(msg, snd, item.recipient, output_owner, item.type);
};
_handle_message = function (item) {
switch (item.type) {
case "global":
_handle_global(item);
return;
case "private":
case "privateaction":
_handle_private(item);
return;
case "action":
msg = "<span class='chat-actor'>" + item.sender + "</span> " + item.text;
break;
case "notice":
msg = "<span class='chat-notice'>" + item.text + "</span>";
item.channel = Relife.current_channel;
break;
default:
msg = "<span class='chat-speaker'>" + item.sender + ":</span> " + item.text;
}
output = $('#' + item.channel + '-output');
if (!output.length) {
$('#chat-pane').fn('newTab', item.channel);
output = $('#' + item.channel + '-output');
if (!output.length) {
return;
}
}
_do_output(msg, item.type, item.channel);
};
try {
$('#chat-pane .drag-handle').fn('ok');
}
catch (err) {
/*
* do nothing. this only happens if the chat poll fires while
* the chat pane is being resized or dragged.
*/
}
$.map(data, _handle_message);
};
this.onFail = function (xhr, textStatus, error) {
$('#chat-pane .drag-handle').fn('error');
};
};
Relife.sendMessageCommand = function (data) {
this.url = '/chat';
this.httpMethod = 'POST';
this.dataType = 'json';
this.sendData = data;
this.onPush = function () {
if (/^\/(?:part|leave)\s*$/.test(this.sendData.msg) && Relife.current_channel) {
this.sendData.msg += " " + Relife.current_channel;
}
};
this.execute = function (callback) {
var self = this;
$.ajax({
url: self.url,
data: self.sendData,
dataType: self.dataType,
success: function (data, status) {
callback(true, data, status);
},
error: function (xhr, textstatus, error) {
callback(false, textstatus, error);
},
type: self.httpMethod
});
};
this.onSuccess = function (data) {
if (data) {
if (data.evaluate && typeof data.evaluate === "string") {
eval(data.evaluate);
}
if (data.error) {
Relife.chatError(data.error);
}
}
Relife.resetChatInterval();
$('#chat-pane').fn('getMessages', data);
};
this.onFail = function () {};
};
Relife.startChatInterval = function () {
Relife.chat_interval = setInterval(function () {
$('#chat-pane').fn('getMessages');
}, 5000);
};
Relife.stopChat = function () {
clearInterval(Relife.chat_interval);
};
Relife.resetChatInterval = function () {
Relife.stopChat();
Relife.startChatInterval();
};
Relife.chatError = function (msg) {
var output = $('.output-box:visible');
output.append("<div class='chat-error'>" + msg + "</div>");
Relife.scroll_output(output);
};
Relife.registerNonLiveFunctions(function () {
$('#chat-pane').fn({
"getMessages": function () {
Relife.ajaxQueue.push(new Relife.getMessagesCommand());
},
"sendMessage": function (msg, target) {
if (msg.length > 0) {
if (target.toLowerCase() === 'global' && !/^\//.test(msg)) {
return Relife.chatError('The global channel is for global notices from the administration.');
}
Relife.ajaxQueue.push(new Relife.sendMessageCommand({target: target, msg: msg}));
}
},
"_makeTab": function (destination, clk, my_private) {
clk = (clk === "undefined" ? true : clk);
var output = $('#chat-output'),
dest,
priv,
header;
if (!output.tabs) {
return;
}
dest = my_private ? "[" + destination + "]" : destination;
priv = my_private ? "private-" : "";
header = output.find('ul li a:contains("' + dest + '")');
if (header.length) {
header.click();
return;
}
output.tabs("add", "#" + priv + destination + "-output", dest);
if (clk) {
output.find('li a:contains("' + dest + '")').click();
}
},
"newPrivateTab": function (dest, clk) {
$(this).fn('_makeTab', dest, clk, true);
},
"newTab": function (dest, clk) {
$(this).fn('_makeTab', dest, clk, false);
},
"closeTab": function (lb) {
var output = $('#chat-output'),
output_ul = output.find('ul'),
to_remove = output_ul.children('li:contains("' + lb + '")'),
output_to_remove = $(to_remove.children('a').attr('href'));
to_remove.remove();
output_to_remove.remove();
// currently ui.tabs remove method does not know about the new indices
// when tabs are reordered via sortable.
// var idx = output_ul.children('li').index(to_remove);
// output.tabs("remove", idx);
Relife.current_channel = 'global';
output.tabs("select", 0);
},
"getChannels": function () {
var self = $(this);
$.getJSON('/chat/channel_list', function (data, status) {
if (data && data.channels) {
$.each(data.channels, function (index, item) {
if (item && item.listened) {
self.fn('newTab', item.name, false);
}
});
}
});
}
});
$('#chat-pane').fn('getChannels');
Relife.startChatInterval();
});
$(function () {
$('.close-tab').live('click', function (event) {
var sib_a = $(this).prevAll('a[href$="output"]'),
lb = sib_a.text();
if (!/^#private-[\w_\-]+-output$/.test(sib_a.attr("href"))) {
$('#chat-pane').fn('sendMessage', '/part ' + lb, Relife.current_channel);
} else {
$('#chat-pane').fn('closeTab', lb);
}
event.preventDefault();
});
$('#chat-input input').keydown(function (event) {
var self = $(this),
val,
match;
if (event.keyCode === 13) {
val = $.trim(self.val());
self.val("");
match = /^\[(\w+)\]$/.exec(Relife.current_channel);
if (match) {
if (!/^\//.test(val)) {
val = "/msg " + match[1] + ' ' + val;
} else if (/^\/me\b/.test(val)) {
val = val.replace(/^\/me\b/, "");
val = "/msgme " + match[1] + ' ' + val;
}
}
self.parents('#chat-pane').fn('sendMessage', val, Relife.current_channel);
}
});
$('#chat-output').tabs({
panelTemplate: '<div class="output-box"></div>',
show: function (event, ui) {
Relife.current_channel = $(ui.tab).find('span').text();
ui.panel.scrollTop = ui.panel.scrollHeight;
},
select: function (event, ui) {
$(ui.tab).parents('li').removeClass("background-chat-activity");
$('#chat-input input').focus();
},
tabTemplate: '<li><a href="#{href}"><span>#{label}</span></a><span class="close-tab">&nbsp;&nbsp;</span></li>'
}).find('.ui-tabs-nav').sortable({axis: 'x'});
});
describe "Ajax Queue"
describe "Relife.ajaxQueue"
before
foo = {
execute : function(callback) {},
onPush : function() {},
onSuccess : function() {},
onFail : function() {}
};
bar = {
execute : function(callback) {},
onPush : function() {}
};
baz = {
execute : function(callback) {},
onPush : function() {}
};
queue = Relife.ajaxQueue;
end
before_each
queue.empty();
queue.processing = false;
pane = $('<div id="chat-pane"></div>');
output = $('<div id="chat-output" data-username="junglebook"></div>');
global_output = $('<div id="global-output" class="output-box"></div>');
af6c_output = $('<div id="af6c-output" class="output-box"></div>');
private_output = $('<div id="private-mubbles-output" class="output-box"></div>');
output.append(af6c_output).append(global_output).append(private_output);
pane.append(output);
$('#dom').append(pane);
Relife.runNonLiveFunctions();
end
describe "the queue"
describe "#empty"
it "empties the queue"
queue.push(foo);
queue.push(bar);
queue.push(baz);
queue.empty();
queue._queue.length.should.equal 0
end
end
describe "#push"
before_each
queue.stub('execute')
end
it "should push the passed item onto the back of the queue"
queue.push(foo);
queue._queue[queue._queue.length-1].should.equal foo
end
it "should call onPush on the passed item"
foo.should.receive('onPush')
queue.push(foo);
end
it "should call execute"
queue.should.receive('execute')
queue.push(foo)
end
end
describe "#head"
it "should return false if the queue is empty"
queue.head().should.be false
end
it "should return the first element in the queue if anything is on the queue"
queue.push(foo);
queue.push(bar);
queue.head().should.equal foo
end
end
describe "#execute"
it "calls execute on the head item, passing the callback"
queue._queue.push(foo);
foo.should.receive('execute').with_args(an_instance_of(Function))
queue.execute();
end
it "does not execute if the queue is already processing"
queue.processing = true;
queue._queue.push(foo);
foo.should.not.receive('execute')
queue.execute();
end
it "does not execute if the queue is empty"
queue.processing.should.be false
queue.execute();
queue.processing.should.be false
end
it "sets the queue's processing flag to true"
queue.push(foo);
queue.processing.should.be true
end
end
describe "#_handler"
before_each
queue._queue.push(foo);
end
describe "success"
it "calls the success handler of the head of the queue"
queue.head().should_receive('onSuccess')
queue._handler(true);
end
it "removes the first item from the queue"
queue._handler(true);
queue.head().should_not.equal foo
end
end
describe "failure"
it "calls the failure handler on the head of the queue"
queue.head().should_receive('onFail')
queue._handler(false);
end
it "removes the first item from the queue"
queue._handler(false);
queue.head().should_not.equal foo
end
end
end
describe "#_shift"
it "should set processing to false"
queue.push(foo);
queue.processing.should.be true
queue._shift();
queue.processing.should.be false
end
it "removes the head from the queue"
queue.push(foo);
queue.head().should.equal foo
queue._shift();
queue.head().should.not.equal foo
end
it "calls execute on the queue"
queue._queue.push(foo);
queue.should.receive('execute')
queue._shift();
end
end
end
describe "Relife.sendMessageCommand"
before_each
sendMessageCommand = Relife.sendMessageCommand;
data = {
msg: 'foobar',
target: 'af6c'
};
cmd = new sendMessageCommand(data);
end
describe "properties"
it "should have url, http method, data type, and data fields"
cmd.url.should.equal '/chat'
cmd.httpMethod.should.equal 'POST'
cmd.dataType.should.equal 'json'
cmd.sendData.should.equal data
end
end
describe "#onPush"
it "should append the current channel if /part is sent, there is a current channel, and no channel is given"
Relife.current_channel = 'af6c';
data = { msg: '/part' };
cmd = new sendMessageCommand(data);
cmd.onPush();
cmd.sendData.msg.should.equal '/part af6c'
end
end
describe "#execute"
before_each
queue.stub("_handler")
JSpec.XHR.returns();
end
it "should make an ajax request to '/chat'"
JSpec.XHR.url = 'asdf';
cmd.execute(queue._handler);
JSpec.XHR.url.should.equal '/chat'
end
describe "success"
it "should call passed callback handler, passing true to it"
queue.should.receive('_handler').with_args(true)
cmd.execute(queue._handler);
end
end
describe "failure"
it "should call the passed handler, passing false to it"
JSpec.XHR.returns(null, 'text/html', 401);
queue.should.receive('_handler').with_args(false);
cmd.execute(queue._handler);
end
end
end
describe "#onFail"
it "should exist"
-{ cmd.onFail() }.should.not.throw_error
end
end
describe "#onSuccess"
before
getMessages_called = false
Relife.stub('resetChatInterval')
data = { "evaluate" : "window.relife.eval_called = true;" };
end
before_each
getMessages_called = false;
eval_called = false;
pane.fn({
getMessages: function(data) {
getMessages_called = data;
}
});
end
it "should call fn.getMessages on the chat pane"
cmd.onSuccess(data);
getMessages_called.should.equal data
end
it "should reset the chat interval"
Relife.should.receive('resetChatInterval')
cmd.onSuccess();
end
it "should add the given text if there is an 'error' field"
data = { "error" : "houston, we have a problem." };
$(global_output, private_output).hide();
cmd.onSuccess(data);
af6c_output.text().should.match /houston, we have a problem/
end
it "should evaluate the response if it has an 'evaluate' field"
//cmd.onSuccess(data);
//window.relife.eval_called.should.be true
end
end
end
describe "Relife.getMessagesCommand"
before_each
cmd = new Relife.getMessagesCommand;
end
describe "properties"
it "should have url, http method, and data type fields"
cmd.url.should.equal '/chat'
cmd.httpMethod.should.equal 'GET'
cmd.dataType.should.equal 'json'
end
end
describe "#onPush"
it "should exist"
-{ cmd.onPush() }.should.not.throw_error
end
end
describe "#execute"
before
passed1 = passed2 = false;
queue.stub('_handler')
data = { "text": "foo", "channel": "af6c", "sender": "foobar" };
end
before_each
passed1 = passed2 = false
end
it "should make an ajax request to '/chat'"
JSpec.XHR.returns({ text: "foo", channel: "af6c", sender: "foobar" });
JSpec.XHR.url = 'asdf';
cmd.execute(queue._handler);
JSpec.XHR.url.should.equal '/chat'
end
describe "success"
it "should call the passed callback handler, passing true"
JSpec.XHR.returns(data);
queue.should.receive('_handler').with_args(true, data);
cmd.execute(queue._handler);
end
end
describe "failure"
it "should call the passed callback handler, passing false"
JSpec.XHR.returns(data, "text/html", 401);
queue.should.receive('_handler').with_args(false);
cmd.execute(queue._handler);
end
end
end
describe "#onFail"
it "should exist"
-{ cmd.onFail() }.should.not.throw_error
end
end
describe "#onSuccess"
it "scrolls the output div"
cmd.onSuccess({});
div = af6c_output.get(0);
div.scrollTop.should.equal div.scrollHeight
end
describe "global messages"
it "puts the messages into the global output div with the correct format"
data = [ { text: 'foobar', sender: 'pobody', type: 'global' } ];
cmd.onSuccess(data);
global_output.text().should.match /Global Notice: foobar/
end
end
describe "public actions"
it "puts the message into the channel output with the format name text"
data = [{ text:"foo", channel:"af6c", sender:"foobar", type:"action"}];
cmd.onSuccess(data);
af6c_output.text().should.match /^foobar foo$/
end
end
describe "private messages"
describe "send by someone else"
it "should put the message into the private-sender div with the correct format"
data = [{ text:"foobar", sender:"mubbles", type:"private", recipient:"junglebook" }];
cmd.onSuccess(data);
private_output.text().should.match /mubbles: foobar/
end
end
describe "sent by the player"
it "should put the message into the private-sender div with the correct format"
data = [{ text:"foobar", sender:"junglebook", type:"private", recipient:"mubbles" }];
cmd.onSuccess(data);
private_output.text().should.match /junglebook: foobar/
end
end
end
describe "private actions"
describe "send by someone else"
it "should put the message into the private-sender div with the correct format"
data = [{ text:"foobar", sender:"mubbles", type:"privateaction", recipient:"junglebook" }];
cmd.onSuccess(data);
private_output.text().should.match /mubbles foobar/
end
end
describe "sent by the player"
it "should put the message into the private-sender div with the correct format"
data = [{ text:"foobar", sender:"junglebook", type:"privateaction", recipient:"mubbles" }];
cmd.onSuccess(data);
private_output.text().should.match /junglebook foobar/
end
end
end
describe "when output does not exist"
before_each
newtab_called = false;
end
it "calls fn.newTab on the pane, passing the channel"
af6c_output.remove();
pane.fn({
newTab: function(dest) { newtab_called = dest; }
});
data = [{ text:"foo", channel:"af6c", sender:"foobar" }];
cmd.onSuccess(data);
newtab_called.should.equal data[0].channel
end
it "inserts the message into the output with the correct format"
data = [{ text: "foo", channel: "af6c", sender: "foobar" }];
cmd.onSuccess(data);
af6c_output.text().should.match /foobar: foo/
end
it "adds the 'background-chat-activity' class to any unselected tabs with new output"
end
it "removes the lines from the top of the output if they go beyond the maximum number"
data = [
{
text: 'AAA',
sender: 'monkey',
channel: 'af6c'
},
{
text: 'BBB',
sender: 'monkey',
channel: 'af6c'
}
];
Relife.max_chat_lines = 1;
cmd.onSuccess(data);
af6c_output.text().should.not.match /AAA/
af6c_output.text().should.match /BBB/
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment