Created
August 19, 2010 18:36
-
-
Save xxx/538566 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"> </span></li>' | |
}).find('.ui-tabs-nav').sortable({axis: 'x'}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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