Copy over all files from master
28
CHANGES.rst
Normal file
@ -0,0 +1,28 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
0.3 (unreleased)
|
||||
----------------
|
||||
|
||||
- Add vCard support [jcbrand]
|
||||
- Remember custom status messages upon reload. [jcbrand]
|
||||
- Remove jquery-ui dependency. [jcbrand]
|
||||
- Use backbone.localStorage to store the contacts roster, open chatboxes and
|
||||
chat messages. [jcbrand]
|
||||
- Fixed user status handling, which wasn't 100% according to the
|
||||
spec. [jcbrand]
|
||||
- Separate messages according to day in chats. [jcbrand]
|
||||
|
||||
|
||||
0.2 (2013-03-28)
|
||||
----------------
|
||||
|
||||
- Performance enhancements and general script cleanup [ichim-david]
|
||||
- Add "Connecting to chat..." info [alecghica]
|
||||
- Various smaller improvements and bugfixes [jcbrand]
|
||||
|
||||
|
||||
0.1 (unreleased)
|
||||
----------------
|
||||
|
||||
- Created [jcbrand]
|
22
CONTRIBUTING.rst
Normal file
@ -0,0 +1,22 @@
|
||||
===========================
|
||||
Contributing to Converse.js
|
||||
===========================
|
||||
|
||||
Contributions to Converse.js are very welcome. Please follow the usual github
|
||||
workflow. Create your own local fork of this repository, make your changes and
|
||||
then submit a pull request.
|
||||
|
||||
Before submitting a pull request
|
||||
================================
|
||||
|
||||
Add tests for your bugfix or feature
|
||||
------------------------------------
|
||||
|
||||
- Please try to add a test for any bug fixed or feature added. We use Jasmine
|
||||
for testing.
|
||||
|
||||
Check that the tests run
|
||||
------------------------
|
||||
|
||||
- Check that the Jasmine BDD tests complete sucessfully. Open test.html in your
|
||||
browser, and the tests will run automatically.
|
18
LICENSE_GPL.txt
Normal file
@ -0,0 +1,18 @@
|
||||
Converse.js
|
||||
A web-based XMPP instant messaging client.
|
||||
|
||||
Copyright (C) 2012 Jan-Carel Brand.
|
||||
http://opkode.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
25
LICENSE_MIT.txt
Normal file
@ -0,0 +1,25 @@
|
||||
Converse.js
|
||||
A web-based XMPP instant messaging client.
|
||||
|
||||
Copyright (C) 2012 Jan-Carel Brand.
|
||||
http://opkode.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
20
Libraries/app.build.js
Normal file
@ -0,0 +1,20 @@
|
||||
({
|
||||
appDir: "../",
|
||||
baseUrl: "scripts/",
|
||||
dir: "../../webapp-build",
|
||||
//Comment out the optimize line if you want
|
||||
//the code minified by UglifyJS
|
||||
optimize: "none",
|
||||
|
||||
paths: {
|
||||
"jquery": "empty:"
|
||||
},
|
||||
|
||||
modules: [
|
||||
//Optimize the application files. jQuery is not
|
||||
//included since it is already in require-jquery.js
|
||||
{
|
||||
name: "main"
|
||||
}
|
||||
]
|
||||
})
|
1571
Libraries/backbone.js
Normal file
192
Libraries/backbone.localStorage.js
Normal file
@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Backbone localStorage Adapter
|
||||
* Version 1.1.0
|
||||
*
|
||||
* https://github.com/jeromegn/Backbone.localStorage
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(["underscore","backbone"], function(_, Backbone) {
|
||||
// Use global variables if the locals is undefined.
|
||||
return factory(_ || root._, Backbone || root.Backbone);
|
||||
});
|
||||
} else {
|
||||
// RequireJS isn't being used. Assume underscore and backbone is loaded in <script> tags
|
||||
factory(_, Backbone);
|
||||
}
|
||||
}(this, function(_, Backbone) {
|
||||
// A simple module to replace `Backbone.sync` with *localStorage*-based
|
||||
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
|
||||
// as that.
|
||||
|
||||
// Hold reference to Underscore.js and Backbone.js in the closure in order
|
||||
// to make things work even if they are removed from the global namespace
|
||||
|
||||
// Generate four random hex digits.
|
||||
function S4() {
|
||||
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
|
||||
};
|
||||
|
||||
// Generate a pseudo-GUID by concatenating random hexadecimal.
|
||||
function guid() {
|
||||
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
|
||||
};
|
||||
|
||||
// Our Store is represented by a single JS object in *localStorage*. Create it
|
||||
// with a meaningful name, like the name you'd give a table.
|
||||
// window.Store is deprectated, use Backbone.LocalStorage instead
|
||||
Backbone.LocalStorage = window.Store = function(name) {
|
||||
this.name = name;
|
||||
var store = this.localStorage().getItem(this.name);
|
||||
this.records = (store && store.split(",")) || [];
|
||||
};
|
||||
|
||||
_.extend(Backbone.LocalStorage.prototype, {
|
||||
|
||||
// Save the current state of the **Store** to *localStorage*.
|
||||
save: function() {
|
||||
this.localStorage().setItem(this.name, this.records.join(","));
|
||||
},
|
||||
|
||||
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
|
||||
// have an id of it's own.
|
||||
create: function(model) {
|
||||
if (!model.id) {
|
||||
model.id = guid();
|
||||
model.set(model.idAttribute, model.id);
|
||||
}
|
||||
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
|
||||
this.records.push(model.id.toString());
|
||||
this.save();
|
||||
return this.find(model);
|
||||
},
|
||||
|
||||
// Update a model by replacing its copy in `this.data`.
|
||||
update: function(model) {
|
||||
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
|
||||
if (!_.include(this.records, model.id.toString()))
|
||||
this.records.push(model.id.toString()); this.save();
|
||||
return this.find(model);
|
||||
},
|
||||
|
||||
// Retrieve a model from `this.data` by id.
|
||||
find: function(model) {
|
||||
return this.jsonData(this.localStorage().getItem(this.name+"-"+model.id));
|
||||
},
|
||||
|
||||
// Return the array of all models currently in storage.
|
||||
findAll: function() {
|
||||
return _(this.records).chain()
|
||||
.map(function(id){
|
||||
return this.jsonData(this.localStorage().getItem(this.name+"-"+id));
|
||||
}, this)
|
||||
.compact()
|
||||
.value();
|
||||
},
|
||||
|
||||
// Delete a model from `this.data`, returning it.
|
||||
destroy: function(model) {
|
||||
if (model.isNew())
|
||||
return false
|
||||
this.localStorage().removeItem(this.name+"-"+model.id);
|
||||
this.records = _.reject(this.records, function(id){
|
||||
return id === model.id.toString();
|
||||
});
|
||||
this.save();
|
||||
return model;
|
||||
},
|
||||
|
||||
localStorage: function() {
|
||||
return localStorage;
|
||||
},
|
||||
|
||||
// fix for "illegal access" error on Android when JSON.parse is passed null
|
||||
jsonData: function (data) {
|
||||
return data && JSON.parse(data);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// localSync delegate to the model or collection's
|
||||
// *localStorage* property, which should be an instance of `Store`.
|
||||
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
|
||||
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
|
||||
var store = model.localStorage || model.collection.localStorage;
|
||||
|
||||
var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
|
||||
|
||||
try {
|
||||
|
||||
switch (method) {
|
||||
case "read":
|
||||
resp = model.id != undefined ? store.find(model) : store.findAll();
|
||||
break;
|
||||
case "create":
|
||||
resp = store.create(model);
|
||||
break;
|
||||
case "update":
|
||||
resp = store.update(model);
|
||||
break;
|
||||
case "delete":
|
||||
resp = store.destroy(model);
|
||||
break;
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0)
|
||||
errorMessage = "Private browsing is unsupported";
|
||||
else
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
if (resp) {
|
||||
if (options && options.success)
|
||||
if (Backbone.VERSION === "0.9.10") {
|
||||
options.success(model, resp, options);
|
||||
} else {
|
||||
options.success(resp);
|
||||
}
|
||||
if (syncDfd)
|
||||
syncDfd.resolve(resp);
|
||||
|
||||
} else {
|
||||
errorMessage = errorMessage ? errorMessage
|
||||
: "Record Not Found";
|
||||
|
||||
if (options && options.error)
|
||||
if (Backbone.VERSION === "0.9.10") {
|
||||
options.error(model, errorMessage, options);
|
||||
} else {
|
||||
options.error(errorMessage);
|
||||
}
|
||||
|
||||
if (syncDfd)
|
||||
syncDfd.reject(errorMessage);
|
||||
}
|
||||
|
||||
// add compatibility with $.ajax
|
||||
// always execute callback for success and error
|
||||
if (options && options.complete) options.complete(resp);
|
||||
|
||||
return syncDfd && syncDfd.promise();
|
||||
};
|
||||
|
||||
Backbone.ajaxSync = Backbone.sync;
|
||||
|
||||
Backbone.getSyncMethod = function(model) {
|
||||
if(model.localStorage || (model.collection && model.collection.localStorage)) {
|
||||
return Backbone.localSync;
|
||||
}
|
||||
|
||||
return Backbone.ajaxSync;
|
||||
};
|
||||
|
||||
// Override 'Backbone.sync' to default to localSync,
|
||||
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
|
||||
Backbone.sync = function(method, model, options) {
|
||||
return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
|
||||
};
|
||||
|
||||
return Backbone.LocalStorage;
|
||||
}));
|
251
Libraries/jarnxmpp.core.handlers.js
Normal file
@ -0,0 +1,251 @@
|
||||
/*global $:false, document:false, window:false, portal_url:false,
|
||||
$msg:false, Strophe:false, setTimeout:false, navigator:false, jarn:false, google:false, jarnxmpp:false, jQuery:false, sessionStorage:false, $iq:false, $pres:false, Image:false, */
|
||||
|
||||
(function (jarnxmpp, $, portal_url) {
|
||||
|
||||
portal_url = portal_url || '';
|
||||
|
||||
jarnxmpp.Storage = {
|
||||
storage: null,
|
||||
init: function () {
|
||||
try {
|
||||
if ('sessionStorage' in window && window.sessionStorage !== null && JSON in window && window.JSON !== null) {
|
||||
jarnxmpp.Storage.storage = sessionStorage;
|
||||
if (!('_user_info' in jarnxmpp.Storage.storage)) {
|
||||
jarnxmpp.Storage.set('_user_info', {});
|
||||
}
|
||||
if (!('_vCards' in jarnxmpp.Storage.storage)) {
|
||||
jarnxmpp.Storage.set('_vCards', {});
|
||||
}
|
||||
if (!('_subscriptions' in jarnxmpp.Storage.storage)) {
|
||||
jarnxmpp.Storage.set('_subscriptions', null);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
get: function (key) {
|
||||
if (key in sessionStorage) {
|
||||
return JSON.parse(sessionStorage[key]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
set: function (key, value) {
|
||||
sessionStorage[key] = JSON.stringify(value);
|
||||
},
|
||||
|
||||
xmppGet: function (key, callback) {
|
||||
var stanza = $iq({type: 'get'})
|
||||
.c('query', {xmlns: 'jabber:iq:private'})
|
||||
.c('jarnxmpp', {xmlns: 'http://jarn.com/ns/jarnxmpp:prefs:' + key})
|
||||
.tree();
|
||||
jarnxmpp.connection.sendIQ(stanza, function success(result) {
|
||||
callback($('jarnxmpp ' + 'value', result).first().text());
|
||||
});
|
||||
},
|
||||
|
||||
xmppSet: function (key, value) {
|
||||
var stanza = $iq({type: 'set'})
|
||||
.c('query', {xmlns: 'jabber:iq:private'})
|
||||
.c('jarnxmpp', {xmlns: 'http://jarn.com/ns/jarnxmpp:prefs:' + key})
|
||||
.c('value', value)
|
||||
.tree();
|
||||
jarnxmpp.connection.sendIQ(stanza);
|
||||
}
|
||||
};
|
||||
|
||||
jarnxmpp.Storage.init();
|
||||
|
||||
jarnxmpp.Presence = {
|
||||
online: {},
|
||||
_user_info: {},
|
||||
|
||||
onlineCount: function () {
|
||||
var me = Strophe.getNodeFromJid(jarnxmpp.connection.jid),
|
||||
counter = 0,
|
||||
user;
|
||||
for (user in jarnxmpp.Presence.online) {
|
||||
if ((jarnxmpp.Presence.online.hasOwnProperty(user)) && user !== me) {
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
return counter;
|
||||
},
|
||||
|
||||
getUserInfo: function (user_id, callback) {
|
||||
// User info on browsers without storage
|
||||
if (jarnxmpp.Storage.storage === null) {
|
||||
if (user_id in jarnxmpp.Presence._user_info) {
|
||||
callback(jarnxmpp.Presence._user_info[user_id]);
|
||||
} else {
|
||||
$.getJSON(portal_url + "/xmpp-userinfo?user_id=" + user_id, function (data) {
|
||||
jarnxmpp.Presence._user_info[user_id] = data;
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var _user_info = jarnxmpp.Storage.get('_user_info');
|
||||
if (user_id in _user_info) {
|
||||
callback(_user_info[user_id]);
|
||||
} else {
|
||||
$.getJSON(portal_url + "/xmpp-userinfo?user_id=" + user_id, function (data) {
|
||||
_user_info[user_id] = data;
|
||||
jarnxmpp.Storage.set('_user_info', _user_info);
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
jarnxmpp.vCard = {
|
||||
|
||||
_vCards: {},
|
||||
|
||||
_getVCard: function (jid, callback) {
|
||||
var stanza =
|
||||
$iq({type: 'get', to: jid})
|
||||
.c('vCard', {xmlns: 'vcard-temp'}).tree();
|
||||
jarnxmpp.connection.sendIQ(stanza, function (data) {
|
||||
var result = {};
|
||||
$('vCard[xmlns="vcard-temp"]', data).children().each(function (idx, element) {
|
||||
result[element.nodeName] = element.textContent;
|
||||
});
|
||||
if (typeof (callback) !== 'undefined') {
|
||||
callback(result);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getVCard: function (jid, callback) {
|
||||
jid = Strophe.getBareJidFromJid(jid);
|
||||
if (jarnxmpp.Storage.storage === null) {
|
||||
if (jid in jarnxmpp.vCard._vCards) {
|
||||
callback(jarnxmpp.vCard._vCards[jid]);
|
||||
} else {
|
||||
jarnxmpp.vCard._getVCard(jid, function (result) {
|
||||
jarnxmpp.vCard._vCards[jid] = result;
|
||||
callback(result);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var _vCards = jarnxmpp.Storage.get('_vCards');
|
||||
if (jid in _vCards) {
|
||||
callback(_vCards[jid]);
|
||||
} else {
|
||||
jarnxmpp.vCard._getVCard(jid, function (result) {
|
||||
_vCards[jid] = result;
|
||||
jarnxmpp.Storage.set('_vCards', _vCards);
|
||||
callback(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setVCard: function (params, photoUrl) {
|
||||
var key,
|
||||
vCard = Strophe.xmlElement('vCard', [['xmlns', 'vcard-temp'], ['version', '2.0']]);
|
||||
for (key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
vCard.appendChild(Strophe.xmlElement(key, [], params[key]));
|
||||
}
|
||||
}
|
||||
var send = function () {
|
||||
var stanza = $iq({type: 'set'}).cnode(vCard).tree();
|
||||
jarnxmpp.connection.sendIQ(stanza);
|
||||
};
|
||||
if (typeof (photoUrl) === 'undefined') {
|
||||
send();
|
||||
} else {
|
||||
jarnxmpp.vCard.getBase64Image(photoUrl, function (base64img) {
|
||||
base64img = base64img.replace(/^data:image\/png;base64,/, "");
|
||||
var photo = Strophe.xmlElement('PHOTO');
|
||||
photo.appendChild(Strophe.xmlElement('TYPE', [], 'image/png'));
|
||||
photo.appendChild(Strophe.xmlElement('BINVAL', [], base64img));
|
||||
vCard.appendChild(photo);
|
||||
send();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getBase64Image: function (url, callback) {
|
||||
// Create the element, then draw it on a canvas to get the base64 data.
|
||||
var img = new Image();
|
||||
$(img).load(function () {
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
callback(canvas.toDataURL('image/png'));
|
||||
}).attr('src', url);
|
||||
}
|
||||
};
|
||||
|
||||
jarnxmpp.onConnect = function (status) {
|
||||
if ((status === Strophe.Status.ATTACHED) || (status === Strophe.Status.CONNECTED)) {
|
||||
$(window).bind('beforeunload', function () {
|
||||
$(document).trigger('jarnxmpp.disconnecting');
|
||||
var presence = $pres({type: 'unavailable'});
|
||||
jarnxmpp.connection.send(presence);
|
||||
jarnxmpp.connection.disconnect();
|
||||
jarnxmpp.connection.flush();
|
||||
});
|
||||
$(document).trigger('jarnxmpp.connected');
|
||||
} else if (status === Strophe.Status.DISCONNECTED) {
|
||||
$(document).trigger('jarnxmpp.disconnected');
|
||||
}
|
||||
};
|
||||
|
||||
jarnxmpp.rawInput = function (data) {
|
||||
var event = jQuery.Event('jarnxmpp.dataReceived');
|
||||
event.text = data;
|
||||
$(document).trigger(event);
|
||||
};
|
||||
|
||||
jarnxmpp.rawOutput = function (data) {
|
||||
var event = jQuery.Event('jarnxmpp.dataSent');
|
||||
event.text = data;
|
||||
$(document).trigger(event);
|
||||
};
|
||||
|
||||
$(document).bind('jarnxmpp.connected', function () {
|
||||
// Logging
|
||||
jarnxmpp.connection.rawInput = jarnxmpp.rawInput;
|
||||
jarnxmpp.connection.rawOutput = jarnxmpp.rawOutput;
|
||||
});
|
||||
|
||||
$(document).bind('jarnxmpp.disconnecting', function () {
|
||||
if (jarnxmpp.Storage.storage !== null) {
|
||||
jarnxmpp.Storage.set('online-count', jarnxmpp.Presence.onlineCount());
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
var resource = jarnxmpp.Storage.get('xmppresource');
|
||||
if (resource) {
|
||||
data = {'resource': resource};
|
||||
} else {
|
||||
data = {};
|
||||
}
|
||||
$.ajax({
|
||||
'url':portal_url + '/@@xmpp-loader',
|
||||
'dataType': 'json',
|
||||
'data': data,
|
||||
'success': function (data) {
|
||||
if (!(('rid' in data) && ('sid' in data) && ('BOSH_SERVICE' in data))) {
|
||||
return;
|
||||
}
|
||||
if (!resource) {
|
||||
jarnxmpp.Storage.set('xmppresource', Strophe.getResourceFromJid(data.jid));
|
||||
}
|
||||
jarnxmpp.BOSH_SERVICE = data.BOSH_SERVICE;
|
||||
jarnxmpp.jid = data.jid;
|
||||
jarnxmpp.connection = new Strophe.Connection(jarnxmpp.BOSH_SERVICE);
|
||||
jarnxmpp.connection.attach(jarnxmpp.jid, data.sid, data.rid, jarnxmpp.onConnect);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})(window.jarnxmpp = window.jarnxmpp || {}, jQuery, portal_url);
|
20
Libraries/jasmine-1.3.1/MIT.LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2008-2011 Pivotal Labs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
681
Libraries/jasmine-1.3.1/jasmine-html.js
Normal file
@ -0,0 +1,681 @@
|
||||
jasmine.HtmlReporterHelpers = {};
|
||||
|
||||
jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) {
|
||||
var el = document.createElement(type);
|
||||
|
||||
for (var i = 2; i < arguments.length; i++) {
|
||||
var child = arguments[i];
|
||||
|
||||
if (typeof child === 'string') {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else {
|
||||
if (child) {
|
||||
el.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var attr in attrs) {
|
||||
if (attr == "className") {
|
||||
el[attr] = attrs[attr];
|
||||
} else {
|
||||
el.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.getSpecStatus = function(child) {
|
||||
var results = child.results();
|
||||
var status = results.passed() ? 'passed' : 'failed';
|
||||
if (results.skipped) {
|
||||
status = 'skipped';
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) {
|
||||
var parentDiv = this.dom.summary;
|
||||
var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite';
|
||||
var parent = child[parentSuite];
|
||||
|
||||
if (parent) {
|
||||
if (typeof this.views.suites[parent.id] == 'undefined') {
|
||||
this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views);
|
||||
}
|
||||
parentDiv = this.views.suites[parent.id].element;
|
||||
}
|
||||
|
||||
parentDiv.appendChild(childElement);
|
||||
};
|
||||
|
||||
|
||||
jasmine.HtmlReporterHelpers.addHelpers = function(ctor) {
|
||||
for(var fn in jasmine.HtmlReporterHelpers) {
|
||||
ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn];
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter = function(_doc) {
|
||||
var self = this;
|
||||
var doc = _doc || window.document;
|
||||
|
||||
var reporterView;
|
||||
|
||||
var dom = {};
|
||||
|
||||
// Jasmine Reporter Public Interface
|
||||
self.logRunningSpecs = false;
|
||||
|
||||
self.reportRunnerStarting = function(runner) {
|
||||
var specs = runner.specs() || [];
|
||||
|
||||
if (specs.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
createReporterDom(runner.env.versionString());
|
||||
doc.body.appendChild(dom.reporter);
|
||||
setExceptionHandling();
|
||||
|
||||
reporterView = new jasmine.HtmlReporter.ReporterView(dom);
|
||||
reporterView.addSpecs(specs, self.specFilter);
|
||||
};
|
||||
|
||||
self.reportRunnerResults = function(runner) {
|
||||
reporterView && reporterView.complete();
|
||||
};
|
||||
|
||||
self.reportSuiteResults = function(suite) {
|
||||
reporterView.suiteComplete(suite);
|
||||
};
|
||||
|
||||
self.reportSpecStarting = function(spec) {
|
||||
if (self.logRunningSpecs) {
|
||||
self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
|
||||
}
|
||||
};
|
||||
|
||||
self.reportSpecResults = function(spec) {
|
||||
reporterView.specComplete(spec);
|
||||
};
|
||||
|
||||
self.log = function() {
|
||||
var console = jasmine.getGlobal().console;
|
||||
if (console && console.log) {
|
||||
if (console.log.apply) {
|
||||
console.log.apply(console, arguments);
|
||||
} else {
|
||||
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.specFilter = function(spec) {
|
||||
if (!focusedSpecName()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return spec.getFullName().indexOf(focusedSpecName()) === 0;
|
||||
};
|
||||
|
||||
return self;
|
||||
|
||||
function focusedSpecName() {
|
||||
var specName;
|
||||
|
||||
(function memoizeFocusedSpec() {
|
||||
if (specName) {
|
||||
return;
|
||||
}
|
||||
|
||||
var paramMap = [];
|
||||
var params = jasmine.HtmlReporter.parameters(doc);
|
||||
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var p = params[i].split('=');
|
||||
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
|
||||
}
|
||||
|
||||
specName = paramMap.spec;
|
||||
})();
|
||||
|
||||
return specName;
|
||||
}
|
||||
|
||||
function createReporterDom(version) {
|
||||
dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' },
|
||||
dom.banner = self.createDom('div', { className: 'banner' },
|
||||
self.createDom('span', { className: 'title' }, "Jasmine "),
|
||||
self.createDom('span', { className: 'version' }, version)),
|
||||
|
||||
dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}),
|
||||
dom.alert = self.createDom('div', {className: 'alert'},
|
||||
self.createDom('span', { className: 'exceptions' },
|
||||
self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'),
|
||||
self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))),
|
||||
dom.results = self.createDom('div', {className: 'results'},
|
||||
dom.summary = self.createDom('div', { className: 'summary' }),
|
||||
dom.details = self.createDom('div', { id: 'details' }))
|
||||
);
|
||||
}
|
||||
|
||||
function noTryCatch() {
|
||||
return window.location.search.match(/catch=false/);
|
||||
}
|
||||
|
||||
function searchWithCatch() {
|
||||
var params = jasmine.HtmlReporter.parameters(window.document);
|
||||
var removed = false;
|
||||
var i = 0;
|
||||
|
||||
while (!removed && i < params.length) {
|
||||
if (params[i].match(/catch=/)) {
|
||||
params.splice(i, 1);
|
||||
removed = true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (jasmine.CATCH_EXCEPTIONS) {
|
||||
params.push("catch=false");
|
||||
}
|
||||
|
||||
return params.join("&");
|
||||
}
|
||||
|
||||
function setExceptionHandling() {
|
||||
var chxCatch = document.getElementById('no_try_catch');
|
||||
|
||||
if (noTryCatch()) {
|
||||
chxCatch.setAttribute('checked', true);
|
||||
jasmine.CATCH_EXCEPTIONS = false;
|
||||
}
|
||||
chxCatch.onclick = function() {
|
||||
window.location.search = searchWithCatch();
|
||||
};
|
||||
}
|
||||
};
|
||||
jasmine.HtmlReporter.parameters = function(doc) {
|
||||
var paramStr = doc.location.search.substring(1);
|
||||
var params = [];
|
||||
|
||||
if (paramStr.length > 0) {
|
||||
params = paramStr.split('&');
|
||||
}
|
||||
return params;
|
||||
}
|
||||
jasmine.HtmlReporter.sectionLink = function(sectionName) {
|
||||
var link = '?';
|
||||
var params = [];
|
||||
|
||||
if (sectionName) {
|
||||
params.push('spec=' + encodeURIComponent(sectionName));
|
||||
}
|
||||
if (!jasmine.CATCH_EXCEPTIONS) {
|
||||
params.push("catch=false");
|
||||
}
|
||||
if (params.length > 0) {
|
||||
link += params.join("&");
|
||||
}
|
||||
|
||||
return link;
|
||||
};
|
||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);
|
||||
jasmine.HtmlReporter.ReporterView = function(dom) {
|
||||
this.startedAt = new Date();
|
||||
this.runningSpecCount = 0;
|
||||
this.completeSpecCount = 0;
|
||||
this.passedCount = 0;
|
||||
this.failedCount = 0;
|
||||
this.skippedCount = 0;
|
||||
|
||||
this.createResultsMenu = function() {
|
||||
this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'},
|
||||
this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'),
|
||||
' | ',
|
||||
this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing'));
|
||||
|
||||
this.summaryMenuItem.onclick = function() {
|
||||
dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, '');
|
||||
};
|
||||
|
||||
this.detailsMenuItem.onclick = function() {
|
||||
showDetails();
|
||||
};
|
||||
};
|
||||
|
||||
this.addSpecs = function(specs, specFilter) {
|
||||
this.totalSpecCount = specs.length;
|
||||
|
||||
this.views = {
|
||||
specs: {},
|
||||
suites: {}
|
||||
};
|
||||
|
||||
for (var i = 0; i < specs.length; i++) {
|
||||
var spec = specs[i];
|
||||
this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views);
|
||||
if (specFilter(spec)) {
|
||||
this.runningSpecCount++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.specComplete = function(spec) {
|
||||
this.completeSpecCount++;
|
||||
|
||||
if (isUndefined(this.views.specs[spec.id])) {
|
||||
this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom);
|
||||
}
|
||||
|
||||
var specView = this.views.specs[spec.id];
|
||||
|
||||
switch (specView.status()) {
|
||||
case 'passed':
|
||||
this.passedCount++;
|
||||
break;
|
||||
|
||||
case 'failed':
|
||||
this.failedCount++;
|
||||
break;
|
||||
|
||||
case 'skipped':
|
||||
this.skippedCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
specView.refresh();
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
this.suiteComplete = function(suite) {
|
||||
var suiteView = this.views.suites[suite.id];
|
||||
if (isUndefined(suiteView)) {
|
||||
return;
|
||||
}
|
||||
suiteView.refresh();
|
||||
};
|
||||
|
||||
this.refresh = function() {
|
||||
|
||||
if (isUndefined(this.resultsMenu)) {
|
||||
this.createResultsMenu();
|
||||
}
|
||||
|
||||
// currently running UI
|
||||
if (isUndefined(this.runningAlert)) {
|
||||
this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" });
|
||||
dom.alert.appendChild(this.runningAlert);
|
||||
}
|
||||
this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount);
|
||||
|
||||
// skipped specs UI
|
||||
if (isUndefined(this.skippedAlert)) {
|
||||
this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" });
|
||||
}
|
||||
|
||||
this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
|
||||
|
||||
if (this.skippedCount === 1 && isDefined(dom.alert)) {
|
||||
dom.alert.appendChild(this.skippedAlert);
|
||||
}
|
||||
|
||||
// passing specs UI
|
||||
if (isUndefined(this.passedAlert)) {
|
||||
this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" });
|
||||
}
|
||||
this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount);
|
||||
|
||||
// failing specs UI
|
||||
if (isUndefined(this.failedAlert)) {
|
||||
this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"});
|
||||
}
|
||||
this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount);
|
||||
|
||||
if (this.failedCount === 1 && isDefined(dom.alert)) {
|
||||
dom.alert.appendChild(this.failedAlert);
|
||||
dom.alert.appendChild(this.resultsMenu);
|
||||
}
|
||||
|
||||
// summary info
|
||||
this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount);
|
||||
this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing";
|
||||
};
|
||||
|
||||
this.complete = function() {
|
||||
dom.alert.removeChild(this.runningAlert);
|
||||
|
||||
this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
|
||||
|
||||
if (this.failedCount === 0) {
|
||||
dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount)));
|
||||
} else {
|
||||
showDetails();
|
||||
}
|
||||
|
||||
dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"));
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
function showDetails() {
|
||||
if (dom.reporter.className.search(/showDetails/) === -1) {
|
||||
dom.reporter.className += " showDetails";
|
||||
}
|
||||
}
|
||||
|
||||
function isUndefined(obj) {
|
||||
return typeof obj === 'undefined';
|
||||
}
|
||||
|
||||
function isDefined(obj) {
|
||||
return !isUndefined(obj);
|
||||
}
|
||||
|
||||
function specPluralizedFor(count) {
|
||||
var str = count + " spec";
|
||||
if (count > 1) {
|
||||
str += "s"
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView);
|
||||
|
||||
|
||||
jasmine.HtmlReporter.SpecView = function(spec, dom, views) {
|
||||
this.spec = spec;
|
||||
this.dom = dom;
|
||||
this.views = views;
|
||||
|
||||
this.symbol = this.createDom('li', { className: 'pending' });
|
||||
this.dom.symbolSummary.appendChild(this.symbol);
|
||||
|
||||
this.summary = this.createDom('div', { className: 'specSummary' },
|
||||
this.createDom('a', {
|
||||
className: 'description',
|
||||
href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()),
|
||||
title: this.spec.getFullName()
|
||||
}, this.spec.description)
|
||||
);
|
||||
|
||||
this.detail = this.createDom('div', { className: 'specDetail' },
|
||||
this.createDom('a', {
|
||||
className: 'description',
|
||||
href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
|
||||
title: this.spec.getFullName()
|
||||
}, this.spec.getFullName())
|
||||
);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SpecView.prototype.status = function() {
|
||||
return this.getSpecStatus(this.spec);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SpecView.prototype.refresh = function() {
|
||||
this.symbol.className = this.status();
|
||||
|
||||
switch (this.status()) {
|
||||
case 'skipped':
|
||||
break;
|
||||
|
||||
case 'passed':
|
||||
this.appendSummaryToSuiteDiv();
|
||||
break;
|
||||
|
||||
case 'failed':
|
||||
this.appendSummaryToSuiteDiv();
|
||||
this.appendFailureDetail();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() {
|
||||
this.summary.className += ' ' + this.status();
|
||||
this.appendToSummary(this.spec, this.summary);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() {
|
||||
this.detail.className += ' ' + this.status();
|
||||
|
||||
var resultItems = this.spec.results().getItems();
|
||||
var messagesDiv = this.createDom('div', { className: 'messages' });
|
||||
|
||||
for (var i = 0; i < resultItems.length; i++) {
|
||||
var result = resultItems[i];
|
||||
|
||||
if (result.type == 'log') {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
|
||||
} else if (result.type == 'expect' && result.passed && !result.passed()) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
|
||||
|
||||
if (result.trace.stack) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messagesDiv.childNodes.length > 0) {
|
||||
this.detail.appendChild(messagesDiv);
|
||||
this.dom.details.appendChild(this.detail);
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) {
|
||||
this.suite = suite;
|
||||
this.dom = dom;
|
||||
this.views = views;
|
||||
|
||||
this.element = this.createDom('div', { className: 'suite' },
|
||||
this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description)
|
||||
);
|
||||
|
||||
this.appendToSummary(this.suite, this.element);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SuiteView.prototype.status = function() {
|
||||
return this.getSpecStatus(this.suite);
|
||||
};
|
||||
|
||||
jasmine.HtmlReporter.SuiteView.prototype.refresh = function() {
|
||||
this.element.className += " " + this.status();
|
||||
};
|
||||
|
||||
jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView);
|
||||
|
||||
/* @deprecated Use jasmine.HtmlReporter instead
|
||||
*/
|
||||
jasmine.TrivialReporter = function(doc) {
|
||||
this.document = doc || document;
|
||||
this.suiteDivs = {};
|
||||
this.logRunningSpecs = false;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
|
||||
var el = document.createElement(type);
|
||||
|
||||
for (var i = 2; i < arguments.length; i++) {
|
||||
var child = arguments[i];
|
||||
|
||||
if (typeof child === 'string') {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else {
|
||||
if (child) { el.appendChild(child); }
|
||||
}
|
||||
}
|
||||
|
||||
for (var attr in attrs) {
|
||||
if (attr == "className") {
|
||||
el[attr] = attrs[attr];
|
||||
} else {
|
||||
el.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
|
||||
var showPassed, showSkipped;
|
||||
|
||||
this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' },
|
||||
this.createDom('div', { className: 'banner' },
|
||||
this.createDom('div', { className: 'logo' },
|
||||
this.createDom('span', { className: 'title' }, "Jasmine"),
|
||||
this.createDom('span', { className: 'version' }, runner.env.versionString())),
|
||||
this.createDom('div', { className: 'options' },
|
||||
"Show ",
|
||||
showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
|
||||
this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
|
||||
showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
|
||||
this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
|
||||
)
|
||||
),
|
||||
|
||||
this.runnerDiv = this.createDom('div', { className: 'runner running' },
|
||||
this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
|
||||
this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
|
||||
this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
|
||||
);
|
||||
|
||||
this.document.body.appendChild(this.outerDiv);
|
||||
|
||||
var suites = runner.suites();
|
||||
for (var i = 0; i < suites.length; i++) {
|
||||
var suite = suites[i];
|
||||
var suiteDiv = this.createDom('div', { className: 'suite' },
|
||||
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
|
||||
this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
|
||||
this.suiteDivs[suite.id] = suiteDiv;
|
||||
var parentDiv = this.outerDiv;
|
||||
if (suite.parentSuite) {
|
||||
parentDiv = this.suiteDivs[suite.parentSuite.id];
|
||||
}
|
||||
parentDiv.appendChild(suiteDiv);
|
||||
}
|
||||
|
||||
this.startedAt = new Date();
|
||||
|
||||
var self = this;
|
||||
showPassed.onclick = function(evt) {
|
||||
if (showPassed.checked) {
|
||||
self.outerDiv.className += ' show-passed';
|
||||
} else {
|
||||
self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
|
||||
}
|
||||
};
|
||||
|
||||
showSkipped.onclick = function(evt) {
|
||||
if (showSkipped.checked) {
|
||||
self.outerDiv.className += ' show-skipped';
|
||||
} else {
|
||||
self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
|
||||
var results = runner.results();
|
||||
var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
|
||||
this.runnerDiv.setAttribute("class", className);
|
||||
//do it twice for IE
|
||||
this.runnerDiv.setAttribute("className", className);
|
||||
var specs = runner.specs();
|
||||
var specCount = 0;
|
||||
for (var i = 0; i < specs.length; i++) {
|
||||
if (this.specFilter(specs[i])) {
|
||||
specCount++;
|
||||
}
|
||||
}
|
||||
var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
|
||||
message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
|
||||
this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
|
||||
|
||||
this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
|
||||
var results = suite.results();
|
||||
var status = results.passed() ? 'passed' : 'failed';
|
||||
if (results.totalCount === 0) { // todo: change this to check results.skipped
|
||||
status = 'skipped';
|
||||
}
|
||||
this.suiteDivs[suite.id].className += " " + status;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
|
||||
if (this.logRunningSpecs) {
|
||||
this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
|
||||
var results = spec.results();
|
||||
var status = results.passed() ? 'passed' : 'failed';
|
||||
if (results.skipped) {
|
||||
status = 'skipped';
|
||||
}
|
||||
var specDiv = this.createDom('div', { className: 'spec ' + status },
|
||||
this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
|
||||
this.createDom('a', {
|
||||
className: 'description',
|
||||
href: '?spec=' + encodeURIComponent(spec.getFullName()),
|
||||
title: spec.getFullName()
|
||||
}, spec.description));
|
||||
|
||||
|
||||
var resultItems = results.getItems();
|
||||
var messagesDiv = this.createDom('div', { className: 'messages' });
|
||||
for (var i = 0; i < resultItems.length; i++) {
|
||||
var result = resultItems[i];
|
||||
|
||||
if (result.type == 'log') {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
|
||||
} else if (result.type == 'expect' && result.passed && !result.passed()) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
|
||||
|
||||
if (result.trace.stack) {
|
||||
messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messagesDiv.childNodes.length > 0) {
|
||||
specDiv.appendChild(messagesDiv);
|
||||
}
|
||||
|
||||
this.suiteDivs[spec.suite.id].appendChild(specDiv);
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.log = function() {
|
||||
var console = jasmine.getGlobal().console;
|
||||
if (console && console.log) {
|
||||
if (console.log.apply) {
|
||||
console.log.apply(console, arguments);
|
||||
} else {
|
||||
console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.getLocation = function() {
|
||||
return this.document.location;
|
||||
};
|
||||
|
||||
jasmine.TrivialReporter.prototype.specFilter = function(spec) {
|
||||
var paramMap = {};
|
||||
var params = this.getLocation().search.substring(1).split('&');
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var p = params[i].split('=');
|
||||
paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
|
||||
}
|
||||
|
||||
if (!paramMap.spec) {
|
||||
return true;
|
||||
}
|
||||
return spec.getFullName().indexOf(paramMap.spec) === 0;
|
||||
};
|
82
Libraries/jasmine-1.3.1/jasmine.css
Normal file
@ -0,0 +1,82 @@
|
||||
body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
|
||||
|
||||
#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
|
||||
#HTMLReporter a { text-decoration: none; }
|
||||
#HTMLReporter a:hover { text-decoration: underline; }
|
||||
#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; }
|
||||
#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; }
|
||||
#HTMLReporter #jasmine_content { position: fixed; right: 100%; }
|
||||
#HTMLReporter .version { color: #aaaaaa; }
|
||||
#HTMLReporter .banner { margin-top: 14px; }
|
||||
#HTMLReporter .duration { color: #aaaaaa; float: right; }
|
||||
#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; }
|
||||
#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; }
|
||||
#HTMLReporter .symbolSummary li.passed { font-size: 14px; }
|
||||
#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; }
|
||||
#HTMLReporter .symbolSummary li.failed { line-height: 9px; }
|
||||
#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
|
||||
#HTMLReporter .symbolSummary li.skipped { font-size: 14px; }
|
||||
#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; }
|
||||
#HTMLReporter .symbolSummary li.pending { line-height: 11px; }
|
||||
#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; }
|
||||
#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
|
||||
#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
|
||||
#HTMLReporter .runningAlert { background-color: #666666; }
|
||||
#HTMLReporter .skippedAlert { background-color: #aaaaaa; }
|
||||
#HTMLReporter .skippedAlert:first-child { background-color: #333333; }
|
||||
#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; }
|
||||
#HTMLReporter .passingAlert { background-color: #a6b779; }
|
||||
#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; }
|
||||
#HTMLReporter .failingAlert { background-color: #cf867e; }
|
||||
#HTMLReporter .failingAlert:first-child { background-color: #b03911; }
|
||||
#HTMLReporter .results { margin-top: 14px; }
|
||||
#HTMLReporter #details { display: none; }
|
||||
#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; }
|
||||
#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
|
||||
#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
|
||||
#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
|
||||
#HTMLReporter.showDetails .summary { display: none; }
|
||||
#HTMLReporter.showDetails #details { display: block; }
|
||||
#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
|
||||
#HTMLReporter .summary { margin-top: 14px; }
|
||||
#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; }
|
||||
#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; }
|
||||
#HTMLReporter .summary .specSummary.failed a { color: #b03911; }
|
||||
#HTMLReporter .description + .suite { margin-top: 0; }
|
||||
#HTMLReporter .suite { margin-top: 14px; }
|
||||
#HTMLReporter .suite a { color: #333333; }
|
||||
#HTMLReporter #details .specDetail { margin-bottom: 28px; }
|
||||
#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; }
|
||||
#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; }
|
||||
#HTMLReporter .resultMessage span.result { display: block; }
|
||||
#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
|
||||
|
||||
#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ }
|
||||
#TrivialReporter a:visited, #TrivialReporter a { color: #303; }
|
||||
#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; }
|
||||
#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; }
|
||||
#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; }
|
||||
#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; }
|
||||
#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; }
|
||||
#TrivialReporter .runner.running { background-color: yellow; }
|
||||
#TrivialReporter .options { text-align: right; font-size: .8em; }
|
||||
#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; }
|
||||
#TrivialReporter .suite .suite { margin: 5px; }
|
||||
#TrivialReporter .suite.passed { background-color: #dfd; }
|
||||
#TrivialReporter .suite.failed { background-color: #fdd; }
|
||||
#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; }
|
||||
#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; }
|
||||
#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; }
|
||||
#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; }
|
||||
#TrivialReporter .spec.skipped { background-color: #bbb; }
|
||||
#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; }
|
||||
#TrivialReporter .passed { background-color: #cfc; display: none; }
|
||||
#TrivialReporter .failed { background-color: #fbb; }
|
||||
#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; }
|
||||
#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; }
|
||||
#TrivialReporter .resultMessage .mismatch { color: black; }
|
||||
#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; }
|
||||
#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; }
|
||||
#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; }
|
||||
#TrivialReporter #jasmine_content { position: fixed; right: 100%; }
|
||||
#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; }
|
2600
Libraries/jasmine-1.3.1/jasmine.js
Normal file
278
Libraries/jquery-GPL-LICENSE.txt
Normal file
@ -0,0 +1,278 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
20
Libraries/jquery-MIT-LICENSE.txt
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2009 John Resig, http://jquery.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
211
Libraries/jquery.tinysort.js
Normal file
@ -0,0 +1,211 @@
|
||||
/*! TinySort 1.4.29
|
||||
* Copyright (c) 2008-2012 Ron Valstar http://www.sjeiti.com/
|
||||
*
|
||||
* Dual licensed under the MIT and GPL licenses:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*//*
|
||||
* Description:
|
||||
* A jQuery plugin to sort child nodes by (sub) contents or attributes.
|
||||
*
|
||||
* Contributors:
|
||||
* brian.gibson@gmail.com
|
||||
* michael.thornberry@gmail.com
|
||||
*
|
||||
* Usage:
|
||||
* $("ul#people>li").tsort();
|
||||
* $("ul#people>li").tsort("span.surname");
|
||||
* $("ul#people>li").tsort("span.surname",{order:"desc"});
|
||||
* $("ul#people>li").tsort({place:"end"});
|
||||
*
|
||||
* Change default like so:
|
||||
* $.tinysort.defaults.order = "desc";
|
||||
*
|
||||
* in this update:
|
||||
* - added plugin hook
|
||||
* - stripped non-latin character ordering and turned it into a plugin
|
||||
*
|
||||
* in last update:
|
||||
* - header comment no longer stripped in minified version
|
||||
* - revision number no longer corresponds to svn revision since it's now git
|
||||
*
|
||||
* Todos:
|
||||
* - todo: uppercase vs lowercase
|
||||
* - todo: 'foobar' != 'foobars' in non-latin
|
||||
*
|
||||
*/
|
||||
;(function($) {
|
||||
// private vars
|
||||
var fls = !1 // minify placeholder
|
||||
,nll = null // minify placeholder
|
||||
,prsflt = parseFloat // minify placeholder
|
||||
,mathmn = Math.min // minify placeholder
|
||||
,rxLastNr = /(-?\d+\.?\d*)$/g // regex for testing strings ending on numbers
|
||||
,aPluginPrepare = []
|
||||
,aPluginSort = []
|
||||
;
|
||||
//
|
||||
// init plugin
|
||||
$.tinysort = {
|
||||
id: 'TinySort'
|
||||
,version: '1.4.29'
|
||||
,copyright: 'Copyright (c) 2008-2012 Ron Valstar'
|
||||
,uri: 'http://tinysort.sjeiti.com/'
|
||||
,licensed: {
|
||||
MIT: 'http://www.opensource.org/licenses/mit-license.php'
|
||||
,GPL: 'http://www.gnu.org/licenses/gpl.html'
|
||||
}
|
||||
,plugin: function(prepare,sort){
|
||||
aPluginPrepare.push(prepare); // function(settings){doStuff();}
|
||||
aPluginSort.push(sort); // function(valuesAreNumeric,sA,sB,iReturn){doStuff();return iReturn;}
|
||||
}
|
||||
,defaults: { // default settings
|
||||
|
||||
order: 'asc' // order: asc, desc or rand
|
||||
|
||||
,attr: nll // order by attribute value
|
||||
,data: nll // use the data attribute for sorting
|
||||
,useVal: fls // use element value instead of text
|
||||
|
||||
,place: 'start' // place ordered elements at position: start, end, org (original position), first
|
||||
,returns: fls // return all elements or only the sorted ones (true/false)
|
||||
|
||||
,cases: fls // a case sensitive sort orders [aB,aa,ab,bb]
|
||||
,forceStrings:fls // if false the string '2' will sort with the value 2, not the string '2'
|
||||
|
||||
,sortFunction: nll // override the default sort function
|
||||
}
|
||||
};
|
||||
$.fn.extend({
|
||||
tinysort: function(_find,_settings) {
|
||||
if (_find&&typeof(_find)!='string') {
|
||||
_settings = _find;
|
||||
_find = nll;
|
||||
}
|
||||
|
||||
var oSettings = $.extend({}, $.tinysort.defaults, _settings)
|
||||
,sParent
|
||||
,oThis = this
|
||||
,iLen = $(this).length
|
||||
,oElements = {} // contains sortable- and non-sortable list per parent
|
||||
,bFind = !(!_find||_find=='')
|
||||
,bAttr = !(oSettings.attr===nll||oSettings.attr=="")
|
||||
,bData = oSettings.data!==nll
|
||||
// since jQuery's filter within each works on array index and not actual index we have to create the filter in advance
|
||||
,bFilter = bFind&&_find[0]==':'
|
||||
,$Filter = bFilter?oThis.filter(_find):oThis
|
||||
,fnSort = oSettings.sortFunction
|
||||
,iAsc = oSettings.order=='asc'?1:-1
|
||||
,aNewOrder = []
|
||||
;
|
||||
|
||||
$.each(aPluginPrepare,function(i,fn){
|
||||
fn.call(fn,oSettings);
|
||||
});
|
||||
|
||||
|
||||
if (!fnSort) fnSort = oSettings.order=='rand'?function() {
|
||||
return Math.random()<.5?1:-1;
|
||||
}:function(a,b) {
|
||||
var bNumeric = fls
|
||||
// maybe toLower
|
||||
,sA = !oSettings.cases?toLowerCase(a.s):a.s
|
||||
,sB = !oSettings.cases?toLowerCase(b.s):b.s;
|
||||
// maybe force Strings
|
||||
// var bAString = typeof(sA)=='string';
|
||||
// var bBString = typeof(sB)=='string';
|
||||
// if (!oSettings.forceStrings&&(bAString||bBString)) {
|
||||
// if (!bAString) sA = ''+sA;
|
||||
// if (!bBString) sB = ''+sB;
|
||||
if (!oSettings.forceStrings) {
|
||||
// maybe mixed
|
||||
var aAnum = sA&&sA.match(rxLastNr)
|
||||
,aBnum = sB&&sB.match(rxLastNr);
|
||||
if (aAnum&&aBnum) {
|
||||
var sAprv = sA.substr(0,sA.length-aAnum[0].length)
|
||||
,sBprv = sB.substr(0,sB.length-aBnum[0].length);
|
||||
if (sAprv==sBprv) {
|
||||
bNumeric = !fls;
|
||||
sA = prsflt(aAnum[0]);
|
||||
sB = prsflt(aBnum[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// return sort-integer
|
||||
var iReturn = iAsc*(sA<sB?-1:(sA>sB?1:0));
|
||||
|
||||
$.each(aPluginSort,function(i,fn){
|
||||
iReturn = fn.call(fn,bNumeric,sA,sB,iReturn);
|
||||
});
|
||||
|
||||
return iReturn;
|
||||
};
|
||||
|
||||
oThis.each(function(i,el) {
|
||||
var $Elm = $(el)
|
||||
// element or sub selection
|
||||
,mElmOrSub = bFind?(bFilter?$Filter.filter(el):$Elm.find(_find)):$Elm
|
||||
// text or attribute value
|
||||
,sSort = bData?''+mElmOrSub.data(oSettings.data):(bAttr?mElmOrSub.attr(oSettings.attr):(oSettings.useVal?mElmOrSub.val():mElmOrSub.text()))
|
||||
// to sort or not to sort
|
||||
,mParent = $Elm.parent();
|
||||
if (!oElements[mParent]) oElements[mParent] = {s:[],n:[]}; // s: sort, n: not sort
|
||||
if (mElmOrSub.length>0) oElements[mParent].s.push({s:sSort,e:$Elm,n:i}); // s:string, e:element, n:number
|
||||
else oElements[mParent].n.push({e:$Elm,n:i});
|
||||
});
|
||||
//
|
||||
// sort
|
||||
for (sParent in oElements) oElements[sParent].s.sort(fnSort);
|
||||
//
|
||||
// order elements and fill new order
|
||||
for (sParent in oElements) {
|
||||
var oParent = oElements[sParent]
|
||||
,aOrg = [] // list for original position
|
||||
,iLow = iLen
|
||||
,aCnt = [0,0] // count how much we've sorted for retreival from either the sort list or the non-sort list (oParent.s/oParent.n)
|
||||
,i;
|
||||
switch (oSettings.place) {
|
||||
case 'first': $.each(oParent.s,function(i,obj) { iLow = mathmn(iLow,obj.n) }); break;
|
||||
case 'org': $.each(oParent.s,function(i,obj) { aOrg.push(obj.n) }); break;
|
||||
case 'end': iLow = oParent.n.length; break;
|
||||
default: iLow = 0;
|
||||
}
|
||||
for (i = 0;i<iLen;i++) {
|
||||
var bSList = contains(aOrg,i)?!fls:i>=iLow&&i<iLow+oParent.s.length
|
||||
,mEl = (bSList?oParent.s:oParent.n)[aCnt[bSList?0:1]].e;
|
||||
mEl.parent().append(mEl);
|
||||
if (bSList||!oSettings.returns) aNewOrder.push(mEl.get(0));
|
||||
aCnt[bSList?0:1]++;
|
||||
}
|
||||
}
|
||||
oThis.length = 0;
|
||||
Array.prototype.push.apply(oThis,aNewOrder);
|
||||
return oThis;
|
||||
}
|
||||
});
|
||||
// toLowerCase
|
||||
function toLowerCase(s) {
|
||||
return s&&s.toLowerCase?s.toLowerCase():s;
|
||||
}
|
||||
// array contains
|
||||
function contains(a,n) {
|
||||
for (var i=0,l=a.length;i<l;i++) if (a[i]==n) return !fls;
|
||||
return fls;
|
||||
}
|
||||
// set functions
|
||||
$.fn.TinySort = $.fn.Tinysort = $.fn.tsort = $.fn.tinysort;
|
||||
})(jQuery);
|
||||
|
||||
/*! Array.prototype.indexOf for IE (issue #26) */
|
||||
if (!Array.prototype.indexOf) {
|
||||
Array.prototype.indexOf = function(elt /*, from*/) {
|
||||
var len = this.length
|
||||
,from = Number(arguments[1])||0;
|
||||
from = from<0?Math.ceil(from):Math.floor(from);
|
||||
if (from<0) from += len;
|
||||
for (;from<len;from++){
|
||||
if (from in this && this[from]===elt) return from;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
1
Libraries/logging
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 1dbed4ac2d34276e7e2b438118214c417ebe5691
|
11421
Libraries/require-jquery.js
Normal file
4153
Libraries/sinon-1.5.2.js
Normal file
42
Libraries/sjcl.js
Normal file
@ -0,0 +1,42 @@
|
||||
"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}};
|
||||
if(typeof module!="undefined"&&module.exports)module.exports=sjcl;
|
||||
sjcl.cipher.aes=function(a){this.h[0][0][0]||this.z();var b,c,d,e,f=this.h[0][4],g=this.h[1];b=a.length;var h=1;if(b!==4&&b!==6&&b!==8)throw new sjcl.exception.invalid("invalid aes key size");this.a=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(a%b===0||b===8&&a%b===4){c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255];if(a%b===0){c=c<<8^c>>>24^h<<24;h=h<<1^(h>>7)*283}}d[a]=d[a-b]^c}for(b=0;a;b++,a--){c=d[b&3?a:a-4];e[b]=a<=4||b<4?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^
|
||||
g[3][f[c&255]]}};
|
||||
sjcl.cipher.aes.prototype={encrypt:function(a){return this.I(a,0)},decrypt:function(a){return this.I(a,1)},h:[[[],[],[],[],[]],[[],[],[],[],[]]],z:function(){var a=this.h[0],b=this.h[1],c=a[4],d=b[4],e,f,g,h=[],i=[],k,j,l,m;for(e=0;e<0x100;e++)i[(h[e]=e<<1^(e>>7)*283)^e]=e;for(f=g=0;!c[f];f^=k||1,g=i[g]||1){l=g^g<<1^g<<2^g<<3^g<<4;l=l>>8^l&255^99;c[f]=l;d[l]=f;j=h[e=h[k=h[f]]];m=j*0x1010101^e*0x10001^k*0x101^f*0x1010100;j=h[l]*0x101^l*0x1010100;for(e=0;e<4;e++){a[e][f]=j=j<<24^j>>>8;b[e][l]=m=m<<24^m>>>8}}for(e=
|
||||
0;e<5;e++){a[e]=a[e].slice(0);b[e]=b[e].slice(0)}},I:function(a,b){if(a.length!==4)throw new sjcl.exception.invalid("invalid aes block size");var c=this.a[b],d=a[0]^c[0],e=a[b?3:1]^c[1],f=a[2]^c[2];a=a[b?1:3]^c[3];var g,h,i,k=c.length/4-2,j,l=4,m=[0,0,0,0];g=this.h[b];var n=g[0],o=g[1],p=g[2],q=g[3],r=g[4];for(j=0;j<k;j++){g=n[d>>>24]^o[e>>16&255]^p[f>>8&255]^q[a&255]^c[l];h=n[e>>>24]^o[f>>16&255]^p[a>>8&255]^q[d&255]^c[l+1];i=n[f>>>24]^o[a>>16&255]^p[d>>8&255]^q[e&255]^c[l+2];a=n[a>>>24]^o[d>>16&
|
||||
255]^p[e>>8&255]^q[f&255]^c[l+3];l+=4;d=g;e=h;f=i}for(j=0;j<4;j++){m[b?3&-j:j]=r[d>>>24]<<24^r[e>>16&255]<<16^r[f>>8&255]<<8^r[a&255]^c[l++];g=d;d=e;e=f;f=a;a=g}return m}};
|
||||
sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.P(a.slice(b/32),32-(b&31)).slice(1);return c===undefined?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<<c)-1},concat:function(a,b){if(a.length===0||b.length===0)return a.concat(b);var c=a[a.length-1],d=sjcl.bitArray.getPartial(c);return d===32?a.concat(b):sjcl.bitArray.P(b,d,c|0,a.slice(0,a.length-1))},bitLength:function(a){var b=a.length;
|
||||
if(b===0)return 0;return(b-1)*32+sjcl.bitArray.getPartial(a[b-1])},clamp:function(a,b){if(a.length*32<b)return a;a=a.slice(0,Math.ceil(b/32));var c=a.length;b&=31;if(c>0&&b)a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>b-1,1);return a},partial:function(a,b,c){if(a===32)return b;return(c?b|0:b<<32-a)+a*0x10000000000},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return false;var c=0,d;for(d=0;d<a.length;d++)c|=
|
||||
a[d]^b[d];return c===0},P:function(a,b,c,d){var e;e=0;if(d===undefined)d=[];for(;b>=32;b-=32){d.push(c);c=0}if(b===0)return d.concat(a);for(e=0;e<a.length;e++){d.push(c|a[e]>>>b);c=a[e]<<32-b}e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,b+a>32?c:d.pop(),1));return d},k:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]}};
|
||||
sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d<c/8;d++){if((d&3)===0)e=a[d/4];b+=String.fromCharCode(e>>>24);e<<=8}return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c<a.length;c++){d=d<<8|a.charCodeAt(c);if((c&3)===3){b.push(d);d=0}}c&3&&b.push(sjcl.bitArray.partial(8*(c&3),d));return b}};
|
||||
sjcl.codec.hex={fromBits:function(a){var b="",c;for(c=0;c<a.length;c++)b+=((a[c]|0)+0xf00000000000).toString(16).substr(4);return b.substr(0,sjcl.bitArray.bitLength(a)/4)},toBits:function(a){var b,c=[],d;a=a.replace(/\s|0x/g,"");d=a.length;a+="00000000";for(b=0;b<a.length;b+=8)c.push(parseInt(a.substr(b,8),16)^0);return sjcl.bitArray.clamp(c,d*4)}};
|
||||
sjcl.codec.base64={F:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fromBits:function(a,b,c){var d="",e=0,f=sjcl.codec.base64.F,g=0,h=sjcl.bitArray.bitLength(a);if(c)f=f.substr(0,62)+"-_";for(c=0;d.length*6<h;){d+=f.charAt((g^a[c]>>>e)>>>26);if(e<6){g=a[c]<<6-e;e+=26;c++}else{g<<=6;e-=6}}for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d=0,e=sjcl.codec.base64.F,f=0,g;if(b)e=e.substr(0,62)+"-_";for(b=0;b<a.length;b++){g=e.indexOf(a.charAt(b));
|
||||
if(g<0)throw new sjcl.exception.invalid("this isn't base64!");if(d>26){d-=26;c.push(f^g>>>d);f=g<<32-d}else{d+=6;f^=g<<32-d}}d&56&&c.push(sjcl.bitArray.partial(d&56,f,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.a[0]||this.z();if(a){this.n=a.n.slice(0);this.i=a.i.slice(0);this.e=a.e}else this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()};
|
||||
sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.n=this.N.slice(0);this.i=[];this.e=0;return this},update:function(a){if(typeof a==="string")a=sjcl.codec.utf8String.toBits(a);var b,c=this.i=sjcl.bitArray.concat(this.i,a);b=this.e;a=this.e=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)this.D(c.splice(0,16));return this},finalize:function(){var a,b=this.i,c=this.n;b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.e/
|
||||
4294967296));for(b.push(this.e|0);b.length;)this.D(b.splice(0,16));this.reset();return c},N:[],a:[],z:function(){function a(e){return(e-Math.floor(e))*0x100000000|0}var b=0,c=2,d;a:for(;b<64;c++){for(d=2;d*d<=c;d++)if(c%d===0)continue a;if(b<8)this.N[b]=a(Math.pow(c,0.5));this.a[b]=a(Math.pow(c,1/3));b++}},D:function(a){var b,c,d=a.slice(0),e=this.n,f=this.a,g=e[0],h=e[1],i=e[2],k=e[3],j=e[4],l=e[5],m=e[6],n=e[7];for(a=0;a<64;a++){if(a<16)b=d[a];else{b=d[a+1&15];c=d[a+14&15];b=d[a&15]=(b>>>7^b>>>18^
|
||||
b>>>3^b<<25^b<<14)+(c>>>17^c>>>19^c>>>10^c<<15^c<<13)+d[a&15]+d[a+9&15]|0}b=b+n+(j>>>6^j>>>11^j>>>25^j<<26^j<<21^j<<7)+(m^j&(l^m))+f[a];n=m;m=l;l=j;j=k+b|0;k=i;i=h;h=g;g=b+(h&i^k&(h^i))+(h>>>2^h>>>13^h>>>22^h<<30^h<<19^h<<10)|0}e[0]=e[0]+g|0;e[1]=e[1]+h|0;e[2]=e[2]+i|0;e[3]=e[3]+k|0;e[4]=e[4]+j|0;e[5]=e[5]+l|0;e[6]=e[6]+m|0;e[7]=e[7]+n|0}};
|
||||
sjcl.mode.ccm={name:"ccm",encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,i=h.bitLength(c)/8,k=h.bitLength(g)/8;e=e||64;d=d||[];if(i<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;f<4&&k>>>8*f;f++);if(f<15-i)f=15-i;c=h.clamp(c,8*(15-f));b=sjcl.mode.ccm.H(a,b,c,d,e,f);g=sjcl.mode.ccm.J(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),i=f.clamp(b,h-e),k=f.bitSlice(b,
|
||||
h-e);h=(h-e)/8;if(g<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;b<4&&h>>>8*b;b++);if(b<15-g)b=15-g;c=f.clamp(c,8*(15-b));i=sjcl.mode.ccm.J(a,i,c,k,e,b);a=sjcl.mode.ccm.H(a,i.data,c,d,e,b);if(!f.equal(i.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match");return i.data},H:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,i=h.k;e/=8;if(e%2||e<4||e>16)throw new sjcl.exception.invalid("ccm: invalid tag length");if(d.length>0xffffffff||b.length>0xffffffff)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data");
|
||||
f=[h.partial(8,(d.length?64:0)|e-2<<2|f-1)];f=h.concat(f,c);f[3]|=h.bitLength(b)/8;f=a.encrypt(f);if(d.length){c=h.bitLength(d)/8;if(c<=65279)g=[h.partial(16,c)];else if(c<=0xffffffff)g=h.concat([h.partial(16,65534)],[c]);g=h.concat(g,d);for(d=0;d<g.length;d+=4)f=a.encrypt(i(f,g.slice(d,d+4).concat([0,0,0])))}for(d=0;d<b.length;d+=4)f=a.encrypt(i(f,b.slice(d,d+4).concat([0,0,0])));return h.clamp(f,e*8)},J:function(a,b,c,d,e,f){var g,h=sjcl.bitArray;g=h.k;var i=b.length,k=h.bitLength(b);c=h.concat([h.partial(8,
|
||||
f-1)],c).concat([0,0,0]).slice(0,4);d=h.bitSlice(g(d,a.encrypt(c)),0,e);if(!i)return{tag:d,data:[]};for(g=0;g<i;g+=4){c[3]++;e=a.encrypt(c);b[g]^=e[0];b[g+1]^=e[1];b[g+2]^=e[2];b[g+3]^=e[3]}return{tag:d,data:h.clamp(b,k)}}};
|
||||
sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(sjcl.bitArray.bitLength(c)!==128)throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.B,i=sjcl.bitArray,k=i.k,j=[0,0,0,0];c=h(a.encrypt(c));var l,m=[];d=d||[];e=e||64;for(g=0;g+4<b.length;g+=4){l=b.slice(g,g+4);j=k(j,l);m=m.concat(k(c,a.encrypt(k(c,l))));c=h(c)}l=b.slice(g);b=i.bitLength(l);g=a.encrypt(k(c,[0,0,0,b]));l=i.clamp(k(l.concat([0,0,0]),g),b);j=k(j,k(l.concat([0,0,0]),g));j=a.encrypt(k(j,k(c,h(c))));
|
||||
if(d.length)j=k(j,f?d:sjcl.mode.ocb2.pmac(a,d));return m.concat(i.concat(l,i.clamp(j,e)))},decrypt:function(a,b,c,d,e,f){if(sjcl.bitArray.bitLength(c)!==128)throw new sjcl.exception.invalid("ocb iv must be 128 bits");e=e||64;var g=sjcl.mode.ocb2.B,h=sjcl.bitArray,i=h.k,k=[0,0,0,0],j=g(a.encrypt(c)),l,m,n=sjcl.bitArray.bitLength(b)-e,o=[];d=d||[];for(c=0;c+4<n/32;c+=4){l=i(j,a.decrypt(i(j,b.slice(c,c+4))));k=i(k,l);o=o.concat(l);j=g(j)}m=n-c*32;l=a.encrypt(i(j,[0,0,0,m]));l=i(l,h.clamp(b.slice(c),
|
||||
m).concat([0,0,0]));k=i(k,l);k=a.encrypt(i(k,i(j,g(j))));if(d.length)k=i(k,f?d:sjcl.mode.ocb2.pmac(a,d));if(!h.equal(h.clamp(k,e),h.bitSlice(b,n)))throw new sjcl.exception.corrupt("ocb: tag doesn't match");return o.concat(h.clamp(l,m))},pmac:function(a,b){var c,d=sjcl.mode.ocb2.B,e=sjcl.bitArray,f=e.k,g=[0,0,0,0],h=a.encrypt([0,0,0,0]);h=f(h,d(d(h)));for(c=0;c+4<b.length;c+=4){h=d(h);g=f(g,a.encrypt(f(h,b.slice(c,c+4))))}b=b.slice(c);if(e.bitLength(b)<128){h=f(h,d(h));b=e.concat(b,[2147483648|0,0,
|
||||
0,0])}g=f(g,b);return a.encrypt(f(d(f(h,d(h))),g))},B:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^(a[0]>>>31)*135]}};sjcl.misc.hmac=function(a,b){this.M=b=b||sjcl.hash.sha256;var c=[[],[]],d=b.prototype.blockSize/32;this.l=[new b,new b];if(a.length>d)a=b.hash(a);for(b=0;b<d;b++){c[0][b]=a[b]^909522486;c[1][b]=a[b]^1549556828}this.l[0].update(c[0]);this.l[1].update(c[1])};
|
||||
sjcl.misc.hmac.prototype.encrypt=sjcl.misc.hmac.prototype.mac=function(a,b){a=(new this.M(this.l[0])).update(a,b).finalize();return(new this.M(this.l[1])).update(a).finalize()};
|
||||
sjcl.misc.pbkdf2=function(a,b,c,d,e){c=c||1E3;if(d<0||c<0)throw sjcl.exception.invalid("invalid params to pbkdf2");if(typeof a==="string")a=sjcl.codec.utf8String.toBits(a);e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,i,k=[],j=sjcl.bitArray;for(i=1;32*k.length<(d||1);i++){e=f=a.encrypt(j.concat(b,[i]));for(g=1;g<c;g++){f=a.encrypt(f);for(h=0;h<f.length;h++)e[h]^=f[h]}k=k.concat(e)}if(d)k=j.clamp(k,d);return k};
|
||||
sjcl.random={randomWords:function(a,b){var c=[];b=this.isReady(b);var d;if(b===0)throw new sjcl.exception.notReady("generator isn't seeded");else b&2&&this.U(!(b&1));for(b=0;b<a;b+=4){(b+1)%0x10000===0&&this.L();d=this.w();c.push(d[0],d[1],d[2],d[3])}this.L();return c.slice(0,a)},setDefaultParanoia:function(a){this.t=a},addEntropy:function(a,b,c){c=c||"user";var d,e,f=(new Date).valueOf(),g=this.q[c],h=this.isReady(),i=0;d=this.G[c];if(d===undefined)d=this.G[c]=this.R++;if(g===undefined)g=this.q[c]=
|
||||
0;this.q[c]=(this.q[c]+1)%this.b.length;switch(typeof a){case "number":if(b===undefined)b=1;this.b[g].update([d,this.u++,1,b,f,1,a|0]);break;case "object":c=Object.prototype.toString.call(a);if(c==="[object Uint32Array]"){e=[];for(c=0;c<a.length;c++)e.push(a[c]);a=e}else{if(c!=="[object Array]")i=1;for(c=0;c<a.length&&!i;c++)if(typeof a[c]!="number")i=1}if(!i){if(b===undefined)for(c=b=0;c<a.length;c++)for(e=a[c];e>0;){b++;e>>>=1}this.b[g].update([d,this.u++,2,b,f,a.length].concat(a))}break;case "string":if(b===
|
||||
undefined)b=a.length;this.b[g].update([d,this.u++,3,b,f,a.length]);this.b[g].update(a);break;default:i=1}if(i)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.j[g]+=b;this.f+=b;if(h===0){this.isReady()!==0&&this.K("seeded",Math.max(this.g,this.f));this.K("progress",this.getProgress())}},isReady:function(a){a=this.C[a!==undefined?a:this.t];return this.g&&this.g>=a?this.j[0]>80&&(new Date).valueOf()>this.O?3:1:this.f>=a?2:0},getProgress:function(a){a=
|
||||
this.C[a?a:this.t];return this.g>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.m){if(window.addEventListener){window.addEventListener("load",this.o,false);window.addEventListener("mousemove",this.p,false)}else if(document.attachEvent){document.attachEvent("onload",this.o);document.attachEvent("onmousemove",this.p)}else throw new sjcl.exception.bug("can't attach event");this.m=true}},stopCollectors:function(){if(this.m){if(window.removeEventListener){window.removeEventListener("load",
|
||||
this.o,false);window.removeEventListener("mousemove",this.p,false)}else if(window.detachEvent){window.detachEvent("onload",this.o);window.detachEvent("onmousemove",this.p)}this.m=false}},addEventListener:function(a,b){this.r[a][this.Q++]=b},removeEventListener:function(a,b){var c;a=this.r[a];var d=[];for(c in a)a.hasOwnProperty(c)&&a[c]===b&&d.push(c);for(b=0;b<d.length;b++){c=d[b];delete a[c]}},b:[new sjcl.hash.sha256],j:[0],A:0,q:{},u:0,G:{},R:0,g:0,f:0,O:0,a:[0,0,0,0,0,0,0,0],d:[0,0,0,0],s:undefined,
|
||||
t:6,m:false,r:{progress:{},seeded:{}},Q:0,C:[0,48,64,96,128,192,0x100,384,512,768,1024],w:function(){for(var a=0;a<4;a++){this.d[a]=this.d[a]+1|0;if(this.d[a])break}return this.s.encrypt(this.d)},L:function(){this.a=this.w().concat(this.w());this.s=new sjcl.cipher.aes(this.a)},T:function(a){this.a=sjcl.hash.sha256.hash(this.a.concat(a));this.s=new sjcl.cipher.aes(this.a);for(a=0;a<4;a++){this.d[a]=this.d[a]+1|0;if(this.d[a])break}},U:function(a){var b=[],c=0,d;this.O=b[0]=(new Date).valueOf()+3E4;for(d=
|
||||
0;d<16;d++)b.push(Math.random()*0x100000000|0);for(d=0;d<this.b.length;d++){b=b.concat(this.b[d].finalize());c+=this.j[d];this.j[d]=0;if(!a&&this.A&1<<d)break}if(this.A>=1<<this.b.length){this.b.push(new sjcl.hash.sha256);this.j.push(0)}this.f-=c;if(c>this.g)this.g=c;this.A++;this.T(b)},p:function(a){sjcl.random.addEntropy([a.x||a.clientX||a.offsetX||0,a.y||a.clientY||a.offsetY||0],2,"mouse")},o:function(){sjcl.random.addEntropy((new Date).valueOf(),2,"loadtime")},K:function(a,b){var c;a=sjcl.random.r[a];
|
||||
var d=[];for(c in a)a.hasOwnProperty(c)&&d.push(a[c]);for(c=0;c<d.length;c++)d[c](b)}};try{var s=new Uint32Array(32);crypto.getRandomValues(s);sjcl.random.addEntropy(s,1024,"crypto['getRandomValues']")}catch(t){}
|
||||
sjcl.json={defaults:{v:1,iter:1E3,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},encrypt:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.c({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.c(f,c);c=f.adata;if(typeof f.salt==="string")f.salt=sjcl.codec.base64.toBits(f.salt);if(typeof f.iv==="string")f.iv=sjcl.codec.base64.toBits(f.iv);if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||typeof a==="string"&&f.iter<=100||f.ts!==64&&f.ts!==96&&f.ts!==128||f.ks!==128&&f.ks!==192&&f.ks!==0x100||f.iv.length<
|
||||
2||f.iv.length>4)throw new sjcl.exception.invalid("json encrypt: invalid parameters");if(typeof a==="string"){g=sjcl.misc.cachedPbkdf2(a,f);a=g.key.slice(0,f.ks/32);f.salt=g.salt}if(typeof b==="string")b=sjcl.codec.utf8String.toBits(b);if(typeof c==="string")c=sjcl.codec.utf8String.toBits(c);g=new sjcl.cipher[f.cipher](a);e.c(d,f);d.key=a;f.ct=sjcl.mode[f.mode].encrypt(g,b,f.iv,c,f.ts);return e.encode(f)},decrypt:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.c(e.c(e.c({},e.defaults),e.decode(b)),
|
||||
c,true);var f;c=b.adata;if(typeof b.salt==="string")b.salt=sjcl.codec.base64.toBits(b.salt);if(typeof b.iv==="string")b.iv=sjcl.codec.base64.toBits(b.iv);if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||typeof a==="string"&&b.iter<=100||b.ts!==64&&b.ts!==96&&b.ts!==128||b.ks!==128&&b.ks!==192&&b.ks!==0x100||!b.iv||b.iv.length<2||b.iv.length>4)throw new sjcl.exception.invalid("json decrypt: invalid parameters");if(typeof a==="string"){f=sjcl.misc.cachedPbkdf2(a,b);a=f.key.slice(0,b.ks/32);b.salt=f.salt}if(typeof c===
|
||||
"string")c=sjcl.codec.utf8String.toBits(c);f=new sjcl.cipher[b.cipher](a);c=sjcl.mode[b.mode].decrypt(f,b.ct,b.iv,c,b.ts);e.c(d,b);d.key=a;return sjcl.codec.utf8String.fromBits(c)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+'"'+b+'":';d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+
|
||||
sjcl.codec.base64.fromBits(a[b],1)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type");}}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c<a.length;c++){if(!(d=a[c].match(/^(?:(["']?)([a-z][a-z0-9]*)\1):(?:(\d+)|"([a-z0-9+\/%*_.@=\-]*)")$/i)))throw new sjcl.exception.invalid("json decode: this isn't json!");b[d[2]]=
|
||||
d[3]?parseInt(d[3],10):d[2].match(/^(ct|salt|iv)$/)?sjcl.codec.base64.toBits(d[4]):unescape(d[4])}return b},c:function(a,b,c){if(a===undefined)a={};if(b===undefined)return a;var d;for(d in b)if(b.hasOwnProperty(d)){if(c&&a[d]!==undefined&&a[d]!==b[d])throw new sjcl.exception.invalid("required parameter overridden");a[d]=b[d]}return a},W:function(a,b){var c={},d;for(d in a)if(a.hasOwnProperty(d)&&a[d]!==b[d])c[d]=a[d];return c},V:function(a,b){var c={},d;for(d=0;d<b.length;d++)if(a[b[d]]!==undefined)c[b[d]]=
|
||||
a[b[d]];return c}};sjcl.encrypt=sjcl.json.encrypt;sjcl.decrypt=sjcl.json.decrypt;sjcl.misc.S={};sjcl.misc.cachedPbkdf2=function(a,b){var c=sjcl.misc.S,d;b=b||{};d=b.iter||1E3;c=c[a]=c[a]||{};d=c[d]=c[d]||{firstSalt:b.salt&&b.salt.length?b.salt.slice(0):sjcl.random.randomWords(2,0)};c=b.salt===undefined?d.firstSalt:b.salt;d[c]=d[c]||sjcl.misc.pbkdf2(a,c,b.iter);return{key:d[c].slice(0),salt:c.slice(0)}};
|
4201
Libraries/strophe.js
Normal file
957
Libraries/strophe.muc.js
Normal file
@ -0,0 +1,957 @@
|
||||
/*
|
||||
*Plugin to implement the MUC extension.
|
||||
http://xmpp.org/extensions/xep-0045.html
|
||||
|
||||
*Previous Author:
|
||||
Nathan Zorn <nathan.zorn@gmail.com>
|
||||
*Complete CoffeeScript rewrite:
|
||||
Andreas Guth <guth@dbis.rwth-aachen.de>
|
||||
*Cleanup, AMD/global registrations and bugfixes:
|
||||
JC Brand <jc@opkode.com>
|
||||
*/
|
||||
|
||||
// AMD/global registrations
|
||||
(function (root, factory) {
|
||||
if (typeof console === undefined || typeof console.log === undefined) {
|
||||
console = { log: function () {}, error: function () {} };
|
||||
}
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define([
|
||||
"strophe"
|
||||
], function () {
|
||||
if (console===undefined || console.log===undefined) {
|
||||
console = { log: function () {}, error: function () {} };
|
||||
}
|
||||
return factory(jQuery, console);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return factory(jQuery, console);
|
||||
}
|
||||
}(this, function ($, console) {
|
||||
|
||||
(function() {
|
||||
var Occupant, RoomConfig, XmppRoom,
|
||||
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
|
||||
Strophe.addConnectionPlugin('muc', {
|
||||
_connection: null,
|
||||
rooms: [],
|
||||
init: function(conn) {
|
||||
/* Initialize the MUC plugin. Sets the correct connection object and
|
||||
* extends the namesace.
|
||||
*/
|
||||
this._connection = conn;
|
||||
this._muc_handler = null;
|
||||
Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
|
||||
Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
|
||||
Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
|
||||
return Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
|
||||
},
|
||||
|
||||
join: function(room, nick, msg_handler_cb, pres_handler_cb, roster_cb, password) {
|
||||
/* Join a multi-user chat room
|
||||
*
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room to join.
|
||||
* (String) nick - Optional nickname to use in the chat room.
|
||||
* (Function) msg_handler_cb - The function call to handle messages from the specified chat room.
|
||||
* (Function) pres_handler_cb - The function call back to handle presence in the chat room.
|
||||
* (String) password - The optional password to use. (password protected rooms only)
|
||||
*/
|
||||
var msg, room_nick, _this = this;
|
||||
|
||||
room_nick = this.test_append_nick(room, nick);
|
||||
msg = $pres({
|
||||
from: this._connection.jid,
|
||||
to: room_nick
|
||||
}).c("x", {
|
||||
xmlns: Strophe.NS.MUC
|
||||
});
|
||||
if (password !== null) {
|
||||
msg.cnode(Strophe.xmlElement("password", [], password));
|
||||
}
|
||||
if (this._muc_handler === null) {
|
||||
this._muc_handler = this._connection.addHandler(function(stanza) {
|
||||
var from, handler, handlers, id, roomname, x, xmlns, xquery, _i, _len;
|
||||
from = stanza.getAttribute('from');
|
||||
roomname = from.split("/")[0];
|
||||
if (!_this.rooms[roomname]) { return true; }
|
||||
room = _this.rooms[roomname];
|
||||
handlers = {};
|
||||
if (stanza.nodeName === "message") {
|
||||
handlers = room._message_handlers;
|
||||
} else if (stanza.nodeName === "presence") {
|
||||
xquery = stanza.getElementsByTagName("x");
|
||||
if (xquery.length > 0) {
|
||||
for (_i = 0, _len = xquery.length; _i < _len; _i++) {
|
||||
x = xquery[_i];
|
||||
xmlns = x.getAttribute("xmlns");
|
||||
if (xmlns && xmlns.match(Strophe.NS.MUC)) {
|
||||
handlers = room._presence_handlers;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_.each(handlers, function (handler, id, handlers) {
|
||||
if (!handler(stanza, room)) { delete handlers[id]; }
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (!_.has(this.rooms, room)) {
|
||||
this.rooms[room] = new XmppRoom(this, room, nick, password);
|
||||
}
|
||||
if (pres_handler_cb) {
|
||||
this.rooms[room].addHandler('presence', pres_handler_cb);
|
||||
}
|
||||
if (msg_handler_cb) {
|
||||
this.rooms[room].addHandler('message', msg_handler_cb);
|
||||
}
|
||||
if (roster_cb) {
|
||||
this.rooms[room].addHandler('roster', roster_cb);
|
||||
}
|
||||
return this._connection.send(msg);
|
||||
},
|
||||
|
||||
leave: function(room, nick, handler_cb, exit_msg) {
|
||||
/* Leave a multi-user chat room
|
||||
*
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room to leave.
|
||||
* (String) nick - The nick name used in the room.
|
||||
* (Function) handler_cb - Optional function to handle the successful leave.
|
||||
* (String) exit_msg - optional exit message.
|
||||
* Returns:
|
||||
* iqid - The unique id for the room leave.
|
||||
*/
|
||||
var presence, presenceid, room_nick;
|
||||
delete this.rooms[room];
|
||||
if (this.rooms.length === 0) {
|
||||
this._connection.deleteHandler(this._muc_handler);
|
||||
this._muc_handler = null;
|
||||
}
|
||||
room_nick = this.test_append_nick(room, nick);
|
||||
presenceid = this._connection.getUniqueId();
|
||||
presence = $pres({
|
||||
type: "unavailable",
|
||||
id: presenceid,
|
||||
from: this._connection.jid,
|
||||
to: room_nick
|
||||
});
|
||||
if (exit_msg !== null) {
|
||||
presence.c("status", exit_msg);
|
||||
}
|
||||
if (handler_cb !== null) {
|
||||
this._connection.addHandler(handler_cb, null, "presence", null, presenceid);
|
||||
}
|
||||
this._connection.send(presence);
|
||||
return presenceid;
|
||||
},
|
||||
|
||||
message: function(room, nick, message, html_message, type) {
|
||||
/* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) nick - The nick name used in the chat room.
|
||||
* (String) message - The plaintext message to send to the room.
|
||||
* (String) html_message - The message to send to the room with html markup.
|
||||
* (String) type - "groupchat" for group chat messages o
|
||||
* "chat" for private chat messages
|
||||
* Returns:
|
||||
* msgiq - the unique id used to send the message
|
||||
*/
|
||||
var msg, msgid, parent, room_nick;
|
||||
room_nick = this.test_append_nick(room, nick);
|
||||
type = type || (nick !== null ? "chat" : "groupchat");
|
||||
msgid = this._connection.getUniqueId();
|
||||
msg = $msg({
|
||||
to: room_nick,
|
||||
from: this._connection.jid,
|
||||
type: type,
|
||||
id: msgid
|
||||
}).c("body", {
|
||||
xmlns: Strophe.NS.CLIENT
|
||||
}).t(message);
|
||||
msg.up();
|
||||
if (html_message !== null) {
|
||||
msg.c("html", {xmlns: Strophe.NS.XHTML_IM}).c("body", {xmlns: Strophe.NS.XHTML}).h(html_message);
|
||||
|
||||
if (msg.node.childNodes.length === 0) {
|
||||
parent = msg.node.parentNode;
|
||||
msg.up().up();
|
||||
msg.node.removeChild(parent);
|
||||
} else {
|
||||
msg.up().up();
|
||||
}
|
||||
}
|
||||
msg.c("x", {
|
||||
xmlns: "jabber:x:event"
|
||||
}).c("composing");
|
||||
this._connection.send(msg);
|
||||
return msgid;
|
||||
},
|
||||
|
||||
groupchat: function(room, message, html_message) {
|
||||
/* Convenience Function to send a Message to all Occupants
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) message - The plaintext message to send to the room.
|
||||
* (String) html_message - The message to send to the room with html markup.
|
||||
* Returns:
|
||||
* msgiq - the unique id used to send the message
|
||||
*/
|
||||
return this.message(room, null, message, html_message);
|
||||
},
|
||||
|
||||
invite: function(room, receiver, reason) {
|
||||
/* Send a mediated invitation.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) receiver - The invitation's receiver.
|
||||
* (String) reason - Optional reason for joining the room.
|
||||
* Returns:
|
||||
* msgiq - the unique id used to send the invitation
|
||||
*/
|
||||
var invitation, msgid;
|
||||
msgid = this._connection.getUniqueId();
|
||||
invitation = $msg({
|
||||
from: this._connection.jid,
|
||||
to: room,
|
||||
id: msgid
|
||||
}).c('x', {
|
||||
xmlns: Strophe.NS.MUC_USER
|
||||
}).c('invite', {
|
||||
to: receiver
|
||||
});
|
||||
if (reason !== null) {
|
||||
invitation.c('reason', reason);
|
||||
}
|
||||
this._connection.send(invitation);
|
||||
return msgid;
|
||||
},
|
||||
|
||||
directInvite: function(room, receiver, reason, password) {
|
||||
/* Send a direct invitation.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) receiver - The invitation's receiver.
|
||||
* (String) reason - Optional reason for joining the room.
|
||||
* (String) password - Optional password for the room.
|
||||
* Returns:
|
||||
* msgiq - the unique id used to send the invitation
|
||||
*/
|
||||
|
||||
var attrs, invitation, msgid;
|
||||
msgid = this._connection.getUniqueId();
|
||||
attrs = {
|
||||
xmlns: 'jabber:x:conference',
|
||||
jid: room
|
||||
};
|
||||
if (reason !== null) { attrs.reason = reason; }
|
||||
if (password !== null) { attrs.password = password; }
|
||||
invitation = $msg({
|
||||
from: this._connection.jid,
|
||||
to: receiver,
|
||||
id: msgid
|
||||
}).c('x', attrs);
|
||||
this._connection.send(invitation);
|
||||
return msgid;
|
||||
},
|
||||
|
||||
queryOccupants: function(room, success_cb, error_cb) {
|
||||
/* Queries a room for a list of occupants
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (Function) success_cb - Optional function to handle the info.
|
||||
* (Function) error_cb - Optional function to handle an error.
|
||||
* Returns:
|
||||
* id - the unique id used to send the info request
|
||||
*/
|
||||
var attrs, info;
|
||||
attrs = {
|
||||
xmlns: Strophe.NS.DISCO_ITEMS
|
||||
};
|
||||
info = $iq({
|
||||
from: this._connection.jid,
|
||||
to: room,
|
||||
type: 'get'
|
||||
}).c('query', attrs);
|
||||
return this._connection.sendIQ(info, success_cb, error_cb);
|
||||
},
|
||||
|
||||
configure: function(room, handler_cb) {
|
||||
/* Start a room configuration.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (Function) handler_cb - Optional function to handle the config form.
|
||||
* Returns:
|
||||
* id - the unique id used to send the configuration request
|
||||
*/
|
||||
|
||||
var config, id, stanza;
|
||||
config = $iq({
|
||||
to: room,
|
||||
type: "get"
|
||||
}).c("query", {
|
||||
xmlns: Strophe.NS.MUC_OWNER
|
||||
});
|
||||
stanza = config.tree();
|
||||
id = this._connection.sendIQ(stanza);
|
||||
if (handler_cb !== null) {
|
||||
this._connection.addHandler(function(stanza) { handler_cb(stanza);
|
||||
return false;
|
||||
}, Strophe.NS.MUC_OWNER, "iq", null, id);
|
||||
}
|
||||
return id;
|
||||
},
|
||||
|
||||
cancelConfigure: function(room) {
|
||||
/* Cancel the room configuration
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* Returns:
|
||||
* id - the unique id used to cancel the configuration.
|
||||
*/
|
||||
var config, stanza;
|
||||
config = $iq({
|
||||
to: room,
|
||||
type: "set"
|
||||
}).c("query", {
|
||||
xmlns: Strophe.NS.MUC_OWNER
|
||||
}).c("x", {
|
||||
xmlns: "jabber:x:data",
|
||||
type: "cancel"
|
||||
});
|
||||
stanza = config.tree();
|
||||
return this._connection.sendIQ(stanza);
|
||||
},
|
||||
|
||||
saveConfiguration: function(room, configarray) {
|
||||
/* Save a room configuration.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (Array) configarray - an array of form elements used to configure the room.
|
||||
* Returns:
|
||||
* id - the unique id used to save the configuration.
|
||||
*/
|
||||
var conf, config, stanza, _i, _len;
|
||||
config = $iq({
|
||||
to: room,
|
||||
type: "set"
|
||||
}).c("query", {
|
||||
xmlns: Strophe.NS.MUC_OWNER
|
||||
}).c("x", {
|
||||
xmlns: "jabber:x:data",
|
||||
type: "submit"
|
||||
});
|
||||
for (_i = 0, _len = configarray.length; _i < _len; _i++) {
|
||||
conf = configarray[_i];
|
||||
config.cnode(conf).up();
|
||||
}
|
||||
stanza = config.tree();
|
||||
return this._connection.sendIQ(stanza);
|
||||
},
|
||||
|
||||
createInstantRoom: function(room) {
|
||||
/* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* Returns:
|
||||
* id - the unique id used to create the chat room.
|
||||
*/
|
||||
var roomiq;
|
||||
roomiq = $iq({
|
||||
to: room,
|
||||
type: "set"
|
||||
}).c("query", {
|
||||
xmlns: Strophe.NS.MUC_OWNER
|
||||
}).c("x", {
|
||||
xmlns: "jabber:x:data",
|
||||
type: "submit"
|
||||
});
|
||||
return this._connection.sendIQ(roomiq.tree());
|
||||
},
|
||||
|
||||
setTopic: function(room, topic) {
|
||||
/* Set the topic of the chat room.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) topic - Topic message.
|
||||
*/
|
||||
var msg;
|
||||
msg = $msg({
|
||||
to: room,
|
||||
from: this._connection.jid,
|
||||
type: "groupchat"
|
||||
}).c("subject", {
|
||||
xmlns: "jabber:client"
|
||||
}).t(topic);
|
||||
return this._connection.send(msg.tree());
|
||||
},
|
||||
|
||||
_modifyPrivilege: function(room, item, reason, handler_cb, error_cb) {
|
||||
/* Internal Function that Changes the role or affiliation of a member
|
||||
* of a MUC room. This function is used by modifyRole and modifyAffiliation.
|
||||
* The modification can only be done by a room moderator. An error will be
|
||||
* returned if the user doesn't have permission.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (Object) item - Object with nick and role or jid and affiliation attribute
|
||||
* (String) reason - Optional reason for the change.
|
||||
* (Function) handler_cb - Optional callback for success
|
||||
* (Function) errer_cb - Optional callback for error
|
||||
* Returns:
|
||||
* iq - the id of the mode change request.
|
||||
*/
|
||||
var iq;
|
||||
iq = $iq({
|
||||
to: room,
|
||||
type: "set"
|
||||
}).c("query", {
|
||||
xmlns: Strophe.NS.MUC_ADMIN
|
||||
}).cnode(item.node);
|
||||
if (reason !== null) { iq.c("reason", reason); }
|
||||
return this._connection.sendIQ(iq.tree(), handler_cb, error_cb);
|
||||
},
|
||||
|
||||
modifyRole: function(room, nick, role, reason, handler_cb, error_cb) {
|
||||
/* Changes the role of a member of a MUC room.
|
||||
* The modification can only be done by a room moderator. An error will be
|
||||
* returned if the user doesn't have permission.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) nick - The nick name of the user to modify.
|
||||
* (String) role - The new role of the user.
|
||||
* (String) affiliation - The new affiliation of the user.
|
||||
* (String) reason - Optional reason for the change.
|
||||
* (Function) handler_cb - Optional callback for success
|
||||
* (Function) errer_cb - Optional callback for error
|
||||
* Returns:
|
||||
* iq - the id of the mode change request.
|
||||
*/
|
||||
var item;
|
||||
item = $build("item", {
|
||||
nick: nick,
|
||||
role: role
|
||||
});
|
||||
return this._modifyPrivilege(room, item, reason, handler_cb, error_cb);
|
||||
},
|
||||
|
||||
kick: function(room, nick, reason, handler_cb, error_cb) {
|
||||
return this.modifyRole(room, nick, 'none', reason, handler_cb, error_cb);
|
||||
},
|
||||
voice: function(room, nick, reason, handler_cb, error_cb) {
|
||||
return this.modifyRole(room, nick, 'participant', reason, handler_cb, error_cb);
|
||||
},
|
||||
mute: function(room, nick, reason, handler_cb, error_cb) {
|
||||
return this.modifyRole(room, nick, 'visitor', reason, handler_cb, error_cb);
|
||||
},
|
||||
op: function(room, nick, reason, handler_cb, error_cb) {
|
||||
return this.modifyRole(room, nick, 'moderator', reason, handler_cb, error_cb);
|
||||
},
|
||||
deop: function(room, nick, reason, handler_cb, error_cb) {
|
||||
return this.modifyRole(room, nick, 'participant', reason, handler_cb, error_cb);
|
||||
},
|
||||
|
||||
modifyAffiliation: function(room, jid, affiliation, reason, handler_cb, error_cb) {
|
||||
/* Changes the affiliation of a member of a MUC room.
|
||||
* The modification can only be done by a room moderator. An error will be
|
||||
* returned if the user doesn't have permission.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) jid - The jid of the user to modify.
|
||||
* (String) affiliation - The new affiliation of the user.
|
||||
* (String) reason - Optional reason for the change.
|
||||
* (Function) handler_cb - Optional callback for success
|
||||
* (Function) errer_cb - Optional callback for error
|
||||
* Returns:
|
||||
* iq - the id of the mode change request.
|
||||
*/
|
||||
var item;
|
||||
item = $build("item", {
|
||||
jid: jid,
|
||||
affiliation: affiliation
|
||||
});
|
||||
return this._modifyPrivilege(room, item, reason, handler_cb, error_cb);
|
||||
},
|
||||
ban: function(room, jid, reason, handler_cb, error_cb) {
|
||||
return this.modifyAffiliation(room, jid, 'outcast', reason, handler_cb, error_cb);
|
||||
},
|
||||
member: function(room, jid, reason, handler_cb, error_cb) {
|
||||
return this.modifyAffiliation(room, jid, 'member', reason, handler_cb, error_cb);
|
||||
},
|
||||
revoke: function(room, jid, reason, handler_cb, error_cb) {
|
||||
return this.modifyAffiliation(room, jid, 'none', reason, handler_cb, error_cb);
|
||||
},
|
||||
owner: function(room, jid, reason, handler_cb, error_cb) {
|
||||
return this.modifyAffiliation(room, jid, 'owner', reason, handler_cb, error_cb);
|
||||
},
|
||||
admin: function(room, jid, reason, handler_cb, error_cb) {
|
||||
return this.modifyAffiliation(room, jid, 'admin', reason, handler_cb, error_cb);
|
||||
},
|
||||
|
||||
changeNick: function(room, user) {
|
||||
/* Change the current users nick name.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) user - The new nick name.
|
||||
*/
|
||||
var presence, room_nick;
|
||||
room_nick = this.test_append_nick(room, user);
|
||||
presence = $pres({
|
||||
from: this._connection.jid,
|
||||
to: room_nick,
|
||||
id: this._connection.getUniqueId()
|
||||
});
|
||||
return this._connection.send(presence.tree());
|
||||
},
|
||||
|
||||
setStatus: function(room, user, show, status) {
|
||||
/* Change the current users status.
|
||||
* Parameters:
|
||||
* (String) room - The multi-user chat room name.
|
||||
* (String) user - The current nick.
|
||||
* (String) show - The new show-text.
|
||||
* (String) status - The new status-text.
|
||||
*/
|
||||
var presence, room_nick;
|
||||
room_nick = this.test_append_nick(room, user);
|
||||
presence = $pres({
|
||||
from: this._connection.jid,
|
||||
to: room_nick
|
||||
});
|
||||
if (show !== null) { presence.c('show', show).up(); }
|
||||
if (status !== null) { presence.c('status', status); }
|
||||
return this._connection.send(presence.tree());
|
||||
},
|
||||
|
||||
listRooms: function(server, handle_cb) {
|
||||
/* List all chat room available on a server.
|
||||
* Parameters:
|
||||
* (String) server - name of chat server.
|
||||
* (String) handle_cb - Function to call for room list return.
|
||||
*/
|
||||
var iq;
|
||||
iq = $iq({
|
||||
to: server,
|
||||
from: this._connection.jid,
|
||||
type: "get"
|
||||
}).c("query", {
|
||||
xmlns: Strophe.NS.DISCO_ITEMS
|
||||
});
|
||||
return this._connection.sendIQ(iq, handle_cb);
|
||||
},
|
||||
test_append_nick: function(room, nick) {
|
||||
return room + (nick !== null ? "/" + (Strophe.escapeNode(nick)) : "");
|
||||
}
|
||||
});
|
||||
|
||||
XmppRoom = (function() {
|
||||
function XmppRoom(client, name, nick, password) {
|
||||
this.roster = {};
|
||||
this._message_handlers = {};
|
||||
this._presence_handlers = {};
|
||||
this._roster_handlers = {};
|
||||
this._handler_ids = 0;
|
||||
|
||||
this.client = client;
|
||||
this.name = name;
|
||||
this.nick = nick;
|
||||
this.password = password;
|
||||
this._roomRosterHandler = __bind(this._roomRosterHandler, this);
|
||||
this._addOccupant = __bind(this._addOccupant, this);
|
||||
if (client.muc) { this.client = client.muc; }
|
||||
this.name = Strophe.getBareJidFromJid(name);
|
||||
this.client.rooms[this.name] = this;
|
||||
this.addHandler('presence', this._roomRosterHandler);
|
||||
}
|
||||
|
||||
XmppRoom.prototype.join = function(msg_handler_cb, pres_handler_cb) {
|
||||
if (!this.client.rooms[this.name]) {
|
||||
return this.client.join(this.name, this.nick, msg_handler_cb, pres_handler_cb, this.password);
|
||||
}
|
||||
};
|
||||
|
||||
XmppRoom.prototype.leave = function(handler_cb, message) {
|
||||
this.client.leave(this.name, this.nick, handler_cb, message);
|
||||
return delete this.client.rooms[this.name];
|
||||
};
|
||||
|
||||
XmppRoom.prototype.message = function(nick, message, html_message, type) {
|
||||
return this.client.message(this.name, nick, message, html_message, type);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.groupchat = function(message, html_message) {
|
||||
return this.client.groupchat(this.name, message, html_message);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.invite = function(receiver, reason) {
|
||||
return this.client.invite(this.name, receiver, reason);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.directInvite = function(receiver, reason) {
|
||||
return this.client.directInvite(this.name, receiver, reason, this.password);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.configure = function(handler_cb) {
|
||||
return this.client.configure(this.name, handler_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.cancelConfigure = function() {
|
||||
return this.client.cancelConfigure(this.name);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.saveConfiguration = function(configarray) {
|
||||
return this.client.saveConfiguration(this.name, configarray);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.queryOccupants = function(success_cb, error_cb) {
|
||||
return this.client.queryOccupants(this.name, success_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.setTopic = function(topic) {
|
||||
return this.client.setTopic(this.name, topic);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.modifyRole = function(nick, role, reason, success_cb, error_cb) {
|
||||
return this.client.modifyRole(this.name, nick, role, reason, success_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.kick = function(nick, reason, handler_cb, error_cb) {
|
||||
return this.client.kick(this.name, nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.voice = function(nick, reason, handler_cb, error_cb) {
|
||||
return this.client.voice(this.name, nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.mute = function(nick, reason, handler_cb, error_cb) {
|
||||
return this.client.mute(this.name, nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.op = function(nick, reason, handler_cb, error_cb) {
|
||||
return this.client.op(this.name, nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.deop = function(nick, reason, handler_cb, error_cb) {
|
||||
return this.client.deop(this.name, nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.modifyAffiliation = function(jid, affiliation, reason, success_cb, error_cb) {
|
||||
return this.client.modifyAffiliation(this.name, jid, affiliation, reason, success_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.ban = function(jid, reason, handler_cb, error_cb) {
|
||||
return this.client.ban(this.name, jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.member = function(jid, reason, handler_cb, error_cb) {
|
||||
return this.client.member(this.name, jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.revoke = function(jid, reason, handler_cb, error_cb) {
|
||||
return this.client.revoke(this.name, jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.owner = function(jid, reason, handler_cb, error_cb) {
|
||||
return this.client.owner(this.name, jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.admin = function(jid, reason, handler_cb, error_cb) {
|
||||
return this.client.admin(this.name, jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.changeNick = function(nick) {
|
||||
this.nick = nick;
|
||||
return this.client.changeNick(this.name, nick);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.setStatus = function(show, status) {
|
||||
return this.client.setStatus(this.name, this.nick, show, status);
|
||||
};
|
||||
|
||||
XmppRoom.prototype.addHandler = function(handler_type, handler) {
|
||||
/* Adds a handler to the MUC room.
|
||||
* Parameters:
|
||||
* (String) handler_type - 'message', 'presence' or 'roster'.
|
||||
* (Function) handler - The handler function.
|
||||
* Returns:
|
||||
* id - the id of handler.
|
||||
*/
|
||||
var id;
|
||||
id = this._handler_ids++;
|
||||
switch (handler_type) {
|
||||
case 'presence':
|
||||
this._presence_handlers[id] = handler;
|
||||
break;
|
||||
case 'message':
|
||||
this._message_handlers[id] = handler;
|
||||
break;
|
||||
case 'roster':
|
||||
this._roster_handlers[id] = handler;
|
||||
break;
|
||||
default:
|
||||
this._handler_ids--;
|
||||
return null;
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
XmppRoom.prototype.removeHandler = function(id) {
|
||||
/* Removes a handler from the MUC room.
|
||||
* This function takes ONLY ids returned by the addHandler function
|
||||
* of this room. passing handler ids returned by connection.addHandler
|
||||
* may brake things!
|
||||
* Parameters:
|
||||
* (number) id - the id of the handler
|
||||
*/
|
||||
delete this._presence_handlers[id];
|
||||
delete this._message_handlers[id];
|
||||
return delete this._roster_handlers[id];
|
||||
};
|
||||
|
||||
XmppRoom.prototype._addOccupant = function(data) {
|
||||
/* Creates and adds an Occupant to the Room Roster.
|
||||
* Parameters:
|
||||
* (Object) data - the data the Occupant is filled with
|
||||
* Returns:
|
||||
* occ - the created Occupant.
|
||||
*/
|
||||
var occ;
|
||||
occ = new Occupant(data, this);
|
||||
this.roster[occ.nick] = occ;
|
||||
return occ;
|
||||
};
|
||||
|
||||
XmppRoom.prototype._roomRosterHandler = function(pres) {
|
||||
/* The standard handler that managed the Room Roster.
|
||||
* Parameters:
|
||||
* (Object) pres - the presence stanza containing user information
|
||||
*/
|
||||
var data, handler, id, newnick, nick, _ref;
|
||||
data = XmppRoom._parsePresence(pres);
|
||||
nick = data.nick;
|
||||
newnick = data.newnick || null;
|
||||
switch (data.type) {
|
||||
case 'error':
|
||||
return;
|
||||
case 'unavailable':
|
||||
if (newnick) {
|
||||
data.nick = newnick;
|
||||
if (this.roster[nick] && this.roster[newnick]) {
|
||||
this.roster[nick].update(this.roster[newnick]);
|
||||
this.roster[newnick] = this.roster[nick];
|
||||
}
|
||||
if (this.roster[nick] && !this.roster[newnick]) {
|
||||
this.roster[newnick] = this.roster[nick].update(data);
|
||||
}
|
||||
}
|
||||
delete this.roster[nick];
|
||||
break;
|
||||
default:
|
||||
if (this.roster[nick]) {
|
||||
this.roster[nick].update(data);
|
||||
} else {
|
||||
this._addOccupant(data);
|
||||
}
|
||||
}
|
||||
_ref = this._roster_handlers;
|
||||
for (id in _ref) {
|
||||
handler = _ref[id];
|
||||
if (!handler(this.roster, this)) { delete this._roster_handlers[id]; }
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
XmppRoom._parsePresence = function(pres) {
|
||||
/* Parses a presence stanza into a map
|
||||
* Parameters:
|
||||
* (Object) pres - the presence stanza
|
||||
* Returns:
|
||||
* (Object) data - the data extracted from the presence stanza
|
||||
*/
|
||||
var $pres, data, i, j, children, item;
|
||||
$pres = $(pres);
|
||||
data = {};
|
||||
data.nick = Strophe.getResourceFromJid($pres.attr('from'));
|
||||
data.type = $pres.attr('type');
|
||||
data.states = [];
|
||||
for (i=0; i < $pres.children().length; i++) {
|
||||
child = $pres.children()[0];
|
||||
switch (child.nodeName) {
|
||||
case 'status':
|
||||
data.status = child.textContent || null;
|
||||
break;
|
||||
case 'show':
|
||||
data.show = child.textContent || null;
|
||||
break;
|
||||
case 'x':
|
||||
if ($(child).attr('xmlns') === Strophe.NS.MUC_USER) {
|
||||
children = $(child).children();
|
||||
for (j=0; j < children.length; j++) {
|
||||
item = children[0];
|
||||
switch (item.nodeName) {
|
||||
case "item":
|
||||
a = item.attributes;
|
||||
data.affiliation = $(item).attr('affiliation') || null;
|
||||
data.role = $(item).attr('role') || null;
|
||||
data.jid = $(item).attr('jid') || null;
|
||||
data.newnick = $(item).attr('nick') || null;
|
||||
break;
|
||||
case "status":
|
||||
if ($(item).attr('code')) {
|
||||
data.states.push($(item).attr('code'));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
return XmppRoom;
|
||||
|
||||
})();
|
||||
|
||||
RoomConfig = (function() {
|
||||
|
||||
function RoomConfig(info) {
|
||||
this.parse = __bind(this.parse, this);
|
||||
if (info !== null) { this.parse(info); }
|
||||
}
|
||||
|
||||
RoomConfig.prototype.parse = function(result) {
|
||||
var attr, attrs, child, field, identity, query, _i, _j, _k, _len, _len2, _len3, _ref;
|
||||
query = result.getElementsByTagName("query")[0].childNodes;
|
||||
this.identities = [];
|
||||
this.features = [];
|
||||
this.x = [];
|
||||
for (_i = 0, _len = query.length; _i < _len; _i++) {
|
||||
child = query[_i];
|
||||
attrs = child.attributes;
|
||||
switch (child.nodeName) {
|
||||
case "identity":
|
||||
identity = {};
|
||||
for (_j = 0, _len2 = attrs.length; _j < _len2; _j++) {
|
||||
attr = attrs[_j];
|
||||
identity[attr.name] = attr.textContent;
|
||||
}
|
||||
this.identities.push(identity);
|
||||
break;
|
||||
case "feature":
|
||||
this.features.push(attrs["var"].textContent);
|
||||
break;
|
||||
case "x":
|
||||
attrs = child.childNodes[0].attributes;
|
||||
if ((!attrs["var"].textContent === 'FORM_TYPE') || (!attrs.type.textContent === 'hidden')) {
|
||||
break;
|
||||
}
|
||||
_ref = child.childNodes;
|
||||
for (_k = 0, _len3 = _ref.length; _k < _len3; _k++) {
|
||||
field = _ref[_k];
|
||||
if (!(!field.attributes.type)) continue;
|
||||
|
||||
attrs = field.attributes;
|
||||
this.x.push({
|
||||
"var": attrs["var"].textContent,
|
||||
label: attrs.label.textContent || "",
|
||||
value: field.firstChild.textContent || ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
"identities": this.identities,
|
||||
"features": this.features,
|
||||
"x": this.x
|
||||
};
|
||||
};
|
||||
|
||||
return RoomConfig;
|
||||
})();
|
||||
|
||||
Occupant = (function() {
|
||||
|
||||
function Occupant(data, room) {
|
||||
this.room = room;
|
||||
this.update = __bind(this.update, this);
|
||||
this.admin = __bind(this.admin, this);
|
||||
this.owner = __bind(this.owner, this);
|
||||
this.revoke = __bind(this.revoke, this);
|
||||
this.member = __bind(this.member, this);
|
||||
this.ban = __bind(this.ban, this);
|
||||
this.modifyAffiliation = __bind(this.modifyAffiliation, this);
|
||||
this.deop = __bind(this.deop, this);
|
||||
this.op = __bind(this.op, this);
|
||||
this.mute = __bind(this.mute, this);
|
||||
this.voice = __bind(this.voice, this);
|
||||
this.kick = __bind(this.kick, this);
|
||||
this.modifyRole = __bind(this.modifyRole, this);
|
||||
this.update(data);
|
||||
}
|
||||
|
||||
Occupant.prototype.modifyRole = function(role, reason, success_cb, error_cb) {
|
||||
return this.room.modifyRole(this.nick, role, reason, success_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.kick = function(reason, handler_cb, error_cb) {
|
||||
return this.room.kick(this.nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.voice = function(reason, handler_cb, error_cb) {
|
||||
return this.room.voice(this.nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.mute = function(reason, handler_cb, error_cb) {
|
||||
return this.room.mute(this.nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.op = function(reason, handler_cb, error_cb) {
|
||||
return this.room.op(this.nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.deop = function(reason, handler_cb, error_cb) {
|
||||
return this.room.deop(this.nick, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.modifyAffiliation = function(affiliation, reason, success_cb, error_cb) {
|
||||
return this.room.modifyAffiliation(this.jid, affiliation, reason, success_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.ban = function(reason, handler_cb, error_cb) {
|
||||
return this.room.ban(this.jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.member = function(reason, handler_cb, error_cb) {
|
||||
return this.room.member(this.jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.revoke = function(reason, handler_cb, error_cb) {
|
||||
return this.room.revoke(this.jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.owner = function(reason, handler_cb, error_cb) {
|
||||
return this.room.owner(this.jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.admin = function(reason, handler_cb, error_cb) {
|
||||
return this.room.admin(this.jid, reason, handler_cb, error_cb);
|
||||
};
|
||||
|
||||
Occupant.prototype.update = function(data) {
|
||||
this.nick = data.nick || null;
|
||||
this.affiliation = data.affiliation || null;
|
||||
this.role = data.role || null;
|
||||
this.jid = data.jid || null;
|
||||
this.status = data.status || null;
|
||||
this.show = data.show || null;
|
||||
return this;
|
||||
};
|
||||
return Occupant;
|
||||
|
||||
})();
|
||||
|
||||
}).call(this);
|
||||
}));
|
443
Libraries/strophe.roster.js
Normal file
@ -0,0 +1,443 @@
|
||||
/*
|
||||
Copyright 2010, François de Metz <francois@2metz.fr>
|
||||
*/
|
||||
/**
|
||||
* Roster Plugin
|
||||
* Allow easily roster management
|
||||
*
|
||||
* Features
|
||||
* * Get roster from server
|
||||
* * handle presence
|
||||
* * handle roster iq
|
||||
* * subscribe/unsubscribe
|
||||
* * authorize/unauthorize
|
||||
* * roster versioning (xep 237)
|
||||
*/
|
||||
Strophe.addConnectionPlugin('roster',
|
||||
{
|
||||
_connection: null,
|
||||
|
||||
_callbacks : [],
|
||||
/** Property: items
|
||||
* Roster items
|
||||
* [
|
||||
* {
|
||||
* name : "",
|
||||
* jid : "",
|
||||
* subscription : "",
|
||||
* ask : "",
|
||||
* groups : ["", ""],
|
||||
* resources : {
|
||||
* myresource : {
|
||||
* show : "",
|
||||
* status : "",
|
||||
* priority : ""
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
items : [],
|
||||
/** Property: ver
|
||||
* current roster revision
|
||||
* always null if server doesn't support xep 237
|
||||
*/
|
||||
ver : null,
|
||||
/** Function: init
|
||||
* Plugin init
|
||||
*
|
||||
* Parameters:
|
||||
* (Strophe.Connection) conn - Strophe connection
|
||||
*/
|
||||
init: function(conn)
|
||||
{
|
||||
this._connection = conn;
|
||||
this.items = [];
|
||||
// Override the connect and attach methods to always add presence and roster handlers.
|
||||
// They are removed when the connection disconnects, so must be added on connection.
|
||||
var oldCallback, roster = this, _connect = conn.connect, _attach = conn.attach;
|
||||
var newCallback = function(status)
|
||||
{
|
||||
if (status == Strophe.Status.ATTACHED || status == Strophe.Status.CONNECTED)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Presence subscription
|
||||
conn.addHandler(roster._onReceivePresence.bind(roster), null, 'presence', null, null, null);
|
||||
conn.addHandler(roster._onReceiveIQ.bind(roster), Strophe.NS.ROSTER, 'iq', "set", null, null);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
Strophe.error(e);
|
||||
}
|
||||
}
|
||||
if (oldCallback !== null)
|
||||
oldCallback.apply(this, arguments);
|
||||
};
|
||||
conn.connect = function(jid, pass, callback, wait, hold)
|
||||
{
|
||||
oldCallback = callback;
|
||||
if (typeof arguments[0] == "undefined")
|
||||
arguments[0] = null;
|
||||
if (typeof arguments[1] == "undefined")
|
||||
arguments[1] = null;
|
||||
arguments[2] = newCallback;
|
||||
_connect.apply(conn, arguments);
|
||||
};
|
||||
conn.attach = function(jid, sid, rid, callback, wait, hold, wind)
|
||||
{
|
||||
oldCallback = callback;
|
||||
if (typeof arguments[0] == "undefined")
|
||||
arguments[0] = null;
|
||||
if (typeof arguments[1] == "undefined")
|
||||
arguments[1] = null;
|
||||
if (typeof arguments[2] == "undefined")
|
||||
arguments[2] = null;
|
||||
arguments[3] = newCallback;
|
||||
_attach.apply(conn, arguments);
|
||||
};
|
||||
|
||||
Strophe.addNamespace('ROSTER_VER', 'urn:xmpp:features:rosterver');
|
||||
Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
|
||||
},
|
||||
/** Function: supportVersioning
|
||||
* return true if roster versioning is enabled on server
|
||||
*/
|
||||
supportVersioning: function()
|
||||
{
|
||||
return (this._connection.features && this._connection.features.getElementsByTagName('ver').length > 0);
|
||||
},
|
||||
/** Function: get
|
||||
* Get Roster on server
|
||||
*
|
||||
* Parameters:
|
||||
* (Function) userCallback - callback on roster result
|
||||
* (String) ver - current rev of roster
|
||||
* (only used if roster versioning is enabled)
|
||||
* (Array) items - initial items of ver
|
||||
* (only used if roster versioning is enabled)
|
||||
* In browser context you can use sessionStorage
|
||||
* to store your roster in json (JSON.stringify())
|
||||
*/
|
||||
get: function(userCallback, ver, items)
|
||||
{
|
||||
var attrs = {xmlns: Strophe.NS.ROSTER};
|
||||
this.items = [];
|
||||
if (this.supportVersioning())
|
||||
{
|
||||
// empty rev because i want an rev attribute in the result
|
||||
attrs.ver = ver || '';
|
||||
this.items = items || [];
|
||||
}
|
||||
var iq = $iq({type: 'get', 'id' : this._connection.getUniqueId('roster')}).c('query', attrs);
|
||||
return this._connection.sendIQ(iq,
|
||||
this._onReceiveRosterSuccess.bind(this, userCallback),
|
||||
this._onReceiveRosterError.bind(this, userCallback));
|
||||
},
|
||||
/** Function: registerCallback
|
||||
* register callback on roster (presence and iq)
|
||||
*
|
||||
* Parameters:
|
||||
* (Function) call_back
|
||||
*/
|
||||
registerCallback: function(call_back)
|
||||
{
|
||||
this._callbacks.push(call_back);
|
||||
},
|
||||
/** Function: findItem
|
||||
* Find item by JID
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid
|
||||
*/
|
||||
findItem : function(jid)
|
||||
{
|
||||
for (var i = 0; i < this.items.length; i++)
|
||||
{
|
||||
if (this.items[i] && this.items[i].jid == jid)
|
||||
{
|
||||
return this.items[i];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/** Function: removeItem
|
||||
* Remove item by JID
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid
|
||||
*/
|
||||
removeItem : function(jid)
|
||||
{
|
||||
for (var i = 0; i < this.items.length; i++)
|
||||
{
|
||||
if (this.items[i] && this.items[i].jid == jid)
|
||||
{
|
||||
this.items.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/** Function: subscribe
|
||||
* Subscribe presence
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid
|
||||
* (String) message (optional)
|
||||
* (String) nick (optional)
|
||||
*/
|
||||
subscribe: function(jid, message, nick) {
|
||||
var pres = $pres({to: jid, type: "subscribe"});
|
||||
if (message && message !== "") {
|
||||
pres.c("status").t(message);
|
||||
}
|
||||
if (nick && nick !== "") {
|
||||
pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick);
|
||||
}
|
||||
this._connection.send(pres);
|
||||
},
|
||||
/** Function: unsubscribe
|
||||
* Unsubscribe presence
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid
|
||||
* (String) message
|
||||
*/
|
||||
unsubscribe: function(jid, message)
|
||||
{
|
||||
var pres = $pres({to: jid, type: "unsubscribe"});
|
||||
if (message && message != "")
|
||||
pres.c("status").t(message);
|
||||
this._connection.send(pres);
|
||||
},
|
||||
/** Function: authorize
|
||||
* Authorize presence subscription
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid
|
||||
* (String) message
|
||||
*/
|
||||
authorize: function(jid, message)
|
||||
{
|
||||
var pres = $pres({to: jid, type: "subscribed"});
|
||||
if (message && message != "")
|
||||
pres.c("status").t(message);
|
||||
this._connection.send(pres);
|
||||
},
|
||||
/** Function: unauthorize
|
||||
* Unauthorize presence subscription
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid
|
||||
* (String) message
|
||||
*/
|
||||
unauthorize: function(jid, message)
|
||||
{
|
||||
var pres = $pres({to: jid, type: "unsubscribed"});
|
||||
if (message && message != "")
|
||||
pres.c("status").t(message);
|
||||
this._connection.send(pres);
|
||||
},
|
||||
/** Function: add
|
||||
* Add roster item
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - item jid
|
||||
* (String) name - name
|
||||
* (Array) groups
|
||||
* (Function) call_back
|
||||
*/
|
||||
add: function(jid, name, groups, call_back)
|
||||
{
|
||||
var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: jid,
|
||||
name: name});
|
||||
for (var i = 0; i < groups.length; i++)
|
||||
{
|
||||
iq.c('group').t(groups[i]).up();
|
||||
}
|
||||
this._connection.sendIQ(iq, call_back, call_back);
|
||||
},
|
||||
/** Function: update
|
||||
* Update roster item
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - item jid
|
||||
* (String) name - name
|
||||
* (Array) groups
|
||||
* (Function) call_back
|
||||
*/
|
||||
update: function(jid, name, groups, call_back)
|
||||
{
|
||||
var item = this.findItem(jid);
|
||||
if (!item)
|
||||
{
|
||||
throw "item not found";
|
||||
}
|
||||
var newName = name || item.name;
|
||||
var newGroups = groups || item.groups;
|
||||
var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid,
|
||||
name: newName});
|
||||
for (var i = 0; i < newGroups.length; i++)
|
||||
{
|
||||
iq.c('group').t(newGroups[i]).up();
|
||||
}
|
||||
return this._connection.sendIQ(iq, call_back, call_back);
|
||||
},
|
||||
/** Function: remove
|
||||
* Remove roster item
|
||||
*
|
||||
* Parameters:
|
||||
* (String) jid - item jid
|
||||
* (Function) call_back
|
||||
*/
|
||||
remove: function(jid, call_back)
|
||||
{
|
||||
var item = this.findItem(jid);
|
||||
if (!item)
|
||||
{
|
||||
throw "item not found";
|
||||
}
|
||||
var iq = $iq({type: 'set'}).c('query', {xmlns: Strophe.NS.ROSTER}).c('item', {jid: item.jid,
|
||||
subscription: "remove"});
|
||||
this._connection.sendIQ(iq, call_back, call_back);
|
||||
},
|
||||
/** PrivateFunction: _onReceiveRosterSuccess
|
||||
*
|
||||
*/
|
||||
_onReceiveRosterSuccess: function(userCallback, stanza)
|
||||
{
|
||||
this._updateItems(stanza);
|
||||
userCallback(this.items);
|
||||
},
|
||||
/** PrivateFunction: _onReceiveRosterError
|
||||
*
|
||||
*/
|
||||
_onReceiveRosterError: function(userCallback, stanza)
|
||||
{
|
||||
userCallback(this.items);
|
||||
},
|
||||
/** PrivateFunction: _onReceivePresence
|
||||
* Handle presence
|
||||
*/
|
||||
_onReceivePresence : function(presence)
|
||||
{
|
||||
// TODO: from is optional
|
||||
var jid = presence.getAttribute('from');
|
||||
var from = Strophe.getBareJidFromJid(jid);
|
||||
var item = this.findItem(from);
|
||||
// not in roster
|
||||
if (!item)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var type = presence.getAttribute('type');
|
||||
if (type == 'unavailable')
|
||||
{
|
||||
delete item.resources[Strophe.getResourceFromJid(jid)];
|
||||
}
|
||||
else if (!type)
|
||||
{
|
||||
// TODO: add timestamp
|
||||
item.resources[Strophe.getResourceFromJid(jid)] = {
|
||||
show : (presence.getElementsByTagName('show').length != 0) ? Strophe.getText(presence.getElementsByTagName('show')[0]) : "",
|
||||
status : (presence.getElementsByTagName('status').length != 0) ? Strophe.getText(presence.getElementsByTagName('status')[0]) : "",
|
||||
priority : (presence.getElementsByTagName('priority').length != 0) ? Strophe.getText(presence.getElementsByTagName('priority')[0]) : ""
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stanza is not a presence notification. (It's probably a subscription type stanza.)
|
||||
return true;
|
||||
}
|
||||
this._call_backs(this.items, item);
|
||||
return true;
|
||||
},
|
||||
/** PrivateFunction: _call_backs
|
||||
*
|
||||
*/
|
||||
_call_backs : function(items, item)
|
||||
{
|
||||
for (var i = 0; i < this._callbacks.length; i++) // [].forEach my love ...
|
||||
{
|
||||
this._callbacks[i](items, item);
|
||||
}
|
||||
},
|
||||
/** PrivateFunction: _onReceiveIQ
|
||||
* Handle roster push.
|
||||
*/
|
||||
_onReceiveIQ : function(iq)
|
||||
{
|
||||
var id = iq.getAttribute('id');
|
||||
var from = iq.getAttribute('from');
|
||||
// Receiving client MUST ignore stanza unless it has no from or from = user's JID.
|
||||
if (from && from != "" && from != this._connection.jid && from != Strophe.getBareJidFromJid(this._connection.jid))
|
||||
return true;
|
||||
var iqresult = $iq({type: 'result', id: id, from: this._connection.jid});
|
||||
this._connection.send(iqresult);
|
||||
this._updateItems(iq);
|
||||
return true;
|
||||
},
|
||||
/** PrivateFunction: _updateItems
|
||||
* Update items from iq
|
||||
*/
|
||||
_updateItems : function(iq)
|
||||
{
|
||||
var query = iq.getElementsByTagName('query');
|
||||
if (query.length != 0)
|
||||
{
|
||||
this.ver = query.item(0).getAttribute('ver');
|
||||
var self = this;
|
||||
Strophe.forEachChild(query.item(0), 'item',
|
||||
function (item)
|
||||
{
|
||||
self._updateItem(item);
|
||||
}
|
||||
);
|
||||
}
|
||||
this._call_backs(this.items);
|
||||
},
|
||||
/** PrivateFunction: _updateItem
|
||||
* Update internal representation of roster item
|
||||
*/
|
||||
_updateItem : function(item)
|
||||
{
|
||||
var jid = item.getAttribute("jid");
|
||||
var name = item.getAttribute("name");
|
||||
var subscription = item.getAttribute("subscription");
|
||||
var ask = item.getAttribute("ask");
|
||||
var groups = [];
|
||||
Strophe.forEachChild(item, 'group',
|
||||
function(group)
|
||||
{
|
||||
groups.push(Strophe.getText(group));
|
||||
}
|
||||
);
|
||||
|
||||
if (subscription == "remove")
|
||||
{
|
||||
this.removeItem(jid);
|
||||
return;
|
||||
}
|
||||
|
||||
var item = this.findItem(jid);
|
||||
if (!item)
|
||||
{
|
||||
this.items.push({
|
||||
name : name,
|
||||
jid : jid,
|
||||
subscription : subscription,
|
||||
ask : ask,
|
||||
groups : groups,
|
||||
resources : {}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
item.name = name;
|
||||
item.subscription = subscription;
|
||||
item.ask = ask;
|
||||
item.groups = groups;
|
||||
}
|
||||
}
|
||||
});
|
66
Libraries/strophe.vcard.js
Normal file
@ -0,0 +1,66 @@
|
||||
// Generated by CoffeeScript 1.3.3
|
||||
/*
|
||||
Plugin to implement the vCard extension.
|
||||
http://xmpp.org/extensions/xep-0054.html
|
||||
|
||||
Author: Nathan Zorn (nathan.zorn@gmail.com)
|
||||
CoffeeScript port: Andreas Guth (guth@dbis.rwth-aachen.de)
|
||||
*/
|
||||
|
||||
/* jslint configuration:
|
||||
*/
|
||||
|
||||
/* global document, window, setTimeout, clearTimeout, console,
|
||||
XMLHttpRequest, ActiveXObject,
|
||||
Base64, MD5,
|
||||
Strophe, $build, $msg, $iq, $pres
|
||||
*/
|
||||
|
||||
var buildIq;
|
||||
|
||||
buildIq = function(type, jid, vCardEl) {
|
||||
var iq;
|
||||
iq = $iq(jid ? {
|
||||
type: type,
|
||||
to: jid
|
||||
} : {
|
||||
type: type
|
||||
});
|
||||
iq.c("vCard", {
|
||||
xmlns: Strophe.NS.VCARD
|
||||
});
|
||||
if (vCardEl) {
|
||||
iq.cnode(vCardEl);
|
||||
}
|
||||
return iq;
|
||||
};
|
||||
|
||||
Strophe.addConnectionPlugin('vcard', {
|
||||
_connection: null,
|
||||
init: function(conn) {
|
||||
this._connection = conn;
|
||||
return Strophe.addNamespace('VCARD', 'vcard-temp');
|
||||
},
|
||||
/*Function
|
||||
Retrieve a vCard for a JID/Entity
|
||||
Parameters:
|
||||
(Function) handler_cb - The callback function used to handle the request.
|
||||
(String) jid - optional - The name of the entity to request the vCard
|
||||
If no jid is given, this function retrieves the current user's vcard.
|
||||
*/
|
||||
|
||||
get: function(handler_cb, jid, error_cb) {
|
||||
var iq;
|
||||
iq = buildIq("get", jid);
|
||||
return this._connection.sendIQ(iq, handler_cb, error_cb);
|
||||
},
|
||||
/* Function
|
||||
Set an entity's vCard.
|
||||
*/
|
||||
|
||||
set: function(handler_cb, vCardEl, jid, error_cb) {
|
||||
var iq;
|
||||
iq = buildIq("set", jid, vCardEl);
|
||||
return this._connection.sendIQ(iq, handler_cb, error_rb);
|
||||
}
|
||||
});
|
1226
Libraries/underscore.js
Normal file
155
Libraries/xml2json.js
Normal file
@ -0,0 +1,155 @@
|
||||
/* This work is licensed under Creative Commons GNU LGPL License.
|
||||
|
||||
License: http://creativecommons.org/licenses/LGPL/2.1/
|
||||
Version: 0.9
|
||||
Author: Stefan Goessner/2006
|
||||
Web: http://goessner.net/
|
||||
*/
|
||||
function xml2json(xml, tab) {
|
||||
var X = {
|
||||
toObj: function(xml) {
|
||||
var o = {};
|
||||
if (xml.nodeType==1) { // element node ..
|
||||
if (xml.attributes.length) // element with attributes ..
|
||||
for (var i=0; i<xml.attributes.length; i++)
|
||||
o["@"+xml.attributes[i].nodeName] = (xml.attributes[i].nodeValue||"").toString();
|
||||
if (xml.firstChild) { // element has child nodes ..
|
||||
var textChild=0, cdataChild=0, hasElementChild=false;
|
||||
for (var n=xml.firstChild; n; n=n.nextSibling) {
|
||||
if (n.nodeType==1) hasElementChild = true;
|
||||
else if (n.nodeType==3 && n.nodeValue.match(/[^ \f\n\r\t\v]/)) textChild++; // non-whitespace text
|
||||
else if (n.nodeType==4) cdataChild++; // cdata section node
|
||||
}
|
||||
if (hasElementChild) {
|
||||
if (textChild < 2 && cdataChild < 2) { // structured element with evtl. a single text or/and cdata node ..
|
||||
X.removeWhite(xml);
|
||||
for (var n=xml.firstChild; n; n=n.nextSibling) {
|
||||
if (n.nodeType == 3) // text node
|
||||
o["#text"] = X.escape(n.nodeValue);
|
||||
else if (n.nodeType == 4) // cdata node
|
||||
o["#cdata"] = X.escape(n.nodeValue);
|
||||
else if (o[n.nodeName]) { // multiple occurence of element ..
|
||||
if (o[n.nodeName] instanceof Array)
|
||||
o[n.nodeName][o[n.nodeName].length] = X.toObj(n);
|
||||
else
|
||||
o[n.nodeName] = [o[n.nodeName], X.toObj(n)];
|
||||
}
|
||||
else // first occurence of element..
|
||||
o[n.nodeName] = X.toObj(n);
|
||||
}
|
||||
}
|
||||
else { // mixed content
|
||||
if (!xml.attributes.length)
|
||||
o = X.escape(X.innerXml(xml));
|
||||
else
|
||||
o["#text"] = X.escape(X.innerXml(xml));
|
||||
}
|
||||
}
|
||||
else if (textChild) { // pure text
|
||||
if (!xml.attributes.length)
|
||||
o = X.escape(X.innerXml(xml));
|
||||
else
|
||||
o["#text"] = X.escape(X.innerXml(xml));
|
||||
}
|
||||
else if (cdataChild) { // cdata
|
||||
if (cdataChild > 1)
|
||||
o = X.escape(X.innerXml(xml));
|
||||
else
|
||||
for (var n=xml.firstChild; n; n=n.nextSibling)
|
||||
o["#cdata"] = X.escape(n.nodeValue);
|
||||
}
|
||||
}
|
||||
if (!xml.attributes.length && !xml.firstChild) o = null;
|
||||
}
|
||||
else if (xml.nodeType==9) { // document.node
|
||||
o = X.toObj(xml.documentElement);
|
||||
}
|
||||
else
|
||||
alert("unhandled node type: " + xml.nodeType);
|
||||
return o;
|
||||
},
|
||||
toJson: function(o, name, ind) {
|
||||
var json = name ? ("\""+name+"\"") : "";
|
||||
if (o instanceof Array) {
|
||||
for (var i=0,n=o.length; i<n; i++)
|
||||
o[i] = X.toJson(o[i], "", ind+"\t");
|
||||
json += (name?":[":"[") + (o.length > 1 ? ("\n"+ind+"\t"+o.join(",\n"+ind+"\t")+"\n"+ind) : o.join("")) + "]";
|
||||
}
|
||||
else if (o == null)
|
||||
json += (name&&":") + "null";
|
||||
else if (typeof(o) == "object") {
|
||||
var arr = [];
|
||||
for (var m in o)
|
||||
arr[arr.length] = X.toJson(o[m], m, ind+"\t");
|
||||
json += (name?":{":"{") + (arr.length > 1 ? ("\n"+ind+"\t"+arr.join(",\n"+ind+"\t")+"\n"+ind) : arr.join("")) + "}";
|
||||
}
|
||||
else if (typeof(o) == "string")
|
||||
json += (name&&":") + "\"" + o.toString() + "\"";
|
||||
else
|
||||
json += (name&&":") + o.toString();
|
||||
return json;
|
||||
},
|
||||
innerXml: function(node) {
|
||||
var s = ""
|
||||
if ("innerHTML" in node)
|
||||
s = node.innerHTML;
|
||||
else {
|
||||
var asXml = function(n) {
|
||||
var s = "";
|
||||
if (n.nodeType == 1) {
|
||||
s += "<" + n.nodeName;
|
||||
for (var i=0; i<n.attributes.length;i++)
|
||||
s += " " + n.attributes[i].nodeName + "=\"" + (n.attributes[i].nodeValue||"").toString() + "\"";
|
||||
if (n.firstChild) {
|
||||
s += ">";
|
||||
for (var c=n.firstChild; c; c=c.nextSibling)
|
||||
s += asXml(c);
|
||||
s += "</"+n.nodeName+">";
|
||||
}
|
||||
else
|
||||
s += "/>";
|
||||
}
|
||||
else if (n.nodeType == 3)
|
||||
s += n.nodeValue;
|
||||
else if (n.nodeType == 4)
|
||||
s += "<![CDATA[" + n.nodeValue + "]]>";
|
||||
return s;
|
||||
};
|
||||
for (var c=node.firstChild; c; c=c.nextSibling)
|
||||
s += asXml(c);
|
||||
}
|
||||
return s;
|
||||
},
|
||||
escape: function(txt) {
|
||||
return txt.replace(/[\\]/g, "\\\\")
|
||||
.replace(/[\"]/g, '\\"')
|
||||
.replace(/[\n]/g, '\\n')
|
||||
.replace(/[\r]/g, '\\r');
|
||||
},
|
||||
removeWhite: function(e) {
|
||||
e.normalize();
|
||||
for (var n = e.firstChild; n; ) {
|
||||
if (n.nodeType == 3) { // text node
|
||||
if (!n.nodeValue.match(/[^ \f\n\r\t\v]/)) { // pure whitespace text node
|
||||
var nxt = n.nextSibling;
|
||||
e.removeChild(n);
|
||||
n = nxt;
|
||||
}
|
||||
else
|
||||
n = n.nextSibling;
|
||||
}
|
||||
else if (n.nodeType == 1) { // element node
|
||||
X.removeWhite(n);
|
||||
n = n.nextSibling;
|
||||
}
|
||||
else // any other node
|
||||
n = n.nextSibling;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
};
|
||||
if (xml.nodeType == 9) // document node
|
||||
xml = xml.documentElement;
|
||||
var json = X.toJson(X.toObj(X.removeWhite(xml)), xml.nodeName, "\t");
|
||||
return "{\n" + tab + (tab ? json.replace(/\t/g, tab) : json.replace(/\t|\n/g, "")) + "\n}";
|
||||
}
|
63
README.rst
Normal file
@ -0,0 +1,63 @@
|
||||
===========
|
||||
converse.js
|
||||
===========
|
||||
|
||||
Converse.js_ implements an XMPP_ based instant messaging client in the browser.
|
||||
|
||||
It is used by collective.xmpp.chat_, which is a Plone_ instant messaging add-on.
|
||||
|
||||
The ultimate goal is to enable anyone to add chat functionality to their websites, regardless of the backend.
|
||||
This is currently possible, except for adding new contacts, which still makes an XHR call to the (Plone) backend to fetch user info.
|
||||
|
||||
--------
|
||||
Features
|
||||
--------
|
||||
|
||||
It has the following features:
|
||||
|
||||
* Manually or automically subscribe to other users.
|
||||
* Accept or decline contact requests
|
||||
* Chat status (online, busy, away, offline)
|
||||
* Custom status messages
|
||||
* Typing notifications
|
||||
* Third person messages (/me )
|
||||
* Multi-user chat in chatrooms
|
||||
* Chatroom Topics
|
||||
* vCard support
|
||||
|
||||
-----------
|
||||
Screencasts
|
||||
-----------
|
||||
|
||||
* `In a static HTML page`_. Here we chat to external XMPP accounts on Jabber.org and Gmail.
|
||||
* `Integrated into a Plone site`_ via collective.xmpp.chat.
|
||||
|
||||
------------
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
It depends on quite a few third party libraries, including:
|
||||
|
||||
* strophe.js_
|
||||
* backbone.js_
|
||||
* require.js_
|
||||
|
||||
-------
|
||||
Licence
|
||||
-------
|
||||
|
||||
``Converse.js`` is released under both the MIT_ and GPL_ licenses.
|
||||
|
||||
.. _Converse.js: http://conversejs.org
|
||||
.. _strophe.js: http://strophe.im/strophejs
|
||||
.. _backbone.js: http:/backbonejs.org
|
||||
.. _require.js: http:/requirejs.org
|
||||
.. _collective.xmpp.chat: http://github.com/collective/collective.xmpp.chat
|
||||
.. _Plone: http://plone.org
|
||||
.. _XMPP: http://xmpp.org
|
||||
.. _MIT: http://opensource.org/licenses/mit-license.php
|
||||
.. _GPL: http://opensource.org/licenses/gpl-license.php
|
||||
.. _here: http://opkode.com/media/blog/instant-messaging-for-plone-with-javascript-and-xmpp
|
||||
.. _Screencast2: http://opkode.com/media/blog/2013/04/02/converse.js-xmpp-instant-messaging-with-javascript
|
||||
.. _`Integrated into a Plone site`: http://opkode.com/media/blog/instant-messaging-for-plone-with-javascript-and-xmpp
|
||||
.. _`In a static HTML page`: http://opkode.com/media/blog/2013/04/02/converse.js-xmpp-instant-messaging-with-javascript
|
10
TODO
Normal file
@ -0,0 +1,10 @@
|
||||
TODO:
|
||||
=====
|
||||
|
||||
* Refresh Rooms panel every time the user goes to it
|
||||
* Add /leave command
|
||||
* Inform users of /help via placeholder in textarea
|
||||
* Create a single sprite image from the current images
|
||||
* Add "keep me logged in" feature when logging in manually
|
||||
* Add support for XMPP server feature detection
|
||||
* Implement "Fetch user vCard" feature.
|
662
converse.css
Normal file
@ -0,0 +1,662 @@
|
||||
.hidden {
|
||||
display: none
|
||||
}
|
||||
|
||||
#chatpanel {
|
||||
z-index: 99; /*--Keeps the panel on top of all other elements--*/
|
||||
position: fixed;
|
||||
bottom: 0; right: 0;
|
||||
height: 332px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#toggle-controlbox {
|
||||
position: fixed;
|
||||
font-size: 80%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
background: #e3e2e2;
|
||||
border: 1px solid #c3c3c3;
|
||||
border-bottom: none;
|
||||
padding: 0.25em 0.5em;
|
||||
margin-right: 1em;
|
||||
height: 1.1em;
|
||||
}
|
||||
|
||||
#connecting-to-chat {
|
||||
background: url(images/spinner.gif) no-repeat left;
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
|
||||
.chat-head {
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
padding: 3px 0 3px 7px;
|
||||
}
|
||||
|
||||
.chat-head-chatbox {
|
||||
background-color: rgb(89, 106, 114);
|
||||
background-color: rgba(89, 106, 114, 1);
|
||||
}
|
||||
|
||||
.chat-head-chatroom {
|
||||
background-color: #2D617A;
|
||||
}
|
||||
|
||||
.chatroom .chat-area {
|
||||
float: left;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.chatroom .chat {
|
||||
overflow: auto;
|
||||
height: 400px;
|
||||
border: solid 1px #ccc;
|
||||
}
|
||||
|
||||
.chatroom .participants {
|
||||
float: left;
|
||||
width: 99px;
|
||||
height: 272px;
|
||||
background-color: white;
|
||||
overflow: auto;
|
||||
border-right: 1px solid #999;
|
||||
border-bottom: 1px solid #999;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.participants ul.participant-list li {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
padding: 0.5em 0 0 0.5em;
|
||||
|
||||
}
|
||||
|
||||
.chatroom form.sendXMPPMessage {
|
||||
-webkit-border-bottom-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.chatroom .participant-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
input.new-chatroom-name {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.chat-blink {
|
||||
background-color: #176689;
|
||||
border-right:1px solid #176689;
|
||||
border-left:1px solid #176689;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
padding: 0.3em;
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
height:193px;
|
||||
overflow-y:auto;
|
||||
border:1px solid #999;
|
||||
border-bottom: 0;
|
||||
border-top: 0;
|
||||
background-color: #ffffff;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.chat-textarea {
|
||||
border: 0;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.chat-textarea-chatbox-selected {
|
||||
border: 1px solid #578308;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.chat-textarea-chatroom-selected {
|
||||
border: 2px solid #2D617A;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.chat-info {
|
||||
color:#666666;
|
||||
|
||||
}
|
||||
|
||||
.chat-message-me {
|
||||
font-weight: bold;
|
||||
color: #436976;
|
||||
}
|
||||
|
||||
.chat-message-room {
|
||||
font-weight: bold;
|
||||
color: #4B7003;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-message-them {
|
||||
font-weight: bold;
|
||||
color: #F62817;
|
||||
white-space: nowrap;
|
||||
max-width: 100px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.chat-event, .chat-date, .chat-help {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.chat-date {
|
||||
display: inline-block;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
div#settings,
|
||||
div#chatrooms,
|
||||
div#login-dialog {
|
||||
height: 272px;
|
||||
}
|
||||
|
||||
p.not-implemented {
|
||||
margin-top: 3em;
|
||||
margin-left: 0.3em;
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
div.delayed .chat-message-them {
|
||||
color: #FB5D50;
|
||||
}
|
||||
|
||||
div.delayed .chat-message-me {
|
||||
color: #7EABBB;
|
||||
}
|
||||
|
||||
.chat-message-error {
|
||||
color:#76797C;
|
||||
font-size:90%;
|
||||
font-weight:normal;
|
||||
}
|
||||
|
||||
.chat-head .avatar {
|
||||
float: left;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
div.chat-title {
|
||||
height: 1.1em;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
line-height: 15px;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
margin-right: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-shadow: rgba(0,0,0,0.51) 0 -1px 0;
|
||||
}
|
||||
|
||||
.chat-head-chatbox,
|
||||
.chat-head-chatroom {
|
||||
background: linear-gradient(top, rgba(206,220,231,1) 0%,rgba(89,106,114,1) 100%);
|
||||
height: 33px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
p.user-custom-message,
|
||||
p.chatroom-topic {
|
||||
font-size: 80%;
|
||||
font-style: italic;
|
||||
height: 1.3em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.activated{
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
a.subscribe-to-user {
|
||||
padding-left: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dl.add-xmpp-contact {
|
||||
margin: 0 0 0 0.5em;
|
||||
padding-top: 3px;
|
||||
z-index: 21;
|
||||
background: url('images/add_icon.png') no-repeat 3px;
|
||||
}
|
||||
|
||||
dt#xmpp-contact-search {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.fancy-dropdown a.choose-xmpp-status,
|
||||
.fancy-dropdown a.add-xmpp-contact {
|
||||
text-shadow: 0 1px 0 rgba(250, 250, 250, 1);
|
||||
padding-left: 1.5em;
|
||||
width: 140px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#fancy-xmpp-status-select {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
#fancy-xmpp-status-select a.change-xmpp-status-message {
|
||||
text-shadow: 0 1px 0 rgba(250, 250, 250, 1);
|
||||
background: url('images/pencil_icon.png') no-repeat right top;
|
||||
float: right;
|
||||
clear: right;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
ul#found-users {
|
||||
padding: 10px 0 5px 5px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
form.search-xmpp-contact {
|
||||
margin: 0;
|
||||
padding-left: 5px;
|
||||
padding: 1em 0 0 5px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
form.search-xmpp-contact input {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.oc-chat-head {
|
||||
margin: 0;
|
||||
color: #FFF;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
height: 35px;
|
||||
clear: right;
|
||||
background-color: #5390C8;
|
||||
padding: 3px 0 0 0;
|
||||
}
|
||||
|
||||
.close-chatbox-button {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
margin-top: 0.2em;
|
||||
margin-right: 0.5em;
|
||||
-moz-box-shadow:inset 0 1px 0 0 #ffffff;
|
||||
-webkit-box-shadow:inset 0 1px 0 0 #ffffff;
|
||||
box-shadow:inset 0 1px 0 0 #ffffff;
|
||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ffffff), color-stop(1, #f6f6f6) );
|
||||
background:-moz-linear-gradient( center top, #ffffff 5%, #f6f6f6 100% );
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f6f6f6');
|
||||
background-color:#ffffff;
|
||||
-moz-border-radius:6px;
|
||||
-webkit-border-radius:6px;
|
||||
border-radius:6px;
|
||||
border:1px solid #888;
|
||||
display:inline-block;
|
||||
color: #666 !important;
|
||||
font-family:arial;
|
||||
font-size:12px;
|
||||
font-weight:bold;
|
||||
padding:0 4px;
|
||||
text-decoration:none;
|
||||
text-shadow:1px 1px 0 #ffffff;
|
||||
}
|
||||
|
||||
.close-chatbox-button:hover {
|
||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #f6f6f6), color-stop(1, #ffffff) );
|
||||
background:-moz-linear-gradient( center top, #f6f6f6 5%, #ffffff 100% );
|
||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f6f6f6', endColorstr='#ffffff');
|
||||
background-color:#f6f6f6;
|
||||
}
|
||||
|
||||
.close-chatbox-button:active {
|
||||
position:relative;
|
||||
top:1px;
|
||||
}
|
||||
|
||||
.oc-chat-content dt {
|
||||
margin: 0;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
#xmppchat-roster dd.odd {
|
||||
background-color: #DCEAC5; /* Make this difference */
|
||||
}
|
||||
|
||||
#xmppchat-roster dd.current-xmpp-contact {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#xmppchat-roster dd.current-xmpp-contact,
|
||||
#xmppchat-roster dd.current-xmpp-contact:hover {
|
||||
background: url(images/user_online_panel.png) no-repeat 5px 2px;
|
||||
}
|
||||
|
||||
#xmppchat-roster dd.current-xmpp-contact.offline:hover,
|
||||
#xmppchat-roster dd.current-xmpp-contact.unavailable:hover,
|
||||
#xmppchat-roster dd.current-xmpp-contact.offline,
|
||||
#xmppchat-roster dd.current-xmpp-contact.unavailable {
|
||||
background: url(images/user_offline_panel.png) no-repeat 5px 2px;
|
||||
}
|
||||
|
||||
#xmppchat-roster dd.current-xmpp-contact.dnd,
|
||||
#xmppchat-roster dd.current-xmpp-contact.dnd:hover {
|
||||
background: url(images/user_busy_panel.png) no-repeat 5px 2px;
|
||||
}
|
||||
|
||||
#xmppchat-roster dd.current-xmpp-contact.away,
|
||||
#xmppchat-roster dd.current-xmpp-contact.away:hover {
|
||||
background: url(images/user_away_panel.png) no-repeat 5px 2px;
|
||||
}
|
||||
|
||||
#xmppchat-roster dd.requesting-xmpp-contact button{
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
#xmppchat-roster dd a {
|
||||
margin-left: 1.5em;
|
||||
text-shadow: 0 1px 0 rgba(250, 250, 250, 1);
|
||||
display: inline-block;
|
||||
width: 113px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.remove-xmpp-contact-dialog .ui-dialog-buttonpane {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#xmppchat-roster {
|
||||
height: 200px;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
top: 0;
|
||||
border: none;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
#available-chatrooms dt,
|
||||
#xmppchat-roster dt {
|
||||
font-weight: normal;
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
border: none;
|
||||
padding: 0.3em 0.5em 0.3em 0.5em;
|
||||
text-shadow: 0 1px 0 rgba(250, 250, 250, 1);
|
||||
}
|
||||
|
||||
#available-chatrooms dt {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
#available-chatrooms li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
dd.available-chatroom,
|
||||
#xmppchat-roster dd {
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
display: block;
|
||||
padding: 0 0.5em 0 0.5em;
|
||||
color: #3f3f3f;
|
||||
text-shadow: 0 1px 0 rgba(250, 250, 250, 1);
|
||||
}
|
||||
|
||||
#xmppchat-roster dd a.remove-xmpp-contact {
|
||||
background: url('images/delete_icon.png') no-repeat right top;
|
||||
padding: 0 1em 1em 0;
|
||||
float: right;
|
||||
width: 22px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chatbox,
|
||||
.chatroom {
|
||||
box-shadow: 1px 1px 5px 1px rgba(0,0,0,0.4);
|
||||
display:none;
|
||||
float: right;
|
||||
margin-right: 15px;
|
||||
z-index: 20; /* So that it's higher than the content actions */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chatbox {
|
||||
width: 201px;
|
||||
}
|
||||
|
||||
.chatroom {
|
||||
width: 300px;
|
||||
height: 311px;
|
||||
}
|
||||
|
||||
.oc-chat-content {
|
||||
height:272px;
|
||||
width: 199px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.oc-chat-content dd {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.oc-chat-content dd.odd {
|
||||
background-color: #DCEAC5;
|
||||
}
|
||||
|
||||
div#controlbox-panes {
|
||||
background: -moz-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(240,240,240,1) 100%); /* FF3.6+ */
|
||||
background: -ms-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(240,240,240,1) 100%); /* IE10+ */
|
||||
background: -o-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(240,240,240,1) 100%); /* Opera 11.10+ */
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,1)), color-stop(100%,rgba(240,240,240,1))); /* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(240,240,240,1) 100%); /* Chrome10+,Safari5.1+ */
|
||||
background: linear-gradient(top, rgba(255,255,255,1) 0%,rgba(240,240,240,1) 100%); /* W3C */
|
||||
background-color: white;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border: 1px solid #999;
|
||||
width: 199px;
|
||||
}
|
||||
|
||||
form#xmppchat-login {
|
||||
background: white;
|
||||
padding: 2em 0 0.3em 0.5em;
|
||||
}
|
||||
|
||||
form#xmppchat-login input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
form.set-xmpp-status,
|
||||
form.add-chatroom {
|
||||
padding: 0.5em 0 0.3em 0.5em;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.set-xmpp-status li {
|
||||
list-style: none;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
select#select-xmpp-status {
|
||||
float: right;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
/* @group Tabs */
|
||||
|
||||
.chat-head #controlbox-tabs {
|
||||
text-align: center;
|
||||
display: inline;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
/* single tab */
|
||||
.chat-head #controlbox-tabs li {
|
||||
float:left;
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
text-shadow: white 0 1px 0;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
ul#controlbox-tabs li a {
|
||||
display:block;
|
||||
font-size:12px;
|
||||
height: 34px;
|
||||
line-height:34px;
|
||||
margin: 0;
|
||||
text-align:center;
|
||||
text-decoration:none;
|
||||
border: 1px solid #999;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
color:#666;
|
||||
text-shadow: 0 1px 0 rgba(250, 250, 250, 1);
|
||||
}
|
||||
|
||||
.chat-head #controlbox-tabs li a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.chat-head #controlbox-tabs li a {
|
||||
background-color: white;
|
||||
box-shadow: inset 0 0 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
ul#controlbox-tabs a.current, ul#controlbox-tabs a.current:hover {
|
||||
box-shadow: none;
|
||||
color: #000;
|
||||
border-bottom: 0;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
div#users,
|
||||
div#chatrooms,
|
||||
div#login-dialog,
|
||||
div#settings {
|
||||
border: 0;
|
||||
font-size: 14px;
|
||||
width: 199px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
form.sendXMPPMessage {
|
||||
background: white;
|
||||
border: 1px solid #999;
|
||||
padding:0.5em;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding-box;
|
||||
background-clip: padding-box;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
#set-custom-xmpp-status {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#set-custom-xmpp-status button {
|
||||
padding: 1px 2px 1px 1px;
|
||||
}
|
||||
|
||||
/* status dropdown styles */
|
||||
dl.dropdown {
|
||||
margin-right: 0.5em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.dropdown dd, .dropdown dt, .dropdown ul { margin:0px; padding:0px; }
|
||||
.dropdown dd { position:relative; }
|
||||
|
||||
div.xmpp-status {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
input.custom-xmpp-status {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.fancy-dropdown {
|
||||
border:1px solid #ddd;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.dropdown dt a span {
|
||||
cursor:pointer; display:block; padding:5px;
|
||||
}
|
||||
|
||||
.dropdown dd ul {
|
||||
list-style:none;
|
||||
position:absolute; left:0; top:0;
|
||||
border:1px solid #ddd;
|
||||
border-top: 0;
|
||||
width: 99%;
|
||||
background-color: #FFF;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
.dropdown dd ul li a:hover {
|
||||
background-color: #bed6e5;
|
||||
}
|
||||
|
||||
.dropdown span.value {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.dropdown dd ul li a {
|
||||
padding:5px 5px 5px 30px;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.dropdown a.online {
|
||||
background: url(images/user_online_panel.png) no-repeat left 3px;
|
||||
}
|
||||
|
||||
.dropdown a.offline {
|
||||
background: url(images/user_offline_panel.png) no-repeat left 3px;
|
||||
}
|
||||
|
||||
.dropdown a.dnd {
|
||||
background: url(images/user_busy_panel.png) no-repeat left 3px;
|
||||
}
|
||||
|
||||
.dropdown a.away {
|
||||
background: url(images/user_away_panel.png) no-repeat left 3px;
|
||||
}
|
||||
|
1994
converse.js
Normal file
BIN
images/add_icon.png
Normal file
After Width: | Height: | Size: 249 B |
BIN
images/delete_icon.png
Normal file
After Width: | Height: | Size: 470 B |
BIN
images/error_icon.png
Normal file
After Width: | Height: | Size: 434 B |
BIN
images/pencil_icon.png
Normal file
After Width: | Height: | Size: 226 B |
BIN
images/spinner.gif
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
images/user_away_panel.png
Normal file
After Width: | Height: | Size: 511 B |
BIN
images/user_busy_panel.png
Normal file
After Width: | Height: | Size: 435 B |
BIN
images/user_offline_panel.png
Normal file
After Width: | Height: | Size: 506 B |
BIN
images/user_online_panel.png
Normal file
After Width: | Height: | Size: 415 B |
436
spec/MainSpec.js
Normal file
@ -0,0 +1,436 @@
|
||||
(function (root, factory) {
|
||||
define([
|
||||
"converse"
|
||||
], function (xmppchat) {
|
||||
return factory(xmppchat);
|
||||
}
|
||||
);
|
||||
} (this, function (xmppchat) {
|
||||
|
||||
return describe("Converse.js", $.proxy(function() {
|
||||
// Names from http://www.fakenamegenerator.com/
|
||||
var req_names = [
|
||||
'Louw Spekman', 'Mohamad Stet', 'Dominik Beyer', 'Dirk Eichel', 'Marco Duerr', 'Ute Schiffer',
|
||||
'Billie Westerhuis', 'Sarah Kuester', 'Sabrina Loewe', 'Laura Duerr', 'Mathias Meyer',
|
||||
'Tijm Keller', 'Lea Gerste', 'Martin Pfeffer', 'Ulrike Abt', 'Zoubida van Rooij',
|
||||
'Maylin Hettema', 'Ruwan Bechan', 'Marco Beich', 'Karin Busch', 'Mathias Müller'
|
||||
];
|
||||
var pend_names = [
|
||||
'Suleyman van Beusichem', 'Nicole Diederich', 'Nanja van Yperen', 'Delany Bloemendaal',
|
||||
'Jannah Hofmeester', 'Christine Trommler', 'Martin Bumgarner', 'Emil Baeten', 'Farshad Brasser',
|
||||
'Gabriele Fisher', 'Sofiane Schopman', 'Sky Wismans', 'Jeffery Stoelwinder', 'Ganesh Waaijenberg',
|
||||
'Dani Boldewijn', 'Katrin Propst', 'Martina Kaiser', 'Philipp Kappel', 'Meeke Grootendorst'
|
||||
];
|
||||
var cur_names = [
|
||||
'Max Frankfurter', 'Candice van der Knijff', 'Irini Vlastuin', 'Rinse Sommer', 'Annegreet Gomez',
|
||||
'Robin Schook', 'Marcel Eberhardt', 'Simone Brauer', 'Asmaa Haakman', 'Felix Amsel',
|
||||
'Lena Grunewald', 'Laura Grunewald', 'Mandy Seiler', 'Sven Bosch', 'Nuriye Cuypers', 'Ben Zomer',
|
||||
'Leah Weiss', 'Francesca Disseldorp', 'Sven Bumgarner', 'Benjamin Zweig'
|
||||
];
|
||||
var num_contacts = req_names.length + pend_names.length + cur_names.length;
|
||||
this.bare_jid = 'dummy@localhost';
|
||||
mock_connection = {
|
||||
'muc': {
|
||||
'listRooms': function () {}
|
||||
},
|
||||
'jid': this.bare_jid,
|
||||
'addHandler': function (handler, ns, name, type, id, from, options) {
|
||||
return function () {};
|
||||
},
|
||||
'roster': {
|
||||
'add': function () {},
|
||||
'authorize': function () {},
|
||||
'unauthorize': function () {},
|
||||
'get': function () {},
|
||||
'subscribe': function () {},
|
||||
'registerCallback': function () {}
|
||||
}
|
||||
};
|
||||
|
||||
// Clear localStorage
|
||||
window.localStorage.removeItem(
|
||||
hex_sha1('converse.rosteritems-'+this.bare_jid));
|
||||
window.localStorage.removeItem(
|
||||
hex_sha1('converse.chatboxes-'+this.bare_jid));
|
||||
window.localStorage.removeItem(
|
||||
hex_sha1('converse.xmppstatus-'+this.bare_jid));
|
||||
|
||||
this.prebind = true;
|
||||
this.onConnected(mock_connection);
|
||||
this.animate = false; // don't use animations
|
||||
|
||||
// The timeout is used to slow down the tests so that one can see
|
||||
// visually what is happening in the page.
|
||||
var timeout = 0;
|
||||
var sleep = function (delay) {
|
||||
// Yes this is blocking and stupid, but these are tests and this is
|
||||
// the easiest way to delay execution without having to use
|
||||
// callbacks.
|
||||
var start = new Date().getTime();
|
||||
while (new Date().getTime() < start + delay) {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
describe("The Contacts Roster", $.proxy(function () {
|
||||
it("is not shown by default", $.proxy(function () {
|
||||
expect(this.rosterview.$el.is(':visible')).toEqual(false);
|
||||
}, xmppchat));
|
||||
|
||||
it("can be opened by clicking a DOM element with id 'toggle-online-users'", $.proxy(function () {
|
||||
spyOn(this, 'toggleControlBox').andCallThrough();
|
||||
$('#toggle-online-users').click();
|
||||
expect(this.toggleControlBox).toHaveBeenCalled();
|
||||
}, xmppchat));
|
||||
|
||||
describe("Pending Contacts", $.proxy(function () {
|
||||
it("do not have a heading if there aren't any", $.proxy(function () {
|
||||
expect(this.rosterview.$el.find('dt#pending-xmpp-contacts').css('display')).toEqual('none');
|
||||
}, xmppchat));
|
||||
|
||||
it("can be added to the roster and they will be sorted alphabetically", $.proxy(function () {
|
||||
var i, t, is_last;
|
||||
spyOn(this.rosterview, 'render').andCallThrough();
|
||||
for (i=0; i<pend_names.length; i++) {
|
||||
is_last = i==(pend_names.length-1);
|
||||
this.roster.create({
|
||||
jid: pend_names[i].replace(' ','.').toLowerCase() + '@localhost',
|
||||
subscription: 'none',
|
||||
ask: 'subscribe',
|
||||
fullname: pend_names[i],
|
||||
is_last: is_last
|
||||
});
|
||||
// For performance reasons, the roster should only be shown once
|
||||
// the last contact has been added.
|
||||
if (is_last) {
|
||||
expect(this.rosterview.$el.is(':visible')).toEqual(true);
|
||||
} else {
|
||||
expect(this.rosterview.$el.is(':visible')).toEqual(false);
|
||||
}
|
||||
expect(this.rosterview.render).toHaveBeenCalled();
|
||||
// Check that they are sorted alphabetically
|
||||
t = this.rosterview.$el.find('dt#pending-xmpp-contacts').siblings('dd.pending-xmpp-contact').text();
|
||||
expect(t).toEqual(pend_names.slice(0,i+1).sort().join(''));
|
||||
}
|
||||
sleep(timeout);
|
||||
}, xmppchat));
|
||||
|
||||
it("will have their own heading once they have been added", $.proxy(function () {
|
||||
expect(this.rosterview.$el.find('dt#pending-xmpp-contacts').css('display')).toEqual('block');
|
||||
}, xmppchat));
|
||||
}, xmppchat));
|
||||
|
||||
describe("Existing Contacts", $.proxy(function () {
|
||||
it("do not have a heading if there aren't any", $.proxy(function () {
|
||||
expect(this.rosterview.$el.find('dt#xmpp-contacts').css('display')).toEqual('none');
|
||||
}, xmppchat));
|
||||
|
||||
it("can be added to the roster and they will be sorted alphabetically", $.proxy(function () {
|
||||
var i, t;
|
||||
spyOn(this.rosterview, 'render').andCallThrough();
|
||||
for (i=0; i<cur_names.length; i++) {
|
||||
this.roster.create({
|
||||
jid: cur_names[i].replace(' ','.').toLowerCase() + '@localhost',
|
||||
subscription: 'both',
|
||||
ask: null,
|
||||
fullname: cur_names[i],
|
||||
is_last: i==(cur_names.length-1)
|
||||
});
|
||||
expect(this.rosterview.render).toHaveBeenCalled();
|
||||
// Check that they are sorted alphabetically
|
||||
t = this.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.offline').find('a.open-chat').text();
|
||||
expect(t).toEqual(cur_names.slice(0,i+1).sort().join(''));
|
||||
}
|
||||
sleep(timeout);
|
||||
}, xmppchat));
|
||||
|
||||
it("will have their own heading once they have been added", $.proxy(function () {
|
||||
expect(this.rosterview.$el.find('dt#xmpp-contacts').css('display')).toEqual('block');
|
||||
}, xmppchat));
|
||||
|
||||
it("can change their status to online and be sorted alphabetically", $.proxy(function () {
|
||||
var item, view, jid, t;
|
||||
spyOn(this.rosterview, 'render').andCallThrough();
|
||||
for (i=0; i<5; i++) {
|
||||
jid = cur_names[i].replace(' ','.').toLowerCase() + '@localhost';
|
||||
view = this.rosterview.rosteritemviews[jid];
|
||||
spyOn(view, 'render').andCallThrough();
|
||||
item = view.model;
|
||||
item.set('chat_status', 'online');
|
||||
expect(view.render).toHaveBeenCalled();
|
||||
expect(this.rosterview.render).toHaveBeenCalled();
|
||||
|
||||
// Check that they are sorted alphabetically
|
||||
t = this.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.online').find('a.open-chat').text();
|
||||
expect(t).toEqual(cur_names.slice(0,i+1).sort().join(''));
|
||||
sleep(timeout);
|
||||
}
|
||||
}, xmppchat));
|
||||
|
||||
it("can change their status to busy and be sorted alphabetically", $.proxy(function () {
|
||||
var item, view, jid, t;
|
||||
spyOn(this.rosterview, 'render').andCallThrough();
|
||||
for (i=5; i<10; i++) {
|
||||
jid = cur_names[i].replace(' ','.').toLowerCase() + '@localhost';
|
||||
view = this.rosterview.rosteritemviews[jid];
|
||||
spyOn(view, 'render').andCallThrough();
|
||||
item = view.model;
|
||||
item.set('chat_status', 'dnd');
|
||||
expect(view.render).toHaveBeenCalled();
|
||||
expect(this.rosterview.render).toHaveBeenCalled();
|
||||
// Check that they are sorted alphabetically
|
||||
t = this.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.dnd').find('a.open-chat').text();
|
||||
expect(t).toEqual(cur_names.slice(5,i+1).sort().join(''));
|
||||
sleep(timeout);
|
||||
}
|
||||
}, xmppchat));
|
||||
|
||||
it("can change their status to away and be sorted alphabetically", $.proxy(function () {
|
||||
var item, view, jid, t;
|
||||
spyOn(this.rosterview, 'render').andCallThrough();
|
||||
for (i=10; i<15; i++) {
|
||||
jid = cur_names[i].replace(' ','.').toLowerCase() + '@localhost';
|
||||
view = this.rosterview.rosteritemviews[jid];
|
||||
spyOn(view, 'render').andCallThrough();
|
||||
item = view.model;
|
||||
item.set('chat_status', 'away');
|
||||
expect(view.render).toHaveBeenCalled();
|
||||
expect(this.rosterview.render).toHaveBeenCalled();
|
||||
|
||||
// Check that they are sorted alphabetically
|
||||
t = this.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.away').find('a.open-chat').text();
|
||||
expect(t).toEqual(cur_names.slice(10,i+1).sort().join(''));
|
||||
sleep(timeout);
|
||||
}
|
||||
}, xmppchat));
|
||||
|
||||
it("can change their status to unavailable and be sorted alphabetically", $.proxy(function () {
|
||||
var item, view, jid, t;
|
||||
spyOn(this.rosterview, 'render').andCallThrough();
|
||||
for (i=15; i<20; i++) {
|
||||
jid = cur_names[i].replace(' ','.').toLowerCase() + '@localhost';
|
||||
view = this.rosterview.rosteritemviews[jid];
|
||||
spyOn(view, 'render').andCallThrough();
|
||||
item = view.model;
|
||||
item.set('chat_status', 'unavailable');
|
||||
expect(view.render).toHaveBeenCalled();
|
||||
expect(this.rosterview.render).toHaveBeenCalled();
|
||||
|
||||
// Check that they are sorted alphabetically
|
||||
t = this.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.unavailable').find('a.open-chat').text();
|
||||
expect(t).toEqual(cur_names.slice(15, i+1).sort().join(''));
|
||||
sleep(timeout);
|
||||
}
|
||||
}, xmppchat));
|
||||
|
||||
it("are ordered according to status: online, busy, away, unavailable, offline", $.proxy(function () {
|
||||
var contacts = this.rosterview.$el.find('dd.current-xmpp-contact');
|
||||
var i;
|
||||
// The first five contacts are online.
|
||||
for (i=0; i<5; i++) {
|
||||
expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('online');
|
||||
}
|
||||
// The next five are busy
|
||||
for (i=5; i<10; i++) {
|
||||
expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('dnd');
|
||||
}
|
||||
// The next five are away
|
||||
for (i=10; i<15; i++) {
|
||||
expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('away');
|
||||
}
|
||||
// The next five are unavailable
|
||||
for (i=15; i<20; i++) {
|
||||
expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('unavailable');
|
||||
}
|
||||
// The next 20 are offline
|
||||
for (i=20; i<cur_names.length; i++) {
|
||||
expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('offline');
|
||||
}
|
||||
}, xmppchat));
|
||||
|
||||
|
||||
}, xmppchat));
|
||||
|
||||
describe("Requesting Contacts", $.proxy(function () {
|
||||
// by default the dts are hidden from css class and only later they will be hidden
|
||||
// by jQuery therefore for the first check we will see if visible instead of none
|
||||
it("do not have a heading if there aren't any", $.proxy(function () {
|
||||
expect(this.rosterview.$el.find('dt#xmpp-contact-requests').is(':visible')).toEqual(false);
|
||||
}, xmppchat));
|
||||
|
||||
it("can be added to the roster and they will be sorted alphabetically", $.proxy(function () {
|
||||
var i, t;
|
||||
spyOn(this.rosterview, 'render').andCallThrough();
|
||||
spyOn(this, 'showControlBox').andCallThrough();
|
||||
for (i=0; i<req_names.length; i++) {
|
||||
this.roster.create({
|
||||
jid: req_names[i].replace(' ','.').toLowerCase() + '@localhost',
|
||||
subscription: 'none',
|
||||
ask: 'request',
|
||||
fullname: req_names[i],
|
||||
is_last: i==(req_names.length-1)
|
||||
});
|
||||
expect(this.rosterview.render).toHaveBeenCalled();
|
||||
// Check that they are sorted alphabetically
|
||||
t = this.rosterview.$el.find('dt#xmpp-contact-requests').siblings('dd.requesting-xmpp-contact').text().replace(/AcceptDecline/g, '');
|
||||
expect(t).toEqual(req_names.slice(0,i+1).sort().join(''));
|
||||
// When a requesting contact is added, the controlbox must
|
||||
// be opened.
|
||||
expect(this.showControlBox).toHaveBeenCalled();
|
||||
}
|
||||
sleep(timeout);
|
||||
}, xmppchat));
|
||||
|
||||
it("will have their own heading once they have been added", $.proxy(function () {
|
||||
expect(this.rosterview.$el.find('dt#xmpp-contact-requests').css('display')).toEqual('block');
|
||||
}, xmppchat));
|
||||
|
||||
it("can have their requests accepted by the user", $.proxy(function () {
|
||||
// TODO: Testing can be more thorough here, the user is
|
||||
// actually not accepted/authorized because of
|
||||
// mock_connection.
|
||||
var jid = req_names.sort()[0].replace(' ','.').toLowerCase() + '@localhost';
|
||||
var view = this.rosterview.rosteritemviews[jid];
|
||||
spyOn(this.connection.roster, 'authorize');
|
||||
spyOn(view, 'acceptRequest').andCallThrough();
|
||||
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
var accept_button = view.$el.find('.accept-xmpp-request');
|
||||
accept_button.click();
|
||||
expect(view.acceptRequest).toHaveBeenCalled();
|
||||
expect(this.connection.roster.authorize).toHaveBeenCalled();
|
||||
sleep(timeout);
|
||||
}, xmppchat));
|
||||
|
||||
it("can have their requests denied by the user", $.proxy(function () {
|
||||
var jid = req_names.sort()[1].replace(' ','.').toLowerCase() + '@localhost';
|
||||
var view = this.rosterview.rosteritemviews[jid];
|
||||
spyOn(this.connection.roster, 'unauthorize');
|
||||
spyOn(this.rosterview, 'removeRosterItem').andCallThrough();
|
||||
spyOn(view, 'declineRequest').andCallThrough();
|
||||
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
var accept_button = view.$el.find('.decline-xmpp-request');
|
||||
accept_button.click();
|
||||
expect(view.declineRequest).toHaveBeenCalled();
|
||||
expect(this.rosterview.removeRosterItem).toHaveBeenCalled();
|
||||
expect(this.connection.roster.unauthorize).toHaveBeenCalled();
|
||||
// There should now be one less contact
|
||||
expect(this.roster.length).toEqual(num_contacts-1);
|
||||
sleep(timeout);
|
||||
}, xmppchat));
|
||||
}, xmppchat));
|
||||
|
||||
describe("All Contacts", $.proxy(function () {
|
||||
|
||||
it("are saved to, and can be retrieved from, localStorage", $.proxy(function () {
|
||||
var new_attrs, old_attrs, attrs, old_roster;
|
||||
// One contact was declined, so we have 1 less contact then originally
|
||||
expect(this.roster.length).toEqual(num_contacts-1);
|
||||
old_roster = this.roster;
|
||||
this.roster = new this.RosterItems();
|
||||
expect(this.roster.length).toEqual(0);
|
||||
|
||||
this.roster.localStorage = new Backbone.LocalStorage(
|
||||
hex_sha1('converse.rosteritems-dummy@localhost'));
|
||||
this.chatboxes.onConnected();
|
||||
|
||||
spyOn(this.roster, 'fetch').andCallThrough();
|
||||
this.rosterview = new this.RosterView({'model':this.roster});
|
||||
expect(this.roster.fetch).toHaveBeenCalled();
|
||||
expect(this.roster.length).toEqual(num_contacts-1);
|
||||
|
||||
// Check that the roster items retrieved from localStorage
|
||||
// have the same attributes values as the original ones.
|
||||
attrs = ['jid', 'fullname', 'subscription', 'ask'];
|
||||
for (i=0; i<attrs.length; i++) {
|
||||
new_attrs = _.pluck(_.pluck(this.roster.models, 'attributes'), attrs[i]);
|
||||
old_attrs = _.pluck(_.pluck(old_roster.models, 'attributes'), attrs[i]);
|
||||
// Roster items in storage are not necessarily sorted,
|
||||
// so we have to sort them here to do a proper
|
||||
// comparison
|
||||
expect(_.isEqual(new_attrs.sort(), old_attrs.sort())).toEqual(true);
|
||||
}
|
||||
this.rosterview.render();
|
||||
}, xmppchat));
|
||||
|
||||
afterEach($.proxy(function () {
|
||||
// Contacts retrieved from localStorage have chat_status of
|
||||
// "offline".
|
||||
// In the next test suite, we need some online contacts, so
|
||||
// we make some online now
|
||||
for (i=0; i<5; i++) {
|
||||
jid = cur_names[i].replace(' ','.').toLowerCase() + '@localhost';
|
||||
view = this.rosterview.rosteritemviews[jid];
|
||||
view.model.set('chat_status', 'online');
|
||||
}
|
||||
}, xmppchat));
|
||||
}, xmppchat));
|
||||
}, xmppchat));
|
||||
|
||||
describe("Chatboxes", $.proxy(function () {
|
||||
|
||||
it("are created when you click on a roster item", $.proxy(function () {
|
||||
var i, $el, click, jid, view;
|
||||
// showControlBox was called earlier, so the controlbox is
|
||||
// visible, but no other chat boxes have been created.
|
||||
expect(this.chatboxes.length).toEqual(1);
|
||||
|
||||
var online_contacts = this.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.online').find('a.open-chat');
|
||||
for (i=0; i<online_contacts.length; i++) {
|
||||
$el = $(online_contacts[i]);
|
||||
jid = $el.text().replace(' ','.').toLowerCase() + '@localhost';
|
||||
view = this.rosterview.rosteritemviews[jid];
|
||||
spyOn(view, 'openChat').andCallThrough();
|
||||
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
$el.click();
|
||||
expect(view.openChat).toHaveBeenCalled();
|
||||
expect(this.chatboxes.length).toEqual(i+2);
|
||||
sleep(timeout);
|
||||
}
|
||||
}, xmppchat));
|
||||
|
||||
it("can be saved to, and retrieved from, localStorage", $.proxy(function () {
|
||||
var old_chatboxes = this.chatboxes;
|
||||
expect(this.chatboxes.length).toEqual(6);
|
||||
this.chatboxes = new this.ChatBoxes();
|
||||
expect(this.chatboxes.length).toEqual(0);
|
||||
|
||||
this.chatboxes.onConnected();
|
||||
expect(this.chatboxes.length).toEqual(6);
|
||||
|
||||
// Check that the roster items retrieved from localStorage
|
||||
// have the same attributes values as the original ones.
|
||||
attrs = ['id', 'box_id', 'visible'];
|
||||
for (i=0; i<attrs.length; i++) {
|
||||
new_attrs = _.pluck(_.pluck(this.chatboxes.models, 'attributes'), attrs[i]);
|
||||
old_attrs = _.pluck(_.pluck(old_chatboxes.models, 'attributes'), attrs[i]);
|
||||
expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
|
||||
}
|
||||
this.rosterview.render();
|
||||
}, xmppchat));
|
||||
|
||||
it("can be closed again by clicking a DOM element with class 'close-chatbox-button'", $.proxy(function () {
|
||||
var chatbox, view, $el;
|
||||
for (i=0; i<this.chatboxes.length; i++) {
|
||||
chatbox = this.chatboxes.models[i];
|
||||
view = this.chatboxesview.views[chatbox.get('id')];
|
||||
spyOn(view, 'closeChat').andCallThrough();
|
||||
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
|
||||
view.$el.find('.close-chatbox-button').click();
|
||||
expect(view.closeChat).toHaveBeenCalled();
|
||||
sleep(timeout);
|
||||
}
|
||||
}, xmppchat));
|
||||
|
||||
it("will be removed from localStorage when closed", $.proxy(function () {
|
||||
var old_chatboxes = this.chatboxes;
|
||||
expect(this.chatboxes.length).toEqual(6);
|
||||
this.chatboxes = new this.ChatBoxes();
|
||||
expect(this.chatboxes.length).toEqual(0);
|
||||
|
||||
this.chatboxes.onConnected();
|
||||
expect(this.chatboxes.length).toEqual(0);
|
||||
}, xmppchat));
|
||||
}, xmppchat));
|
||||
|
||||
}, xmppchat));
|
||||
}));
|
@ -126,12 +126,12 @@ a {
|
||||
|
||||
#main_content a:hover {
|
||||
color: #0069ba;
|
||||
text-shadow: #0090ff 0px 0px 2px;
|
||||
text-shadow: #5390c8 0px 0px 2px;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #43adff;
|
||||
text-shadow: #0090ff 0px 0px 2px;
|
||||
text-shadow: #5390c8 0px 0px 2px;
|
||||
}
|
||||
|
||||
em {
|
||||
@ -284,7 +284,7 @@ Full-Width Styles
|
||||
z-index: 10;
|
||||
padding: 10px 50px 10px 10px;
|
||||
color: #fff;
|
||||
background: url('../images/blacktocat.png') #0090ff no-repeat 95% 50%;
|
||||
background: url('../images/blacktocat.png') #5390c8 no-repeat 95% 50%;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.5);
|
||||
border-bottom-left-radius: 2px;
|
||||
|
33
tests.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
||||
"http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Converse.js Tests</title>
|
||||
<meta name="description" content="Converse.js: Open Source Browser-Based Instant Messaging" />
|
||||
<link rel="shortcut icon" type="image/png" href="Libraries/jasmine-1.3.1/jasmine_favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="Libraries/jasmine-1.3.1/jasmine.css">
|
||||
<script type="text/javascript" src="Libraries/jasmine-1.3.1/jasmine.js"></script>
|
||||
<script type="text/javascript" src="Libraries/jasmine-1.3.1/jasmine-html.js"></script>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="stylesheets/stylesheet.css">
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="converse.css">
|
||||
<script data-main="tests_main" src="Libraries/require-jquery.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="header_wrap" class="outer">
|
||||
<header class="inner">
|
||||
<h1 id="project_title">Converse.js</h1>
|
||||
<h2 id="project_tagline">Tests</h2>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div id="chatpanel">
|
||||
<div id="collective-xmpp-chat-data"></div>
|
||||
<div id="toggle-controlbox">
|
||||
<a href="#" class="chat" id="toggle-online-users">
|
||||
<span class="conn-feedback">Click here to chat</span> <strong style="display: none" id="online-count">(0)</strong>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
16
tests_main.js
Normal file
@ -0,0 +1,16 @@
|
||||
require(["jquery", "spec/MainSpec"], function($) {
|
||||
|
||||
$(function($) {
|
||||
var jasmineEnv = jasmine.getEnv();
|
||||
jasmineEnv.updateInterval = 1000;
|
||||
|
||||
var htmlReporter = new jasmine.HtmlReporter();
|
||||
|
||||
jasmineEnv.addReporter(htmlReporter);
|
||||
|
||||
jasmineEnv.specFilter = function(spec) {
|
||||
return htmlReporter.specFilter(spec);
|
||||
};
|
||||
jasmineEnv.execute();
|
||||
});
|
||||
});
|